diff --git a/dart_test.yaml b/dart_test.yaml new file mode 100644 index 000000000..f150a85f7 --- /dev/null +++ b/dart_test.yaml @@ -0,0 +1,3 @@ +tags: + benchmark: + timeout: 2x \ No newline at end of file diff --git a/example/filter_bank.dart b/example/filter_bank.dart new file mode 100644 index 000000000..710096c3e --- /dev/null +++ b/example/filter_bank.dart @@ -0,0 +1,118 @@ +// 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 'filter_bank/filter_bank_modules.dart'; + +// Re-export so downstream consumers (e.g. devtools loopback) can use. +export 'filter_bank/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 samples = List.generate(2, (ch) => FilterSample(name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samples, + 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); + samples[0].data.inject(0); + samples[0].valid.inject(0); + samples[1].data.inject(0); + samples[1].valid.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); + samples[0].valid.inject(1); + samples[1].valid.inject(1); + + // ── Feed sample stream: impulse response test ── + // Send a single '1' followed by zeros to get the impulse response + samples[0].data.inject(1); + samples[1].data.inject(1); + await clk.nextPosedge; + + for (var i = 0; i < 8; i++) { + samples[0].data.inject(0); + samples[1].data.inject(0); + await clk.nextPosedge; + } + + // ── Signal end of input ── + samples[0].valid.inject(0); + samples[1].valid.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/filter_bank/coeff_bank.dart b/example/filter_bank/coeff_bank.dart new file mode 100644 index 000000000..da7523f6d --- /dev/null +++ b/example/filter_bank/coeff_bank.dart @@ -0,0 +1,62 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// coeff_bank.dart +// Coefficient storage module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// 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; + } +} diff --git a/example/filter_bank/filter_bank.dart b/example/filter_bank/filter_bank.dart new file mode 100644 index 000000000..f0e973472 --- /dev/null +++ b/example/filter_bank/filter_bank.dart @@ -0,0 +1,246 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_bank.dart +// Top-level polyphase FIR filter bank module for the example library. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +import 'filter_channel.dart'; +import 'filter_controller.dart'; +import 'filter_data_interface.dart'; +import 'filter_sample.dart'; +import 'shared_data_bus.dart'; + +/// 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 +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'); + + /// Input [FilterSample] port for channel [ch]. + @protected + FilterSample samplePin(int ch) => input('sample$ch') as FilterSample; + + /// 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`. + /// [samples] is a [LogicArray] with one element per channel. + /// [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, + List samples, + 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 (numChannels <= 0) { + throw ArgumentError.value( + numChannels, + 'numChannels', + 'must be greater than zero', + ); + } + if (numTaps <= 0) { + throw ArgumentError.value( + numTaps, + 'numTaps', + 'must be greater than zero', + ); + } + if (dataWidth <= 0) { + throw ArgumentError.value( + dataWidth, + 'dataWidth', + 'must be greater than zero', + ); + } + if (samples.length != numChannels) { + throw ArgumentError.value( + samples.length, + 'samples', + 'must have $numChannels entries (one per channel)', + ); + } + if (coefficients.length != numChannels) { + throw ArgumentError.value( + coefficients.length, + 'coefficients', + 'must have $numChannels entries (one per channel)', + ); + } + for (var ch = 0; ch < numChannels; ch++) { + if (coefficients[ch].length != numTaps) { + throw ArgumentError.value( + coefficients[ch].length, + 'coefficients[$ch]', + 'must have $numTaps entries (one per tap)', + ); + } + } + + // ── Register ports ── + clk = addInput('clk', clk); + reset = addInput('reset', reset); + start = addInput('start', start); + inputDone = addInput('inputDone', inputDone); + + // One typed FilterSample input port per channel. + final inPorts = []; + for (var ch = 0; ch < numChannels; ch++) { + inPorts.add(addTypedInput('sample$ch', samples[ch])); + } + + final channelOut = addTypedOutput( + 'channelOut', + ({name = 'channelOut'}) => + LogicArray([numChannels], dataWidth, name: name)); + final validOut = addOutput('validOut'); + final done = addOutput('done'); + final state = addOutput('state', width: 3); + + // ── Controller FSM ── + // Drain cycles: numTaps cycles per accumulation + pipeline depth (2) + 1 + final controller = FilterController( + clk, + reset, + start, + inPorts[0].valid, // valid is shared across channels + 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 <= inPorts[ch].data; + srcIntf.validIn <= inPorts[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/example/filter_bank/filter_bank_modules.dart b/example/filter_bank/filter_bank_modules.dart new file mode 100644 index 000000000..5341784d8 --- /dev/null +++ b/example/filter_bank/filter_bank_modules.dart @@ -0,0 +1,17 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_bank_modules.dart +// Barrel file for the polyphase FIR filter bank example modules. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +export 'coeff_bank.dart'; +export 'filter_bank.dart'; +export 'filter_channel.dart'; +export 'filter_controller.dart'; +export 'filter_data_interface.dart'; +export 'filter_sample.dart'; +export 'mac_unit.dart'; +export 'shared_data_bus.dart'; diff --git a/example/filter_bank/filter_channel.dart b/example/filter_bank/filter_channel.dart new file mode 100644 index 000000000..317c9f934 --- /dev/null +++ b/example/filter_bank/filter_channel.dart @@ -0,0 +1,235 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_channel.dart +// Single FIR channel module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +import 'coeff_bank.dart'; +import 'filter_data_interface.dart'; +import 'mac_unit.dart'; + +/// 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; + } +} diff --git a/example/filter_bank/filter_controller.dart b/example/filter_bank/filter_controller.dart new file mode 100644 index 000000000..cfc730ef4 --- /dev/null +++ b/example/filter_bank/filter_controller.dart @@ -0,0 +1,177 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_controller.dart +// FSM controller module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// 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), + ]), + ]); + } +} diff --git a/example/filter_bank/filter_data_interface.dart b/example/filter_bank/filter_data_interface.dart new file mode 100644 index 000000000..06faf7dba --- /dev/null +++ b/example/filter_bank/filter_data_interface.dart @@ -0,0 +1,63 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_data_interface.dart +// Interface definition for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// 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); +} diff --git a/example/filter_bank/filter_sample.dart b/example/filter_bank/filter_sample.dart new file mode 100644 index 000000000..290e9dc76 --- /dev/null +++ b/example/filter_bank/filter_sample.dart @@ -0,0 +1,51 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_sample.dart +// LogicStructure sample word for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// A structured signal bundling a data sample with metadata. +/// +/// Packs two fields — [data] and [valid] — into a single bus that can be +/// driven and sampled as a unit. Used throughout the +/// filter bank 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; + + /// 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'), + ], + name: name ?? 'filter_sample', + ) { + data = elements[0]; + valid = elements[1]; + } + + // Private constructor for clone to share element structure. + FilterSample._clone(super.elements, {required super.name}) { + data = elements[0]; + valid = elements[1]; + } + + @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, + ); +} diff --git a/example/filter_bank/mac_unit.dart b/example/filter_bank/mac_unit.dart new file mode 100644 index 000000000..0e63c6f59 --- /dev/null +++ b/example/filter_bank/mac_unit.dart @@ -0,0 +1,88 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// mac_unit.dart +// Multiply-accumulate module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// 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); + final stall = (~enable).named('stall', naming: Naming.mergeable); + + // A 2-stage pipeline: multiply, then accumulate + final pipe = Pipeline( + clk, + reset: reset, + stalls: [stall, stall], + 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); + } +} diff --git a/example/filter_bank/shared_data_bus.dart b/example/filter_bank/shared_data_bus.dart new file mode 100644 index 000000000..1d86462d5 --- /dev/null +++ b/example/filter_bank/shared_data_bus.dart @@ -0,0 +1,88 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// shared_data_bus.dart +// Bidirectional data bus module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// 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); + } +} diff --git a/lib/src/module.dart b/lib/src/module.dart index a1cb8ec5c..4a6a9e07f 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -118,7 +118,7 @@ abstract class Module { ..._inputs.values, ..._outputs.values, ..._inOuts.values, - ...internalSignals, + ...internalSignals ]); /// Accesses the [Logic] associated with this [Module]s [input] port @@ -1162,10 +1162,3 @@ abstract class Module { ).getSynthFileContents().join('\n\n////////////////////\n\n'); } } - -extension on LogicStructure { - /// Indicates that a [LogicStructure] has a [Const] element within it or - /// within one of its [elements]. - bool get hasConsts => - elements.any((e) => e is Const || (e is LogicStructure && e.hasConsts)); -} diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index cd9aa8750..4930d6c96 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // flop.dart @@ -92,6 +92,10 @@ class FlipFlop extends Module with SystemVerilog { /// reset. If no `reset` is provided, this will have no effect. final bool asyncReset; + /// The constant reset value, or `null` when reset is absent or data-driven. + LogicValue? get constantResetValue => + _reset == null || _resetValuePort != null ? null : _resetValueConst; + /// Constructs a flip flop which is positive edge triggered on [clk]. /// /// When optional [en] is provided, an additional input will be created for diff --git a/lib/src/signals/const.dart b/lib/src/signals/const.dart index 3e6989145..72c2544b2 100644 --- a/lib/src/signals/const.dart +++ b/lib/src/signals/const.dart @@ -75,28 +75,20 @@ class Const extends Logic { /// outputs and its normalized name. Supported values are 2, 8, 10, and 16. /// If omitted, generated outputs select a radix automatically and the name /// uses decimal. Values containing `x` or `z` may fall back to binary. - Const( - dynamic val, { - int? width, - bool fill = false, - int? preferredRadix, - }) : this._( - LogicValue.of( - val, - width: width ?? (val is LogicValue ? val.width : 1), - fill: fill, - ), - preferredRadix: _validatePreferredRadix(preferredRadix), - ); + Const(dynamic val, {int? width, bool fill = false, int? preferredRadix}) + : this._( + LogicValue.of(val, + width: width ?? (val is LogicValue ? val.width : 1), + fill: fill), + preferredRadix: _validatePreferredRadix(preferredRadix)); /// Constructs a [Const] from an already normalized [value]. Const._(LogicValue value, {required this.preferredRadix}) : super( - name: _constName(value, preferredRadix), - width: value.width, - // we don't care about maintaining this node unless necessary - naming: Naming.unnamed, - ) { + name: _constName(value, preferredRadix), + width: value.width, + // we don't care about maintaining this node unless necessary + naming: Naming.unnamed) { _wire ..put(value, signalName: name) ..makeImmutable(this, reason: _unassignableMessage); diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index ef463b9bb..0ee7b17dd 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // logic_structure.dart @@ -640,6 +640,10 @@ class LogicStructure implements Logic { elements.any((e) => e.isNet || (e is LogicStructure && e.hasNets)) || isNet; + /// Indicates whether this structure contains a [Const] at any depth. + bool get hasConsts => _hasConsts; + late final bool _hasConsts = leafElements.any((element) => element is Const); + @override Iterable get srcConnections => { for (final element in elements) ...element.srcConnections diff --git a/lib/src/synthesizers/netlist/netlist.dart b/lib/src/synthesizers/netlist/netlist.dart new file mode 100644 index 000000000..0e86e506f --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist.dart @@ -0,0 +1,11 @@ +// 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 'netlist_synthesizer.dart'; +export 'netlist_synthesizer_configuration.dart'; diff --git a/lib/src/synthesizers/netlist/netlist_cell.dart b/lib/src/synthesizers/netlist/netlist_cell.dart new file mode 100644 index 000000000..2f920288c --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_cell.dart @@ -0,0 +1,53 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_cell.dart +// Typed representation of a serialized netlist cell. +// +// 2026 August 24 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; + +/// A cell in a synthesized netlist. +@internal +class NetlistCell { + /// Whether consumers should hide this cell's name. + final int hideName; + + /// The Yosys cell type or module definition name. + final String type; + + /// Parameters configuring the cell. + final Map parameters; + + /// Attributes attached to the cell. + final Map attributes; + + /// Directions of the cell's ports. + final Map portDirections; + + /// Bits connected to each cell port. + final Map> connections; + + /// Creates a netlist cell. + const NetlistCell({ + required this.type, + required this.portDirections, + required this.connections, + this.parameters = const {}, + this.attributes = const {}, + this.hideName = 0, + }); + + /// Serializes this cell to the Yosys-compatible JSON structure. + Map toJson() => { + 'hide_name': hideName, + 'type': type, + 'parameters': parameters, + 'attributes': attributes, + 'port_directions': serializePortDirections(portDirections), + 'connections': connections, + }; +} diff --git a/lib/src/synthesizers/netlist/netlist_cell_mapper.dart b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart new file mode 100644 index 000000000..eb3a16742 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart @@ -0,0 +1,634 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_cell_mapper.dart +// Maps selected ROHD modules to Yosys-primitive cell representations. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; + +/// The result of mapping a netlist cell module to a Yosys-style cell. +@internal +typedef NetlistCellMapping = ({ + String cellType, + Map portDirs, + Map> connections, + Map parameters, +}); + +/// Context provided to each netlist-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. +@internal +class NetlistCellContext { + /// The ROHD [Module] being mapped. + final Module module; + + /// Raw ROHD port-direction map. + final Map rawPortDirs; + + /// Raw ROHD connection map (`{'portName': [wireId, ...]}`). + final Map> rawConns; + + /// Creates a [NetlistCellContext]. + NetlistCellContext( + this.module, + Map rawPortDirs, + Map> rawConns, + ) : rawPortDirs = + Map.unmodifiable(rawPortDirs), + rawConns = Map>.unmodifiable({ + for (final entry in rawConns.entries) + entry.key: List.unmodifiable(entry.value), + }); + + // ── 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] ?? NetlistPortDirection.output; + cn[netlistPortName] = rawConns[rohdName] ?? []; + } + return (portDirs: pd, connections: cn); + } +} + +/// Signature for a netlist-cell mapping handler. +/// +/// Returns a [NetlistCellMapping] if the handler recognises the module, +/// or `null` to let the next handler try. +@internal +typedef NetlistCellHandler = NetlistCellMapping? Function( + NetlistCellContext ctx); + +/// Maps modules already selected as netlist leaves to Yosys-primitive cell +/// representations. +/// +/// Handlers are registered via [register] and tried in registration order. +/// Hierarchy stopping is controlled separately by [SynthModuleStopPolicy]. +@internal +class NetlistCellMapper { + /// Ordered list of registered handlers. + final _handlers = []; + + /// Creates an empty [NetlistCellMapper] with no registered handlers. + NetlistCellMapper(); + + /// Creates a mapper with all built-in ROHD netlist cell types registered. + factory NetlistCellMapper.withDefaults() => + NetlistCellMapper().._registerDefaults(); + + /// 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(NetlistCellHandler handler) { + _handlers.add(handler); + } + + /// Try to map [module] to a Yosys-primitive cell. + /// + /// Returns `null` if no registered handler matches. + NetlistCellMapping? map( + Module module, + Map rawPortDirs, + Map> rawConns, + ) { + final ctx = NetlistCellContext(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 NetlistCellMapping? unaryAY(NetlistCellContext 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 NetlistCellMapping? binaryABY( + NetlistCellContext 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), + }, + ); + } + + /// Maps a shift gate to a Yosys binary shift cell. + static NetlistCellMapping? shiftABY( + NetlistCellContext ctx, + String cellType, { + required bool aSigned, + }) { + final a = ctx.findInput('_in'); + final b = ctx.findInput('_shiftAmount'); + final y = ctx.firstOutput; + if (a == null || b == null || y == null) { + return null; + } + final r = ctx.remap({a: 'A', b: 'B', y: 'Y'}); + return ( + cellType: cellType, + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_SIGNED': aSigned ? 1 : 0, + 'A_WIDTH': ctx.width(a), + 'B_SIGNED': 0, + 'B_WIDTH': ctx.width(b), + 'Y_WIDTH': ctx.width(y), + }, + ); + } + + /// Map a two-input gate with ports A, B, Y (e.g. `$pow`, `$div`, `$mod`), + /// including the standard Yosys `A_SIGNED`/`B_SIGNED` parameters. + /// + /// Unlike [binaryABY], this always emits the full standard parameter set + /// (`A_SIGNED`, `A_WIDTH`, `B_SIGNED`, `B_WIDTH`, `Y_WIDTH`) so the result + /// is directly consumable by standard Yosys tooling that expects these + /// arithmetic cells to be fully specified. + static NetlistCellMapping? binaryABYSigned( + NetlistCellContext ctx, + String cellType, { + required String inAPrefix, + required String inBPrefix, + bool aSigned = false, + bool bSigned = false, + }) { + 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_SIGNED': aSigned ? 1 : 0, + 'A_WIDTH': ctx.width(a), + 'B_SIGNED': bSigned ? 1 : 0, + 'B_WIDTH': ctx.width(b), + 'Y_WIDTH': ctx.width(out), + }, + ); + } + + // ══════════════════════════════════════════════════════════════════════ + // Built-in handler registration + // ══════════════════════════════════════════════════════════════════════ + + /// Registers the built-in ROHD-to-Yosys primitive cell mappings. + void _registerDefaults() { + // Helper to reduce boilerplate for type-map-based handlers. + void registerByTypeMap( + Map typeMap, + NetlistCellMapping? Function(NetlistCellContext ctx, String cellType) + handler, + ) { + register((ctx) { + final cellType = typeMap[ctx.module.runtimeType]; + return cellType == null ? null : handler(ctx, cellType); + }); + } + + this + // ── 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] = NetlistPortDirection.input; + cn[label] = ctx.rawConns[ik] ?? []; + params['IN${i}_WIDTH'] = w; + bitOffset += w; + } + if (outName != null) { + pd['Y'] = NetlistPortDirection.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 sumBits = ctx.rawConns[sumName] ?? []; + final carryBits = carryName.isEmpty + ? const [] + : ctx.rawConns[carryName] ?? []; + final pd = { + 'A': NetlistPortDirection.input, + 'B': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, + }; + final cn = >{ + 'A': ctx.rawConns[in0] ?? [], + 'B': ctx.rawConns[in1] ?? [], + 'Y': [...sumBits, ...carryBits], + }; + return ( + cellType: r'$add', + portDirs: pd, + connections: cn, + parameters: { + 'A_WIDTH': ctx.width(in0), + 'B_WIDTH': ctx.width(in1), + 'Y_WIDTH': sumBits.length + carryBits.length, + }, + ); + }) + // ── FlipFlop → Yosys register cells ─────────────────────────────── + ..register((ctx) { + final flipFlop = ctx.module; + if (flipFlop 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 hasEnable = en != null && ctx.rawConns.containsKey(en); + final hasReset = rst != null && ctx.rawConns.containsKey(rst); + final rstVal = + ctx.findInput('_resetValue') ?? ctx.findInput('resetValue'); + final hasDynamicResetValue = + hasReset && rstVal != null && ctx.rawConns.containsKey(rstVal); + + String cellType; + if (!hasReset) { + cellType = hasEnable ? r'$dffe' : r'$dff'; + } else if (flipFlop.asyncReset) { + cellType = hasDynamicResetValue + ? (hasEnable ? r'$aldffe' : r'$aldff') + : (hasEnable ? r'$adffe' : r'$adff'); + } else if (!hasDynamicResetValue) { + cellType = hasEnable ? r'$sdffe' : r'$sdff'; + } else { + // Dynamic synchronous reset values are lowered to standard mux cells + // by NetlistModuleTranslation. + return null; + } + + final pd = { + 'CLK': NetlistPortDirection.input, + 'D': NetlistPortDirection.input, + 'Q': NetlistPortDirection.output, + }; + final cn = >{ + 'CLK': ctx.rawConns[clk] ?? [], + 'D': ctx.rawConns[d] ?? [], + 'Q': ctx.rawConns[q] ?? [], + }; + if (hasEnable) { + pd['EN'] = NetlistPortDirection.input; + cn['EN'] = ctx.rawConns[en] ?? []; + } + if (hasReset) { + final resetPort = flipFlop.asyncReset + ? (hasDynamicResetValue ? 'ALOAD' : 'ARST') + : 'SRST'; + pd[resetPort] = NetlistPortDirection.input; + cn[resetPort] = ctx.rawConns[rst] ?? []; + } + if (hasDynamicResetValue) { + pd['AD'] = NetlistPortDirection.input; + cn['AD'] = ctx.rawConns[rstVal] ?? []; + } + + final parameters = { + 'WIDTH': ctx.width(d), + 'CLK_POLARITY': 1, + if (hasEnable) 'EN_POLARITY': 1, + if (hasReset && flipFlop.asyncReset) + (hasDynamicResetValue ? 'ALOAD_POLARITY' : 'ARST_POLARITY'): 1, + if (hasReset && !flipFlop.asyncReset) 'SRST_POLARITY': 1, + if (hasReset && !hasDynamicResetValue) + if (flipFlop.asyncReset) + 'ARST_VALUE': + flipFlop.constantResetValue!.toString(includeWidth: false) + else + 'SRST_VALUE': + flipFlop.constantResetValue!.toString(includeWidth: false), + }; + return ( + cellType: cellType, + portDirs: pd, + connections: cn, + parameters: parameters, + ); + }); + + // ── Type-map-based gates ─────────────────────────────────────────── + final gateRegistrations = <( + Map, + NetlistCellMapping? Function(NetlistCellContext, 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'}, + (ctx, type) => shiftABY(ctx, type, aSigned: false), + ), + ( + const {ARShift: r'$sshr'}, + (ctx, type) => shiftABY(ctx, type, aSigned: true), + ), + ( + const { + Power: r'$pow', + Divide: r'$div', + Modulo: r'$mod', + }, + (ctx, type) => + binaryABYSigned(ctx, type, inAPrefix: '_in0', inBPrefix: '_in1'), + ), + ]; + for (final (typeMap, handler) in gateRegistrations) { + registerByTypeMap(typeMap, handler); + } + + // ── IndexGate → $shiftx ───────────────────────────────────────────── + // + // `$shiftx` extracts `Y_WIDTH` bits of `A` starting at bit offset `B`, + // producing `x` when the offset is out of range. This matches + // [IndexGate]'s bit-select semantics (`original[index]`, `Y_WIDTH == 1`) + // exactly, including its out-of-range-selects-`x` behavior. + register((ctx) { + if (ctx.module is! IndexGate) { + return null; + } + final inputNames = ctx.module.inputs.keys.toList(); + if (inputNames.length != 2) { + return null; + } + final a = inputNames[0]; + final b = inputNames[1]; + final y = ctx.firstOutput; + if (y == null) { + return null; + } + final r = ctx.remap({a: 'A', b: 'B', y: 'Y'}); + return ( + cellType: r'$shiftx', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_SIGNED': 0, + 'A_WIDTH': ctx.width(a), + 'B_SIGNED': 0, + 'B_WIDTH': ctx.width(b), + 'Y_WIDTH': ctx.width(y), + }, + ); + }); + + // ── TriStateBuffer → $tribuf ────────────────────────────────────── + 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'}); + r.portDirs['Y'] = NetlistPortDirection.output; + return ( + cellType: r'$tribuf', + portDirs: r.portDirs, + connections: r.connections, + parameters: {'WIDTH': ctx.width(inName)}, + ); + }); + } +} diff --git a/lib/src/synthesizers/netlist/netlist_module_translation.dart b/lib/src/synthesizers/netlist/netlist_module_translation.dart new file mode 100644 index 000000000..eead0033a --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_module_translation.dart @@ -0,0 +1,925 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_module_translation.dart +// Per-module state and ordered phases for netlist synthesis. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_utils.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; + +/// Mutable state for translating one module level into a netlist. +@internal +class NetlistModuleTranslation { + /// The module being translated. + final Module _module; + + /// The synthesis definition for this module level, when one can be built. + final NetlistSynthModuleDefinition? synthDef; + + final NetlistCellMapper _netlistCellMapper; + final bool Function(Module module) _generatesDefinition; + final String Function(Module module) _getInstanceTypeOfModule; + + /// The next available integer wire identifier. + /// + /// Starts at 2 so consumers never confuse wire IDs 0 or 1 with the + /// Yosys-JSON constant bit strings `"0"` and `"1"`. + int _nextId = 2; + + final Map> _synthLogicIds = {}; + + /// Emitted module ports. + final Map> ports = {}; + + /// Emitted cells. + final Map> cells = {}; + + /// Emitted netnames. + final Map netnames = {}; + + final Set _blockedConstSynthLogics = {}; + + late final ({ + Set arrayConcatOutputs, + Set directSubmoduleOutputs, + }) _submoduleOutputDrivers = _indexSubmoduleOutputDrivers(); + + late final NetlistAlwaysBlockPortCollapseIndex? + _alwaysBlockPortCollapseIndex = + synthDef == null ? null : NetlistAlwaysBlockPortCollapseIndex(synthDef!); + + /// Creates translation state for one [module]. + NetlistModuleTranslation( + Module module, { + required NetlistCellMapper netlistCellMapper, + required bool Function(Module module) generatesDefinition, + required String Function(Module module) getInstanceTypeOfModule, + }) : _module = module, + _netlistCellMapper = netlistCellMapper, + _generatesDefinition = generatesDefinition, + _getInstanceTypeOfModule = getInstanceTypeOfModule, + synthDef = module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none + ? null + : NetlistSynthModuleDefinition(module); + + /// Allocates the next wire identifier. + int allocateWireId() => _nextId++; + + /// Allocates or returns the wire identifiers for [synthLogic]. + List getIds(SynthLogic synthLogic) { + final resolved = synthLogic.isConstant ? synthLogic : synthLogic.resolved; + return _synthLogicIds.putIfAbsent( + resolved, + () => List.generate(resolved.width, (_) => allocateWireId()), + ); + } + + /// Emits input, output, and inout ports in canonical allocation order. + void processPorts() { + final portGroups = [ + (NetlistPortDirection.input, synthDef?.inputs, _module.inputs), + (NetlistPortDirection.output, synthDef?.outputs, _module.outputs), + (NetlistPortDirection.inout, synthDef?.inOuts, _module.inOuts), + ]; + for (final (direction, synthLogics, modulePorts) in portGroups) { + if (synthLogics != null) { + final portNames = + NetlistUtils.portNamesForSynthLogics(synthLogics, modulePorts); + for (final synthLogic in synthLogics) { + final portName = portNames[synthLogic]; + if (portName != null) { + final portLogic = modulePorts[portName]; + final emitOutputArrayConcat = + direction == NetlistPortDirection.output && + portLogic is LogicArray && + !_hasExistingOutputArrayConcat(synthLogic) && + !_hasDirectSubmoduleOutputDriver(synthLogic); + final originalIds = getIds(synthLogic); + final ids = emitOutputArrayConcat + ? List.generate(synthLogic.width, (_) => allocateWireId()) + : originalIds; + ports[portName] = { + 'direction': direction.name, + 'bits': ids, + if (portLogic != null) + 'logic_type': NetlistUtils.buildLogicType(portLogic, ids), + }; + if (emitOutputArrayConcat) { + _emitOutputArrayConcat(portName, portLogic, ids); + } + } + } + } else { + for (final entry in modulePorts.entries) { + final ids = List.generate( + entry.value.width, + (_) => allocateWireId(), + ); + ports[entry.key] = { + 'direction': direction.name, + 'bits': ids, + 'logic_type': NetlistUtils.buildLogicType(entry.value, ids), + }; + } + } + } + } + + /// Emits a concat cell that assembles a LogicArray output port. + void _emitOutputArrayConcat( + String portName, + LogicArray array, + List outputIds, + ) { + _emitOutputArrayConcatForArray(portName, array, outputIds); + } + + /// Recursively emits concat cells for nested LogicArray output elements. + bool _emitOutputArrayConcatForArray( + String concatName, + LogicArray array, + List outputIds, + ) { + final definition = synthDef; + if (definition == null) { + return false; + } + + final concatConnections = >{}; + final concatDirections = {}; + var lowerIndex = 0; + + for (final (index, element) in array.elements.indexed) { + final synthLogic = definition.logicToSynthMap[element]; + if (synthLogic == null) { + return false; + } + var elementIds = getIds(synthLogic); + if (element is LogicArray && + !_hasExistingOutputArrayConcat(synthLogic) && + !_hasDirectSubmoduleOutputDriver(synthLogic)) { + final aggregateIds = List.generate( + synthLogic.width, + (_) => allocateWireId(), + ); + if (!_emitOutputArrayConcatForArray( + '${concatName}_$index', + element, + aggregateIds, + )) { + return false; + } + elementIds = aggregateIds; + } + final upperIndex = lowerIndex + elementIds.length - 1; + concatConnections['[$upperIndex:$lowerIndex]'] = + elementIds.cast(); + concatDirections['[$upperIndex:$lowerIndex]'] = + NetlistPortDirection.input; + lowerIndex = upperIndex + 1; + } + + if (lowerIndex != outputIds.length) { + return false; + } + + concatConnections['Y'] = outputIds.cast(); + concatDirections['Y'] = NetlistPortDirection.output; + + final cellName = NetlistUtils.synthesizedCellName( + operationName: 'array_concat_output', + destination: array, + ); + cells[cellName] = NetlistCell( + type: r'$concat', + parameters: { + for (var index = 0; index < array.elements.length; index++) + 'IN${index}_WIDTH': array.elements[index].width, + }, + portDirections: concatDirections, + connections: concatConnections, + ).toJson(); + + return true; + } + + /// Checks whether [synthLogic] is already driven by an output concat cell. + bool _hasExistingOutputArrayConcat(SynthLogic synthLogic) => + _submoduleOutputDrivers.arrayConcatOutputs.contains(synthLogic.resolved); + + /// Checks whether [synthLogic] is driven directly by a non-concat submodule. + bool _hasDirectSubmoduleOutputDriver(SynthLogic synthLogic) => + _submoduleOutputDrivers.directSubmoduleOutputs + .contains(synthLogic.resolved); + + ({ + Set arrayConcatOutputs, + Set directSubmoduleOutputs, + }) _indexSubmoduleOutputDrivers() { + final definition = synthDef; + if (definition == null) { + return ( + arrayConcatOutputs: {}, + directSubmoduleOutputs: {}, + ); + } + + final arrayConcatOutputs = {}; + final directSubmoduleOutputs = {}; + for (final instance in definition.subModuleInstantiations) { + (instance.module is SynthArrayConcat + ? arrayConcatOutputs + : directSubmoduleOutputs) + .addAll( + instance.outputMapping.values.map((output) => output.resolved), + ); + } + + return ( + arrayConcatOutputs: arrayConcatOutputs, + directSubmoduleOutputs: directSubmoduleOutputs, + ); + } + + /// Preallocates internal wires in [Module.internalSignals] order. + void processInternalWires() { + final definition = synthDef; + if (definition == null) { + return; + } + _module.internalSignals + .map((signal) => definition.logicToSynthMap[signal]) + .whereType() + .where((synthLogic) => !synthLogic.isConstant) + .forEach(getIds); + } + + /// Emits cells and removes instances cleared by procedural-port collapsing. + void processCells() { + final definition = synthDef; + if (definition == null) { + return; + } + + final emittedCellKeys = {}; + for (final instance in definition.subModuleInstantiations) { + if (!instance.needsInstantiation) { + continue; + } + + final submodule = instance.module; + final cellKey = instance.name; + final isLeaf = !_generatesDefinition(submodule); + final defaultCellType = isLeaf + ? submodule.definitionName + : _getInstanceTypeOfModule(submodule); + final rawPortDirs = {}; + final rawConnections = >{}; + + for (final (direction, mapping) in [ + (NetlistPortDirection.input, instance.inputMapping), + (NetlistPortDirection.output, instance.outputMapping), + (NetlistPortDirection.inout, instance.inOutMapping), + ]) { + for (final entry in mapping.entries) { + rawPortDirs[entry.key] = direction; + rawConnections[entry.key] = getIds(entry.value).cast(); + } + } + + final mapped = isLeaf + ? _netlistCellMapper.map(submodule, rawPortDirs, rawConnections) + : null; + if (mapped == null && + submodule is FlipFlop && + _emitDynamicSynchronousResetFlipFlop( + cellKey, + submodule, + rawPortDirs, + rawConnections, + )) { + emittedCellKeys[instance] = cellKey; + continue; + } + final cellPortDirs = mapped?.portDirs ?? rawPortDirs; + final cellConnections = mapped?.connections ?? rawConnections; + emittedCellKeys[instance] = cellKey; + + if (submodule is Combinational || submodule is Sequential) { + NetlistUtils.collapseAlwaysBlockPorts( + _alwaysBlockPortCollapseIndex!, + instance, + cellPortDirs, + cellConnections, + getIds, + ); + _filterProceduralConstants(instance, cellPortDirs, cellConnections); + _renameProceduralPorts(instance, cellPortDirs, cellConnections); + } + + if (!isLeaf) { + for (final portEntry in submodule.inputs.entries) { + final portName = portEntry.key; + final port = portEntry.value; + if (port is! LogicArray || + cellPortDirs[portName] != NetlistPortDirection.input) { + continue; + } + final bits = cellConnections[portName]; + if (bits == null || bits.length != port.width) { + continue; + } + + final concatConnections = >{}; + final concatDirections = {}; + var lowerIndex = 0; + for (final element in port.elements) { + final upperIndex = lowerIndex + element.width - 1; + final concatPort = '[$upperIndex:$lowerIndex]'; + concatConnections[concatPort] = bits.sublist( + lowerIndex, + upperIndex + 1, + ); + concatDirections[concatPort] = NetlistPortDirection.input; + lowerIndex = upperIndex + 1; + } + + final concatOutput = [ + for (var i = 0; i < bits.length; i++) allocateWireId(), + ]; + concatConnections['Y'] = concatOutput; + concatDirections['Y'] = NetlistPortDirection.output; + cellConnections[portName] = concatOutput; + + cells['array_concat_${cellKey}_$portName'] = NetlistCell( + type: r'$concat', + parameters: { + for (var index = 0; index < port.elements.length; index++) + 'IN${index}_WIDTH': port.elements[index].width, + }, + portDirections: concatDirections, + connections: concatConnections, + ).toJson(); + } + } + + cells[cellKey] = NetlistCell( + type: mapped?.cellType ?? defaultCellType, + parameters: mapped?.parameters ?? const {}, + portDirections: cellPortDirs, + connections: cellConnections, + ).toJson(); + } + + definition.subModuleInstantiations + .where((instance) => !instance.needsInstantiation) + .map((instance) => emittedCellKeys[instance]) + .whereType() + .forEach(cells.remove); + } + + /// Lowers a flip-flop with a dynamic synchronous reset value to Yosys cells. + /// + /// `$sdff` requires a constant reset value. The reset mux precedes the + /// enable, and the enable is ORed with reset so reset retains priority. + bool _emitDynamicSynchronousResetFlipFlop( + String cellKey, + FlipFlop flipFlop, + Map rawPortDirs, + Map> rawConnections, + ) { + if (flipFlop.asyncReset) { + return false; + } + + String? findInput(String unpreferredName, String name) { + for (final entry in rawPortDirs.entries) { + if (entry.value == NetlistPortDirection.input && + (entry.key.startsWith(unpreferredName) || entry.key == name)) { + return entry.key; + } + } + return null; + } + + final clk = findInput('_clk', 'clk'); + final d = findInput('_d', 'd'); + final en = findInput('_en', 'en'); + final reset = findInput('_reset', 'reset'); + final resetValue = findInput('_resetValue', 'resetValue'); + final q = rawPortDirs.entries + .where((entry) => entry.value == NetlistPortDirection.output) + .map((entry) => entry.key) + .firstOrNull; + if (clk == null || + d == null || + reset == null || + resetValue == null || + q == null) { + return false; + } + + final dBits = rawConnections[d] ?? const []; + final resetValueBits = rawConnections[resetValue] ?? const []; + final resetBits = rawConnections[reset] ?? const []; + final clkBits = rawConnections[clk] ?? const []; + final qBits = rawConnections[q] ?? const []; + if (dBits.isEmpty || + dBits.length != resetValueBits.length || + resetBits.length != 1 || + clkBits.length != 1 || + qBits.length != dBits.length) { + return false; + } + + final resetMuxOutput = + List.generate(dBits.length, (_) => allocateWireId()); + cells['${cellKey}_reset_mux'] = NetlistCell( + type: r'$mux', + parameters: {'WIDTH': dBits.length}, + portDirections: { + 'A': NetlistPortDirection.input, + 'B': NetlistPortDirection.input, + 'S': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, + }, + connections: { + 'A': dBits, + 'B': resetValueBits, + 'S': resetBits, + 'Y': resetMuxOutput, + }, + ).toJson(); + + final hasEnable = en != null && rawConnections.containsKey(en); + final dffConnections = >{ + 'CLK': clkBits, + 'D': resetMuxOutput, + 'Q': qBits, + }; + final dffDirections = { + 'CLK': NetlistPortDirection.input, + 'D': NetlistPortDirection.input, + 'Q': NetlistPortDirection.output, + }; + final dffParameters = { + 'WIDTH': dBits.length, + 'CLK_POLARITY': 1, + }; + if (hasEnable) { + final enableBits = rawConnections[en] ?? const []; + if (enableBits.length != 1) { + return false; + } + final effectiveEnable = [allocateWireId()]; + cells['${cellKey}_reset_enable'] = NetlistCell( + type: r'$or', + parameters: { + 'A_WIDTH': 1, + 'B_WIDTH': 1, + 'Y_WIDTH': 1, + }, + portDirections: { + 'A': NetlistPortDirection.input, + 'B': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, + }, + connections: { + 'A': enableBits, + 'B': resetBits, + 'Y': effectiveEnable, + }, + ).toJson(); + dffConnections['EN'] = effectiveEnable; + dffDirections['EN'] = NetlistPortDirection.input; + dffParameters['EN_POLARITY'] = 1; + } + + cells[cellKey] = NetlistCell( + type: hasEnable ? r'$dffe' : r'$dff', + parameters: dffParameters, + portDirections: dffDirections, + connections: dffConnections, + ).toJson(); + return true; + } + + /// Emits port and internal netnames, fills unnamed connection coverage, + /// and optionally removes names for undriven wires. + void processNetnames({ + required List Function(List bits) applyAlias, + required Map arraySliceOldToNew, + required Map arrayConcatOldToNew, + required bool pruneUndriven, + required Set drivenBits, + }) { + final emittedNames = {}; + final isInlineSystemVerilog = _module is InlineSystemVerilog; + + void addNetname( + String name, + List bits, { + bool hideName = false, + bool computed = false, + Map? logicType, + }) { + if (!emittedNames.add(name)) { + return; + } + netnames[name] = { + 'bits': bits, + if (hideName) 'hide_name': 1, + if (logicType != null) 'logic_type': logicType, + 'attributes': { + if (computed || isInlineSystemVerilog) 'computed': 1, + }, + }; + } + + for (final port in ports.entries) { + addNetname( + Sanitizer.sanitizeSV(port.key), + (port.value['bits']! as List).cast(), + logicType: port.value['logic_type'] as Map?, + ); + } + + final aggregateConstructors = + inputBits, List outputBits})>>{}; + for (final cellEntry in cells.entries) { + final cell = cellEntry.value; + final cellType = cell['type'] as String?; + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + final inputBits = []; + final outputBits = []; + + if (cellType == r'$concat') { + for (final portEntry in conns.entries) { + final bits = (portEntry.value as List).cast(); + if (dirs[portEntry.key] == 'output') { + outputBits.addAll(bits); + } else { + inputBits.addAll(bits); + } + } + } else if (cellType == r'$struct_pack') { + for (final portEntry in conns.entries) { + final bits = (portEntry.value as List).cast(); + if (portEntry.key == 'Y' && dirs[portEntry.key] == 'output') { + outputBits.addAll(bits); + } else if (dirs[portEntry.key] == 'input') { + inputBits.addAll(bits); + } + } + } else { + continue; + } + + if (inputBits.length == outputBits.length) { + aggregateConstructors + .putIfAbsent(Object.hashAll(inputBits), () => []) + .add(( + inputBits: inputBits, + outputBits: outputBits, + )); + } + } + + List resolveAggregateBits(List bits) { + final candidates = aggregateConstructors[Object.hashAll(bits)]; + if (candidates == null) { + return bits; + } + for (final constructorBits in candidates) { + if (bits.length == constructorBits.inputBits.length) { + var matches = true; + for (var index = 0; index < bits.length; index++) { + if (bits[index] != constructorBits.inputBits[index]) { + matches = false; + break; + } + } + if (matches) { + return constructorBits.outputBits; + } + } + } + return bits; + } + + if (synthDef != null) { + for (final entry in _synthLogicIds.entries.where( + (entry) => !entry.key.isConstant && !entry.key.declarationCleared, + )) { + final synthLogic = entry.key; + final name = NetlistUtils.tryGetSynthLogicName(synthLogic); + if (name == null) { + continue; + } + var bits = applyAlias(entry.value.cast()); + if (arraySliceOldToNew.isNotEmpty && + synthLogic is SynthLogicArrayElement) { + bits = [ + for (final bit in bits) + if (bit is int) arraySliceOldToNew[bit] ?? bit else bit, + ]; + } + if (arrayConcatOldToNew.isNotEmpty && + synthLogic is SynthLogicArrayElement) { + bits = [ + for (final bit in bits) + if (bit is int) arrayConcatOldToNew[bit] ?? bit else bit, + ]; + } + bits = resolveAggregateBits(bits); + final typeLogic = NetlistUtils.typeLogicFromSynthLogic(synthLogic); + addNetname( + Sanitizer.sanitizeSV(name), + bits, + logicType: typeLogic == null + ? null + : NetlistUtils.buildLogicType(typeLogic, bits), + ); + } + } + + for (final cell in cells.entries.where( + (entry) => entry.value['type'] == r'$const', + )) { + final connections = + cell.value['connections'] as Map>?; + if (connections != null && connections.isNotEmpty) { + addNetname(cell.key, connections.values.first, computed: true); + } + } + + final coveredIds = netnames.values + .expand( + (netname) => + ((netname! as Map)['bits'] as List?) ?? [], + ) + .whereType() + .toSet(); + for (final cell in cells.entries) { + final connections = + cell.value['connections'] as Map? ?? {}; + for (final connection in connections.entries) { + final missingBits = []; + for (final bit in connection.value as List) { + if (bit is int && coveredIds.add(bit)) { + missingBits.add(bit); + } + } + if (missingBits.isNotEmpty) { + addNetname( + Sanitizer.sanitizeSV('${cell.key}_${connection.key}'), + missingBits, + hideName: true, + ); + } + } + } + + if (pruneUndriven) { + netnames.removeWhere((_, rawNetname) { + final netname = rawNetname as Map?; + final bits = netname?['bits'] as List?; + if (bits == null) { + return false; + } + final integerBits = bits.whereType(); + return integerBits.isNotEmpty && !integerBits.any(drivenBits.contains); + }); + } + } + + /// Separates passthrough outputs and removes dead cells when requested. + void processCellCleanup({required bool enableDce}) { + final inputBitIds = ports.values + .where( + (port) => + port['direction'] == 'input' || port['direction'] == 'inout', + ) + .expand((port) => port['bits']! as List) + .whereType() + .toSet(); + var bufferIndex = 0; + for (final port in ports.entries.where( + (entry) => entry.value['direction'] == 'output', + )) { + final outputBits = (port.value['bits']! as List).cast(); + if (!outputBits.any((bit) => bit is int && inputBitIds.contains(bit))) { + continue; + } + final freshBits = List.generate( + outputBits.length, + (_) => allocateWireId(), + ); + cells['passthrough_buf_$bufferIndex'] = NetlistUtils.makeBufCell( + outputBits.length, + outputBits, + freshBits, + ); + port.value['bits'] = freshBits; + bufferIndex++; + } + + if (!enableDce) { + return; + } + var changed = true; + while (changed) { + changed = false; + final drivenIds = NetlistValidation.connectedBits( + ports, + cells, + portDirections: const {'input', 'inout'}, + cellDirection: 'output', + ); + final consumedIds = NetlistValidation.connectedBits( + ports, + cells, + portDirections: const {'output', 'inout'}, + cellDirection: 'input', + ); + + cells + ..removeWhere((_, rawCell) { + final cell = rawCell as Map; + final connections = cell['connections']! as Map; + final directions = cell['port_directions']! as Map; + final inputPorts = connections.entries.where( + (port) => directions[port.key] == 'input', + ); + if (inputPorts.isEmpty) { + return false; + } + final allUndriven = !inputPorts + .expand((port) => port.value as List) + .any( + (bit) => + (bit is int && drivenIds.contains(bit)) || bit is String, + ); + if (allUndriven) { + changed = true; + } + return allUndriven; + }) + ..removeWhere((_, rawCell) { + final cell = rawCell as Map; + final cellType = cell['type'] as String? ?? ''; + if (!cellType.startsWith(r'$')) { + return false; + } + final connections = cell['connections']! as Map; + final directions = cell['port_directions']! as Map; + final outputPorts = connections.entries.where( + (port) => directions[port.key] == 'output', + ); + if (outputPorts.isEmpty) { + return false; + } + final allUnconsumed = !outputPorts + .expand((port) => port.value as List) + .whereType() + .any(consumedIds.contains); + if (allUnconsumed) { + changed = true; + } + return allUnconsumed; + }); + } + } + + /// Emits constant driver cells and optionally removes floating constants. + void processConstants({ + required List Function(List bits) applyAlias, + required bool pruneFloating, + }) { + var constantIndex = 0; + final emittedConstantWires = {}; + for (final entry in _synthLogicIds.entries + .where((entry) => entry.key.isConstant) + .where((entry) => !_blockedConstSynthLogics.contains(entry.key)) + .where((entry) => entry.value.isNotEmpty)) { + final constant = NetlistUtils.constValueFromSynthLogic(entry.key); + if (constant == null) { + continue; + } + final resolvedIds = applyAlias(entry.value.cast()); + final firstWire = resolvedIds.firstWhere( + (bit) => bit is int, + orElse: () => -1, + ); + if (firstWire is int && firstWire >= 0) { + if (emittedConstantWires.contains(firstWire)) { + continue; + } + emittedConstantWires.addAll(resolvedIds.whereType()); + } + + final valuePart = NetlistUtils.constValuePart(constant); + final cellName = 'const_${constantIndex}_$valuePart'; + final valueLiteral = valuePart.replaceFirst('_', "'"); + cells[cellName] = NetlistCell( + type: r'$const', + portDirections: { + valueLiteral: NetlistPortDirection.output, + }, + connections: >{valueLiteral: resolvedIds}, + ).toJson(); + constantIndex++; + } + + if (!pruneFloating) { + return; + } + final consumedIds = NetlistValidation.connectedBits( + ports, + cells, + portDirections: const {'output', 'inout'}, + cellDirection: 'input', + ); + cells.removeWhere((_, rawCell) { + final cell = rawCell as Map; + if (cell['type'] != r'$const') { + return false; + } + final connections = cell['connections']! as Map; + final directions = cell['port_directions']! as Map; + return !connections.entries + .where((port) => directions[port.key] == 'output') + .expand((port) => port.value as List) + .whereType() + .any(consumedIds.contains); + }); + } + + /// Removes procedural constant ports and records their constants as blocked. + void _filterProceduralConstants( + SynthSubModuleInstantiation instance, + Map portDirections, + Map> connections, + ) { + final portsToRemove = []; + for (final port in connections.entries) { + final synthLogic = + instance.inputMapping[port.key] ?? instance.inOutMapping[port.key]; + if (synthLogic != null && NetlistUtils.isConstantSynthLogic(synthLogic)) { + portsToRemove.add(port.key); + _blockedConstSynthLogics.add(synthLogic.resolved); + } + } + for (final portName in portsToRemove) { + connections.remove(portName); + portDirections.remove(portName); + } + } + + /// Renames procedural ports to match their resolved synth logic names. + void _renameProceduralPorts( + SynthSubModuleInstantiation instance, + Map portDirections, + Map> connections, + ) { + final renames = {}; + for (final portName in connections.keys.toList()) { + final synthLogic = instance.inputMapping[portName] ?? + instance.outputMapping[portName] ?? + instance.inOutMapping[portName]; + if (synthLogic == null) { + continue; + } + final resolvedName = NetlistUtils.tryGetSynthLogicName( + synthLogic.resolved, + ); + if (resolvedName != null && resolvedName != portName) { + renames[portName] = resolvedName; + } + } + + for (final rename in renames.entries) { + final bits = connections.remove(rename.key)!; + final direction = portDirections.remove(rename.key)!; + var newName = rename.value; + if (connections.containsKey(newName)) { + newName = '${rename.value}_${rename.key}'; + } + connections[newName] = bits; + portDirections[newName] = direction; + } + } +} diff --git a/lib/src/synthesizers/netlist/netlist_passes.dart b/lib/src/synthesizers/netlist/netlist_passes.dart new file mode 100644 index 000000000..4f22c5d0b --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_passes.dart @@ -0,0 +1,734 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_passes.dart +// Post-processing optimization passes for netlist synthesis. +// +// 2025 February 11 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_utils.dart'; + +/// Post-processing optimization passes for netlist synthesis. +/// +/// All methods are static — no instances are created. +@internal +class NetlistPasses { + /// Prevents construction of this static utility class. + NetlistPasses._(); + + /// Collects a combined modules map from [SynthesisResult]s suitable for + /// JSON emission. + static Map> collectModuleEntries( + Iterable results, { + Module? topModule, + bool includeCellConnections = true, + }) { + final allModules = >{}; + for (final result in results) { + if (result is NetlistSynthesisResult) { + final typeName = result.instanceTypeName; + final attrs = _copyObjectMap(result.attributes); + if (topModule != null && result.module == topModule) { + attrs['top'] = 1; + } + allModules[typeName] = { + 'attributes': attrs, + 'ports': _copyNestedMaps(result.ports), + 'cells': _copyCells( + result.cells, + includeConnections: includeCellConnections, + ), + 'netnames': _copyObjectMap(result.netnames), + }; + } + } + return allModules; + } + + /// Deep-copies cell maps, optionally omitting connection payloads. + static Map> _copyCells( + Map> source, { + required bool includeConnections, + }) => + { + for (final entry in source.entries) + entry.key: _copyObjectMap( + includeConnections + ? entry.value + : (Map.of(entry.value)..remove('connections')), + ), + }; + + /// Deep-copies a map whose values are JSON-like object maps. + static Map> _copyNestedMaps( + Map> source, + ) => + { + for (final entry in source.entries) + entry.key: _copyObjectMap(entry.value), + }; + + /// Deep-copies a JSON-like object map. + static Map _copyObjectMap(Map source) => { + for (final entry in source.entries) + entry.key: _copyJsonValue(entry.value), + }; + + /// Deep-copies a JSON-like value while preserving scalar objects. + static Object? _copyJsonValue(Object? value) { + if (value is Map) { + return { + for (final entry in value.entries) + entry.key as String: _copyJsonValue(entry.value), + }; + } + if (value is List) { + return [for (final element in value) _copyJsonValue(element)]; + } + return value; + } + + // ════════════════════════════════════════════════════════════════════ + // Unified transparent-cell clustering + // ════════════════════════════════════════════════════════════════════ + + /// Transparent cell types that only reshuffle / rename bits and can be + /// cleaned up when their outputs are unconsumed. + static const _transparentCleanupTypes = { + r'$buf', + r'$slice', + r'$concat', + r'$struct_unpack', + r'$struct_pack', + }; + + /// Transparent cell types whose bit mappings can be safely clustered. + static const _clusterableTransparentTypes = { + r'$buf', + r'$slice', + }; + + /// Unified transparent-cell clustering pass. + /// + /// **Phase 1 — Cluster identification:** + /// Builds an undirected graph over transparent cells (two cells are + /// neighbours when one's output wire feeds the other's input) and + /// finds connected components via BFS. + /// + /// **Phase 2 — Cluster collapse:** + /// For every multi-cell component, traces each externally-consumed + /// output bit backward through the component's bit-level mapping + /// until reaching an external source bit, then replaces the entire + /// component with a single `$buf` wired from traced sources to + /// destinations. + static void applyTransparentClustering( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + final ports = moduleDef['ports'] as Map? ?? {}; + + // ── Gather transparent cells ── + + final tCells = { + for (final e in cells.entries) + if (_clusterableTransparentTypes.contains( + e.value['type'] as String?, + )) + e.key, + }; + if (tCells.isEmpty) { + continue; + } + + // ── Wire maps ── + + final wireConsumers = >{}; + + for (final e in cells.entries) { + final dirs = e.value['port_directions'] as Map? ?? {}; + final conns = e.value['connections'] as Map? ?? {}; + for (final pe in conns.entries) { + if ((dirs[pe.key] as String?) == 'output') { + continue; + } + for (final b in pe.value as List) { + if (b is int) { + (wireConsumers[b] ??= {}).add(e.key); + } + } + } + } + + // Bits consumed by module output / inout ports. + final portOutBits = {}; + for (final pv in ports.values) { + final pm = pv as Map; + final dir = pm['direction'] as String?; + if (dir == 'output' || dir == 'inout') { + for (final b in pm['bits'] as List) { + if (b is int) { + portOutBits.add(b); + } + } + } + } + + // ── Phase 1: connected components ── + + final adj = >{for (final tc in tCells) tc: {}}; + + for (final tc in tCells) { + final dirs = + cells[tc]!['port_directions'] as Map? ?? {}; + final conns = cells[tc]!['connections'] as Map? ?? {}; + for (final pe in conns.entries) { + if ((dirs[pe.key] as String?) != 'output') { + continue; + } + for (final b in pe.value as List) { + if (b is! int) { + continue; + } + for (final c in wireConsumers[b] ?? const {}) { + if (c != tc && tCells.contains(c)) { + adj[tc]!.add(c); + adj[c]!.add(tc); + } + } + } + } + } + + final visited = {}; + final components = >[]; + + for (final tc in tCells) { + if (!visited.add(tc)) { + continue; + } + final comp = {tc}; + final stack = [tc]; + while (stack.isNotEmpty) { + final cur = stack.removeLast(); + for (final nb in adj[cur]!) { + if (visited.add(nb)) { + comp.add(nb); + stack.add(nb); + } + } + } + if (comp.length >= 2) { + components.add(comp); + } + } + + if (components.isEmpty) { + continue; + } + + // ── Phase 2: trace & replace ── + + final cellsToRemove = {}; + final cellsToAdd = >{}; + + for (final comp in components) { + // Build output-bit → input-bit map for the whole cluster. + final bitMap = {}; + for (final cn in comp) { + _mapCellBits(cells[cn]!, bitMap); + } + + // External output bits: produced by the cluster but consumed + // by something outside it (another cell or module output port). + final extOut = []; + for (final cn in comp) { + final dirs = + cells[cn]!['port_directions'] as Map? ?? {}; + final conns = + cells[cn]!['connections'] as Map? ?? {}; + for (final pe in conns.entries) { + if ((dirs[pe.key] as String?) != 'output') { + continue; + } + for (final b in pe.value as List) { + if (b is! int) { + continue; + } + if (portOutBits.contains(b) || + (wireConsumers[b]?.any((c) => !comp.contains(c)) ?? false)) { + extOut.add(b); + } + } + } + } + + if (extOut.isEmpty) { + // Fully dead cluster — remove. + cellsToRemove.addAll(comp); + continue; + } + + // Trace each external output back through the cluster to an + // external source bit. + final aList = []; + final yList = []; + var ok = true; + + for (final ob in extOut) { + Object cur = ob; + final seen = {}; + while (cur is int && bitMap.containsKey(cur)) { + if (!seen.add(cur)) { + ok = false; + break; + } + cur = bitMap[cur]!; + } + if (!ok) { + break; + } + aList.add(cur); + yList.add(ob); + } + + if (!ok) { + continue; + } + + cellsToAdd['cluster_buf_${comp.first}'] = NetlistUtils.makeBufCell( + aList.length, + aList, + yList, + ); + cellsToRemove.addAll(comp); + } + + cellsToRemove.forEach(cells.remove); + cells.addAll(cellsToAdd); + } + } + + /// Removes transparent helper cells whose outputs are not consumed by any + /// other cell or module output. + static void removeUnconsumedTransparentCells( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + final ports = moduleDef['ports'] as Map? ?? {}; + var changed = true; + while (changed) { + changed = false; + final consumedBits = {}; + + for (final cell in cells.values) { + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + for (final entry in conns.entries) { + final direction = dirs[entry.key] as String?; + if (direction != 'input' && direction != 'inout') { + continue; + } + consumedBits.addAll((entry.value as List).whereType()); + } + } + for (final port in ports.values) { + final portMap = port as Map; + final direction = portMap['direction'] as String?; + if (direction != 'output' && direction != 'inout') { + continue; + } + consumedBits.addAll((portMap['bits'] as List).whereType()); + } + + cells.removeWhere((_, cell) { + if (!_transparentCleanupTypes.contains(cell['type'] as String?)) { + return false; + } + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + final outputBits = {}; + for (final entry in conns.entries) { + final direction = dirs[entry.key] as String?; + if (direction != 'output' && direction != 'inout') { + continue; + } + outputBits.addAll((entry.value as List).whereType()); + } + final remove = + outputBits.isNotEmpty && !outputBits.any(consumedBits.contains); + changed = changed || remove; + return remove; + }); + } + } + } + + /// Removes `$concat` cells that only rename an already-named bit vector. + /// + /// Explicit array concat cells are useful when they show a real regrouping, + /// but a concat whose flattened inputs exactly match an existing netname is + /// just an alias. Redirect its consumers to the named source bits and remove + /// the cell. + static void removeTrivialConcatAliases( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + final netnames = moduleDef['netnames'] as Map?; + if (cells == null || cells.isEmpty || netnames == null) { + continue; + } + + final namedBitVectors = [ + for (final rawNetname in netnames.values) + if (rawNetname is Map && rawNetname['bits'] is List) + (rawNetname['bits'] as List).cast(), + ]; + if (namedBitVectors.isEmpty) { + continue; + } + + var changed = true; + while (changed) { + changed = false; + final replacementByOutputBit = {}; + final cellsToRemove = {}; + + for (final entry in cells.entries) { + final cell = entry.value; + if (cell['type'] != r'$concat' || + entry.key.startsWith('array_concat_output_')) { + continue; + } + + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + final outputBits = []; + final inputBits = []; + + for (final portEntry in conns.entries) { + final bits = (portEntry.value as List).cast(); + if ((dirs[portEntry.key] as String?) == 'output') { + outputBits.addAll(bits); + } else { + inputBits.addAll(bits); + } + } + + if (outputBits.length != inputBits.length || + !_matchesNamedVector(inputBits, namedBitVectors)) { + continue; + } + + for (var index = 0; index < outputBits.length; index++) { + final outputBit = outputBits[index]; + if (outputBit is int) { + replacementByOutputBit[outputBit] = inputBits[index]; + } + } + cellsToRemove.add(entry.key); + } + + if (replacementByOutputBit.isEmpty) { + continue; + } + + void rewriteBits(List bits) { + for (var index = 0; index < bits.length; index++) { + final bit = bits[index]; + if (bit is int && replacementByOutputBit.containsKey(bit)) { + bits[index] = replacementByOutputBit[bit]!; + } + } + } + + final ports = moduleDef['ports'] as Map? ?? {}; + for (final rawPort in ports.values) { + final port = rawPort as Map; + rewriteBits((port['bits'] as List).cast()); + } + + for (final entry in cells.entries) { + if (cellsToRemove.contains(entry.key)) { + continue; + } + final cell = entry.value; + final conns = cell['connections'] as Map? ?? {}; + for (final rawBits in conns.values) { + rewriteBits((rawBits as List).cast()); + } + } + + for (final rawNetname in netnames.values) { + if (rawNetname is Map && rawNetname['bits'] is List) { + rewriteBits((rawNetname['bits'] as List).cast()); + } + } + + cellsToRemove.forEach(cells.remove); + changed = true; + } + } + } + + /// Replaces a `$concat` of adjacent `$slice` outputs from the same source + /// with one wider `$slice`. + static void collapseConcatOfAdjacentSlices( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + final ports = moduleDef['ports'] as Map? ?? {}; + final cellsToRemove = {}; + + for (final concatEntry in cells.entries.toList()) { + final concat = concatEntry.value; + if (concat['type'] != r'$concat' || + concatEntry.key.startsWith('array_concat_output_')) { + continue; + } + + final concatDirs = + concat['port_directions'] as Map? ?? {}; + final concatConns = + concat['connections'] as Map? ?? {}; + final inputSliceRefs = <({String name, Map cell})>[]; + final outputBits = []; + var valid = true; + + for (final portEntry in concatConns.entries) { + if ((concatDirs[portEntry.key] as String?) == 'output') { + outputBits.addAll((portEntry.value as List).cast()); + continue; + } + + final inputBits = (portEntry.value as List).cast(); + final sliceEntry = _findSliceDrivingBits(cells, inputBits); + if (sliceEntry == null) { + valid = false; + break; + } + inputSliceRefs.add((name: sliceEntry.key, cell: sliceEntry.value)); + } + + if (!valid || inputSliceRefs.isEmpty || outputBits.isEmpty) { + continue; + } + + final firstSlice = inputSliceRefs.first.cell; + final firstParams = + firstSlice['parameters'] as Map? ?? {}; + final firstConnections = + firstSlice['connections'] as Map?; + final sourceRawBits = firstConnections?['A'] as List?; + if (sourceRawBits == null) { + continue; + } + final sourceBits = sourceRawBits.cast(); + final startOffset = firstParams['OFFSET'] as int?; + final sourceWidth = firstParams['A_WIDTH'] as int?; + if (startOffset == null || sourceWidth == null) { + continue; + } + + var expectedOffset = startOffset; + var combinedWidth = 0; + for (final sliceRef in inputSliceRefs) { + final slice = sliceRef.cell; + final params = slice['parameters'] as Map? ?? {}; + final conns = slice['connections'] as Map? ?? {}; + final sliceSourceBits = (conns['A'] as List).cast(); + final offset = params['OFFSET'] as int?; + final width = params['Y_WIDTH'] as int?; + + if (offset != expectedOffset || + width == null || + params['A_WIDTH'] != sourceWidth || + !_sameBits(sliceSourceBits, sourceBits)) { + valid = false; + break; + } + + expectedOffset += width; + combinedWidth += width; + } + + if (!valid || combinedWidth != outputBits.length) { + continue; + } + + cells[concatEntry.key] = NetlistCell( + hideName: concat['hide_name'] as int? ?? 0, + type: r'$slice', + parameters: { + 'OFFSET': startOffset, + 'A_WIDTH': sourceWidth, + 'Y_WIDTH': combinedWidth, + }, + attributes: (concat['attributes'] as Map?)?.cast() ?? + const {}, + portDirections: const { + 'A': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, + }, + connections: >{ + 'A': sourceBits, + 'Y': outputBits, + }, + ).toJson(); + + for (final sliceRef in inputSliceRefs) { + if (!_sliceOutputConsumedOutside( + sliceRef.name, + sliceRef.cell, + cells, + ports, + )) { + cellsToRemove.add(sliceRef.name); + } + } + } + + cellsToRemove.forEach(cells.remove); + } + } + + /// Finds a slice cell whose output bits exactly match [bits]. + static MapEntry>? _findSliceDrivingBits( + Map> cells, + List bits, + ) { + for (final entry in cells.entries) { + final cell = entry.value; + if (cell['type'] != r'$slice') { + continue; + } + final conns = cell['connections'] as Map? ?? {}; + final yBits = (conns['Y'] as List?)?.cast(); + if (yBits != null && _sameBits(yBits, bits)) { + return entry; + } + } + return null; + } + + /// Checks whether a slice output is still consumed outside that slice cell. + static bool _sliceOutputConsumedOutside( + String sliceName, + Map slice, + Map> cells, + Map ports, + ) { + final sliceConns = slice['connections'] as Map? ?? {}; + final outputBits = + ((sliceConns['Y'] as List?) ?? const []).whereType(); + final outputBitSet = outputBits.toSet(); + if (outputBitSet.isEmpty) { + return false; + } + + for (final rawPort in ports.values) { + final port = rawPort as Map; + final direction = port['direction'] as String?; + if (direction != 'output' && direction != 'inout') { + continue; + } + final bits = (port['bits'] as List).whereType(); + if (bits.any(outputBitSet.contains)) { + return true; + } + } + + for (final entry in cells.entries) { + if (entry.key == sliceName) { + continue; + } + final cell = entry.value; + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + for (final portEntry in conns.entries) { + final direction = dirs[portEntry.key] as String?; + if (direction != 'input' && direction != 'inout') { + continue; + } + final bits = (portEntry.value as List).whereType(); + if (bits.any(outputBitSet.contains)) { + return true; + } + } + } + return false; + } + + /// Checks whether [bits] exactly matches any known named bit vector. + static bool _matchesNamedVector( + List bits, + List> namedBitVectors, + ) => + namedBitVectors.any( + (namedBits) => + namedBits.length == bits.length && + namedBits.indexed.every((entry) => entry.$2 == bits[entry.$1]), + ); + + /// Checks whether two bit vectors have identical contents and order. + static bool _sameBits(List left, List right) => + left.length == right.length && + left.indexed.every((entry) => entry.$2 == right[entry.$1]); + + /// Populates [bitMap] with output-wire-bit → input-wire-bit entries + /// for a single transparent cell. + static void _mapCellBits(Map cell, Map bitMap) { + final type = cell['type']! as String; + final conns = cell['connections'] as Map? ?? {}; + final params = cell['parameters'] as Map? ?? {}; + + switch (type) { + case r'$buf': + _mapPairwise(conns['A'] as List, conns['Y'] as List, bitMap); + + case r'$slice': + final a = conns['A'] as List; + final y = conns['Y'] as List; + final off = params['OFFSET'] as int? ?? 0; + for (var i = 0; i < y.length; i++) { + if (y[i] is int && (off + i) < a.length) { + bitMap[y[i] as int] = a[off + i] as Object; + } + } + } + } + + /// Maps `Y[i]` → `A[i]` for identity-shaped cells. + static void _mapPairwise( + List a, + List y, + Map bitMap, + ) { + for (var i = 0; i < y.length && i < a.length; i++) { + if (y[i] is int) { + bitMap[y[i] as int] = a[i] as Object; + } + } + } +} diff --git a/lib/src/synthesizers/netlist/netlist_port_direction.dart b/lib/src/synthesizers/netlist/netlist_port_direction.dart new file mode 100644 index 000000000..610ab774e --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_port_direction.dart @@ -0,0 +1,27 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_port_direction.dart +// Type-safe netlist port directions and JSON serialization. +// +// 2026 August 24 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; + +/// A port direction while constructing a netlist. +@internal +enum NetlistPortDirection { + input, + output, + inout, +} + +/// Converts typed [directions] to the strings required by Yosys JSON. +@internal +Map serializePortDirections( + Map directions, +) => + { + for (final entry in directions.entries) entry.key: entry.value.name, + }; diff --git a/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart new file mode 100644 index 000000000..4ba4fe581 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart @@ -0,0 +1,139 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synth_module_definition.dart +// Synth module definition specialization for netlist synthesis. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// A [SynthModuleDefinition] that preserves cells for netlist synthesis. +@internal +class NetlistSynthModuleDefinition extends SynthModuleDefinition { + /// Creates a netlist synthesis definition for [module]. + 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). + final subModuleOutputArrays = module.subModules + .expand((sub) => sub.outputs.values) + .whereType() + .toSet() + ..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. + // Also skip submodule output arrays that already received $slice cells. + final portArrays = { + ...module.inputs.values.whereType(), + ...module.outputs.values.whereType(), + ...module.inOuts.values.whereType(), + }; + final excludedArrays = { + ...portArrays, + ...subModuleOutputArrays, + }; + + void addNestedArrays(LogicArray array) { + for (final element in array.elements) { + if (element is LogicArray) { + excludedArrays.add(element); + addNestedArrays(element); + } + } + } + + { + ...portArrays, + ...subModuleOutputArrays, + }.forEach(addNestedArrays); + final portArraySynthLogics = {}; + for (final portArray in excludedArrays) { + final synthLogic = logicToSynthMap[portArray]; + if (synthLogic != null) { + portArraySynthLogics.add(synthLogic.resolved); + } + } + module.internalSignals.whereType().where((signal) { + if (excludedArrays.contains(signal)) { + return false; + } + final synthLogic = logicToSynthMap[signal]; + if (synthLogic == null) { + return false; + } + return !portArraySynthLogics.contains(synthLogic.resolved); + }).forEach(_concatAssembleArray); + } + + /// Adds slice cells that decompose a LogicArray port into element signals. + void _subsetReceiveArrayPort(LogicArray port) { + final portSynth = getSynthLogic(port)!; + + var index = 0; + for (final element in port.elements) { + final elementSynth = getSynthLogic(element)!; + internalSignals.add(elementSynth); + + final subsetModule = SynthArraySlice( + Logic(width: port.width, name: 'DUMMY'), + index, + index + element.width - 1, + destination: element, + ); + + getSynthSubModuleInstantiation(subsetModule) + ..setOutputMapping(subsetModule.subset.name, elementSynth) + ..setInputMapping(subsetModule.original.name, portSynth) + ..pickName(module); + + index += element.width; + } + } + + /// Adds a concat cell that assembles independent LogicArray element signals. + void _concatAssembleArray(LogicArray array) { + final arraySynth = getSynthLogic(array)!; + final dummyElements = [ + for (final element in array.elements) + Logic(width: element.width, name: 'DUMMY'), + ]; + + // Swizzle reverses its inputs, so reverse here to keep in0 aligned with + // element[0], the least-significant array element. + final concatModule = SynthArrayConcat( + dummyElements.reversed.toList(), + destination: array, + ); + final instantiation = getSynthSubModuleInstantiation(concatModule) + ..setOutputMapping(concatModule.out.name, arraySynth); + + for (var index = 0; index < array.elements.length; index++) { + final elementSynth = getSynthLogic(array.elements[index])!; + internalSignals.add(elementSynth); + final inputName = concatModule.inputs.keys.elementAt(index); + instantiation.setInputMapping(inputName, elementSynth); + } + + instantiation.pickName(module); + } + + @override + void process() { + // Netlist synthesis preserves every submodule as a cell. + } +} 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..4e312cbb1 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart @@ -0,0 +1,122 @@ +// 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:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A [SynthesisResult] that holds the netlist representation of a single +/// module level: its ports, cells, and netnames. +@internal +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 Map> ports, + required Map> cells, + required Map netnames, + Map attributes = const {}, + }) : ports = _freezeNestedMap(ports), + cells = _freezeNestedMap(cells), + netnames = _freezeObjectMap(netnames), + attributes = _freezeObjectMap(attributes); + + /// Builds the JSON representation for this single module entry. + 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)', + 'version': NetlistSynthesizer.formatVersion, + 'modules': {typeName: moduleEntry}, + }); + return [ + SynthFileContents( + name: '$typeName.rohd.json', + description: 'netlist for $typeName', + contents: contents, + ), + ]; + } +} + +Map> _freezeNestedMap( + Map> source, +) => + Map.unmodifiable({ + for (final entry in source.entries) + entry.key: _freezeObjectMap(entry.value), + }); + +Map _freezeObjectMap(Map source) => + Map.unmodifiable({ + for (final entry in source.entries) entry.key: _freezeObject(entry.value), + }); + +Object? _freezeObject(Object? value) { + if (value is Map) { + return _freezeObjectMap(value); + } + if (value is Map) { + return Map.unmodifiable({ + for (final entry in value.entries) entry.key: _freezeObject(entry.value), + }); + } + if (value is List) { + return List.unmodifiable(value.map(_freezeObject)); + } + if (value is Set) { + return Set.unmodifiable(value.map(_freezeObject)); + } + return value; +} diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart new file mode 100644 index 000000000..db921fb26 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -0,0 +1,1121 @@ +// 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:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_module_translation.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_utils.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; + +/// 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 configuration = NetlistSynthesizerConfiguration( +/// collapseTransparentClusters: true, +/// ); +/// final synth = NetlistSynthesizer(configuration: configuration); +/// final builder = SynthBuilder(topModule, synth); +/// final json = synth.synthesizeToJson(topModule); +/// ``` +class NetlistSynthesizer extends Synthesizer { + /// The version of the ROHD extensions to the Yosys JSON netlist format. + /// + /// Consumers of ROHD-generated netlists must reject an unsupported version. + /// This version changes when ROHD adds or changes fields that affect how a + /// consumer interprets the netlist. + static const String formatVersion = '0.0.1'; + + /// The configuration controlling netlist synthesis. + /// + /// See [NetlistSynthesizerConfiguration] for documentation on individual + /// fields. + final NetlistSynthesizerConfiguration configuration; + + final SynthModuleStopPolicy _moduleStopPolicy; + + final NetlistCellMapper _netlistCellMapper; + + /// The hierarchy stopping policy used by this synthesizer. + SynthModuleStopPolicy get moduleStopPolicy => _moduleStopPolicy; + + /// Convenience accessor for the netlist-cell mapper. + @visibleForTesting + NetlistCellMapper get netlistCellMapper => _netlistCellMapper; + + /// Creates a [NetlistSynthesizer]. + /// + /// All synthesis parameters are bundled in [configuration]; see + /// [NetlistSynthesizerConfiguration] for documentation on each field. + NetlistSynthesizer({ + this.configuration = const NetlistSynthesizerConfiguration(), + }) : _moduleStopPolicy = configuration.moduleStopPolicy ?? + SynthModuleStopPolicy.netlist( + leafModulePredicate: configuration.leafModulePredicate), + _netlistCellMapper = + configuration.netlistCellMapper ?? NetlistCellMapper.withDefaults(); + + @override + bool generatesDefinition(Module module) => + moduleStopPolicy.generatesDefinition(module); + + @override + SynthesisResult synthesize( + Module module, + String Function(Module module) getInstanceTypeOfModule, { + SynthesisResult? Function(Module module)? lookupExistingResult, + Map? existingResults, + }) { + final attr = {'src': 'generated'}; + + final translation = NetlistModuleTranslation(module, + netlistCellMapper: netlistCellMapper, + generatesDefinition: generatesDefinition, + getInstanceTypeOfModule: getInstanceTypeOfModule) + ..processPorts() + ..processInternalWires() + ..processCells(); + final synthDef = translation.synthDef; + final ports = translation.ports; + final cells = translation.cells; + final getIds = translation.getIds; + + // -- 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 elemIds, + int offset, + int width, + Logic elemLogic, + Logic parentLogic, + List fullParentIds + })>[]; + + // Pending $struct_pack fields: for output struct ports, instead of + // aliasing port bits to leaf bits (which causes "shorting"), we + // collect structure-pack field operations and emit explicit cells later. + // Each entry records: field (src) → port sub-range [lower:upper]. + final structPackFields = <({ + 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). + void aliasArrayChildren(SynthLogic src, SynthLogic dst) { + final srcLogic = src.logics.firstOrNull; + final dstLogic = dst.logics.firstOrNull; + if (srcLogic is! LogicArray || dstLogic is! LogicArray) { + return; + } + if (srcLogic.elements.length != dstLogic.elements.length) { + return; + } + + for (final (index, srcElement) in srcLogic.elements.indexed) { + final dstElement = dstLogic.elements[index]; + final srcElementSynth = synthDef.logicToSynthMap[srcElement]; + final dstElementSynth = synthDef.logicToSynthMap[dstElement]; + if (srcElementSynth == null || dstElementSynth == null) { + continue; + } + + final srcElementLogic = srcElementSynth.logics.firstOrNull; + final dstElementLogic = dstElementSynth.logics.firstOrNull; + if (srcElementLogic is LogicArray && dstElementLogic is LogicArray) { + aliasArrayChildren(srcElementSynth, dstElementSynth); + } + + final srcElementIds = getIds(srcElementSynth); + final dstElementIds = getIds(dstElementSynth); + final len = srcElementIds.length < dstElementIds.length + ? srcElementIds.length + : dstElementIds.length; + for (var i = 0; i < len; i++) { + if (dstElementIds[i] != srcElementIds[i]) { + idAlias[dstElementIds[i]] = srcElementIds[i]; + } + } + } + } + + 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]; + } + } + aliasArrayChildren(assignment.src, assignment.dst); + } + + // 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. + structPackFields.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 + // SynthArraySlice / SynthArrayConcat), 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 SynthArraySlice + // 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 SynthArraySlice) { + // 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 = inputSL.resolved; + 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 SynthArrayConcat) { + // 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_pack 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(( + elemIds: elemIds.sublist(0, sliceLen), + offset: idx, + width: sliceLen, + elemLogic: elem, + parentLogic: logic, + fullParentIds: parentIds, + )); + } + } else if (elem is LogicStructure && elem is! LogicArray) { + // Nested InterfaceStructure: the intermediate struct + // itself has no SynthLogic, but its leaf elements do + // (created by _subsetReceiveStructPort). Walk leaf + // elements and emit struct field entries for each, + // using the top-level parent as the parent Logic. + var leafIdx = idx; + for (final leaf in elem.leafElements) { + final leafSL = synthDef.logicToSynthMap[leaf]; + if (leafSL != null) { + final leafIds = getIds(leafSL); + final sliceLen = leafIds.length < parentIds.length - leafIdx + ? leafIds.length + : parentIds.length - leafIdx; + if (sliceLen > 0) { + structFieldCells.add(( + elemIds: leafIds.sublist(0, sliceLen), + offset: leafIdx, + width: sliceLen, + elemLogic: leaf, + parentLogic: logic, + fullParentIds: parentIds, + )); + } + } + leafIdx += leaf.width; + } + } + 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(); + + // -- Break shared wire IDs for array slice/concat cells ----------------- + // (Populated inside the alias block below; declared here so netnames + // can reference it later.) + final arraySliceOldToNew = {}; + + // 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()); + } + } + + // After aliasing, the slice output Y bits share the same wire IDs + // as the corresponding sub-range of input A (because LogicArray + // elements share the parent's bit storage). This makes the slice + // trivial and it would be elided below. + // + // To preserve the structural decomposition in the schematic, we + // allocate fresh wire IDs for each array_slice Y output, then + // redirect all other cells that consume those IDs as inputs to + // read from the fresh IDs instead. The slice input A keeps the + // original parent-array IDs, so the data flow becomes: + // parent (original IDs) → slice A → slice Y (fresh IDs) → consumer + + for (final cellEntry in cells.entries) { + if (!cellEntry.key.startsWith( + SynthArraySlice.operationName, + )) { + 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] != 'output') { + continue; + } + final oldBits = (portEntry.value as List).cast(); + conns[portEntry.key] = [ + for (final b in oldBits) + if (b is int) + arraySliceOldToNew.putIfAbsent( + b, + translation.allocateWireId, + ) + else + b, + ]; + } + } + + // Redirect other cells: any input port bit that matches an old ID + // gets replaced with the corresponding fresh ID. + if (arraySliceOldToNew.isNotEmpty) { + for (final cellEntry in cells.entries) { + if (cellEntry.key.startsWith( + SynthArraySlice.operationName, + )) { + continue; // skip the slice 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] != 'input') { + continue; + } + final bits = (portEntry.value as List).cast(); + final newBits = [ + for (final b in bits) + if (b is int) arraySliceOldToNew[b] ?? b else b, + ]; + if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { + conns[portEntry.key] = newBits; + } + } + } + } + } + + // -- Elide trivial $slice cells ---------------------------------- + // Also elide struct_slice cells ([SynthStructureSlice] 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(SynthStructureSlice.operationName)) { + 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 = 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); + + final structLayout = parentLogic is LogicStructure + ? SynthStructureLayout(parentLogic) + : null; + + // Build port_directions and connections with one output per field. + final portDirs = { + 'A': NetlistPortDirection.input, + }; + final conns = >{'A': resolvedParentBits}; + + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + final fieldName = structLayout?.fieldNameAt(f.offset, + fallbackName: f.elemLogic.name, anonymousUnpreferred: true) ?? + f.elemLogic.name; + // Disambiguate duplicate field names with index suffix. + var portName = fieldName; + if (portDirs.containsKey(portName)) { + portName = '${fieldName}_$i'; + } + portDirs[portName] = NetlistPortDirection.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'] = structLayout?.fieldNameAt(f.offset, + fallbackName: f.elemLogic.name, anonymousUnpreferred: true) ?? + f.elemLogic.name; + params['FIELD_${i}_OFFSET'] = f.offset; + params['FIELD_${i}_WIDTH'] = f.width; + } + + cells['struct_unpack_${suIdx}_$structName'] = NetlistCell( + type: r'$struct_unpack', + parameters: params, + portDirections: portDirs, + connections: conns, + ).toJson(); + 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 emits explicit structure packing cells. + if (structPackFields.isNotEmpty) { + // Group by destination SynthLogic identity. + final packGroups = srcIds, + List dstIds, + int dstLowerIndex, + int dstUpperIndex, + SynthLogic srcSynthLogic, + SynthLogic dstSynthLogic, + })>>{}; + for (final sc in structPackFields) { + (packGroups[sc.dstSynthLogic] ??= []).add(sc); + } + + for (final entry in packGroups.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 metadata from the destination Logic. + final dstLogic = dstSynthLogic.logics.firstOrNull; + final structName = + dstLogic != null ? Sanitizer.sanitizeSV(dstLogic.name) : 'struct'; + final structLayout = + dstLogic is LogicStructure ? SynthStructureLayout(dstLogic) : null; + final cellName = dstLogic != null + ? NetlistUtils.synthesizedCellName( + operationName: SynthStructureConcat.operationName, + destination: dstLogic, + ) + : SynthStructureConcat.operationName; + + // Build port_directions and connections. + final portDirs = {}; + final conns = >{}; + + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + final fieldName = structLayout?.fieldNameAt(f.dstLowerIndex, + fallbackName: f.srcSynthLogic.resolved.name) ?? + f.srcSynthLogic.resolved.name; + var portName = fieldName; + if (portDirs.containsKey(portName)) { + portName = '${fieldName}_$i'; + } + portDirs[portName] = NetlistPortDirection.input; + conns[portName] = f.resolvedSrcBits; + } + + // Output port Y: full destination bus. + portDirs['Y'] = NetlistPortDirection.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'] = structLayout?.fieldNameAt(f.dstLowerIndex, + fallbackName: f.srcSynthLogic.resolved.name) ?? + f.srcSynthLogic.resolved.name; + params['FIELD_${i}_OFFSET'] = f.dstLowerIndex; + params['FIELD_${i}_WIDTH'] = f.dstUpperIndex - f.dstLowerIndex + 1; + } + + cells['${cellName}_$structName'] = NetlistCell( + type: r'$struct_pack', + parameters: params, + portDirections: portDirs, + connections: conns, + ).toJson(); + } + } + + translation + ..processCellCleanup(enableDce: configuration.enableDeadCellElimination) + ..processConstants( + applyAlias: applyAlias, + pruneFloating: configuration.enableDeadCellElimination); + + // -- Break shared wire IDs for array_concat cells -------------------- + // After aliasing, concat Y can share wire IDs with the independently + // driven element inputs (because LogicArray elements share the parent's + // bit storage). This makes concat Y a second driver of the element wires. + // + // Allocate fresh IDs for concat Y and redirect downstream consumers to + // those fresh IDs. The concat inputs keep the original element IDs, so + // data flow is: + // element drivers → concat input → concat Y (fresh IDs) → consumer + final arrayConcatOldToNew = {}; + final arrayConcatReplacements = + <({String cellKey, List oldBits, List newBits})>[]; + final outputPortBitSets = [ + for (final port in ports.values) + if ((port as Map)['direction'] == 'output') + (port['bits'] as List).whereType().toSet(), + ]; + + for (final cellEntry in cells.entries) { + if (!cellEntry.key.startsWith(SynthArrayConcat.operationName)) { + continue; + } + if (cellEntry.key.startsWith('array_concat_output_')) { + 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] != 'output') { + continue; + } + final oldBits = (portEntry.value as List).cast(); + final oldBitSet = oldBits.whereType().toSet(); + if (outputPortBitSets.any((outputBits) => + outputBits.length == oldBitSet.length && + outputBits.containsAll(oldBitSet))) { + continue; + } + final newBits = [ + for (final b in oldBits) + if (b is int) translation.allocateWireId() else b, + ]; + conns[portEntry.key] = newBits; + arrayConcatReplacements + .add((cellKey: cellEntry.key, oldBits: oldBits, newBits: newBits)); + } + } + + final arrayConcatOutputProducers = >{}; + for (final (index, replacement) in arrayConcatReplacements.indexed) { + for (final bit in replacement.oldBits) { + if (bit is int) { + (arrayConcatOutputProducers[bit] ??= []).add(index); + } + } + } + + List rewriteArrayConcatConsumerBits( + List bits, { + String? consumingCellKey, + }) { + for (final replacement in arrayConcatReplacements) { + if (replacement.cellKey == consumingCellKey || + replacement.oldBits.length != bits.length) { + continue; + } + if (bits.indexed + .every((entry) => entry.$2 == replacement.oldBits[entry.$1])) { + return replacement.newBits; + } + } + + final newBits = []; + var changed = false; + for (final bit in bits) { + if (bit is! int) { + newBits.add(bit); + continue; + } + final producerIndices = arrayConcatOutputProducers[bit] + ?.where((index) => + arrayConcatReplacements[index].cellKey != consumingCellKey) + .toList(); + if (producerIndices == null || producerIndices.length != 1) { + newBits.add(bit); + continue; + } + final producer = arrayConcatReplacements[producerIndices.single]; + final bitIndex = producer.oldBits.indexOf(bit); + if (bitIndex < 0) { + newBits.add(bit); + continue; + } + newBits.add(producer.newBits[bitIndex]); + changed = true; + } + return changed ? newBits : bits; + } + + // Redirect downstream consumers: any input port or module output bit that + // matches an old concat Y ID gets replaced with the corresponding fresh ID. + if (arrayConcatReplacements.isNotEmpty) { + for (final portEntry in ports.values) { + final port = portEntry as Map; + if (port['direction'] != 'output') { + continue; + } + final bits = (port['bits'] as List).cast(); + final newBits = rewriteArrayConcatConsumerBits(bits); + if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { + port['bits'] = newBits; + } + } + + for (final cellEntry in cells.entries) { + 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 bits = (portEntry.value as List).cast(); + final newBits = rewriteArrayConcatConsumerBits(bits, + consumingCellKey: cellEntry.key); + if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { + conns[portEntry.key] = newBits; + } + } + } + } + + translation.processNetnames( + applyAlias: applyAlias, + arraySliceOldToNew: arraySliceOldToNew, + arrayConcatOldToNew: arrayConcatOldToNew, + pruneUndriven: configuration.enableDeadCellElimination, + drivenBits: configuration.enableDeadCellElimination + ? NetlistValidation.connectedBits(ports, cells, + portDirections: const {'input', 'inout'}, + cellDirection: 'output') + : const {}); + final netnames = translation.netnames; + + // -- Structural validation ------------------------------------------- + NetlistValidation.validate(ports, cells, module.name, netnames: netnames); + + 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 (configuration.collapseTransparentClusters) { + NetlistPasses.collapseConcatOfAdjacentSlices(modules); + NetlistPasses.removeTrivialConcatAliases(modules); + NetlistPasses.applyTransparentClustering(modules); + NetlistPasses.removeUnconsumedTransparentCells(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. [slimMode] overrides the configured + /// default for this projection without modifying the retained results. + Map> buildModulesMap( + SynthBuilder synth, Module top, + {bool? slimMode}) { + final effectiveSlimMode = slimMode ?? configuration.slimMode; + final swEntries = Stopwatch()..start(); + final modules = NetlistPasses.collectModuleEntries(synth.synthesisResults, + topModule: top, includeCellConnections: !effectiveSlimMode); + swEntries.stop(); + + final swPasses = Stopwatch()..start(); + applyPostProcessingPasses(modules); + swPasses.stop(); + + return modules; + } + + /// Generate the combined netlist JSON from a [SynthBuilder]'s results. + String generateCombinedJson(SynthBuilder synth, Module top, + {bool? slimMode}) { + final swCollect = Stopwatch()..start(); + final modules = buildModulesMap(synth, top, slimMode: slimMode); + swCollect.stop(); + + final swCompress = Stopwatch()..start(); + if (configuration.compressBitRanges) { + _compressModulesMap(modules); + } + swCompress.stop(); + + final combined = { + 'creator': 'NetlistSynthesizer (rohd)', + 'version': formatVersion, + 'modules': modules + }; + + final swEncode = Stopwatch()..start(); + final encoder = configuration.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] as List).cast()); + } + } + } + } + + 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. + /// + /// The [packageRoot] parameter is accepted for API compatibility with + /// downstream trace-enabled branches. [slimMode] overrides the configured + /// output mode for this call, allowing expansion after a slim request. + @visibleForTesting + String synthesizeToJson(Module top, {String? packageRoot, bool? slimMode}) { + final sb = SynthBuilder(top, this); + return generateCombinedJson(sb, top, slimMode: slimMode); + } +} diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart new file mode 100644 index 000000000..6d60ad4f8 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart @@ -0,0 +1,114 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synthesizer_configuration.dart +// Configuration for netlist synthesis. +// +// 2026 March 12 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; +export '../utilities/synth_module_stop_policy.dart'; + +/// Configuration for netlist synthesis. +/// +/// The netlist synthesizer serves two main consumer flows, both configured +/// through this configuration: +/// +/// **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 retain complete per-module synthesis results. Flow 1 skips cell +/// connection copying while collecting the emitted JSON projection. This keeps +/// slim output lightweight while guaranteeing a later expanded request has the +/// same cell keys, wire IDs, and connectivity as an initially expanded request. +/// +/// 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 +/// final synth = NetlistSynthesizer(); +/// ``` +class NetlistSynthesizerConfiguration { + /// The policy used to decide which modules stop hierarchy traversal and are + /// emitted as cells in their parent instead of as separate module + /// definitions. When `null`, [SynthModuleStopPolicy.netlist] is used. + /// + /// When this is provided, it owns the complete stopping policy and + /// [leafModulePredicate] is ignored. + final SynthModuleStopPolicy? moduleStopPolicy; + + /// Determines which modules should stop netlist hierarchy traversal and be + /// emitted as cells in their parent. + /// + /// Defaults to matching [FlipFlop] and its subclasses, which contain internal + /// sequential submodules but should be emitted as `$dff` netlist cells. + final SynthModuleLeafPredicate leafModulePredicate; + + /// The netlist-internal mapper used to convert selected leaf modules to + /// Yosys primitive cell types. When `null`, each synthesizer creates its own + /// mapper containing the default handlers. + @internal + final NetlistCellMapper? netlistCellMapper; + + /// When `true`, a single unified pass finds connected components of + /// all transparent cells (`$buf`, `$slice`, `$concat`, + /// `$struct_unpack`, `$struct_pack`), traces each cluster's output + /// bits back to their ultimate source bits, and replaces every + /// multi-cell cluster with a direct `$buf`. This subsumes all of + /// the individual collapse passes above. + @internal + final bool collapseTransparentClusters; + + /// When `true`, dead-cell elimination is performed after aliasing to + /// remove cells whose inputs are entirely undriven or whose outputs + /// are entirely unconsumed. + @internal + final bool enableDeadCellElimination; + + /// When `true`, the synthesizer produces "slim" output: cell connection maps + /// are not copied into the emitted JSON projection. Netnames and ports are + /// still emitted with full wire-ID fidelity, while per-module synthesis + /// results retain complete connectivity. + 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 `:`. + @internal + 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 configuration for netlist synthesis. + const NetlistSynthesizerConfiguration({ + this.moduleStopPolicy, + this.leafModulePredicate = _isFlipFlop, + this.netlistCellMapper, + @visibleForTesting this.collapseTransparentClusters = false, + @visibleForTesting this.enableDeadCellElimination = true, + this.slimMode = false, + @visibleForTesting this.compressBitRanges = false, + this.compactJson = false, + }); +} + +bool _isFlipFlop(Module module) => module is FlipFlop; diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart new file mode 100644 index 000000000..638afbd45 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -0,0 +1,536 @@ +// 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:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; + +typedef _BusSubsetCollapseInfo = ( + BusSubset, + SynthLogic, + SynthSubModuleInstantiation, +); + +typedef _SwizzleCollapseInfo = ( + String, + int, + int, + SynthLogic, + SynthSubModuleInstantiation, +); + +/// Reusable indexes for collapsing procedural-cell ports. +@internal +class NetlistAlwaysBlockPortCollapseIndex { + final Module _module; + final Map _busSubsets = {}; + final Map _swizzles = {}; + + /// Indexes aggregate-producing submodules in [synthDef]. + NetlistAlwaysBlockPortCollapseIndex(SynthModuleDefinition synthDef) + : _module = synthDef.module { + for (final instance in synthDef.subModuleInstantiations) { + final module = instance.module; + if (module is BusSubset) { + final output = instance.outputMapping.values.firstOrNull; + final input = instance.inputMapping.values.firstOrNull; + if (output != null && input != null) { + _busSubsets[output.resolved] = (module, input.resolved, instance); + } + } else if (module is Swizzle) { + final output = instance.outputMapping.values.firstOrNull; + if (output == null) { + continue; + } + + var offset = 0; + for (final input in instance.inputMapping.entries) { + final resolvedInput = input.value.resolved; + _swizzles[resolvedInput] = ( + input.key, + offset, + resolvedInput.width, + output.resolved, + instance, + ); + offset += resolvedInput.width; + } + } + } + } +} + +/// Shared utility functions for netlist synthesis and post-processing passes. +/// +/// All methods are static. +@internal +abstract class NetlistUtils { + /// Returns a deterministic cell name for an operation producing + /// [destination]. + static String synthesizedCellName({ + required String operationName, + required Logic destination, + }) => + '${Sanitizer.sanitizeSV(operationName)}_' + '${_destinationSuffix(destination)}'; + + static String _destinationSuffix(Logic destination) { + final module = destination.parentModule; + if (module == null) { + throw SynthException( + 'Cannot derive a netlist cell key for ${destination.name}: ' + 'the destination has no parent module.', + ); + } + + final parts = [ + _rootSignalIndexInModule(module, _rootLogic(destination)), + ..._logicElementPathIndices(destination), + ]; + return parts.map((part) => part.toString()).join('_'); + } + + static Logic _rootLogic(Logic destination) { + var root = destination; + while (root.parentStructure != null) { + root = root.parentStructure!; + } + return root; + } + + static List _logicElementPathIndices(Logic destination) { + final elementPath = []; + var current = destination; + while (current.parentStructure != null) { + final parent = current.parentStructure!; + final index = parent.elements.indexWhere( + (element) => identical(element, current), + ); + elementPath.insert(0, index < 0 ? current.arrayIndex ?? 0 : index); + current = parent; + } + return elementPath; + } + + static int _rootSignalIndexInModule(Module module, Logic root) { + final inputIndex = _identityIndex(module.inputs.values, root); + if (inputIndex != null) { + return inputIndex; + } + + final outputIndex = _identityIndex(module.outputs.values, root); + if (outputIndex != null) { + return module.inputs.length + outputIndex; + } + + final inOutIndex = _identityIndex(module.inOuts.values, root); + if (inOutIndex != null) { + return module.inputs.length + module.outputs.length + inOutIndex; + } + + final internalIndex = _identityIndex(module.internalSignals, root); + if (internalIndex != null) { + return module.inputs.length + + module.outputs.length + + module.inOuts.length + + internalIndex; + } + + throw SynthException( + 'Cannot derive a netlist cell key for ${root.name}: ' + 'the logic root is not registered with module ${module.name}.', + ); + } + + static int? _identityIndex(Iterable logics, Logic target) { + var index = 0; + for (final logic in logics) { + if (identical(logic, target)) { + return index; + } + index++; + } + return null; + } + + /// Indexes [synthLogics] by their corresponding name in [portMap]. + static Map portNamesForSynthLogics( + Iterable synthLogics, + Map portMap, + ) { + final namesByLogic = Map.identity() + ..addEntries( + portMap.entries.map((entry) => MapEntry(entry.value, entry.key)), + ); + final portNames = Map.identity(); + for (final synthLogic in synthLogics) { + for (final logic in synthLogic.logics) { + final portName = namesByLogic[logic]; + if (portName != null) { + portNames[synthLogic] = portName; + break; + } + } + } + return portNames; + } + + /// 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) => sl.nameOrNull; + + /// Create a `$buf` cell map. + static Map makeBufCell( + int width, + List aBits, + List yBits, + ) => + NetlistCell( + type: r'$buf', + parameters: {'WIDTH': width}, + portDirections: { + 'A': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, + }, + connections: >{'A': aBits, 'Y': yBits}, + ).toJson(); + + /// 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( + NetlistAlwaysBlockPortCollapseIndex index, + SynthSubModuleInstantiation instance, + Map portDirs, + Map> connections, + List Function(SynthLogic) getIds, + ) { + // ── Input-side collapsing (BusSubset → Combinational) ────────────── + + // 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 = e.value.resolved; + final info = index._busSubsets[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 Dead Cell Elimination 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] = NetlistPortDirection.input; + } + + // ── Output-side collapsing (Combinational → Swizzle) ─────────────── + + // 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 = e.value.resolved; + final info = index._swizzles[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; + } + + // Skip collapsing when any member's SynthLogic is a port of the + // parent module. Collapsing replaces the individual output ports + // with a single aggregate that uses the downstream Swizzle's bit + // IDs, which would orphan the module-level port bits (they would + // no longer be driven by any cell). + final hasModulePort = entry.value.any((member) { + final sl = instance.outputMapping[member.$1]; + if (sl == null) { + return false; + } + final resolved = sl.resolved; + return resolved.isPort(index._module); + }); + if (hasModulePort) { + 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] = NetlistPortDirection.output; + } + } + + /// Builds a JSON-serializable type descriptor for [logic]. + /// + /// Returns: + /// - For a plain [Logic] or [LogicArray]: `{'width': N}` (bitvector is the + /// default) + /// - For a [LogicStructure] (non-array): `{'typeName': className, 'fields': + /// [field, ...]}` where each field is `{'name': fieldName, 'width': W}` for + /// leaf fields or `{'name': fieldName, 'type': {...}}` for nested + /// [LogicStructure]s. + /// + /// Fields are listed in LSB-to-MSB order (matching ROHD's element ordering + /// via `rswizzle`: `elements[0]` occupies the lowest bits). + /// + /// When [bits] is provided, each field entry also includes a `'bits'` key + /// containing the slice of [bits] that belongs to that field. This allows + /// consumers to identify which net IDs map to which field even when the + /// signal is only partially connected (where computing offsets from the flat + /// top-level `bits` array would be ambiguous). + static Map buildLogicType( + Logic logic, [ + List? bits, + ]) { + if (logic is LogicArray) { + final result = { + 'width': logic.width, + 'arrayDims': logic.dimensions, + 'elementWidth': logic.elementWidth, + }; + // If the leaf elements are LogicStructures (array of structs), + // include the element type metadata for recursive expansion. + if (logic.elements.isNotEmpty) { + final first = logic.elements.first; + if (first is LogicStructure && first is! LogicArray) { + result['elementType'] = buildLogicType(first); + } else if (first is LogicArray) { + // Nested array — encode inner dimensions via recursive call. + result['elementType'] = buildLogicType(first); + } + } + return result; + } else if (logic is LogicStructure) { + var offset = 0; + final fields = logic.elements.map((e) { + final fieldBits = bits?.sublist(offset, offset + e.width); + offset += e.width; + if (e is LogicStructure && e is! LogicArray) { + return { + 'name': e.name, + if (fieldBits != null) 'bits': fieldBits, + 'type': buildLogicType(e, fieldBits), + }; + } else if (e is LogicArray) { + return { + 'name': e.name, + 'width': e.width, + if (fieldBits != null) 'bits': fieldBits, + 'type': buildLogicType(e, fieldBits), + }; + } else { + return { + 'name': e.name, + 'width': e.width, + if (fieldBits != null) 'bits': fieldBits, + }; + } + }).toList(); + return {'typeName': logic.runtimeType.toString(), 'fields': fields}; + } else { + return {'width': logic.width}; + } + } + + /// Returns the most type-specific [Logic] from [sl]'s [Logic] list for + /// use in [buildLogicType]. + /// + /// Prefers a [LogicStructure] (non-array) over a plain [Logic], since it + /// carries richer field metadata. + static Logic? typeLogicFromSynthLogic(SynthLogic sl) { + final logics = sl.logics; + return logics + .whereType() + .where((l) => l is! LogicArray) + .firstOrNull ?? + logics.firstOrNull; + } + + /// Check if a SynthLogic is a constant (following replacement chain). + static bool isConstantSynthLogic(SynthLogic sl) => sl.resolved.isConstant; + + /// Extract the Const value from a constant SynthLogic. + static Const? constValueFromSynthLogic(SynthLogic sl) { + final resolved = sl.resolved; + 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/netlist/netlist_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart new file mode 100644 index 000000000..f2c8a6b87 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -0,0 +1,202 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_validation.dart +// Structural validation utilities for emitted netlists. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/src/exceptions/synth_exception.dart'; + +/// Graph queries and structural checks for an emitted module netlist. +@internal +class NetlistValidation { + static const _nonDrivingAliasTypes = { + r'$slice', + r'$concat', + r'$struct_unpack', + r'$struct_pack', + }; + + /// Prevents construction of this static utility class. + NetlistValidation._(); + + /// Collects module-port and cell-connection bits with matching directions. + static Set connectedBits( + Map> ports, + Map> cells, { + required Set portDirections, + required String cellDirection, + }) => + { + ...ports.values + .where((port) => portDirections.contains(port['direction'])) + .expand((port) => (port['bits'] as List?) ?? const []) + .whereType(), + ...cells.values.expand((cell) { + final connections = + cell['connections'] as Map? ?? const {}; + final directions = + cell['port_directions'] as Map? ?? const {}; + return connections.entries + .where((port) => directions[port.key] == cellDirection) + .expand((port) => (port.value as List?) ?? const []) + .whereType(); + }), + }; + + /// Throws [NetlistValidationException] if the netlist has structural errors. + static void validate( + Map> ports, + Map> cells, + String moduleName, { + Map? netnames, + }) { + final issues = []; + + final driversByBit = _driversByBit(ports, cells); + + for (final entry in driversByBit.entries) { + if (entry.value.length <= 1) { + continue; + } + issues.add(NetlistValidationIssue( + 'wire bit ${entry.key} has multiple drivers: ' + '${entry.value.join(', ')}', + wireBit: entry.key, + drivers: entry.value, + )); + } + + if (netnames != null) { + for (final entry in netnames.entries) { + final netname = entry.value; + if (netname is! Map) { + continue; + } + final logicType = netname['logic_type']; + if (logicType is! Map || + (logicType['arrayDims'] is! List && logicType['fields'] is! List)) { + continue; + } + final bits = (netname['bits'] as List?)?.whereType() ?? const []; + final aggregateDrivers = { + for (final bit in bits) ...driversByBit[bit] ?? const [], + }; + if (aggregateDrivers.length <= 1) { + continue; + } + issues.add(NetlistValidationIssue( + 'aggregate net "${entry.key}" is reached from multiple drivers: ' + '${aggregateDrivers.join(', ')}', + netname: entry.key, + drivers: aggregateDrivers.toList(), + )); + } + } + + if (issues.isNotEmpty) { + throw NetlistValidationException(moduleName, issues); + } + } + + /// Collects the port and cell output drivers for each integer bit ID. + static Map> _driversByBit( + Map> ports, + Map> cells, + ) { + final drivers = >{}; + + void addDriver(int bit, String driver) => + (drivers[bit] ??= []).add(driver); + + for (final entry in ports.entries) { + final direction = entry.value['direction'] as String?; + if (direction != 'input') { + continue; + } + for (final bit in (entry.value['bits'] as List?) ?? const []) { + if (bit is int) { + addDriver(bit, 'port ${entry.key} ($direction)'); + } + } + } + + for (final entry in cells.entries) { + final connections = entry.value['connections'] as Map?; + final directions = + entry.value['port_directions'] as Map?; + if (connections == null || directions == null) { + continue; + } + final type = entry.value['type'] as String? ?? 'unknown'; + if (_nonDrivingAliasTypes.contains(type)) { + continue; + } + for (final port in connections.entries) { + final direction = directions[port.key] as String?; + final isTriStateOutput = type == r'$tribuf' && direction == 'inout'; + if (direction != 'output' && !isTriStateOutput) { + continue; + } + for (final bit in (port.value as List?) ?? const []) { + if (bit is int) { + addDriver(bit, 'cell ${entry.key}.${port.key} ($type)'); + } + } + } + } + + return drivers; + } +} + +/// A structural netlist validation failure. +@internal +class NetlistValidationException extends SynthException { + /// The module containing the structural errors. + final String moduleName; + + /// The structural errors found in [moduleName]. + final List issues; + + /// Creates a validation exception for [moduleName]. + NetlistValidationException( + this.moduleName, Iterable issues) + : issues = List.unmodifiable(issues), + super('Netlist validation failed for $moduleName.'); + + @override + String toString() => 'Netlist validation failed for $moduleName: ' + '${issues.length} issue(s) found.\n' + '${issues.join('\n')}'; +} + +/// A structural problem found while validating an emitted netlist. +@internal +class NetlistValidationIssue { + /// A human-readable explanation of the structural problem. + final String description; + + /// The affected wire bit, when the problem concerns one bit. + final int? wireBit; + + /// The affected aggregate net name, when applicable. + final String? netname; + + /// The drivers involved in the problem, when applicable. + final List drivers; + + /// Creates a structural validation issue. + NetlistValidationIssue( + this.description, { + this.wireBit, + this.netname, + Iterable drivers = const [], + }) : drivers = List.unmodifiable(drivers); + + @override + String toString() => description; +} diff --git a/lib/src/synthesizers/synthesis_result.dart b/lib/src/synthesizers/synthesis_result.dart index 27abb8fe9..b1b34e9b9 100644 --- a/lib/src/synthesizers/synthesis_result.dart +++ b/lib/src/synthesizers/synthesis_result.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synthesis_result.dart diff --git a/lib/src/synthesizers/synthesizers.dart b/lib/src/synthesizers/synthesizers.dart index b8c8523ec..da5d76586 100644 --- a/lib/src/synthesizers/synthesizers.dart +++ b/lib/src/synthesizers/synthesizers.dart @@ -1,6 +1,7 @@ -// 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'; diff --git a/lib/src/synthesizers/utilities/synth_array_concat.dart b/lib/src/synthesizers/utilities/synth_array_concat.dart new file mode 100644 index 000000000..168ec0e01 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_array_concat.dart @@ -0,0 +1,35 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_array_concat.dart +// Shared array concatenation helper for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A [Swizzle] used by synthesis backends to explicitly assemble a +/// [LogicArray] from its elements. +@internal +class SynthArrayConcat extends Swizzle { + /// The canonical base name for synthesized array concat operations. + static const String operationName = 'array_concat'; + + final LogicArray _destination; + + /// Creates a synthesis array concatenation from [signals]. + SynthArrayConcat(super.signals, {required LogicArray destination}) + : _destination = destination, + super(name: operationName); + + @override + bool get hasBuilt => true; + + @override + Object get instanceNameKey => ( + operationName: operationName, + destination: _destination, + ); +} diff --git a/lib/src/synthesizers/utilities/synth_array_slice.dart b/lib/src/synthesizers/utilities/synth_array_slice.dart new file mode 100644 index 000000000..ec4446826 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_array_slice.dart @@ -0,0 +1,39 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_array_slice.dart +// Shared array slice helper for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A [BusSubset] used by synthesis backends to explicitly extract a +/// [LogicArray] element from its packed parent representation. +@internal +class SynthArraySlice extends BusSubset { + /// The canonical base name for synthesized array slice operations. + static const String operationName = 'array_slice'; + + final Logic _destination; + + /// Creates a synthesis array slice over the selected indices of [bus]. + SynthArraySlice( + super.bus, + super.startIndex, + super.endIndex, { + required Logic destination, + }) : _destination = destination, + super(name: operationName); + + @override + bool get hasBuilt => true; + + @override + Object get instanceNameKey => ( + operationName: operationName, + destination: _destination, + ); +} diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index d29cc84f3..d5fe10223 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -220,6 +220,21 @@ class SynthLogic { return _name!; } + /// The chosen name of this, or `null` if a name has not been picked or this + /// has been replaced. + String? get nameOrNull { + if (_name == null || _replacement != null) { + return null; + } + + assert( + isConstant || Sanitizer.isSanitary(_name!), + 'Signal names should be sanitary, but found $_name.', + ); + + return _name; + } + /// The name of this, if it has been picked. String? _name; diff --git a/lib/src/synthesizers/utilities/synth_module_stop_policy.dart b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart new file mode 100644 index 000000000..9447f2764 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart @@ -0,0 +1,65 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_module_stop_policy.dart +// Shared module hierarchy stopping policy for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Determines whether a synthesizer should stop hierarchy traversal at a +/// [Module] and treat it as a leaf in its parent. +typedef SynthModuleLeafPredicate = bool Function(Module module); + +/// Determines whether a [Module] would normally receive its own synthesized +/// definition before leaf predicates are applied. +typedef SynthModuleDefinitionPredicate = bool Function(Module module); + +/// Shared hierarchy stopping policy for synthesis backends. +/// +/// A synthesizer configures this with backend-specific leaf predicates and a +/// default definition rule, then queries [isLeaf] or [generatesDefinition] +/// while walking a module hierarchy. +class SynthModuleStopPolicy { + final List _leafPredicates; + final SynthModuleDefinitionPredicate _generatesDefinitionByDefault; + + /// Creates a module stopping policy. + SynthModuleStopPolicy({ + SynthModuleDefinitionPredicate? generatesDefinitionByDefault, + Iterable leafPredicates = const [], + }) : _generatesDefinitionByDefault = + generatesDefinitionByDefault ?? ((_) => true), + _leafPredicates = List.unmodifiable(leafPredicates); + + /// Creates the default SystemVerilog stopping policy. + factory SynthModuleStopPolicy.systemVerilog() => SynthModuleStopPolicy( + leafPredicates: [ + (module) => + module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none, + ], + ); + + /// Creates the default netlist stopping policy. + factory SynthModuleStopPolicy.netlist({ + SynthModuleLeafPredicate leafModulePredicate = _isFlipFlop, + }) => + SynthModuleStopPolicy( + generatesDefinitionByDefault: (module) => module.subModules.isNotEmpty, + leafPredicates: [leafModulePredicate], + ); + + /// Returns `true` when [module] should be treated as a leaf cell in its + /// parent instead of receiving its own generated definition. + bool isLeaf(Module module) => + !_generatesDefinitionByDefault(module) || + _leafPredicates.any((predicate) => predicate(module)); + + /// Returns `true` when [module] should receive its own generated definition. + bool generatesDefinition(Module module) => !isLeaf(module); +} + +bool _isFlipFlop(Module module) => module is FlipFlop; diff --git a/lib/src/synthesizers/utilities/synth_structure_concat.dart b/lib/src/synthesizers/utilities/synth_structure_concat.dart new file mode 100644 index 000000000..dfc23408a --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_structure_concat.dart @@ -0,0 +1,35 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_structure_concat.dart +// Shared structure concatenation helper for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A [Swizzle] used by synthesis backends to explicitly assemble a +/// [LogicStructure] from its leaf elements. +@internal +class SynthStructureConcat extends Swizzle { + /// The canonical base name for synthesized structure concat operations. + static const String operationName = 'struct_concat'; + + final LogicStructure _destination; + + /// Creates a synthesis structure concatenation from [signals]. + SynthStructureConcat(super.signals, {required LogicStructure destination}) + : _destination = destination, + super(name: operationName); + + @override + bool get hasBuilt => true; + + @override + Object get instanceNameKey => ( + operationName: operationName, + destination: _destination, + ); +} diff --git a/lib/src/synthesizers/utilities/synth_structure_layout.dart b/lib/src/synthesizers/utilities/synth_structure_layout.dart new file mode 100644 index 000000000..db972c929 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_structure_layout.dart @@ -0,0 +1,125 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_structure_layout.dart +// Shared packed LogicStructure layout utility for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// An exclusive-end bit range within a packed [LogicStructure]. +typedef SynthStructureBitRange = ({int start, int end}); + +typedef _SynthStructureRange = ({ + int start, + int end, + String name, + String path, + String fieldPath, + int indexInParent, +}); + +/// Provides bit ranges and field names for a packed [LogicStructure]. +class SynthStructureLayout { + final List<_SynthStructureRange> _ranges = []; + + /// Creates a layout with elements ordered from least to most significant. + SynthStructureLayout(LogicStructure structure) { + _addStructure(structure, 0, '', ''); + } + + void _addStructure( + LogicStructure structure, + int baseOffset, + String parentPath, + String parentFieldPath, + ) { + var offset = baseOffset; + for (var index = 0; index < structure.elements.length; index++) { + final element = structure.elements[index]; + final end = offset + element.width; + final path = + parentPath.isEmpty ? element.name : '${parentPath}_${element.name}'; + final fieldPath = parentFieldPath.isEmpty + ? element.name + : '$parentFieldPath.${element.name}'; + _ranges.add(( + start: offset, + end: end, + name: element.name, + path: path, + fieldPath: fieldPath, + indexInParent: index, + )); + if (element is LogicStructure && element is! LogicArray) { + _addStructure(element, offset, path, fieldPath); + } + offset = end; + } + } + + /// Returns the exclusive-end bit range for a dot-separated [fieldPath]. + /// + /// For example, `a.b` returns the range for the nested `b` field in `a`. + /// Returns `null` when [fieldPath] does not identify a field. + SynthStructureBitRange? bitRangeForPath(String fieldPath) { + for (final range in _ranges) { + if (range.fieldPath == fieldPath) { + return (start: range.start, end: range.end); + } + } + return null; + } + + /// Returns the best field name containing [bitOffset]. + /// + /// When [anonymousUnpreferred] is true, an unpreferred leaf with no named + /// ancestor is represented by its index rather than its raw name. + String fieldNameAt( + int bitOffset, { + required String fallbackName, + bool anonymousUnpreferred = false, + }) { + _SynthStructureRange? bestNamed; + _SynthStructureRange? narrowest; + + for (final range in _ranges) { + if (bitOffset < range.start || bitOffset >= range.end) { + continue; + } + final span = range.end - range.start; + if (narrowest == null || span < narrowest.end - narrowest.start) { + narrowest = range; + } + if (!Naming.isUnpreferred(range.name) && + (bestNamed == null || span < bestNamed.end - bestNamed.start)) { + bestNamed = range; + } + } + + if (bestNamed != null) { + if (narrowest != null && + narrowest.end - narrowest.start < bestNamed.end - bestNamed.start) { + final prefix = bestNamed.path; + if (narrowest.path.length > prefix.length && + narrowest.path.startsWith(prefix)) { + final suffix = narrowest.path.substring(prefix.length + 1); + if (!Naming.isUnpreferred(suffix)) { + return '${bestNamed.name}_$suffix'; + } + } + return '${bestNamed.name}_${narrowest.indexInParent}'; + } + return bestNamed.name; + } + + if (anonymousUnpreferred && + narrowest != null && + Naming.isUnpreferred(narrowest.name)) { + return 'anonymous_${narrowest.indexInParent}'; + } + return narrowest?.name ?? fallbackName; + } +} diff --git a/lib/src/synthesizers/utilities/synth_structure_slice.dart b/lib/src/synthesizers/utilities/synth_structure_slice.dart new file mode 100644 index 000000000..d3c7dfc18 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_structure_slice.dart @@ -0,0 +1,39 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_structure_slice.dart +// Shared structure slice helper for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A [BusSubset] used by synthesis backends to explicitly extract a +/// [LogicStructure] leaf from its packed parent representation. +@internal +class SynthStructureSlice extends BusSubset { + /// The canonical base name for synthesized structure slice operations. + static const String operationName = 'struct_slice'; + + final Logic _destination; + + /// Creates a synthesis structure slice over the selected indices of [bus]. + SynthStructureSlice( + super.bus, + super.startIndex, + super.endIndex, { + required Logic destination, + }) : _destination = destination, + super(name: operationName); + + @override + bool get hasBuilt => true; + + @override + Object get instanceNameKey => ( + operationName: operationName, + destination: _destination, + ); +} diff --git a/lib/src/synthesizers/utilities/utilities.dart b/lib/src/synthesizers/utilities/utilities.dart index c3cccdf32..1a02d1952 100644 --- a/lib/src/synthesizers/utilities/utilities.dart +++ b/lib/src/synthesizers/utilities/utilities.dart @@ -1,7 +1,13 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'synth_array_concat.dart'; +export 'synth_array_slice.dart'; export 'synth_assignment.dart'; export 'synth_logic.dart'; export 'synth_module_definition.dart'; +export 'synth_module_stop_policy.dart'; +export 'synth_structure_concat.dart'; +export 'synth_structure_layout.dart'; +export 'synth_structure_slice.dart'; export 'synth_sub_module_instantiation.dart'; diff --git a/packages/rohd_hierarchy/analysis_options.yaml b/packages/rohd_hierarchy/analysis_options.yaml index f04c6cf0f..a96029588 100644 --- a/packages/rohd_hierarchy/analysis_options.yaml +++ b/packages/rohd_hierarchy/analysis_options.yaml @@ -1 +1,10 @@ +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** include: ../../analysis_options.yaml diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_models.dart b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart index 2f7cb3f76..dc79dbb6c 100644 --- a/packages/rohd_hierarchy/lib/src/hierarchy_models.dart +++ b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart @@ -12,5 +12,6 @@ export 'hierarchy_occurrence.dart'; export 'hierarchy_search_result.dart'; export 'occurrence_address.dart'; export 'occurrence_search_result.dart'; +export 'occurrence_trie.dart'; export 'signal_occurrence.dart'; export 'signal_search_result.dart'; diff --git a/packages/rohd_hierarchy/lib/src/occurrence_trie.dart b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart new file mode 100644 index 000000000..31ea2d647 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart @@ -0,0 +1,108 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_trie.dart +// Compact storage for values keyed by hierarchy occurrence addresses. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/occurrence_address.dart'; + +/// A prefix-sharing map from [OccurrenceAddress] values to values of type [T]. +/// +/// Common address prefixes are stored once, making this more compact than a +/// conventional map when many values belong to the same hierarchy subtree. +class OccurrenceTrie { + final _OccurrenceTrieNode _root = _OccurrenceTrieNode(); + + /// Whether this trie contains no values. + bool get isEmpty => _root.isEmpty; + + /// The value stored at [address], if any. + T? operator [](OccurrenceAddress address) { + var node = _root; + for (final index in _validatedPath(address)) { + final child = node.children[index]; + if (child == null) { + return null; + } + node = child; + } + return node.value; + } + + /// Associates [value] with [address]. + /// + /// Returns the value previously stored at [address], if any. + T? set(OccurrenceAddress address, T value) { + var node = _root; + for (final index in _validatedPath(address)) { + node = node.children.putIfAbsent(index, _OccurrenceTrieNode.new); + } + final previous = node.value; + node.value = value; + return previous; + } + + /// Removes and returns the value stored at [address], if any. + T? remove(OccurrenceAddress address) { + final path = _validatedPath(address); + final nodes = <_OccurrenceTrieNode>[_root]; + var node = _root; + for (final index in path) { + final child = node.children[index]; + if (child == null) { + return null; + } + nodes.add(child); + node = child; + } + + final previous = node.value; + if (previous == null) { + return null; + } + node.value = null; + for (var index = path.length - 1; index >= 0; index--) { + final child = nodes[index + 1]; + if (!child.isEmpty) { + break; + } + nodes[index].children.remove(path[index]); + } + return previous; + } + + /// Removes every value from this trie. + void clear() { + _root + ..value = null + ..children.clear(); + } + + static List _validatedPath(OccurrenceAddress address) { + if (address.path.isEmpty) { + throw ArgumentError.value( + address, + 'address', + 'A signal occurrence address must not be empty.', + ); + } + if (address.path.any((index) => index < 0)) { + throw ArgumentError.value( + address, + 'address', + 'A signal occurrence address must contain non-negative indices.', + ); + } + return address.path; + } +} + +class _OccurrenceTrieNode { + final Map> children = {}; + T? value; + + bool get isEmpty => value == null && children.isEmpty; +} diff --git a/packages/rohd_hierarchy/pubspec.yaml b/packages/rohd_hierarchy/pubspec.yaml index c75cd3d34..8a3351519 100644 --- a/packages/rohd_hierarchy/pubspec.yaml +++ b/packages/rohd_hierarchy/pubspec.yaml @@ -5,8 +5,6 @@ repository: https://github.com/intel/rohd version: 0.1.0 issue_tracker: https://github.com/intel/rohd/issues -publish_to: none - environment: sdk: '>=3.0.0 <4.0.0' diff --git a/packages/rohd_hierarchy/test/occurrence_trie_test.dart b/packages/rohd_hierarchy/test/occurrence_trie_test.dart new file mode 100644 index 000000000..ef9e7d4bc --- /dev/null +++ b/packages/rohd_hierarchy/test/occurrence_trie_test.dart @@ -0,0 +1,54 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_trie_test.dart +// Tests for compact occurrence-address trie storage. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + test('stores values with shared occurrence-address prefixes', () { + final trie = OccurrenceTrie(); + const first = OccurrenceAddress([0, 2, 4]); + const second = OccurrenceAddress([0, 2, 5]); + + expect(trie.set(first, 'first'), isNull); + expect(trie.set(second, 'second'), isNull); + + expect(trie[first], 'first'); + expect(trie[second], 'second'); + expect(trie[const OccurrenceAddress([0, 2, 6])], isNull); + }); + + test('prunes an address branch after removing its final value', () { + final trie = OccurrenceTrie(); + const first = OccurrenceAddress([0, 2, 4]); + const second = OccurrenceAddress([0, 2, 5]); + trie + ..set(first, 'first') + ..set(second, 'second'); + + expect(trie.remove(first), 'first'); + expect(trie[first], isNull); + expect(trie[second], 'second'); + expect(trie.remove(second), 'second'); + expect(trie.isEmpty, isTrue); + }); + + test('rejects an address that cannot identify a signal', () { + final trie = OccurrenceTrie(); + + expect( + () => trie.set(OccurrenceAddress.root, 'root'), + throwsArgumentError, + ); + expect( + () => trie[const OccurrenceAddress([0, -1])], + throwsArgumentError, + ); + }); +} diff --git a/packages/rohd_waveform/analysis_options.yaml b/packages/rohd_waveform/analysis_options.yaml index f04c6cf0f..a96029588 100644 --- a/packages/rohd_waveform/analysis_options.yaml +++ b/packages/rohd_waveform/analysis_options.yaml @@ -1 +1,10 @@ +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** include: ../../analysis_options.yaml diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md b/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md index e40b328bb..fa99b412a 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md @@ -26,6 +26,48 @@ across DevTools packages. - ROHD extension client/status abstractions: `RohdExtensionClient`, `NullExtensionClient`, `RohdModuleInfo`, and `RohdFormatInfo`. +## Widgets & Utilities + +### UI Controls & Buttons + +- **`MarkdownHelpButton`** — A help button that displays Markdown content from an asset file in a dialog. Supports tooltip text and rich formatting. + +- **`ExportPngButton`** — A camera icon button for triggering PNG export functionality. Includes customizable tooltip text. + +- **`CrossProbeButton`** — A toolbar button for toggling cross-probing between viewers. Shows a bidirectional arrows icon that reflects the active/inactive state. + +### Overlays & Layout + +- **`AppBarOverlay`** — An auto-hiding AppBar that slides in from the top edge when the mouse approaches. When disabled, behaves like a standard AppBar. + +### Export & Capture + +- **`CaptureBoundary`** — Utility for capturing a `RepaintBoundary` as PNG, saving/downloading, and showing user feedback via toast notifications. + +- **`ExportToast`** — Toast notification widget for export feedback and status messages. + +### Cross-Probing + +- **`CrossProbeService`** — Service for managing cross-probe state between multiple viewers/debuggers. Handles bidirectional signal selection synchronization. + +- **`CrossProbeMenu`** — Shared context menu integration for cross-probing actions across different ROHD DevTools surfaces. + +### Signal & Bit Field Utilities + +- **`LogicTypeUtils`** — Utilities for working with ROHD logic types and formatting logic values for display. + +- **`BitFieldUtils`** — Utilities for parsing, validating, and formatting bit field ranges and named bit fields. + +- **`BitExpansionMenu`** — Shared popup menu items for "Expand Bits" and "Define Bit Fields" actions used across signal selection overlays and panels. + +- **`SignalValueFormatRegistry`** — Shared registry for signal display-format preferences, allowing consistent formatting across multiple viewers. + +### Extension Integration + +- **`RohdExtensionClient`** — Abstract interface for querying the ROHD VS Code extension. Supports multiple implementations (DevTools, VS Code webview, offline mode). + +- **`RohdExtensionStatus`** — Status information and connection state for the ROHD extension. + ## Usage Add this package as a path dependency from a ROHD DevTools package and import the shared widgets you need: diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart index 452567fae..ca31b9ace 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart @@ -36,6 +36,9 @@ export 'src/bit_field_utils.dart'; // Shared "Expand Bits" / "Define Bit Fields" popup-menu helpers export 'src/bit_expansion_menu.dart'; +// Shared signal display-format preferences and value formatting +export 'src/signal_value_format_registry.dart'; + // ROHD extension client export 'src/rohd_extension_status.dart'; export 'src/rohd_extension_client.dart'; diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart new file mode 100644 index 000000000..bf5fdcad7 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart @@ -0,0 +1,239 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_value_format_registry.dart +// Shared signal display-format preferences and value formatting. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:flutter/foundation.dart'; +import 'package:rohd/rohd.dart' + show LogicValue, LogicValueConstructionException; +import 'package:rohd_hierarchy/rohd_hierarchy.dart' + show OccurrenceAddress, OccurrenceTrie; + +/// The available display formats for signal values. +enum SignalValueFormat { + /// The source waveform representation. + waveform, + + /// A binary representation. + binary, + + /// A hexadecimal representation. + hexadecimal, + + /// An unsigned decimal representation. + unsignedDecimal, + + /// A two's-complement signed decimal representation. + signedDecimal, + + /// An octal representation. + octal, + + /// An ASCII representation. + ascii, +} + +/// A format preference for one signal occurrence address. +class SignalValueFormatPreference { + /// Creates a preference for [address] using [format]. + SignalValueFormatPreference( + this.address, + this.format, + ); + + /// The occurrence address, including the signal index. + final OccurrenceAddress address; + + /// The selected display format. + final SignalValueFormat format; +} + +/// Shared display-format preferences keyed by occurrence address. +/// +/// Viewer packages publish [SignalValueFormat] values; embedded surfaces use +/// the same values without depending on viewer-local format enums. +class SignalValueFormatRegistry { + SignalValueFormatRegistry._(); + + static final _formatTrie = OccurrenceTrie(); + + /// Notifies listeners whenever occurrence-format preferences change. + static final changes = ValueNotifier(0); + + /// Replaces all occurrence-format preferences with [preferences]. + static void update(Iterable preferences) { + _formatTrie.clear(); + for (final preference in preferences) { + _formatTrie.set(preference.address, preference.format); + } + _notifyListeners(); + } + + /// Removes all occurrence-format preferences. + static void clear() { + if (_formatTrie.isEmpty) { + return; + } + _formatTrie.clear(); + _notifyListeners(); + } + + /// Sets [format] for each signal occurrence in [addresses]. + static void setFormatFor( + Iterable addresses, + SignalValueFormat format, + ) { + var changed = false; + for (final address in addresses) { + changed = (_formatTrie.set(address, format) != format) || changed; + } + if (changed) { + _notifyListeners(); + } + } + + /// Converts a serialized format name to its corresponding enum value. + /// + /// Returns `null` when [value] is not a known format name. + static SignalValueFormat? formatFromString(String value) { + for (final format in SignalValueFormat.values) { + if (format.name == value) { + return format; + } + } + return null; + } + + /// Converts [format] to its serialized format name. + static String formatToString(SignalValueFormat format) => format.name; + + /// Returns the requested format for [address], or the waveform default. + static SignalValueFormat formatFor(OccurrenceAddress address) { + return formatForAny([address]); + } + + /// Returns the first registered format matching [addresses]. + static SignalValueFormat formatForAny( + Iterable addresses, { + SignalValueFormat fallback = SignalValueFormat.waveform, + }) { + for (final address in addresses) { + if (address == null) { + continue; + } + final format = _formatTrie[address]; + if (format != null) { + return format; + } + } + return fallback; + } + + static void _notifyListeners() => changes.value++; + + static bool _containsUnknownDigits(String value) { + final lower = value.toLowerCase(); + final apostrophe = lower.indexOf("'"); + final digits = apostrophe > 0 && apostrophe + 2 <= lower.length + ? lower.substring(apostrophe + 2) + : lower.startsWith('0x') || lower.startsWith('0b') + ? lower.substring(2) + : lower; + return digits.contains('x') || digits.contains('z'); + } + + /// Formats a ROHD radix literal according to [format]. + static String formatValue( + String value, + SignalValueFormat format, + int width, + ) { + final waveformValue = _waveformValue(value, width); + if (waveformValue == null) { + return _canonicalWaveformValue(value, width); + } + final (logicValue, canonical) = waveformValue; + if (format == SignalValueFormat.waveform || + _containsUnknownDigits(canonical)) { + return canonical; + } + return switch (format) { + SignalValueFormat.binary => logicValue.toRadixString( + leadingZeros: true, + includeWidth: false, + sepChar: '', + ), + SignalValueFormat.hexadecimal => + logicValue.toRadixString(radix: 16, sepChar: ''), + SignalValueFormat.unsignedDecimal => + logicValue.toRadixString(radix: 10, includeWidth: false, sepChar: ''), + SignalValueFormat.signedDecimal => + logicValue.toBigInt().toSigned(logicValue.width).toString(), + SignalValueFormat.octal => + '0o${logicValue.toRadixString(radix: 8, includeWidth: false, sepChar: '')}', + SignalValueFormat.ascii => String.fromCharCodes( + List.generate( + ((logicValue.width + 7) ~/ 8).clamp(1, 32), + (index) { + final shift = + (((logicValue.width + 7) ~/ 8).clamp(1, 32) - index - 1) * 8; + final code = + ((logicValue.toBigInt() >> shift) & BigInt.from(0xff)) + .toInt(); + return code >= 0x20 && code <= 0x7e ? code : 0x2e; + }, + ), + ), + SignalValueFormat.waveform => canonical, + }; + } + + static (LogicValue, String)? _waveformValue(String value, int width) { + final trimmed = value.trim().replaceAll('\u0000', ''); + if (trimmed.isEmpty || _containsUnknownDigits(trimmed)) { + return null; + } + final lower = trimmed.toLowerCase(); + final displayWidth = width > 0 ? width : 1; + final isRadixLiteral = RegExp(r"^\d+'[bqodh]").hasMatch(lower); + final digits = lower.startsWith('0x') || lower.startsWith('0b') + ? lower.substring(2) + : lower; + final radix = lower.startsWith('0x') + ? 'h' + : lower.startsWith('0b') + ? 'b' + : isRadixLiteral + ? lower[lower.indexOf("'") + 1] + : digits.codeUnits.every( + (codeUnit) => codeUnit == 0x30 || codeUnit == 0x31, + ) + ? 'b' + : digits.codeUnits.any( + (codeUnit) => + (codeUnit >= 0x61 && codeUnit <= 0x66) || + (codeUnit >= 0x41 && codeUnit <= 0x46), + ) + ? 'h' + : 'd'; + final radixLiteral = isRadixLiteral ? lower : "$displayWidth'$radix$digits"; + try { + final logicValue = LogicValue.ofRadixString(radixLiteral); + return ( + logicValue, + isRadixLiteral ? trimmed : logicValue.toString(), + ); + } on LogicValueConstructionException { + return null; + } + } + + static String _canonicalWaveformValue(String value, int width) { + final waveformValue = _waveformValue(value, width); + return waveformValue?.$2 ?? value.trim().replaceAll('\u0000', ''); + } +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml b/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml index dcb1b7a95..3f24d6f92 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml @@ -8,6 +8,8 @@ environment: dependencies: flutter: {sdk: flutter} rohd: ^0.6.9 + rohd_hierarchy: + path: ../../../packages/rohd_hierarchy web: ^1.0.0 dev_dependencies: flutter_test: {sdk: flutter} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart new file mode 100644 index 000000000..3b0eec1a7 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart @@ -0,0 +1,173 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_value_format_registry_test.dart +// Tests for shared signal display-format preferences and value formatting. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:flutter_test/flutter_test.dart'; +import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; + +void main() { + tearDown(SignalValueFormatRegistry.clear); + + test('formats bare binary and hexadecimal waveform values', () { + expect( + SignalValueFormatRegistry.formatValue( + '0000', + SignalValueFormat.waveform, + 4, + ), + "4'h0", + ); + expect( + SignalValueFormatRegistry.formatValue( + '11111111', + SignalValueFormat.signedDecimal, + 8, + ), + '-1', + ); + expect( + SignalValueFormatRegistry.formatValue( + '11111111', + SignalValueFormat.hexadecimal, + 8, + ), + "8'hff", + ); + expect( + SignalValueFormatRegistry.formatValue( + 'ff', + SignalValueFormat.unsignedDecimal, + 8, + ), + '255', + ); + expect( + SignalValueFormatRegistry.formatValue( + '0x0', + SignalValueFormat.unsignedDecimal, + 4, + ), + '0', + ); + }); + + test('uses ROHD radix literals for typed format conversions', () { + expect( + SignalValueFormatRegistry.formatValue( + "8'd255", + SignalValueFormat.hexadecimal, + 8, + ), + "8'hff", + ); + expect( + SignalValueFormatRegistry.formatValue( + '1010', + SignalValueFormat.octal, + 4, + ), + '0o12', + ); + expect( + SignalValueFormatRegistry.formatValue( + '0x4142', + SignalValueFormat.ascii, + 16, + ), + 'AB', + ); + }); + + test('looks up an occurrence address from the format trie', () { + SignalValueFormatRegistry.setFormatFor( + [ + const OccurrenceAddress([0, 2, 4]), + ], + SignalValueFormat.signedDecimal, + ); + + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 4])), + SignalValueFormat.signedDecimal, + ); + }); + + test('looks up a fallback occurrence address', () { + SignalValueFormatRegistry.setFormatFor( + [ + const OccurrenceAddress([0, 2, 4]), + ], + SignalValueFormat.unsignedDecimal, + ); + + expect( + SignalValueFormatRegistry.formatForAny([ + const OccurrenceAddress([7, 8, 9]), + const OccurrenceAddress([0, 2, 4]), + ]), + SignalValueFormat.unsignedDecimal, + ); + }); + + test('stores shared address prefixes once in the format trie', () { + SignalValueFormatRegistry.update([ + SignalValueFormatPreference( + const OccurrenceAddress([0, 2, 4]), + SignalValueFormat.unsignedDecimal, + ), + SignalValueFormatPreference( + const OccurrenceAddress([0, 2, 5]), + SignalValueFormat.signedDecimal, + ), + ]); + + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 4])), + SignalValueFormat.unsignedDecimal, + ); + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 5])), + SignalValueFormat.signedDecimal, + ); + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 6])), + SignalValueFormat.waveform, + ); + }); + + test('rejects an invalid signal occurrence address', () { + expect( + () => SignalValueFormatRegistry.setFormatFor( + const [OccurrenceAddress([])], + SignalValueFormat.unsignedDecimal, + ), + throwsArgumentError, + ); + expect( + () => SignalValueFormatRegistry.formatFor( + const OccurrenceAddress([0, -1]), + ), + throwsArgumentError, + ); + }); + + test('converts between serialized names and format enum values', () { + expect( + SignalValueFormatRegistry.formatFromString('signedDecimal'), + SignalValueFormat.signedDecimal, + ); + expect( + SignalValueFormatRegistry.formatToString( + SignalValueFormat.signedDecimal, + ), + 'signedDecimal', + ); + expect(SignalValueFormatRegistry.formatFromString('unknown'), isNull); + }); +} diff --git a/test/fixtures/gate_catalog.rohd.json b/test/fixtures/gate_catalog.rohd.json new file mode 100644 index 000000000..ffa54ef77 --- /dev/null +++ b/test/fixtures/gate_catalog.rohd.json @@ -0,0 +1,4773 @@ +{ + "creator": "NetlistSynthesizer (rohd)", + "version": "0.0.1", + "modules": { + "GateCatalog": { + "attributes": { + "src": "generated", + "top": 1 + }, + "ports": { + "clk": { + "direction": "input", + "bits": [ + 2 + ], + "logic_type": { + "width": 1 + } + }, + "en": { + "direction": "input", + "bits": [ + 3 + ], + "logic_type": { + "width": 1 + } + }, + "reset": { + "direction": "input", + "bits": [ + 4 + ], + "logic_type": { + "width": 1 + } + }, + "muxSel": { + "direction": "input", + "bits": [ + 5 + ], + "logic_type": { + "width": 1 + } + }, + "enableTri": { + "direction": "input", + "bits": [ + 6 + ], + "logic_type": { + "width": 1 + } + }, + "a4": { + "direction": "input", + "bits": [ + 7, + 8, + 9, + 10 + ], + "logic_type": { + "width": 4 + } + }, + "b4": { + "direction": "input", + "bits": [ + 11, + 12, + 13, + 14 + ], + "logic_type": { + "width": 4 + } + }, + "a8": { + "direction": "input", + "bits": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "logic_type": { + "width": 8 + } + }, + "b8": { + "direction": "input", + "bits": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "logic_type": { + "width": 8 + } + }, + "d4": { + "direction": "input", + "bits": [ + 31, + 32, + 33, + 34 + ], + "logic_type": { + "width": 4 + } + }, + "shamt4": { + "direction": "input", + "bits": [ + 35, + 36, + 37, + 38 + ], + "logic_type": { + "width": 4 + } + }, + "idx3": { + "direction": "input", + "bits": [ + 39, + 40, + 41 + ], + "logic_type": { + "width": 3 + } + }, + "idx5": { + "direction": "input", + "bits": [ + 42, + 43, + 44, + 45, + 46 + ], + "logic_type": { + "width": 5 + } + }, + "resetValueDyn4": { + "direction": "input", + "bits": [ + 47, + 48, + 49, + 50 + ], + "logic_type": { + "width": 4 + } + }, + "not_out": { + "direction": "output", + "bits": [ + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58 + ], + "logic_type": { + "width": 8 + } + }, + "and_ll_out": { + "direction": "output", + "bits": [ + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66 + ], + "logic_type": { + "width": 8 + } + }, + "and_lc_out": { + "direction": "output", + "bits": [ + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74 + ], + "logic_type": { + "width": 8 + } + }, + "or_ll_out": { + "direction": "output", + "bits": [ + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82 + ], + "logic_type": { + "width": 8 + } + }, + "or_lc_out": { + "direction": "output", + "bits": [ + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90 + ], + "logic_type": { + "width": 8 + } + }, + "xor_ll_out": { + "direction": "output", + "bits": [ + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98 + ], + "logic_type": { + "width": 8 + } + }, + "xor_lc_out": { + "direction": "output", + "bits": [ + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106 + ], + "logic_type": { + "width": 8 + } + }, + "reduce_and_out": { + "direction": "output", + "bits": [ + 107 + ], + "logic_type": { + "width": 1 + } + }, + "reduce_or_out": { + "direction": "output", + "bits": [ + 108 + ], + "logic_type": { + "width": 1 + } + }, + "reduce_xor_out": { + "direction": "output", + "bits": [ + 109 + ], + "logic_type": { + "width": 1 + } + }, + "add_ll_sum": { + "direction": "output", + "bits": [ + 110, + 111, + 112, + 113 + ], + "logic_type": { + "width": 4 + } + }, + "add_ll_carry": { + "direction": "output", + "bits": [ + 114 + ], + "logic_type": { + "width": 1 + } + }, + "add_lc_sum": { + "direction": "output", + "bits": [ + 115, + 116, + 117, + 118 + ], + "logic_type": { + "width": 4 + } + }, + "add_lc_carry": { + "direction": "output", + "bits": [ + 119 + ], + "logic_type": { + "width": 1 + } + }, + "sub_ll_out": { + "direction": "output", + "bits": [ + 120, + 121, + 122, + 123 + ], + "logic_type": { + "width": 4 + } + }, + "sub_lc_out": { + "direction": "output", + "bits": [ + 124, + 125, + 126, + 127 + ], + "logic_type": { + "width": 4 + } + }, + "mul_ll_out": { + "direction": "output", + "bits": [ + 128, + 129, + 130, + 131 + ], + "logic_type": { + "width": 4 + } + }, + "mul_lc_out": { + "direction": "output", + "bits": [ + 132, + 133, + 134, + 135 + ], + "logic_type": { + "width": 4 + } + }, + "div_ll_out": { + "direction": "output", + "bits": [ + 136, + 137, + 138, + 139 + ], + "logic_type": { + "width": 4 + } + }, + "div_lc_out": { + "direction": "output", + "bits": [ + 140, + 141, + 142, + 143 + ], + "logic_type": { + "width": 4 + } + }, + "mod_ll_out": { + "direction": "output", + "bits": [ + 144, + 145, + 146, + 147 + ], + "logic_type": { + "width": 4 + } + }, + "mod_lc_out": { + "direction": "output", + "bits": [ + 148, + 149, + 150, + 151 + ], + "logic_type": { + "width": 4 + } + }, + "pow_ll_out": { + "direction": "output", + "bits": [ + 152, + 153, + 154, + 155 + ], + "logic_type": { + "width": 4 + } + }, + "pow_lc_out": { + "direction": "output", + "bits": [ + 156, + 157, + 158, + 159 + ], + "logic_type": { + "width": 4 + } + }, + "eq_ll_out": { + "direction": "output", + "bits": [ + 160 + ], + "logic_type": { + "width": 1 + } + }, + "eq_lc_out": { + "direction": "output", + "bits": [ + 161 + ], + "logic_type": { + "width": 1 + } + }, + "neq_ll_out": { + "direction": "output", + "bits": [ + 162 + ], + "logic_type": { + "width": 1 + } + }, + "neq_lc_out": { + "direction": "output", + "bits": [ + 163 + ], + "logic_type": { + "width": 1 + } + }, + "lt_ll_out": { + "direction": "output", + "bits": [ + 164 + ], + "logic_type": { + "width": 1 + } + }, + "lt_lc_out": { + "direction": "output", + "bits": [ + 165 + ], + "logic_type": { + "width": 1 + } + }, + "gt_ll_out": { + "direction": "output", + "bits": [ + 166 + ], + "logic_type": { + "width": 1 + } + }, + "gt_lc_out": { + "direction": "output", + "bits": [ + 167 + ], + "logic_type": { + "width": 1 + } + }, + "le_ll_out": { + "direction": "output", + "bits": [ + 168 + ], + "logic_type": { + "width": 1 + } + }, + "le_lc_out": { + "direction": "output", + "bits": [ + 169 + ], + "logic_type": { + "width": 1 + } + }, + "ge_ll_out": { + "direction": "output", + "bits": [ + 170 + ], + "logic_type": { + "width": 1 + } + }, + "ge_lc_out": { + "direction": "output", + "bits": [ + 171 + ], + "logic_type": { + "width": 1 + } + }, + "lshift_ll_out": { + "direction": "output", + "bits": [ + 172, + 173, + 174, + 175 + ], + "logic_type": { + "width": 4 + } + }, + "lshift_lc_out": { + "direction": "output", + "bits": [ + 176, + 177, + 178, + 179 + ], + "logic_type": { + "width": 4 + } + }, + "rshift_ll_out": { + "direction": "output", + "bits": [ + 180, + 181, + 182, + 183 + ], + "logic_type": { + "width": 4 + } + }, + "rshift_lc_out": { + "direction": "output", + "bits": [ + 184, + 185, + 186, + 187 + ], + "logic_type": { + "width": 4 + } + }, + "arshift_ll_out": { + "direction": "output", + "bits": [ + 188, + 189, + 190, + 191 + ], + "logic_type": { + "width": 4 + } + }, + "arshift_lc_out": { + "direction": "output", + "bits": [ + 192, + 193, + 194, + 195 + ], + "logic_type": { + "width": 4 + } + }, + "mux_class_out": { + "direction": "output", + "bits": [ + 196, + 197, + 198, + 199 + ], + "logic_type": { + "width": 4 + } + }, + "mux_fn_dynamic_out": { + "direction": "output", + "bits": [ + 200, + 201, + 202, + 203 + ], + "logic_type": { + "width": 4 + } + }, + "mux_fn_const1_out": { + "direction": "output", + "bits": [ + 407, + 408, + 409, + 410 + ], + "logic_type": { + "width": 4 + } + }, + "mux_fn_const0_out": { + "direction": "output", + "bits": [ + 411, + 412, + 413, + 414 + ], + "logic_type": { + "width": 4 + } + }, + "index_natural_out": { + "direction": "output", + "bits": [ + 212 + ], + "logic_type": { + "width": 1 + } + }, + "index_oversized_out": { + "direction": "output", + "bits": [ + 213 + ], + "logic_type": { + "width": 1 + } + }, + "replicate_x3_out": { + "direction": "output", + "bits": [ + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225 + ], + "logic_type": { + "width": 12 + } + }, + "replicate_x5_out": { + "direction": "output", + "bits": [ + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245 + ], + "logic_type": { + "width": 20 + } + }, + "slice_out": { + "direction": "output", + "bits": [ + 246, + 247, + 248, + 249 + ], + "logic_type": { + "width": 4 + } + }, + "swizzle_out": { + "direction": "output", + "bits": [ + 250, + 251, + 252, + 253, + 254, + 255, + 256, + 257 + ], + "logic_type": { + "width": 8 + } + }, + "tribuf_readback_out": { + "direction": "output", + "bits": [ + 415, + 416, + 417, + 418, + 419, + 420, + 421, + 422 + ], + "logic_type": { + "width": 8 + } + }, + "q_dff": { + "direction": "output", + "bits": [ + 266, + 267, + 268, + 269 + ], + "logic_type": { + "width": 4 + } + }, + "q_dffe": { + "direction": "output", + "bits": [ + 270, + 271, + 272, + 273 + ], + "logic_type": { + "width": 4 + } + }, + "q_sdff": { + "direction": "output", + "bits": [ + 274, + 275, + 276, + 277 + ], + "logic_type": { + "width": 4 + } + }, + "q_sdffe": { + "direction": "output", + "bits": [ + 278, + 279, + 280, + 281 + ], + "logic_type": { + "width": 4 + } + }, + "q_adff": { + "direction": "output", + "bits": [ + 282, + 283, + 284, + 285 + ], + "logic_type": { + "width": 4 + } + }, + "q_adffe": { + "direction": "output", + "bits": [ + 286, + 287, + 288, + 289 + ], + "logic_type": { + "width": 4 + } + }, + "q_aldff": { + "direction": "output", + "bits": [ + 290, + 291, + 292, + 293 + ], + "logic_type": { + "width": 4 + } + }, + "q_aldffe": { + "direction": "output", + "bits": [ + 294, + 295, + 296, + 297 + ], + "logic_type": { + "width": 4 + } + }, + "q_dynsync_noen": { + "direction": "output", + "bits": [ + 298, + 299, + 300, + 301 + ], + "logic_type": { + "width": 4 + } + }, + "q_dynsync_en": { + "direction": "output", + "bits": [ + 302, + 303, + 304, + 305 + ], + "logic_type": { + "width": 4 + } + }, + "bus": { + "direction": "inout", + "bits": [ + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313 + ], + "logic_type": { + "width": 8 + } + } + }, + "cells": { + "not_": { + "hide_name": 0, + "type": "$not", + "parameters": { + "A_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58 + ] + } + }, + "and_": { + "hide_name": 0, + "type": "$and", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "Y": [ + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66 + ] + } + }, + "and__0": { + "hide_name": 0, + "type": "$and", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321 + ], + "Y": [ + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74 + ] + } + }, + "or_": { + "hide_name": 0, + "type": "$or", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "Y": [ + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82 + ] + } + }, + "or__0": { + "hide_name": 0, + "type": "$or", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329 + ], + "Y": [ + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90 + ] + } + }, + "xor_": { + "hide_name": 0, + "type": "$xor", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "Y": [ + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98 + ] + } + }, + "xor__0": { + "hide_name": 0, + "type": "$xor", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 337 + ], + "Y": [ + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106 + ] + } + }, + "uand": { + "hide_name": 0, + "type": "$reduce_and", + "parameters": { + "A_WIDTH": 8, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 107 + ] + } + }, + "uor": { + "hide_name": 0, + "type": "$reduce_or", + "parameters": { + "A_WIDTH": 8, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 108 + ] + } + }, + "uxor": { + "hide_name": 0, + "type": "$reduce_xor", + "parameters": { + "A_WIDTH": 8, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 109 + ] + } + }, + "add": { + "hide_name": 0, + "type": "$add", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 5 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 110, + 111, + 112, + 113, + 114 + ] + } + }, + "add_0": { + "hide_name": 0, + "type": "$add", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 5 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 338, + 339, + 340, + 341 + ], + "Y": [ + 115, + 116, + 117, + 118, + 119 + ] + } + }, + "subtract": { + "hide_name": 0, + "type": "$sub", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 120, + 121, + 122, + 123 + ] + } + }, + "subtract_0": { + "hide_name": 0, + "type": "$sub", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 342, + 343, + 344, + 345 + ], + "Y": [ + 124, + 125, + 126, + 127 + ] + } + }, + "multiply": { + "hide_name": 0, + "type": "$mul", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 128, + 129, + 130, + 131 + ] + } + }, + "multiply_0": { + "hide_name": 0, + "type": "$mul", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 346, + 347, + 348, + 349 + ], + "Y": [ + 132, + 133, + 134, + 135 + ] + } + }, + "divide": { + "hide_name": 0, + "type": "$div", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 136, + 137, + 138, + 139 + ] + } + }, + "divide_0": { + "hide_name": 0, + "type": "$div", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 350, + 351, + 352, + 353 + ], + "Y": [ + 140, + 141, + 142, + 143 + ] + } + }, + "modulo": { + "hide_name": 0, + "type": "$mod", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 144, + 145, + 146, + 147 + ] + } + }, + "modulo_0": { + "hide_name": 0, + "type": "$mod", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 354, + 355, + 356, + 357 + ], + "Y": [ + 148, + 149, + 150, + 151 + ] + } + }, + "power": { + "hide_name": 0, + "type": "$pow", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 152, + 153, + 154, + 155 + ] + } + }, + "power_0": { + "hide_name": 0, + "type": "$pow", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 358, + 359, + 360, + 361 + ], + "Y": [ + 156, + 157, + 158, + 159 + ] + } + }, + "equals": { + "hide_name": 0, + "type": "$eq", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 160 + ] + } + }, + "equals_0": { + "hide_name": 0, + "type": "$eq", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 362, + 363, + 364, + 365 + ], + "Y": [ + 161 + ] + } + }, + "notEquals": { + "hide_name": 0, + "type": "$ne", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 162 + ] + } + }, + "notEquals_0": { + "hide_name": 0, + "type": "$ne", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 366, + 367, + 368, + 369 + ], + "Y": [ + 163 + ] + } + }, + "lessthan": { + "hide_name": 0, + "type": "$lt", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 164 + ] + } + }, + "lessthan_0": { + "hide_name": 0, + "type": "$lt", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 370, + 371, + 372, + 373 + ], + "Y": [ + 165 + ] + } + }, + "greaterThan": { + "hide_name": 0, + "type": "$gt", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 166 + ] + } + }, + "greaterThan_0": { + "hide_name": 0, + "type": "$gt", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 374, + 375, + 376, + 377 + ], + "Y": [ + 167 + ] + } + }, + "lessThanOrEqual": { + "hide_name": 0, + "type": "$le", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 168 + ] + } + }, + "lessThanOrEqual_0": { + "hide_name": 0, + "type": "$le", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 378, + 379, + 380, + 381 + ], + "Y": [ + 169 + ] + } + }, + "greaterThanOrEqual": { + "hide_name": 0, + "type": "$ge", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 170 + ] + } + }, + "greaterThanOrEqual_0": { + "hide_name": 0, + "type": "$ge", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 382, + 383, + 384, + 385 + ], + "Y": [ + 171 + ] + } + }, + "lshift": { + "hide_name": 0, + "type": "$shl", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 35, + 36, + 37, + 38 + ], + "Y": [ + 172, + 173, + 174, + 175 + ] + } + }, + "lshift_0": { + "hide_name": 0, + "type": "$shl", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 403, + 404, + 405, + 406 + ], + "Y": [ + 176, + 177, + 178, + 179 + ] + } + }, + "rshift": { + "hide_name": 0, + "type": "$shr", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 35, + 36, + 37, + 38 + ], + "Y": [ + 180, + 181, + 182, + 183 + ] + } + }, + "rshift_0": { + "hide_name": 0, + "type": "$shr", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 2, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 390, + 391 + ], + "Y": [ + 184, + 185, + 186, + 187 + ] + } + }, + "arshift": { + "hide_name": 0, + "type": "$sshr", + "parameters": { + "A_SIGNED": 1, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 35, + 36, + 37, + 38 + ], + "Y": [ + 188, + 189, + 190, + 191 + ] + } + }, + "arshift_0": { + "hide_name": 0, + "type": "$sshr", + "parameters": { + "A_SIGNED": 1, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 2, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 392, + 393 + ], + "Y": [ + 192, + 193, + 194, + 195 + ] + } + }, + "mux": { + "hide_name": 0, + "type": "$mux", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "S": "input", + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "S": [ + 5 + ], + "A": [ + 11, + 12, + 13, + 14 + ], + "B": [ + 7, + 8, + 9, + 10 + ], + "Y": [ + 196, + 197, + 198, + 199 + ] + } + }, + "mux_0": { + "hide_name": 0, + "type": "$mux", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "S": "input", + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "S": [ + 5 + ], + "A": [ + 11, + 12, + 13, + 14 + ], + "B": [ + 7, + 8, + 9, + 10 + ], + "Y": [ + 200, + 201, + 202, + 203 + ] + } + }, + "unnamed_module": { + "hide_name": 0, + "type": "$shiftx", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 8, + "B_SIGNED": 0, + "B_WIDTH": 3, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 39, + 40, + 41 + ], + "Y": [ + 212 + ] + } + }, + "unnamed_module_0": { + "hide_name": 0, + "type": "$shiftx", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 8, + "B_SIGNED": 0, + "B_WIDTH": 5, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 42, + 43, + 44, + 45, + 46 + ], + "Y": [ + 213 + ] + } + }, + "unnamed_module_1": { + "hide_name": 0, + "type": "ReplicationOp", + "parameters": {}, + "attributes": {}, + "port_directions": { + "_a4": "input", + "_replicated_a4": "output" + }, + "connections": { + "_a4": [ + 7, + 8, + 9, + 10 + ], + "_replicated_a4": [ + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225 + ] + } + }, + "unnamed_module_2": { + "hide_name": 0, + "type": "ReplicationOp", + "parameters": {}, + "attributes": {}, + "port_directions": { + "_a4": "input", + "_replicated_a4": "output" + }, + "connections": { + "_a4": [ + 7, + 8, + 9, + 10 + ], + "_replicated_a4": [ + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245 + ] + } + }, + "bussubset": { + "hide_name": 0, + "type": "$slice", + "parameters": { + "OFFSET": 2, + "A_WIDTH": 8, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 246, + 247, + 248, + 249 + ] + } + }, + "swizzle": { + "hide_name": 0, + "type": "$concat", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 11, + 12, + 13, + 14 + ], + "B": [ + 7, + 8, + 9, + 10 + ], + "Y": [ + 250, + 251, + 252, + 253, + 254, + 255, + 256, + 257 + ] + } + }, + "flipflop": { + "hide_name": 0, + "type": "$dff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 266, + 267, + 268, + 269 + ] + } + }, + "flipflop_0": { + "hide_name": 0, + "type": "$dffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 270, + 271, + 272, + 273 + ], + "EN": [ + 3 + ] + } + }, + "flipflop_1": { + "hide_name": 0, + "type": "$sdff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "SRST_POLARITY": 1, + "SRST_VALUE": "1001" + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "SRST": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 274, + 275, + 276, + 277 + ], + "SRST": [ + 4 + ] + } + }, + "flipflop_2": { + "hide_name": 0, + "type": "$sdffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1, + "SRST_POLARITY": 1, + "SRST_VALUE": "1001" + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input", + "SRST": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 278, + 279, + 280, + 281 + ], + "EN": [ + 3 + ], + "SRST": [ + 4 + ] + } + }, + "flipflop_3": { + "hide_name": 0, + "type": "$adff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "ARST_POLARITY": 1, + "ARST_VALUE": "1001" + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "ARST": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 282, + 283, + 284, + 285 + ], + "ARST": [ + 4 + ] + } + }, + "flipflop_4": { + "hide_name": 0, + "type": "$adffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1, + "ARST_POLARITY": 1, + "ARST_VALUE": "1001" + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input", + "ARST": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 286, + 287, + 288, + 289 + ], + "EN": [ + 3 + ], + "ARST": [ + 4 + ] + } + }, + "flipflop_5": { + "hide_name": 0, + "type": "$aldff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "ALOAD_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "ALOAD": "input", + "AD": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 290, + 291, + 292, + 293 + ], + "ALOAD": [ + 4 + ], + "AD": [ + 47, + 48, + 49, + 50 + ] + } + }, + "flipflop_6": { + "hide_name": 0, + "type": "$aldffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1, + "ALOAD_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input", + "ALOAD": "input", + "AD": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 294, + 295, + 296, + 297 + ], + "EN": [ + 3 + ], + "ALOAD": [ + 4 + ], + "AD": [ + 47, + 48, + 49, + 50 + ] + } + }, + "flipflop_7_reset_mux": { + "hide_name": 0, + "type": "$mux", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "S": "input", + "Y": "output" + }, + "connections": { + "A": [ + 31, + 32, + 33, + 34 + ], + "B": [ + 47, + 48, + 49, + 50 + ], + "S": [ + 4 + ], + "Y": [ + 394, + 395, + 396, + 397 + ] + } + }, + "flipflop_7": { + "hide_name": 0, + "type": "$dff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 394, + 395, + 396, + 397 + ], + "Q": [ + 298, + 299, + 300, + 301 + ] + } + }, + "flipflop_8_reset_mux": { + "hide_name": 0, + "type": "$mux", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "S": "input", + "Y": "output" + }, + "connections": { + "A": [ + 31, + 32, + 33, + 34 + ], + "B": [ + 47, + 48, + 49, + 50 + ], + "S": [ + 4 + ], + "Y": [ + 398, + 399, + 400, + 401 + ] + } + }, + "flipflop_8_reset_enable": { + "hide_name": 0, + "type": "$or", + "parameters": { + "A_WIDTH": 1, + "B_WIDTH": 1, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 3 + ], + "B": [ + 4 + ], + "Y": [ + 402 + ] + } + }, + "flipflop_8": { + "hide_name": 0, + "type": "$dffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 398, + 399, + 400, + 401 + ], + "Q": [ + 302, + 303, + 304, + 305 + ], + "EN": [ + 402 + ] + } + }, + "tsb": { + "hide_name": 0, + "type": "$tribuf", + "parameters": { + "WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "EN": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "EN": [ + 6 + ], + "Y": [ + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313 + ] + } + }, + "passthrough_buf_0": { + "hide_name": 0, + "type": "$buf", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "Y": [ + 407, + 408, + 409, + 410 + ] + } + }, + "passthrough_buf_1": { + "hide_name": 0, + "type": "$buf", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 411, + 412, + 413, + 414 + ] + } + }, + "passthrough_buf_2": { + "hide_name": 0, + "type": "$buf", + "parameters": { + "WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313 + ], + "Y": [ + 415, + 416, + 417, + 418, + 419, + 420, + 421, + 422 + ] + } + }, + "const_0_8_haa": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "8'haa": "output" + }, + "connections": { + "8'haa": [ + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321 + ] + } + }, + "const_1_8_h55": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "8'h55": "output" + }, + "connections": { + "8'h55": [ + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329 + ] + } + }, + "const_2_8_hf": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "8'hf": "output" + }, + "connections": { + "8'hf": [ + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 337 + ] + } + }, + "const_3_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 338, + 339, + 340, + 341 + ] + } + }, + "const_4_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 342, + 343, + 344, + 345 + ] + } + }, + "const_5_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 346, + 347, + 348, + 349 + ] + } + }, + "const_6_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 350, + 351, + 352, + 353 + ] + } + }, + "const_7_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 354, + 355, + 356, + 357 + ] + } + }, + "const_8_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 358, + 359, + 360, + 361 + ] + } + }, + "const_9_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 362, + 363, + 364, + 365 + ] + } + }, + "const_10_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 366, + 367, + 368, + 369 + ] + } + }, + "const_11_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 370, + 371, + 372, + 373 + ] + } + }, + "const_12_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 374, + 375, + 376, + 377 + ] + } + }, + "const_13_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 378, + 379, + 380, + 381 + ] + } + }, + "const_14_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 382, + 383, + 384, + 385 + ] + } + }, + "const_15_4_h2": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h2": "output" + }, + "connections": { + "4'h2": [ + 403, + 404, + 405, + 406 + ] + } + }, + "const_16_2_h2": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "2'h2": "output" + }, + "connections": { + "2'h2": [ + 390, + 391 + ] + } + }, + "const_17_2_h2": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "2'h2": "output" + }, + "connections": { + "2'h2": [ + 392, + 393 + ] + } + } + }, + "netnames": { + "clk": { + "bits": [ + 2 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "en": { + "bits": [ + 3 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "reset": { + "bits": [ + 4 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "muxSel": { + "bits": [ + 5 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "enableTri": { + "bits": [ + 6 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "a4": { + "bits": [ + 7, + 8, + 9, + 10 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "b4": { + "bits": [ + 11, + 12, + 13, + 14 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "a8": { + "bits": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "b8": { + "bits": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "d4": { + "bits": [ + 31, + 32, + 33, + 34 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "shamt4": { + "bits": [ + 35, + 36, + 37, + 38 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "idx3": { + "bits": [ + 39, + 40, + 41 + ], + "logic_type": { + "width": 3 + }, + "attributes": {} + }, + "idx5": { + "bits": [ + 42, + 43, + 44, + 45, + 46 + ], + "logic_type": { + "width": 5 + }, + "attributes": {} + }, + "resetValueDyn4": { + "bits": [ + 47, + 48, + 49, + 50 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "not_out": { + "bits": [ + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "and_ll_out": { + "bits": [ + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "and_lc_out": { + "bits": [ + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "or_ll_out": { + "bits": [ + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "or_lc_out": { + "bits": [ + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "xor_ll_out": { + "bits": [ + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "xor_lc_out": { + "bits": [ + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "reduce_and_out": { + "bits": [ + 107 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "reduce_or_out": { + "bits": [ + 108 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "reduce_xor_out": { + "bits": [ + 109 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "add_ll_sum": { + "bits": [ + 110, + 111, + 112, + 113 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "add_ll_carry": { + "bits": [ + 114 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "add_lc_sum": { + "bits": [ + 115, + 116, + 117, + 118 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "add_lc_carry": { + "bits": [ + 119 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "sub_ll_out": { + "bits": [ + 120, + 121, + 122, + 123 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "sub_lc_out": { + "bits": [ + 124, + 125, + 126, + 127 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mul_ll_out": { + "bits": [ + 128, + 129, + 130, + 131 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mul_lc_out": { + "bits": [ + 132, + 133, + 134, + 135 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "div_ll_out": { + "bits": [ + 136, + 137, + 138, + 139 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "div_lc_out": { + "bits": [ + 140, + 141, + 142, + 143 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mod_ll_out": { + "bits": [ + 144, + 145, + 146, + 147 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mod_lc_out": { + "bits": [ + 148, + 149, + 150, + 151 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "pow_ll_out": { + "bits": [ + 152, + 153, + 154, + 155 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "pow_lc_out": { + "bits": [ + 156, + 157, + 158, + 159 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "eq_ll_out": { + "bits": [ + 160 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "eq_lc_out": { + "bits": [ + 161 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "neq_ll_out": { + "bits": [ + 162 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "neq_lc_out": { + "bits": [ + 163 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "lt_ll_out": { + "bits": [ + 164 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "lt_lc_out": { + "bits": [ + 165 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "gt_ll_out": { + "bits": [ + 166 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "gt_lc_out": { + "bits": [ + 167 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "le_ll_out": { + "bits": [ + 168 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "le_lc_out": { + "bits": [ + 169 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "ge_ll_out": { + "bits": [ + 170 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "ge_lc_out": { + "bits": [ + 171 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "lshift_ll_out": { + "bits": [ + 172, + 173, + 174, + 175 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "lshift_lc_out": { + "bits": [ + 176, + 177, + 178, + 179 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "rshift_ll_out": { + "bits": [ + 180, + 181, + 182, + 183 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "rshift_lc_out": { + "bits": [ + 184, + 185, + 186, + 187 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "arshift_ll_out": { + "bits": [ + 188, + 189, + 190, + 191 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "arshift_lc_out": { + "bits": [ + 192, + 193, + 194, + 195 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mux_class_out": { + "bits": [ + 196, + 197, + 198, + 199 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mux_fn_dynamic_out": { + "bits": [ + 200, + 201, + 202, + 203 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mux_fn_const1_out": { + "bits": [ + 407, + 408, + 409, + 410 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mux_fn_const0_out": { + "bits": [ + 411, + 412, + 413, + 414 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "index_natural_out": { + "bits": [ + 212 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "index_oversized_out": { + "bits": [ + 213 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "replicate_x3_out": { + "bits": [ + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225 + ], + "logic_type": { + "width": 12 + }, + "attributes": {} + }, + "replicate_x5_out": { + "bits": [ + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245 + ], + "logic_type": { + "width": 20 + }, + "attributes": {} + }, + "slice_out": { + "bits": [ + 246, + 247, + 248, + 249 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "swizzle_out": { + "bits": [ + 250, + 251, + 252, + 253, + 254, + 255, + 256, + 257 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "tribuf_readback_out": { + "bits": [ + 415, + 416, + 417, + 418, + 419, + 420, + 421, + 422 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "q_dff": { + "bits": [ + 266, + 267, + 268, + 269 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_dffe": { + "bits": [ + 270, + 271, + 272, + 273 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_sdff": { + "bits": [ + 274, + 275, + 276, + 277 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_sdffe": { + "bits": [ + 278, + 279, + 280, + 281 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_adff": { + "bits": [ + 282, + 283, + 284, + 285 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_adffe": { + "bits": [ + 286, + 287, + 288, + 289 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_aldff": { + "bits": [ + 290, + 291, + 292, + 293 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_aldffe": { + "bits": [ + 294, + 295, + 296, + 297 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_dynsync_noen": { + "bits": [ + 298, + 299, + 300, + 301 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_dynsync_en": { + "bits": [ + 302, + 303, + 304, + 305 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "bus": { + "bits": [ + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "const_0_8_haa": { + "bits": [ + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321 + ], + "attributes": { + "computed": 1 + } + }, + "const_1_8_h55": { + "bits": [ + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329 + ], + "attributes": { + "computed": 1 + } + }, + "const_2_8_hf": { + "bits": [ + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 337 + ], + "attributes": { + "computed": 1 + } + }, + "const_3_4_h5": { + "bits": [ + 338, + 339, + 340, + 341 + ], + "attributes": { + "computed": 1 + } + }, + "const_4_4_h3": { + "bits": [ + 342, + 343, + 344, + 345 + ], + "attributes": { + "computed": 1 + } + }, + "const_5_4_h3": { + "bits": [ + 346, + 347, + 348, + 349 + ], + "attributes": { + "computed": 1 + } + }, + "const_6_4_h3": { + "bits": [ + 350, + 351, + 352, + 353 + ], + "attributes": { + "computed": 1 + } + }, + "const_7_4_h3": { + "bits": [ + 354, + 355, + 356, + 357 + ], + "attributes": { + "computed": 1 + } + }, + "const_8_4_h3": { + "bits": [ + 358, + 359, + 360, + 361 + ], + "attributes": { + "computed": 1 + } + }, + "const_9_4_h5": { + "bits": [ + 362, + 363, + 364, + 365 + ], + "attributes": { + "computed": 1 + } + }, + "const_10_4_h5": { + "bits": [ + 366, + 367, + 368, + 369 + ], + "attributes": { + "computed": 1 + } + }, + "const_11_4_h5": { + "bits": [ + 370, + 371, + 372, + 373 + ], + "attributes": { + "computed": 1 + } + }, + "const_12_4_h5": { + "bits": [ + 374, + 375, + 376, + 377 + ], + "attributes": { + "computed": 1 + } + }, + "const_13_4_h5": { + "bits": [ + 378, + 379, + 380, + 381 + ], + "attributes": { + "computed": 1 + } + }, + "const_14_4_h5": { + "bits": [ + 382, + 383, + 384, + 385 + ], + "attributes": { + "computed": 1 + } + }, + "const_15_4_h2": { + "bits": [ + 403, + 404, + 405, + 406 + ], + "attributes": { + "computed": 1 + } + }, + "const_16_2_h2": { + "bits": [ + 390, + 391 + ], + "attributes": { + "computed": 1 + } + }, + "const_17_2_h2": { + "bits": [ + 392, + 393 + ], + "attributes": { + "computed": 1 + } + }, + "flipflop_7_reset_mux_Y": { + "bits": [ + 394, + 395, + 396, + 397 + ], + "hide_name": 1, + "attributes": {} + }, + "flipflop_8_reset_mux_Y": { + "bits": [ + 398, + 399, + 400, + 401 + ], + "hide_name": 1, + "attributes": {} + }, + "flipflop_8_reset_enable_Y": { + "bits": [ + 402 + ], + "hide_name": 1, + "attributes": {} + } + } + } + } +} \ No newline at end of file diff --git a/test/fixtures/gate_catalog_module.dart b/test/fixtures/gate_catalog_module.dart new file mode 100644 index 000000000..ccc6fe95e --- /dev/null +++ b/test/fixtures/gate_catalog_module.dart @@ -0,0 +1,207 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// gate_catalog_module.dart +// A single ROHD module that instantiates every public gate API in +// `lib/src/modules/gates.dart` (plus a few closely related primitives: +// FlipFlop variants, TriStateBuffer, BusSubset, and Swizzle) so that the +// netlist synthesizer's cell-mapper coverage can be captured in one +// deterministic, checked-in JSON asset. +// +// Every instantiated gate's output (or, for multi-output gates, every +// output) is wired directly to a uniquely named top-level output port. This +// guarantees dead-cell elimination cannot prune any of the cells this file +// is meant to exercise. +// +// See `test/gate_catalog_test.dart` for the test that verifies the checked-in +// `test/fixtures/gate_catalog.rohd.json` asset still matches what this module +// produces, and `tool/generate_gate_catalog.dart` for the script that +// (re)generates that asset. +// +// 2026 August 20 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// A gate-catalog top-level module. +/// +/// Instantiates one (or a small number of representative variants) of every +/// gate [Module] and top-level gate-building function exposed by +/// `lib/src/modules/gates.dart`, along with [FlipFlop] (in all mapper- +/// supported configurations), [TriStateBuffer], [BusSubset], and [Swizzle]. +/// +/// All inputs are plain free (unconnected) top-level signals; this module is +/// intended purely for structural (netlist) synthesis, not simulation. +class GateCatalog extends Module { + /// Creates the gate catalog module. + /// + /// All inputs are supplied by the caller so that construction is fully + /// deterministic and repeatable byte-for-byte across runs. + GateCatalog({ + required Logic clk, + required Logic en, + required Logic reset, + required Logic muxSel, + required Logic enableTri, + required Logic a4, + required Logic b4, + required Logic a8, + required Logic b8, + required Logic d4, + required Logic shamt4, + required Logic idx3, + required Logic idx5, + required Logic resetValueDyn4, + required LogicNet busNet, + }) : super(name: 'gate_catalog', definitionName: 'GateCatalog') { + clk = addInput('clk', clk); + en = addInput('en', en); + reset = addInput('reset', reset); + muxSel = addInput('muxSel', muxSel); + enableTri = addInput('enableTri', enableTri); + a4 = addInput('a4', a4, width: 4); + b4 = addInput('b4', b4, width: 4); + a8 = addInput('a8', a8, width: 8); + b8 = addInput('b8', b8, width: 8); + d4 = addInput('d4', d4, width: 4); + shamt4 = addInput('shamt4', shamt4, width: 4); + idx3 = addInput('idx3', idx3, width: 3); + idx5 = addInput('idx5', idx5, width: 5); + resetValueDyn4 = addInput('resetValueDyn4', resetValueDyn4, width: 4); + final bus = addInOut('bus', busNet, width: 8); + + // ── NotGate → $not ─────────────────────────────────────────────── + addOutput('not_out', width: 8) <= ~a8; + + // ── And2Gate → $and (logic/logic and logic/const variants) ─────── + addOutput('and_ll_out', width: 8) <= a8 & b8; + addOutput('and_lc_out', width: 8) <= + And2Gate(a8, Const(0xaa, width: 8)).out; + + // ── Or2Gate → $or (logic/logic and logic/const variants) ───────── + addOutput('or_ll_out', width: 8) <= a8 | b8; + addOutput('or_lc_out', width: 8) <= Or2Gate(a8, Const(0x55, width: 8)).out; + + // ── Xor2Gate → $xor (logic/logic and logic/const variants) ─────── + addOutput('xor_ll_out', width: 8) <= a8 ^ b8; + addOutput('xor_lc_out', width: 8) <= + Xor2Gate(a8, Const(0x0f, width: 8)).out; + + // ── Unary reductions → $reduce_and / $reduce_or / $reduce_xor ───── + addOutput('reduce_and_out') <= a8.and(); + addOutput('reduce_or_out') <= a8.or(); + addOutput('reduce_xor_out') <= a8.xor(); + + // ── Add → $add (logic/logic and logic/const variants) ──────────── + final addLl = Add(a4, b4); + addOutput('add_ll_sum', width: 4) <= addLl.sum; + addOutput('add_ll_carry') <= addLl.carry; + final addLc = Add(a4, 5); + addOutput('add_lc_sum', width: 4) <= addLc.sum; + addOutput('add_lc_carry') <= addLc.carry; + + // ── Subtract → $sub (logic/logic and logic/const variants) ─────── + addOutput('sub_ll_out', width: 4) <= a4 - b4; + addOutput('sub_lc_out', width: 4) <= a4 - 3; + + // ── Multiply → $mul (logic/logic and logic/const variants) ─────── + addOutput('mul_ll_out', width: 4) <= a4 * b4; + addOutput('mul_lc_out', width: 4) <= a4 * 3; + + // ── Divide → $div (logic/logic and logic/const variants) ───────── + addOutput('div_ll_out', width: 4) <= a4 / b4; + addOutput('div_lc_out', width: 4) <= a4 / 3; + + // ── Modulo → $mod (logic/logic and logic/const variants) ───────── + addOutput('mod_ll_out', width: 4) <= a4 % b4; + addOutput('mod_lc_out', width: 4) <= a4 % 3; + + // ── Power → $pow (logic/logic and logic/const variants) ────────── + addOutput('pow_ll_out', width: 4) <= a4.pow(b4); + addOutput('pow_lc_out', width: 4) <= a4.pow(3); + + // ── Comparisons → $eq/$ne/$lt/$gt/$le/$ge ───────────────────────── + addOutput('eq_ll_out') <= a4.eq(b4); + addOutput('eq_lc_out') <= a4.eq(5); + addOutput('neq_ll_out') <= a4.neq(b4); + addOutput('neq_lc_out') <= a4.neq(5); + addOutput('lt_ll_out') <= a4.lt(b4); + addOutput('lt_lc_out') <= a4.lt(5); + addOutput('gt_ll_out') <= (a4 > b4); + addOutput('gt_lc_out') <= (a4 > 5); + addOutput('le_ll_out') <= a4.lte(b4); + addOutput('le_lc_out') <= a4.lte(5); + addOutput('ge_ll_out') <= (a4 >= b4); + addOutput('ge_lc_out') <= (a4 >= 5); + + // ── Shifts → $shl / $shr / $sshr (dynamic and constant amounts) ── + addOutput('lshift_ll_out', width: 4) <= LShift(a4, shamt4).out; + addOutput('lshift_lc_out', width: 4) <= LShift(a4, 2).out; + addOutput('rshift_ll_out', width: 4) <= RShift(a4, shamt4).out; + addOutput('rshift_lc_out', width: 4) <= RShift(a4, 2).out; + addOutput('arshift_ll_out', width: 4) <= ARShift(a4, shamt4).out; + addOutput('arshift_lc_out', width: 4) <= ARShift(a4, 2).out; + + // ── Mux / mux() ──────────────────────────────────────────────── + // + // Dynamic control ⇒ a real `$mux` cell is instantiated. + addOutput('mux_class_out', width: 4) <= Mux(muxSel, a4, b4).out; + addOutput('mux_fn_dynamic_out', width: 4) <= mux(muxSel, a4, b4); + + // Constant, valid control ⇒ `mux()` folds to the selected input + // directly at *build* time: no `$mux` cell is instantiated for these two + // outputs at all. This documents/validates the function-level constant + // fold described by `mux()`'s doc comment. These outputs are wired + // directly to `a4`/`b4` (via a `$buf`-shaped netlist alias, if any) with + // no arithmetic/select cell in between. + addOutput('mux_fn_const1_out', width: 4) <= mux(Const(1, width: 1), a4, b4); + addOutput('mux_fn_const0_out', width: 4) <= mux(Const(0, width: 1), a4, b4); + + // ── IndexGate → $shiftx (natural and oversized index widths) ───── + addOutput('index_natural_out') <= a8[idx3]; + addOutput('index_oversized_out') <= a8[idx5]; + + // ── ReplicationOp (retained as an explicit, unmapped cell; no + // standard Yosys `$concat`/`$pos`-style cell models replication of a + // single dynamic operand cleanly, so it is intentionally left visible + // as its own `ReplicationOp`-typed cell rather than force-mapped) ──── + addOutput('replicate_x3_out', width: 12) <= a4.replicate(3); + addOutput('replicate_x5_out', width: 20) <= a4.replicate(5); + + // ── BusSubset → $slice ──────────────────────────────────────────── + addOutput('slice_out', width: 4) <= a8.getRange(2, 6); + + // ── Swizzle → $concat ───────────────────────────────────────────── + addOutput('swizzle_out', width: 8) <= [a4, b4].swizzle(); + + // ── TriStateBuffer → $tribuf ─────────────────────────────────────── + TriStateBuffer(a8, enable: enableTri, name: 'tsb').out.gets(bus); + addOutput('tribuf_readback_out', width: 8) <= bus; + + // ── FlipFlop variants → $dff/$dffe/$sdff/$sdffe/$adff/$adffe/ + // $aldff/$aldffe, plus dynamic-synchronous-reset lowering ──────── + addOutput('q_dff', width: 4) <= flop(clk, d4); + addOutput('q_dffe', width: 4) <= flop(clk, d4, en: en); + addOutput('q_sdff', width: 4) <= flop(clk, d4, reset: reset, resetValue: 9); + addOutput('q_sdffe', width: 4) <= + flop(clk, d4, en: en, reset: reset, resetValue: 9); + addOutput('q_adff', width: 4) <= + flop(clk, d4, reset: reset, resetValue: 9, asyncReset: true); + addOutput('q_adffe', width: 4) <= + flop(clk, d4, en: en, reset: reset, resetValue: 9, asyncReset: true); + addOutput('q_aldff', width: 4) <= + flop(clk, d4, + reset: reset, resetValue: resetValueDyn4, asyncReset: true); + addOutput('q_aldffe', width: 4) <= + flop(clk, d4, + en: en, reset: reset, resetValue: resetValueDyn4, asyncReset: true); + // Dynamic synchronous reset value: `$sdff`/`$sdffe` require a *constant* + // reset value, so the netlist translator lowers these to a `$mux` + // (selecting the reset value) feeding a plain `$dff`/`$dffe` (the enable + // ORed with reset so reset retains priority when enabled). + addOutput('q_dynsync_noen', width: 4) <= + flop(clk, d4, reset: reset, resetValue: resetValueDyn4); + addOutput('q_dynsync_en', width: 4) <= + flop(clk, d4, en: en, reset: reset, resetValue: resetValueDyn4); + } +} diff --git a/test/gate_catalog_test.dart b/test/gate_catalog_test.dart new file mode 100644 index 000000000..965decb45 --- /dev/null +++ b/test/gate_catalog_test.dart @@ -0,0 +1,289 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// gate_catalog_test.dart +// Verifies that the checked-in gate-catalog netlist asset +// (`test/fixtures/gate_catalog.rohd.json`) is byte-for-byte reproducible from +// `GateCatalog` (see `test/fixtures/gate_catalog_module.dart`), and spot +// checks coverage of every gate API this catalog is meant to exercise. +// +// If this test fails only because of an intentional change to gate lowering +// or the netlist cell mapper, regenerate the asset with: +// dart run tool/generate_gate_catalog.dart +// and review the diff before committing it. +// +// 2026 August 20 +// Author: Desmond Kirkpatrick + +@TestOn('vm') +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import 'fixtures/gate_catalog_module.dart'; + +/// Builds a fresh [GateCatalog] with deterministic, freshly-allocated input +/// signals. +GateCatalog _buildCatalog() => GateCatalog( + clk: Logic(name: 'clk'), + en: Logic(name: 'en'), + reset: Logic(name: 'reset'), + muxSel: Logic(name: 'muxSel'), + enableTri: Logic(name: 'enableTri'), + a4: Logic(name: 'a4', width: 4), + b4: Logic(name: 'b4', width: 4), + a8: Logic(name: 'a8', width: 8), + b8: Logic(name: 'b8', width: 8), + d4: Logic(name: 'd4', width: 4), + shamt4: Logic(name: 'shamt4', width: 4), + idx3: Logic(name: 'idx3', width: 3), + idx5: Logic(name: 'idx5', width: 5), + resetValueDyn4: Logic(name: 'resetValueDyn4', width: 4), + busNet: LogicNet(name: 'busNet', width: 8), + ); + +/// Synthesizes [GateCatalog] to combined netlist JSON using the default +/// [NetlistSynthesizerConfiguration] (the same defaults a typical consumer +/// would use). +Future _synthesizeCatalogJson() async { + final catalog = _buildCatalog(); + await catalog.build(); + final synth = NetlistSynthesizer(); + return synth.synthesizeToJson(catalog); +} + +/// Path (relative to the package root, where `dart test` runs) to the +/// checked-in fixture asset. +const _fixturePath = 'test/fixtures/gate_catalog.rohd.json'; + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + test('GateCatalog netlist JSON matches checked-in fixture byte-for-byte', + () async { + final generated = await _synthesizeCatalogJson(); + final fixtureFile = File(_fixturePath); + + expect(fixtureFile.existsSync(), isTrue, + reason: 'Missing fixture at $_fixturePath. Generate it with: ' + 'dart run tool/generate_gate_catalog.dart'); + + final checkedIn = fixtureFile.readAsStringSync(); + expect( + generated, + equals(checkedIn), + reason: 'Generated gate-catalog netlist JSON no longer matches the ' + 'checked-in fixture. If this change is intentional, regenerate ' + 'the asset with `dart run tool/generate_gate_catalog.dart` and ' + 'review/commit the diff.', + ); + }); + + test('GateCatalog netlist JSON is deterministic across repeated synthesis', + () async { + final first = await _synthesizeCatalogJson(); + await Simulator.reset(); + final second = await _synthesizeCatalogJson(); + expect(second, equals(first)); + }); + + group('gate catalog coverage', () { + late Map json; + late Map moduleDef; + late Map cells; + + setUpAll(() async { + final text = await _synthesizeCatalogJson(); + json = jsonDecode(text) as Map; + final modules = json['modules'] as Map; + moduleDef = modules['GateCatalog'] as Map; + cells = moduleDef['cells'] as Map; + }); + + List> cellsOfType(String type) => cells.values + .cast>() + .where((c) => c['type'] == type) + .toList(); + + test('every standard Yosys arithmetic/logic/compare/shift cell exists', () { + const expectedTypes = { + r'$not', + r'$and', + r'$or', + r'$xor', + r'$reduce_and', + r'$reduce_or', + r'$reduce_xor', + r'$add', + r'$sub', + r'$mul', + r'$div', + r'$mod', + r'$pow', + r'$eq', + r'$ne', + r'$lt', + r'$gt', + r'$le', + r'$ge', + r'$shl', + r'$shr', + r'$sshr', + r'$mux', + r'$shiftx', + r'$slice', + r'$concat', + r'$tribuf', + r'$dff', + r'$dffe', + r'$sdff', + r'$sdffe', + r'$adff', + r'$adffe', + r'$aldff', + r'$aldffe', + }; + for (final type in expectedTypes) { + expect(cellsOfType(type), isNotEmpty, reason: 'missing $type cell'); + } + }); + + test('Power/Divide/Modulo cells have full standard A/B/Y parameters', () { + for (final type in [r'$pow', r'$div', r'$mod']) { + final matches = cellsOfType(type); + expect(matches, isNotEmpty, reason: type); + for (final cell in matches) { + expect( + cell['parameters'], + equals({ + 'A_SIGNED': 0, + 'A_WIDTH': 4, + 'B_SIGNED': 0, + 'B_WIDTH': 4, + 'Y_WIDTH': 4, + }), + reason: type, + ); + expect( + cell['port_directions'], + equals({'A': 'input', 'B': 'input', 'Y': 'output'}), + reason: type, + ); + } + } + }); + + test(r'IndexGate cells map to $shiftx with full standard parameters', () { + final matches = cellsOfType(r'$shiftx'); + // One "natural" 3-bit index and one "oversized" 5-bit index. + expect(matches, hasLength(2)); + final bWidths = + matches.map((c) => (c['parameters'] as Map)['B_WIDTH']).toSet(); + expect(bWidths, equals({3, 5})); + for (final cell in matches) { + final params = cell['parameters'] as Map; + expect(params['A_SIGNED'], 0); + expect(params['B_SIGNED'], 0); + expect(params['A_WIDTH'], 8); + expect(params['Y_WIDTH'], 1); + expect( + cell['port_directions'], + equals({'A': 'input', 'B': 'input', 'Y': 'output'}), + ); + } + }); + + test('ReplicationOp is retained as an explicit, visible (unmapped) cell', + () { + final replicationCells = cellsOfType('ReplicationOp'); + expect(replicationCells, hasLength(2)); + // Confirm it is not force-mapped to any standard Yosys cell type + // (e.g. `$concat`): its cell `type` field is the raw ROHD + // `definitionName`, not a `$`-prefixed standard primitive. + for (final cell in replicationCells) { + expect(cell['type'], isNot(startsWith(r'$'))); + } + final outputWidths = replicationCells.map((c) { + final connections = + (c['connections'] as Map).values.cast>(); + return connections.map((l) => l.length).reduce((a, b) => a > b ? a : b); + }).toSet(); + expect(outputWidths, equals({12, 20})); + }); + + test(r'constant-control mux() folds away at build time (no extra $mux)', + () { + // Two dynamic-control mux instantiations (Mux class + mux() function) + // produce two explicit `$mux` cells; the two dynamic-synchronous-reset + // flip-flops (see below) each lower to an additional `$mux` (selecting + // the reset value) for four `$mux` cells total. The two + // constant-control mux() calls fold away entirely at build time and + // contribute no additional `$mux` cells. + expect(cellsOfType(r'$mux'), hasLength(4)); + + final ports = moduleDef['ports'] as Map; + final const1Bits = + (ports['mux_fn_const1_out'] as Map)['bits'] as List; + final const0Bits = + (ports['mux_fn_const0_out'] as Map)['bits'] as List; + final aBits = (ports['a4'] as Map)['bits'] as List; + final bBits = (ports['b4'] as Map)['bits'] as List; + + // Find the (non-$mux) driver of a port's bits: since `mux()` folded + // away, the only thing between the port and its source signal is a + // plain `$buf` passthrough (emitted whenever an output port aliases an + // input directly), never a `$mux`. + // + // Note: `List`'s `==` is identity-based, not element-wise, so bit + // lists must be compared with an explicit element-wise check. + bool sameBits(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; + } + + Map driverOf(List outputBits) => + cells.values.cast>().singleWhere((c) { + final y = (c['connections'] as Map)['Y']; + return y is List && sameBits(y, outputBits); + }); + + final const1Driver = driverOf(const1Bits); + final const0Driver = driverOf(const0Bits); + expect(const1Driver['type'], r'$buf'); + expect(const0Driver['type'], r'$buf'); + + // mux(Const(1), a4, b4) folds to a4; mux(Const(0), a4, b4) folds to b4. + expect((const1Driver['connections'] as Map)['A'], equals(aBits)); + expect((const0Driver['connections'] as Map)['A'], equals(bBits)); + }); + + test(r'dynamic synchronous reset flip-flops lower to $mux + $dff/$dffe', + () { + // 8 explicit register cells (dff/dffe/sdff/sdffe/adff/adffe/aldff/ + // aldffe) + 2 lowered dynamic-sync-reset flops (1 dff-shaped, 1 + // dffe-shaped) = 9 $dff-family cells total (dffe used twice: q_dffe + // and the lowered en-variant). + expect(cellsOfType(r'$dff'), hasLength(2)); // q_dff, q_dynsync_noen + expect(cellsOfType(r'$dffe'), hasLength(2)); // q_dffe, q_dynsync_en + expect(cellsOfType(r'$sdff'), hasLength(1)); + expect(cellsOfType(r'$sdffe'), hasLength(1)); + expect(cellsOfType(r'$adff'), hasLength(1)); + expect(cellsOfType(r'$adffe'), hasLength(1)); + expect(cellsOfType(r'$aldff'), hasLength(1)); + expect(cellsOfType(r'$aldffe'), hasLength(1)); + }); + }); +} diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index a1de7e79a..695942bbe 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -191,6 +191,19 @@ void main() { expect(s.name, 'structure'); }); + test('hasConsts detects constants at any depth', () { + final withoutConsts = LogicStructure([Logic()]); + final withDirectConst = LogicStructure([Logic(), Const(0)]); + final withNestedConst = LogicStructure([ + Logic(), + LogicStructure([Logic(), Const(1)]), + ]); + + expect(withoutConsts.hasConsts, isFalse); + expect(withDirectConst.hasConsts, isTrue); + expect(withNestedConst.hasConsts, isTrue); + }); + test('sub logic in two structures throws exception', () { final s = LogicStructure([ Logic(), diff --git a/test/mac_unit_test.dart b/test/mac_unit_test.dart new file mode 100644 index 000000000..1dcda6026 --- /dev/null +++ b/test/mac_unit_test.dart @@ -0,0 +1,82 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// mac_unit_test.dart +// Tests for the filter-bank multiply-accumulate example. +// +// 2026 August 24 +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../example/filter_bank/mac_unit.dart'; + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + test('disabled pipeline holds its result and intermediate stages', () async { + const dataWidth = 8; + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final enable = Logic(); + final sample = Logic(width: dataWidth); + final coefficient = Logic(width: dataWidth); + final accumulator = Logic(width: dataWidth); + final dut = MacUnit( + sample, + coefficient, + accumulator, + clk, + reset, + enable, + dataWidth: dataWidth, + ); + await dut.build(); + + reset.inject(1); + enable.inject(0); + sample.inject(0); + coefficient.inject(0); + accumulator.inject(0); + Simulator.setMaxSimTime(200); + unawaited(Simulator.run()); + + await clk.nextPosedge; + reset.inject(0); + enable.inject(1); + sample.inject(3); + coefficient.inject(4); + accumulator.inject(5); + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextNegedge; + expect(dut.result.value.toInt(), 17); + + enable.inject(0); + sample.inject(7); + coefficient.inject(8); + accumulator.inject(9); + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextNegedge; + expect( + dut.result.value.toInt(), + 17, + reason: 'Both pipeline stages must hold while enable is low.', + ); + + enable.inject(1); + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextNegedge; + expect(dut.result.value.toInt(), 65); + + await Simulator.endSimulation(); + }); +} diff --git a/test/nested_array_struct_port_synthesis_test.dart b/test/nested_array_struct_port_synthesis_test.dart index bd3bc8f6d..7c1ff5ee0 100644 --- a/test/nested_array_struct_port_synthesis_test.dart +++ b/test/nested_array_struct_port_synthesis_test.dart @@ -7,6 +7,9 @@ // 2026 August 18 // Author: Max Korbel +@TestOn('vm') +library; + import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:test/test.dart'; diff --git a/test/netlist_example_test.dart b/test/netlist_example_test.dart new file mode 100644 index 000000000..08b56b7cc --- /dev/null +++ b/test/netlist_example_test.dart @@ -0,0 +1,298 @@ +// 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 = (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 = (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 = (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..8486814e2 --- /dev/null +++ b/test/netlist_synthesizer_test.dart @@ -0,0 +1,2606 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synthesizer_test.dart +// Comprehensive tests for the netlist synthesizer. +// +// 2026 April 13 +// Author: Desmond Kirkpatrick + +import 'dart:async'; +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_structure_concat.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart'; +import '../example/filter_bank/filter_bank_modules.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); + } +} + +/// A custom [FlipFlop] used to verify inheritance-aware leaf matching. +class CustomFlipFlop extends FlipFlop { + CustomFlipFlop(super.clk, super.d); +} + +/// Exercises flip-flops with optional control signals. +class ControlledFlopModule extends Module { + ControlledFlopModule( + Logic clk, + Logic d, { + Logic? en, + Logic? reset, + Logic? resetValue, + int? constantResetValue, + bool asyncReset = false, + }) : super(name: 'controlledflop') { + clk = addInput('clk', clk); + d = addInput('d', d, width: d.width); + if (en != null) { + en = addInput('en', en); + } + if (reset != null) { + reset = addInput('reset', reset); + } + if (resetValue != null) { + resetValue = addInput('resetValue', resetValue, width: d.width); + } + addOutput('q', width: d.width) <= + flop( + clk, + d, + en: en, + reset: reset, + resetValue: resetValue ?? constantResetValue, + asyncReset: asyncReset, + ); + } +} + +/// 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 both the sum and carry outputs of [Add]. +class AddWithCarryModule extends Module { + AddWithCarryModule(Logic a, Logic b, {int width = 8}) + : super(name: 'addwithcarry') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + final add = Add(a, b); + addOutput('sum', width: width) <= add.sum; + addOutput('carry') <= add.carry; + } +} + +/// Wraps an [AddModule] so stop-policy tests can choose whether the child +/// receives its own definition or is emitted as a netlist cell. +class AddWrapperModule extends Module { + Logic get sum => output('sum'); + + AddWrapperModule({int width = 8}) : super(name: 'addwrapper') { + final a = addInput('a', Logic(width: width), width: width); + final b = addInput('b', Logic(width: width), width: width); + final child = AddModule(a, b, width: width); + addOutput('sum', width: width) <= child.sum; + } +} + +/// 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(); + } +} + +/// Child with a LogicArray input, used to exercise array port netlisting. +class ArrayInputChildModule extends Module { + LogicArray get values => input('values') as LogicArray; + + Logic get packedOut => output('packedOut'); + + ArrayInputChildModule(LogicArray values) : super(name: 'arrayinputchild') { + values = addInputArray( + 'values', + values, + dimensions: values.dimensions, + elementWidth: values.elementWidth, + ); + addOutput('packedOut', width: values.width) <= + [for (final element in values.elements.reversed) element].swizzle(); + } +} + +/// Child with a LogicArray output, used to exercise regrouping array output +/// elements into another child array input. +class ArrayOutputChildModule extends Module { + LogicArray get values => output('values') as LogicArray; + + ArrayOutputChildModule() : super(name: 'arrayoutputchild') { + addOutputArray('values', dimensions: [4], elementWidth: 8); + } +} + +/// Provides multiple array outputs to verify synthesized concat cell names. +class MultipleArrayOutputModule extends Module { + MultipleArrayOutputModule() + : super( + name: 'multiplearrayoutput', + definitionName: 'MultipleArrayOutputModule', + ) { + final dataA = addInput('dataA', Logic(width: 8), width: 8); + final dataB = addInput('dataB', Logic(width: 8), width: 8); + final first = addOutputArray('first', dimensions: [2], elementWidth: 8); + final second = addOutputArray('second', dimensions: [2], elementWidth: 8); + for (final element in first.elements) { + element <= dataA; + } + for (final element in second.elements) { + element <= dataB; + } + final child = NotModule(dataA[0]); + addOutput('childOut') <= child.y; + } +} + +/// Parent whose internal LogicArray elements independently feed a child array +/// input port. +class InternalArrayToChildModule extends Module { + InternalArrayToChildModule() : super(name: 'internalarraytochild') { + final first = addInput('first', Logic(width: 8), width: 8); + final second = addInput('second', Logic(width: 8), width: 8); + final values = LogicArray([2], 8, name: 'values'); + + values.elements[0] <= first; + values.elements[1] <= second; + final child = ArrayInputChildModule(values); + addOutput('packedOut', width: values.width) <= child.packedOut; + } +} + +/// Parent whose internal LogicArray groups elements from another child output +/// array before feeding a child input port. +class RegroupedArrayOutputToChildModule extends Module { + RegroupedArrayOutputToChildModule() + : super(name: 'regroupedarrayoutputtochild') { + final source = ArrayOutputChildModule(); + final values = LogicArray([2], 8, name: 'values'); + + values.elements[0] <= source.values.elements[2]; + values.elements[1] <= source.values.elements[3]; + final child = ArrayInputChildModule(values); + addOutput('packedOut', width: values.width) <= child.packedOut; + } +} + +/// Parent with nested internal LogicArrays, used to exercise concat-to-concat +/// consumer rewrites after array concat outputs receive fresh wire IDs. +class NestedInternalArrayToChildModule extends Module { + NestedInternalArrayToChildModule() + : super(name: 'nestedinternalarraytochild') { + final inputs = [ + for (var index = 0; index < 4; index++) + addInput('in$index', Logic(width: 8), width: 8), + ]; + final lower = LogicArray([2], 8, name: 'lower'); + final upper = LogicArray([2], 8, name: 'upper'); + final values = LogicArray([2], 16, name: 'values'); + + lower.elements[0] <= inputs[0]; + lower.elements[1] <= inputs[1]; + upper.elements[0] <= inputs[2]; + upper.elements[1] <= inputs[3]; + values.elements[0] <= lower; + values.elements[1] <= upper; + final child = ArrayInputChildModule(values); + addOutput('packedOut', width: values.width) <= child.packedOut; + } +} + +/// Parent whose 2D LogicArray.net rows are driven by independent child array +/// outputs before feeding a child array input port. +class NestedNetArrayRowsToChildModule extends Module { + NestedNetArrayRowsToChildModule() : super(name: 'nestednetarrayrowstochild') { + final lower = ArrayOutputChildModule(); + final upper = ArrayOutputChildModule(); + final values = LogicArray.net([2, 4], 8, name: 'values'); + + values.elements[0] <= lower.values; + values.elements[1] <= upper.values; + final child = ArrayInputChildModule(values); + addOutput('packedOut', width: values.width) <= child.packedOut; + } +} + +/// Simple two-field structure used to demonstrate netlist struct unpack/pack +/// cells. +class NetlistPairStruct extends LogicStructure { + Logic get low => elements[0]; + + Logic get high => elements[1]; + + NetlistPairStruct({super.name = 'pair'}) + : super([Logic(name: 'low', width: 4), Logic(name: 'high', width: 4)]); + + @override + NetlistPairStruct clone({String? name}) => NetlistPairStruct(name: name); +} + +/// Consumes fields of a typed structure input independently, requiring the +/// netlist to unpack the aggregate port into named field connections. +class StructInputConsumerModule extends Module { + StructInputConsumerModule(NetlistPairStruct pair) + : super(name: 'structinputconsumer') { + pair = addTypedInput('pair', pair); + addOutput('packedOut', width: pair.width) <= + [pair.high, pair.low].swizzle(); + } +} + +/// Drives fields of a typed structure output independently, requiring the +/// netlist to pack the field wires back into the aggregate output port. +class StructOutputProducerModule extends Module { + StructOutputProducerModule() : super(name: 'structoutputproducer') { + final low = addInput('low', Logic(width: 4), width: 4); + final high = addInput('high', Logic(width: 4), width: 4); + final pair = NetlistPairStruct(name: 'pairValue'); + + pair.low <= low; + pair.high <= high ^ Const(1, width: 4); + addTypedOutput('pair', pair.clone).gets(pair); + } +} + +/// Instantiates identical structure-packing children at distinct parent paths. +class StructOutputProducerDedupTop extends Module { + StructOutputProducerDedupTop() : super(name: 'structoutputproducerdeduptop') { + final first = StructOutputProducerModule(); + final second = StructOutputProducerModule(); + + addOutput('first', width: 8) <= first.output('pair'); + addOutput('second', width: 8) <= second.output('pair'); + } +} + +/// 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; + } +} + +/// A module with a named constant (Logic..gets(Const)) used inside a +/// Combinational block — exercises the named-constant fix. +class _NamedConstModule extends Module { + _NamedConstModule(Logic clk, Logic reset) : super(name: 'namedConstMod') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + final dataIn = addInput('dataIn', Logic(width: 8), width: 8); + final result = addOutput('result', width: 8); + + // Named constant driven by Const — this is the pattern from + // _dynamicInputToLogic in SummationBase. + final myConst = Logic(name: 'myConst', width: 8)..gets(Const(0, width: 8)); + + Combinational([result < mux(dataIn.or(), dataIn, myConst)]); + } +} + +// ──────────────────────────────────────────────────────────────────── +// 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 samples = List.generate(2, (ch) => FilterSample(name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + + return FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: [coeffs0, coeffs1], + ); +} + +/// Build a module and synthesize to a parsed JSON map. +Future> _synthToMap( + Module mod, { + NetlistSynthesizerConfiguration configuration = + const NetlistSynthesizerConfiguration(), +}) async { + await mod.build(); + final synth = + SynthBuilder(mod, NetlistSynthesizer(configuration: configuration)); + final json = (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; + }); + }); +} + +({List undrivenInputs, Map> driversByBit}) + _connectivityReport(Map moduleDef) { + final ports = _ports(moduleDef); + final cells = _cells(moduleDef); + final producedBits = {}; + final driversByBit = >{}; + + void addDriver(int bit, String driver) { + producedBits.add(bit); + (driversByBit[bit] ??= []).add(driver); + } + + for (final entry in ports.entries) { + final port = entry.value as Map; + final direction = port['direction'] as String?; + if (direction != 'input' && direction != 'inout') { + continue; + } + for (final bit in (port['bits'] as List).whereType()) { + addDriver(bit, 'port ${entry.key}'); + } + } + + for (final entry in cells.entries) { + final cell = entry.value as Map; + final directions = cell['port_directions'] as Map? ?? {}; + final connections = cell['connections'] as Map? ?? {}; + for (final portEntry in connections.entries) { + if (directions[portEntry.key] != 'output' && + directions[portEntry.key] != 'inout') { + continue; + } + for (final bit in (portEntry.value as List).whereType()) { + addDriver(bit, 'cell ${entry.key}.${portEntry.key}'); + } + } + } + + final undrivenInputs = []; + for (final entry in cells.entries) { + final cell = entry.value as Map; + final directions = cell['port_directions'] as Map? ?? {}; + final connections = cell['connections'] as Map? ?? {}; + for (final portEntry in connections.entries) { + if (directions[portEntry.key] != 'input') { + continue; + } + final undrivenBits = (portEntry.value as List) + .whereType() + .where((bit) => !producedBits.contains(bit)) + .toList(); + if (undrivenBits.isNotEmpty) { + undrivenInputs.add( + '${entry.key}.${portEntry.key}: ${undrivenBits.take(8).join(', ')}', + ); + } + } + } + + return (undrivenInputs: undrivenInputs, driversByBit: driversByBit); +} + +// ──────────────────────────────────────────────────────────────────── +// Tests +// ──────────────────────────────────────────────────────────────────── + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + // ── Group 1: Leaf cell mapper — individual gate mappings ─────────── + + group('netlist 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('FlipFlop controls map to standard Yosys register cells', () async { + final clk = SimpleClockGenerator(10).clk; + final d = Logic(width: 4); + final en = Logic(); + final reset = Logic(); + final resetValue = Logic(width: 4); + final cases = <( + ControlledFlopModule module, + String type, + Set ports, + Map parameters, + )>[ + ( + ControlledFlopModule(clk, d, en: en), + r'$dffe', + {'CLK', 'D', 'EN', 'Q'}, + {'WIDTH': 4, 'CLK_POLARITY': 1, 'EN_POLARITY': 1}, + ), + ( + ControlledFlopModule(clk, d, reset: reset, constantResetValue: 9), + r'$sdff', + {'CLK', 'D', 'SRST', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'SRST_POLARITY': 1, + 'SRST_VALUE': '1001', + }, + ), + ( + ControlledFlopModule( + clk, + d, + en: en, + reset: reset, + constantResetValue: 9, + ), + r'$sdffe', + {'CLK', 'D', 'EN', 'SRST', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'EN_POLARITY': 1, + 'SRST_POLARITY': 1, + 'SRST_VALUE': '1001', + }, + ), + ( + ControlledFlopModule( + clk, + d, + reset: reset, + constantResetValue: 9, + asyncReset: true, + ), + r'$adff', + {'CLK', 'D', 'ARST', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'ARST_POLARITY': 1, + 'ARST_VALUE': '1001', + }, + ), + ( + ControlledFlopModule( + clk, + d, + en: en, + reset: reset, + constantResetValue: 9, + asyncReset: true, + ), + r'$adffe', + {'CLK', 'D', 'EN', 'ARST', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'EN_POLARITY': 1, + 'ARST_POLARITY': 1, + 'ARST_VALUE': '1001', + }, + ), + ( + ControlledFlopModule( + clk, + d, + reset: reset, + resetValue: resetValue, + asyncReset: true, + ), + r'$aldff', + {'CLK', 'D', 'ALOAD', 'AD', 'Q'}, + {'WIDTH': 4, 'CLK_POLARITY': 1, 'ALOAD_POLARITY': 1}, + ), + ( + ControlledFlopModule( + clk, + d, + en: en, + reset: reset, + resetValue: resetValue, + asyncReset: true, + ), + r'$aldffe', + {'CLK', 'D', 'EN', 'ALOAD', 'AD', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'EN_POLARITY': 1, + 'ALOAD_POLARITY': 1, + }, + ), + ]; + + for (final testCase in cases) { + final (module, type, ports, parameters) = testCase; + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cell = + _cells(moduleDef).values.cast>().singleWhere( + (cell) => cell['type'] == type, + ); + expect( + (cell['port_directions'] as Map).keys.toSet(), + equals(ports), + reason: type, + ); + expect( + cell['parameters'], + equals(parameters), + reason: type, + ); + } + }); + + test('FlipFlop dynamic synchronous reset is lowered to standard cells', + () async { + final module = ControlledFlopModule( + SimpleClockGenerator(10).clk, + Logic(width: 4), + en: Logic(), + reset: Logic(), + resetValue: Logic(width: 4), + ); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cells = _cells(moduleDef).values.cast>(); + final dff = cells.singleWhere((cell) => cell['type'] == r'$dffe'); + + expect(_hasCellType(json, r'$mux'), isTrue); + expect(_hasCellType(json, r'$or'), isTrue); + expect( + (dff['port_directions'] as Map).keys.toSet(), + equals({'CLK', 'D', 'EN', 'Q'}), + ); + }); + + 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'Add maps carry into the high bit of standard $add Y', () async { + final json = await _synthToMap( + AddWithCarryModule(Logic(width: 8), Logic(width: 8)), + ); + final addCell = _modules(json) + .values + .cast>() + .expand((definition) => _cells(definition).values) + .cast>() + .singleWhere((cell) => cell['type'] == r'$add'); + final directions = addCell['port_directions'] as Map; + final connections = addCell['connections'] as Map; + final parameters = addCell['parameters'] as Map; + + expect(directions.keys.toSet(), equals({'A', 'B', 'Y'})); + expect(connections.keys.toSet(), equals({'A', 'B', 'Y'})); + expect(connections['Y'], hasLength(9)); + expect(parameters['Y_WIDTH'], 9); + }); + + 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 $sshr cell', () async { + final json = await _synthToMap( + ARShiftModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$sshr'), isTrue); + }); + + test('shift cells use standard ports and signedness parameters', () async { + final cases = <(Module Function() moduleGen, String type, int aSigned)>[ + (() => ShiftModule(Logic(width: 8), Logic(width: 8)), r'$shl', 0), + (() => ShiftModule(Logic(width: 8), Logic(width: 8)), r'$shr', 0), + (() => ARShiftModule(Logic(width: 8), Logic(width: 8)), r'$sshr', 1), + ]; + + for (final (moduleGen, type, aSigned) in cases) { + final module = moduleGen(); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cell = + _cells(moduleDef).values.cast>().singleWhere( + (cell) => cell['type'] == type, + ); + + expect( + cell['port_directions'], + equals({'A': 'input', 'B': 'input', 'Y': 'output'}), + reason: type, + ); + expect( + cell['parameters'], + equals({ + 'A_SIGNED': aSigned, + 'A_WIDTH': 8, + 'B_SIGNED': 0, + 'B_WIDTH': 8, + 'Y_WIDTH': 8, + }), + reason: type, + ); + } + }); + + 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); + final tribuf = _modules(json) + .values + .cast>() + .expand((moduleDef) => _cells(moduleDef).values) + .cast>() + .singleWhere((cell) => cell['type'] == r'$tribuf'); + expect( + tribuf['port_directions'], + equals({'A': 'input', 'EN': 'input', 'Y': 'output'}), + ); + }); + }); + + // ── 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', + ); + }); + + test('structure-pack children at different paths are deduplicated', + () async { + final json = await _synthToMap(StructOutputProducerDedupTop()); + final structProducerDefs = _modules(json) + .keys + .where( + (definitionName) => + definitionName.startsWith('StructOutputProducerModule'), + ) + .toList(); + + expect( + structProducerDefs, + hasLength(1), + reason: + 'The structure-pack cell keys must be local to each child module.', + ); + }); + }); + + // ── Group 4: NetlistSynthesizerConfiguration permutations ────────────────── + + group('NetlistSynthesizerConfiguration', () { + late Module filterBank; + + setUp(() async { + await Simulator.reset(); + filterBank = _buildFilterBank(); + await filterBank.build(); + }); + + test('default configuration produce valid netlist', () { + final synth = SynthBuilder(filterBank, NetlistSynthesizer()); + final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + filterBank, + ); + final parsed = jsonDecode(json) as Map; + expect(parsed['creator'], equals('NetlistSynthesizer (rohd)')); + expect(parsed['version'], equals(NetlistSynthesizer.formatVersion)); + expect(_modules(parsed), isNotEmpty); + }); + + test('slimMode omits connections', () { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer( + configuration: + const NetlistSynthesizerConfiguration(slimMode: true)), + ); + final json = (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('slim then expanded matches initially expanded output', () async { + final module = _buildFilterBank(); + await module.build(); + + final translator = NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration(slimMode: true), + ); + final slim = translator.synthesizeToJson(module); + final expanded = translator.synthesizeToJson(module, slimMode: false); + final initiallyExpanded = NetlistSynthesizer().synthesizeToJson(module); + + final slimModules = _modules(jsonDecode(slim) as Map); + expect( + slimModules.values + .expand( + (definition) => _cells(definition as Map).values, + ) + .every((cell) => !(cell as Map).containsKey('connections')), + isTrue, + ); + expect(expanded, initiallyExpanded); + }); + + test( + 'filter bank can stop traversal at an opaque custom SV module', + () { + final synthesizer = NetlistSynthesizer( + configuration: NetlistSynthesizerConfiguration( + leafModulePredicate: (module) => + module is FlipFlop || module is MacUnit, + ), + ); + final json = jsonDecode(synthesizer.synthesizeToJson(filterBank)) + as Map; + final modules = _modules(json); + + expect( + modules.keys.any((name) => name.contains('MacUnit')), + isFalse, + reason: 'MacUnit is treated like externally supplied/custom SV, so ' + 'the netlist should not emit a definition for it.', + ); + + final channelDefs = modules.entries.where( + (entry) => entry.key.contains('FilterChannel'), + ); + expect(channelDefs, isNotEmpty); + + final macCells = channelDefs.expand((entry) { + final def = entry.value as Map; + return _cells(def).values.where((cell) { + final cellMap = cell as Map; + return (cellMap['type'] as String).contains('MacUnit'); + }); + }).toList(); + + expect( + macCells, + isNotEmpty, + reason: 'FilterChannel should still instantiate the opaque MacUnit ' + 'cell; only hierarchy traversal stops at that boundary.', + ); + }, + ); + + test('DCE disabled still produces valid netlist', () { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration( + enableDeadCellElimination: false)), + ); + final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + filterBank, + ); + final parsed = jsonDecode(json) as Map; + expect(_modules(parsed), isNotEmpty); + }); + + test('all optimizations disabled produces valid netlist', () { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration( + enableDeadCellElimination: false)), + ); + final json = (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 = (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( + configuration: + const NetlistSynthesizerConfiguration(slimMode: true)), + ); + final slimJson = + (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 = (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 sample0/sample1 and channelOut as array ports + expect( + ports.keys.any((k) => k.contains('sample') || 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 8: Wire ID and structural invariants ───────────────────── + + group('wire ID and structural invariants', () { + test('default synthesizers do not share mutable leaf mappers', () { + final first = NetlistSynthesizer(); + final second = NetlistSynthesizer(); + + expect( + identical(first.netlistCellMapper, second.netlistCellMapper), + isFalse, + ); + }); + + test( + 'leaf module predicate controls which modules stop traversal', + () async { + final module = AddWrapperModule(); + await module.build(); + + final childDefinitionName = module.subModules.single.definitionName; + final synthesizer = NetlistSynthesizer( + configuration: NetlistSynthesizerConfiguration( + leafModulePredicate: (module) => module is AddModule, + ), + ); + + final json = jsonDecode(synthesizer.synthesizeToJson(module)) + as Map; + final modules = json['modules'] as Map; + final top = modules[module.definitionName] as Map; + final cells = _cells(top); + + expect(modules, isNot(contains(childDefinitionName))); + expect( + cells.values, + contains( + predicate>( + (cell) => cell['type'] == childDefinitionName, + ), + ), + ); + }, + ); + + test('default leaf predicate matches FlipFlop subclasses', () { + const configuration = NetlistSynthesizerConfiguration(); + + expect( + configuration.leafModulePredicate(CustomFlipFlop(Logic(), Logic())), + isTrue, + ); + }); + + test('repeated translation of the same module is identical', () async { + final module = LogicArrayExample( + LogicArray([4], 8, name: 'arrayA'), + Logic(name: 'id', width: 3), + Logic(name: 'selectIndexValue', width: 8), + Logic(name: 'selectFromValue', width: 8), + ); + await module.build(); + + final synthesizer = NetlistSynthesizer(); + final first = synthesizer.synthesizeToJson(module); + final second = synthesizer.synthesizeToJson(module); + + expect(second, first); + }); + + test('reusing a synthesizer resets wire IDs for each module', () async { + final firstModule = AddModule( + Logic(name: 'firstA', width: 8), + Logic(name: 'firstB', width: 8), + ); + final secondModule = AddModule( + Logic(name: 'secondA', width: 8), + Logic(name: 'secondB', width: 8), + ); + await firstModule.build(); + await secondModule.build(); + + final synthesizer = NetlistSynthesizer(); + + int firstWireId(Module module) { + final json = jsonDecode(synthesizer.synthesizeToJson(module)) + as Map; + final definition = + _modules(json)[module.definitionName] as Map; + return _ports(definition) + .values + .expand((port) => (port as Map)['bits'] as List) + .whereType() + .reduce((first, second) => first < second ? first : second); + } + + expect(firstWireId(firstModule), 2); + expect(firstWireId(secondModule), 2); + }); + + test('array concat outputs use fresh wire IDs', () async { + final json = await _synthToMap(InternalArrayToChildModule()); + final moduleDef = + _modules(json)['InternalArrayToChildModule'] as Map; + final cells = _cells(moduleDef); + final arrayConcats = cells.entries.where( + (entry) => entry.key.startsWith('array_concat'), + ); + + expect(arrayConcats, isNotEmpty); + for (final arrayConcat in arrayConcats) { + final connections = (arrayConcat.value + as Map)['connections'] as Map; + final inputBits = connections.entries + .where((entry) => entry.key != 'Y') + .expand((entry) => entry.value as List) + .toSet(); + final outputBits = + (connections['Y'] as List).whereType().toSet(); + + expect(inputBits.intersection(outputBits), isEmpty); + } + }); + + test('array concat output names use unique destination addresses', + () async { + final module = MultipleArrayOutputModule(); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final arrayConcatNames = _cells(moduleDef) + .entries + .where( + (entry) => + (entry.value as Map)['type'] == r'$concat', + ) + .map((entry) => entry.key) + .where((name) => name.startsWith('array_concat_output_')) + .toList(); + + expect(arrayConcatNames, hasLength(2)); + expect(arrayConcatNames.toSet(), hasLength(2)); + }); + + test('regrouped array output elements get explicit concat', () async { + final module = RegroupedArrayOutputToChildModule(); + final json = await _synthToMap(module); + final moduleDef = + _modules(json).values.cast>().reduce( + (left, right) => + _cells(left).length >= _cells(right).length ? left : right, + ); + final cells = _cells(moduleDef); + final arrayConcatEntries = cells.entries.where( + (entry) => entry.key.startsWith('array_concat'), + ); + + expect(arrayConcatEntries, isNotEmpty, reason: cells.keys.join(', ')); + expect( + arrayConcatEntries.any((entry) { + final connections = (entry.value + as Map)['connections'] as Map; + final outputBits = connections['Y'] as List?; + return outputBits?.length == 16; + }), + isTrue, + ); + }); + + test('nested array concats feed downstream concat inputs', () async { + final json = await _synthToMap(NestedInternalArrayToChildModule()); + final moduleDef = + _modules(json).values.cast>().reduce( + (left, right) => + _cells(left).length >= _cells(right).length ? left : right, + ); + final cells = _cells(moduleDef); + final concatEntries = cells.entries.where((entry) { + final cell = entry.value as Map; + return cell['type'] == r'$concat'; + }).toList(); + + final concatOutputBits = {}; + for (final entry in concatEntries) { + final cell = entry.value as Map; + final connections = cell['connections'] as Map; + concatOutputBits.addAll((connections['Y'] as List).whereType()); + } + + final concatInputConsumers = {}; + for (final entry in concatEntries) { + final cell = entry.value as Map; + final connections = cell['connections'] as Map; + final directions = cell['port_directions'] as Map; + for (final portEntry in connections.entries) { + if (directions[portEntry.key] == 'input') { + concatInputConsumers.addAll( + (portEntry.value as List).whereType(), + ); + } + } + } + + expect(concatOutputBits.intersection(concatInputConsumers), isNotEmpty); + }); + + test( + 'nested LogicArray.net aggregate ports have connected concat inputs', + () async { + for (final configuration in [ + const NetlistSynthesizerConfiguration( + enableDeadCellElimination: false), + const NetlistSynthesizerConfiguration( + collapseTransparentClusters: true, + enableDeadCellElimination: false, + ), + ]) { + final json = await _synthToMap( + NestedNetArrayRowsToChildModule(), + configuration: configuration, + ); + final moduleDef = _modules(json) + .values + .cast>() + .reduce( + (left, right) => + _cells(left).length >= _cells(right).length ? left : right, + ); + final cells = _cells(moduleDef); + final nestedArrayConcats = cells.entries.where((entry) { + final cell = entry.value as Map; + return entry.key.startsWith('array_concat') && + cell['type'] == r'$concat'; + }); + final report = _connectivityReport(moduleDef); + final multipleDrivers = report.driversByBit.entries + .where((entry) => entry.value.length > 1) + .toList(); + + expect(nestedArrayConcats, isNotEmpty, reason: cells.keys.join(', ')); + expect( + report.undrivenInputs, + isEmpty, + reason: report.undrivenInputs.join('\n'), + ); + expect( + multipleDrivers, + isEmpty, + reason: multipleDrivers.take(8).join('\n'), + ); + } + }, + ); + + test('struct input fields get explicit unpack cell', () async { + final module = StructInputConsumerModule(NetlistPairStruct()); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cells = _cells(moduleDef); + final structUnpacks = cells.entries.where((entry) { + final cell = entry.value as Map; + return cell['type'] == r'$struct_unpack'; + }).toList(); + + expect(structUnpacks, isNotEmpty, reason: cells.keys.join(', ')); + expect( + structUnpacks.any((entry) { + final cell = entry.value as Map; + final directions = cell['port_directions'] as Map; + return directions['A'] == 'input' && + directions['low'] == 'output' && + directions['high'] == 'output'; + }), + isTrue, + ); + }); + + test('struct output fields get explicit pack cell', () async { + final module = StructOutputProducerModule(); + final json = await _synthToMap(module); + final moduleDef = + _modules(json).values.cast>().reduce( + (left, right) => + _cells(left).length >= _cells(right).length ? left : right, + ); + final cells = _cells(moduleDef); + final structPacks = cells.entries.where((entry) { + final cell = entry.value as Map; + return entry.key.startsWith( + SynthStructureConcat.operationName, + ) && + cell['type'] == r'$struct_pack'; + }).toList(); + + expect(structPacks, isNotEmpty, reason: cells.keys.join(', ')); + expect( + structPacks.any((entry) { + final cell = entry.value as Map; + final directions = cell['port_directions'] as Map; + return directions['low'] == 'input' && + directions['high'] == 'input' && + directions['Y'] == 'output'; + }), + isTrue, + ); + }); + + test('struct aggregate netnames cannot span multiple drivers', () { + final ports = >{}; + final cells = >{ + 'first_driver': { + 'hide_name': 0, + 'type': r'$buf', + 'parameters': {'WIDTH': 8}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': List.generate(8, (index) => 100 + index), + 'Y': List.generate(8, (index) => 200 + index), + }, + }, + 'second_driver': { + 'hide_name': 0, + 'type': r'$buf', + 'parameters': {'WIDTH': 8}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': List.generate(8, (index) => 300 + index), + 'Y': List.generate(8, (index) => 400 + index), + }, + }, + }; + final netnames = { + 'values': { + 'bits': [ + ...List.generate(8, (index) => 200 + index), + ...List.generate(8, (index) => 400 + index), + ], + 'logic_type': { + 'typeName': 'PairStructure', + 'fields': [ + {'name': 'first', 'width': 8}, + {'name': 'second', 'width': 8}, + ], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + ports, + cells, + 'struct_module', + netnames: netnames, + ), + throwsA(isA()), + ); + }); + + test('optimized netlist removes concat aliases of named vectors', () async { + final json = await _synthToMap( + NestedInternalArrayToChildModule(), + configuration: const NetlistSynthesizerConfiguration( + collapseTransparentClusters: true), + ); + + for (final moduleDef in _modules( + json, + ).values.cast>()) { + final namedBitVectors = [ + for (final netname + in (moduleDef['netnames'] as Map).values) + if (netname is Map && netname['bits'] is List) + (netname['bits'] as List).cast(), + ]; + + for (final entry in _cells(moduleDef).entries) { + final cell = entry.value as Map; + if (cell['type'] != r'$concat') { + continue; + } + + final connections = cell['connections'] as Map; + final directions = cell['port_directions'] as Map; + final inputBits = [ + for (final portEntry in connections.entries) + if (directions[portEntry.key] != 'output') + ...(portEntry.value as List).cast(), + ]; + + expect( + namedBitVectors.any( + (bits) => + bits.length == inputBits.length && + bits.indexed.every( + (bitEntry) => bitEntry.$2 == inputBits[bitEntry.$1], + ), + ), + isFalse, + reason: '${entry.key} aliases an already named vector', + ); + } + } + }); + + test('concat of adjacent slices collapses to one slice', () { + final sourceBits = List.generate(32, (index) => 100 + index); + final modules = >{ + 'top': { + 'attributes': {}, + 'ports': >{}, + 'netnames': >{}, + 'cells': >{ + 'slice0': { + 'hide_name': 0, + 'type': r'$slice', + 'parameters': {'OFFSET': 8, 'A_WIDTH': 32, 'Y_WIDTH': 4}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': sourceBits, + 'Y': [1, 2, 3, 4], + }, + }, + 'slice1': { + 'hide_name': 0, + 'type': r'$slice', + 'parameters': {'OFFSET': 12, 'A_WIDTH': 32, 'Y_WIDTH': 4}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': sourceBits, + 'Y': [5, 6, 7, 8], + }, + }, + 'concat': { + 'hide_name': 0, + 'type': r'$concat', + 'parameters': {'IN0_WIDTH': 4, 'IN1_WIDTH': 4}, + 'attributes': {}, + 'port_directions': { + '[3:0]': 'input', + '[7:4]': 'input', + 'Y': 'output', + }, + 'connections': { + '[3:0]': [1, 2, 3, 4], + '[7:4]': [5, 6, 7, 8], + 'Y': [9, 10, 11, 12, 13, 14, 15, 16], + }, + }, + }, + }, + }; + + NetlistPasses.collapseConcatOfAdjacentSlices(modules); + + final topModule = modules['top']!; + final cells = topModule['cells']! as Map>; + final concat = cells['concat']!; + final concatConnections = concat['connections']! as Map; + expect(cells, isNot(contains('slice0'))); + expect(cells, isNot(contains('slice1'))); + expect(concat['type'], equals(r'$slice')); + expect( + concat['parameters'], + equals({'OFFSET': 8, 'A_WIDTH': 32, 'Y_WIDTH': 8}), + ); + expect(concatConnections['A'], equals(sourceBits)); + expect(concatConnections['Y'], equals([9, 10, 11, 12, 13, 14, 15, 16])); + }); + + 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, + configuration: const NetlistSynthesizerConfiguration( + enableDeadCellElimination: 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, + configuration: const NetlistSynthesizerConfiguration( + enableDeadCellElimination: 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 configuration', () { + test('collapseTransparentClusters produces valid netlist', () async { + final fb = _buildFilterBank(); + final json = await _synthToMap( + fb, + configuration: const NetlistSynthesizerConfiguration( + collapseTransparentClusters: true), + ); + expect(_modules(json), isNotEmpty); + }); + + test('validation reports multiple drivers with their locations', () { + final ports = { + 'a': { + 'direction': 'input', + 'bits': [1], + }, + 'y': { + 'direction': 'output', + 'bits': [2], + }, + }; + final cells = { + 'driver': { + 'type': r'$buf', + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'Y': [1], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + ports, + cells, + 'ShortedModule', + ), + throwsA( + isA() + .having( + (error) => error.moduleName, 'module name', 'ShortedModule') + .having((error) => error.issues, 'issues', hasLength(1)) + .having((error) => error.issues.single.wireBit, 'wire bit', 1) + .having( + (error) => error.issues.single.drivers, + 'drivers', + containsAll(['port a (input)', r'cell driver.Y ($buf)']), + ), + ), + ); + }); + + test('validation ignores structural aliases but counts buffers as drivers', + () { + final ports = { + 'a': { + 'direction': 'input', + 'bits': [1], + }, + }; + final concatAlias = { + 'concat': { + 'type': r'$concat', + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'Y': [1], + }, + }, + }; + + expect( + () => NetlistValidation.validate(ports, concatAlias, 'StructuralAlias'), + returnsNormally, + ); + + final buffers = { + 'firstBuffer': { + 'type': r'$buf', + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'Y': [2], + }, + }, + 'secondBuffer': { + 'type': r'$buf', + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'Y': [2], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + ports, + buffers, + 'BufferedShort', + ), + throwsA(isA()), + ); + }); + + test('validation accepts inout module boundaries', () { + final ports = >{ + 'dataBus': { + 'direction': 'inout', + 'bits': [1], + }, + }; + final cells = >{ + 'SharedDataBus': { + 'type': 'SharedDataBus', + 'port_directions': {'dataBus': 'inout'}, + 'connections': { + 'dataBus': [1], + }, + }, + }; + + expect( + () => NetlistValidation.validate(ports, cells, 'FilterBank'), + returnsNormally, + ); + }); + + test('validation allows disconnected cell outputs', () { + final ports = { + 'a': { + 'direction': 'input', + 'bits': [1], + }, + 'b': { + 'direction': 'input', + 'bits': [2], + }, + }; + final cells = { + 'unusedAnd': { + 'type': r'$and', + 'port_directions': {'A': 'input', 'B': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'B': [2], + 'Y': [3], + }, + }, + }; + + expect( + () => NetlistValidation.validate(ports, cells, 'UnusedOutputModule'), + returnsNormally, + ); + }); + + test('validation allows cells to drive inout ports', () { + final ports = { + 'bus': { + 'direction': 'inout', + 'bits': [1], + }, + }; + final cells = { + 'driver': { + 'type': r'$tribuf', + 'port_directions': {'A': 'input', 'EN': 'input', 'Y': 'output'}, + 'connections': { + 'A': [2], + 'EN': [3], + 'Y': [1], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + ports, + cells, + 'InOutModule', + ), + returnsNormally, + ); + }); + }); + + // ── Group 11: Named constant signals ───────────────────────────── + + group('named constant signals', () { + test(r'Logic..gets(Const) produces $const cell and netname', () async { + final mod = _NamedConstModule(Logic(name: 'clk'), Logic(name: 'reset')); + final json = await _synthToMap(mod); + final mods = _modules(json); + + // Find the module definition for _NamedConstModule. + final modDef = mods.values.firstWhere((m) { + final def = m as Map; + return (def['cells'] as Map?)?.isNotEmpty ?? false; + }, orElse: () => mods.values.first) as Map; + + final netnames = _netnames(modDef); + final cells = _cells(modDef); + + // The signal 'myConst' should appear as a netname. + expect( + netnames.keys.any((n) => n.contains('myConst')), + isTrue, + reason: "Logic('myConst')..gets(Const(0)) should produce a netname", + ); + + // There should be a $const cell driving it. + expect( + cells.values.any( + (c) => (c as Map)['type'] == r'$const', + ), + isTrue, + reason: r'Named constant should have a $const driver cell', + ); + + // The netname bits should be integer wire IDs (not string literals). + final constNetname = netnames.entries.firstWhere( + (e) => e.key.contains('myConst'), + ); + final bits = (constNetname.value as Map)['bits'] as List; + expect( + bits.every((b) => b is int), + isTrue, + reason: 'Named constant netname should have integer wire IDs ' + r'(driven by a $const cell)', + ); + }); + }); +} diff --git a/test/netlist_test.dart b/test/netlist_test.dart new file mode 100644 index 000000000..5efb405a8 --- /dev/null +++ b/test/netlist_test.dart @@ -0,0 +1,1070 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_test.dart +// Tests for the netlist synthesizer public surface. +// +// 2026 March 31 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart'; +import '../example/filter_bank/filter_bank_modules.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 wrapper that lets tests synthesize a built submodule as the requested top. +class _CompositeWrapperModule extends Module { + late final _CompositeModule child; + + _CompositeWrapperModule(Logic a, Logic b) : super(name: 'composite_wrapper') { + a = addInput('a', a); + b = addInput('b', b); + child = _CompositeModule(a, b); + addOutput('out') <= child.out; + } +} + +/// 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; + } +} + +/// Example for the netlist-only adjacent-slice/concat collapse. +class _AdjacentSliceConcatExample extends Module { + Logic get out => output('out'); + + _AdjacentSliceConcatExample(Logic data) + : super(definitionName: 'AdjacentSliceConcatExample') { + data = addInput('data', data, width: 8); + + final low = data.getRange(0, 4).named('low'); + final high = data.getRange(4, 8).named('high'); + addOutput('out', width: 8) <= [high, low].swizzle(); + } +} + +/// Example for the netlist-only transparent slice/buf cluster collapse. +class _SliceAliasClusterExample extends Module { + Logic get out => output('out'); + + _SliceAliasClusterExample(Logic data) + : super(definitionName: 'SliceAliasClusterExample') { + data = addInput('data', data, width: 8); + + final low = data.getRange(0, 4).named('low'); + final alias = Swizzle([low]).out.named('alias'); + addOutput('out', width: 4) <= alias; + } +} + +// --------------------------------------------------------------------------- +// 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 = (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 samples = List.generate( + coefficients.length, + (ch) => FilterSample(dataWidth: dataWidth, name: 'sample$ch'), + ); + final inputDone = Logic(name: 'inputDone'); + + return FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: coefficients, + ); +} + +Map _topModuleFromJson(Module module, String json) { + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + return modules[module.definitionName] as Map; +} + +int _cellCount(Map module, String cellType) { + final cells = module['cells'] as Map? ?? {}; + return cells.values.where((cell) { + final cellMap = cell as Map; + return cellMap['type'] == cellType; + }).length; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('FilterBank argument validation', () { + FilterBank construct({ + int numChannels = 2, + int numTaps = 3, + int dataWidth = 16, + List? samples, + List> coefficients = const [ + [1, 2, 1], + [1, -2, 1], + ], + }) => + FilterBank( + Logic(), + Logic(), + Logic(), + samples ?? + List.generate( + numChannels, + (ch) => FilterSample( + dataWidth: dataWidth, + name: 'sample$ch', + ), + ), + Logic(), + numChannels: numChannels, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: coefficients, + ); + + test('rejects an empty channel count', () { + expect( + () => construct( + numChannels: 0, + samples: const [], + coefficients: const [], + ), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'numChannels', + ), + ), + ); + }); + + test('rejects an empty tap count', () { + expect( + () => construct( + numTaps: 0, + coefficients: const [[], []], + ), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'numTaps', + ), + ), + ); + }); + + test('rejects a non-positive data width', () { + expect( + () => construct( + dataWidth: 0, + samples: [ + FilterSample(name: 'sample0'), + FilterSample(name: 'sample1'), + ], + ), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'dataWidth', + ), + ), + ); + }); + + test('rejects a sample count that differs from the channel count', () { + expect( + () => construct(samples: [FilterSample(name: 'sample0')]), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'samples', + ), + ), + ); + }); + + test('rejects a coefficient count that differs from the channel count', () { + expect( + () => construct(coefficients: const [ + [1, 2, 1], + ]), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'coefficients', + ), + ), + ); + }); + + test('rejects a coefficient row with the wrong tap count', () { + expect( + () => construct(coefficients: const [ + [1, 2, 1], + [1, -2], + ]), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'coefficients[1]', + ), + ), + ); + }); + }); + + // ── 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 = 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 = 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 = 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('requested submodule can be synthesized as top', () async { + final wrapper = _CompositeWrapperModule( + Logic(name: 'a'), + Logic(name: 'b'), + ); + await wrapper.build(); + + final submodule = wrapper.child; + final json = NetlistSynthesizer().synthesizeToJson(submodule); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + + expect(modules, contains(submodule.definitionName)); + expect(modules, isNot(contains(wrapper.definitionName))); + + final topModules = modules.values.where((module) { + final attrs = (module as Map)['attributes'] + as Map?; + return attrs?['top'] == 1; + }); + expect(topModules, hasLength(1)); + + final attrs = (modules[submodule.definitionName] + as Map)['attributes'] as Map; + 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 = 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 = 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 = synthesizer.generateCombinedJson(synth, mod); + final fromConvenience = 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); + }); + + test('result maps and nested values are unmodifiable', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final result = SynthBuilder(mod, NetlistSynthesizer()) + .synthesisResults + .whereType() + .firstWhere((result) => result.module == mod); + final firstCell = result.cells.values.first; + final connections = firstCell['connections']! as Map; + + expect( + () => result.cells['replacement'] = {}, + throwsUnsupportedError, + ); + expect( + () => firstCell['type'] = r'$replacement', + throwsUnsupportedError, + ); + expect( + () => connections['replacement'] = [], + throwsUnsupportedError, + ); + }); + }); + + // ── 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 = 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); + } + }); + }); + + // ── NetlistSynthesizerConfiguration ────────────────────────────────── + group('NetlistSynthesizerConfiguration', () { + test('slimMode omits cell connections', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final slimSynth = NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration(slimMode: true), + ); + final json = 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'); + } + } + } + }); + }); + + // ── Netlist-only transparent optimizations ─────────────────────────── + + group('Netlist-only transparent optimizations', () { + test('adjacent slices feeding concat collapse into one wider slice', + () async { + final mod = _AdjacentSliceConcatExample(Logic(name: 'data', width: 8)); + await mod.build(); + + final rawTop = _topModuleFromJson( + mod, + NetlistSynthesizer().synthesizeToJson(mod), + ); + expect(_cellCount(rawTop, r'$slice'), equals(2)); + expect(_cellCount(rawTop, r'$concat'), equals(1)); + + final collapsedTop = _topModuleFromJson( + mod, + NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration( + collapseTransparentClusters: true), + ).synthesizeToJson(mod), + ); + expect(_cellCount(collapsedTop, r'$slice'), equals(1)); + expect(_cellCount(collapsedTop, r'$concat'), equals(0)); + }); + + test('slice feeding alias buffer collapses into one buffer', () async { + final mod = _SliceAliasClusterExample(Logic(name: 'data', width: 8)); + await mod.build(); + + final rawTop = _topModuleFromJson( + mod, + NetlistSynthesizer().synthesizeToJson(mod), + ); + expect(_cellCount(rawTop, r'$slice'), equals(1)); + expect(_cellCount(rawTop, r'$buf'), equals(1)); + + final collapsedTop = _topModuleFromJson( + mod, + NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration( + collapseTransparentClusters: true), + ).synthesizeToJson(mod), + ); + expect(_cellCount(collapsedTop, r'$slice'), equals(0)); + expect(_cellCount(collapsedTop, r'$buf'), equals(1)); + }); + }); + + // ── 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 = 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 = 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('post-processing does not mutate synthesis results', () async { + final module = _AdderModule( + Logic(name: 'a', width: 8), + Logic(name: 'b', width: 8), + ); + await module.build(); + + final synthesizer = NetlistSynthesizer( + configuration: + const NetlistSynthesizerConfiguration(compressBitRanges: true), + ); + final builder = SynthBuilder(module, synthesizer); + final result = builder.synthesisResults + .whereType() + .firstWhere((result) => result.module == module); + final before = jsonEncode({ + 'ports': result.ports, + 'cells': result.cells, + 'netnames': result.netnames, + }); + + synthesizer.generateCombinedJson(builder, module); + + final after = jsonEncode({ + 'ports': result.ports, + 'cells': result.cells, + 'netnames': result.netnames, + }); + expect(after, before); + }); + + 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( + configuration: + const NetlistSynthesizerConfiguration(compressBitRanges: true), + ); + final jsonCompressed = synthCompressed.synthesizeToJson(mod); + + final synthNormal = NetlistSynthesizer(); + final jsonNormal = 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( + configuration: + const NetlistSynthesizerConfiguration(compressBitRanges: true), + ); + final json = 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( + configuration: const NetlistSynthesizerConfiguration(compactJson: true), + ); + final jsonCompact = synthCompact.synthesizeToJson(mod); + + final synthNormal = NetlistSynthesizer(); + final jsonNormal = 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 configuration 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( + configuration: const NetlistSynthesizerConfiguration( + compressBitRanges: true, + compactJson: true, + ), + ); + final jsonBoth = synthBoth.synthesizeToJson(mod); + + final synthCompressOnly = NetlistSynthesizer( + configuration: + const NetlistSynthesizerConfiguration(compressBitRanges: true), + ); + final jsonCompressOnly = synthCompressOnly.synthesizeToJson(mod); + + final synthCompactOnly = NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration(compactJson: true), + ); + final jsonCompactOnly = 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 = synthNormal.synthesizeToJson(mod); + final normalModules = (jsonDecode(jsonNormal) + as Map)['modules'] as Map; + + final synthCompressed = NetlistSynthesizer( + configuration: + const NetlistSynthesizerConfiguration(compressBitRanges: true), + ); + final jsonCompressed = 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/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/synth_name_parity_test.dart b/test/synth_name_parity_test.dart new file mode 100644 index 000000000..dc10da6e1 --- /dev/null +++ b/test/synth_name_parity_test.dart @@ -0,0 +1,378 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_name_parity_test.dart +// Tests that verify signalNameOfBest works consistently across +// different synthesis paths (SV and netlist). +// +// 2026 April 14 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +import '../example/filter_bank.dart'; + +extension _NetlistTestModule on Module { + String generateNetlist( + {NetlistSynthesizerConfiguration configuration = + const NetlistSynthesizerConfiguration(), + String? packageRoot}) { + if (!hasBuilt) { + throw ModuleNotBuiltException(this); + } + + return NetlistSynthesizer(configuration: configuration) + .synthesizeToJson(this, packageRoot: packageRoot); + } +} + +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]), + ], + ), + ], + ); + } +} + +class _CollidingNames extends Module { + late final Logic firstDup; + late final Logic secondDup; + + _CollidingNames(Logic a, Logic b) : super(name: 'collidingNames') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + + firstDup = Logic(name: 'dup'); + secondDup = Logic(name: 'dup'); + + firstDup <= a & b; + secondDup <= a | b; + y <= firstDup ^ secondDup; + } +} + +class _PartiallyInlineCollidingNames extends Module { + late final Logic inlinedDup; + late final Logic retainedDup; + + _PartiallyInlineCollidingNames(Logic a, Logic b) + : super(name: 'partiallyInlineCollidingNames') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + final z = addOutput('z'); + + inlinedDup = Logic(name: 'dup'); + retainedDup = Logic(name: 'dup'); + + inlinedDup <= a & b; + retainedDup <= a | b; + y <= inlinedDup ^ retainedDup; + z <= retainedDup & a; + } +} + +class _CollapsedInstanceCollidingNames extends Module { + late final Logic retainedDup; + + _CollapsedInstanceCollidingNames(Logic a, Logic b) + : super(name: 'collapsedInstanceCollidingNames') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + final z = addOutput('z'); + + final collapsedInstanceOut = And2Gate(a, b, name: 'dup').out; + retainedDup = Logic(name: 'dup'); + + retainedDup <= a | b; + y <= collapsedInstanceOut ^ retainedDup; + z <= retainedDup; + } +} + +class _ReverseInternalSignalOrderSynthModuleDefinition + extends SynthModuleDefinition { + _ReverseInternalSignalOrderSynthModuleDefinition(super.module); + + @override + void process() { + internalSignals + ..clear() + ..addAll(internalSignals.toList().reversed); + } +} + +Future> _collisionNamesAfter( + Iterable synthesize, +) async { + final mod = _CollidingNames(Logic(), Logic()); + await mod.build(); + + for (final synth in synthesize) { + synth(mod); + } + + return { + 'firstDup': mod.namer.signalNameOfBest([mod.firstDup]), + 'secondDup': mod.namer.signalNameOfBest([mod.secondDup]), + }; +} + +Future> _collisionNamesAfterSynthDefinition( + SynthModuleDefinition Function(_CollidingNames) createSynthDefinition, +) async { + final mod = _CollidingNames(Logic(), Logic()); + await mod.build(); + + createSynthDefinition(mod); + + return { + 'firstDup': mod.namer.signalNameOfBest([mod.firstDup]), + 'secondDup': mod.namer.signalNameOfBest([mod.secondDup]), + }; +} + +Future> _partialInlineCollisionNamesAfter( + Iterable synthesize, +) async { + final mod = _PartiallyInlineCollidingNames(Logic(), Logic()); + await mod.build(); + + for (final synth in synthesize) { + synth(mod); + } + + return { + 'retainedDup': mod.namer.signalNameOfBest([mod.retainedDup]), + 'inlinedDup': mod.namer.signalNameOfBest([mod.inlinedDup]), + }; +} + +Future> _collapsedInstanceCollisionNamesAfter( + Iterable synthesize, +) async { + final mod = _CollapsedInstanceCollidingNames(Logic(), Logic()); + await mod.build(); + + for (final synth in synthesize) { + synth(mod); + } + + return { + 'retainedDup': mod.namer.signalNameOfBest([mod.retainedDup]), + }; +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('signalNameOfBest after netlist synthesis', () { + test('counter — returns names after netlist synthesis', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + mod.generateNetlist(); + + expect(mod.namer.signalNameOfBest([mod.input('en')]), equals('en')); + expect(mod.namer.signalNameOfBest([mod.input('reset')]), equals('reset')); + expect(mod.namer.signalNameOfBest([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 samples = List.generate(2, (ch) => FilterSample(name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: [ + [1, 2, 1], + [1, -2, 1], + ], + ); + await dut.build(); + dut.generateNetlist(); + + expect(dut.namer.signalNameOfBest([dut.input('clk')]), equals('clk')); + expect(dut.namer.signalNameOfBest([dut.input('reset')]), equals('reset')); + expect(dut.namer.signalNameOfBest([dut.output('done')]), equals('done')); + }); + }); + + group('signalNameOfBest after SV synthesis', () { + test('counter — returns best signal name after SV synth', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + mod.generateSynth(); + + expect(mod.namer.signalNameOfBest([mod.input('en')]), equals('en')); + expect(mod.namer.signalNameOfBest([mod.input('reset')]), equals('reset')); + }); + }); + + group('cross-synthesizer parity', () { + test( + 'counter — SV and netlist produce identical signalNameOfBest', + () async { + final modNetlist = _Counter(Logic(), Logic()); + await modNetlist.build(); + modNetlist.generateNetlist(); + 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.signalNameOfBest([ + modNetlist.input('en'), + ]); + final enSv = modSv.namer.signalNameOfBest([modSv.input('en')]); + + expect( + enSv, + equals(enNetlist), + reason: 'SV and netlist should produce identical canonical names', + ); + }, + ); + + test( + 'colliding mergeable names remain stable across synthesis order', + () async { + void runNetlist(_CollidingNames mod) => mod.generateNetlist(); + void runSv(_CollidingNames mod) => mod.generateSynth(); + + final netlistOnly = await _collisionNamesAfter([runNetlist]); + await Simulator.reset(); + + final svOnly = await _collisionNamesAfter([runSv]); + await Simulator.reset(); + + final netlistThenSv = await _collisionNamesAfter([runNetlist, runSv]); + await Simulator.reset(); + + final svThenNetlist = await _collisionNamesAfter([runSv, runNetlist]); + + expect(netlistOnly, equals(svOnly)); + expect(netlistThenSv, equals(netlistOnly)); + expect(svThenNetlist, equals(netlistOnly)); + + expect( + netlistOnly['secondDup'], + isNot(equals(netlistOnly['firstDup'])), + ); + }, + ); + + test( + 'colliding mergeable names ignore internal signal walk order', + () async { + final forward = await _collisionNamesAfterSynthDefinition( + SynthModuleDefinition.new, + ); + await Simulator.reset(); + + final reversed = await _collisionNamesAfterSynthDefinition( + _ReverseInternalSignalOrderSynthModuleDefinition.new, + ); + + expect(reversed, equals(forward)); + expect(forward['firstDup'], equals('dup')); + expect(forward['secondDup'], equals('dup_0')); + }, + ); + + test('colliding names stay stable when SV inlines one signal', () async { + void runNetlist(_PartiallyInlineCollidingNames mod) => + mod.generateNetlist(); + void runSv(_PartiallyInlineCollidingNames mod) => mod.generateSynth(); + + final netlistOnly = await _partialInlineCollisionNamesAfter([runNetlist]); + await Simulator.reset(); + + final svOnly = await _partialInlineCollisionNamesAfter([runSv]); + await Simulator.reset(); + + final netlistThenSv = await _partialInlineCollisionNamesAfter([ + runNetlist, + runSv, + ]); + await Simulator.reset(); + + final svThenNetlist = await _partialInlineCollisionNamesAfter([ + runSv, + runNetlist, + ]); + + expect(svOnly, equals(netlistOnly)); + expect(netlistThenSv, equals(netlistOnly)); + expect(svThenNetlist, equals(netlistOnly)); + expect(netlistOnly['inlinedDup'], equals('dup')); + expect(netlistOnly['retainedDup'], equals('dup_0')); + }); + + test( + 'signal names stay stable when SV collapses a colliding instance', + () async { + void runNetlist(_CollapsedInstanceCollidingNames mod) => + mod.generateNetlist(); + void runSv(_CollapsedInstanceCollidingNames mod) => mod.generateSynth(); + + final netlistOnly = await _collapsedInstanceCollisionNamesAfter([ + runNetlist, + ]); + await Simulator.reset(); + + final svOnly = await _collapsedInstanceCollisionNamesAfter([runSv]); + await Simulator.reset(); + + final netlistThenSv = await _collapsedInstanceCollisionNamesAfter([ + runNetlist, + runSv, + ]); + await Simulator.reset(); + + final svThenNetlist = await _collapsedInstanceCollisionNamesAfter([ + runSv, + runNetlist, + ]); + + expect(svOnly, equals(netlistOnly)); + expect(netlistThenSv, equals(netlistOnly)); + expect(svThenNetlist, equals(netlistOnly)); + expect(netlistOnly['retainedDup'], equals('dup')); + }, + ); + }); +} diff --git a/test/synth_structure_layout_test.dart b/test/synth_structure_layout_test.dart new file mode 100644 index 000000000..9491598ff --- /dev/null +++ b/test/synth_structure_layout_test.dart @@ -0,0 +1,93 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_structure_layout_test.dart +// Tests for packed LogicStructure layout synthesis utilities. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +void main() { + group('SynthStructureLayout', () { + test('uses least-significant-first element offsets', () { + final structure = LogicStructure([ + Logic(name: 'low', width: 2), + Logic(name: 'high', width: 3), + ]); + final layout = SynthStructureLayout(structure); + + expect(layout.fieldNameAt(0, fallbackName: 'fallback'), 'low'); + expect(layout.fieldNameAt(1, fallbackName: 'fallback'), 'low'); + expect(layout.fieldNameAt(2, fallbackName: 'fallback'), 'high'); + expect(layout.fieldNameAt(4, fallbackName: 'fallback'), 'high'); + expect(layout.fieldNameAt(5, fallbackName: 'fallback'), 'fallback'); + }); + + test('qualifies an unpreferred nested leaf by parent and index', () { + final nested = LogicStructure([ + Logic(name: Naming.unpreferredName('first'), width: 2), + Logic(name: Naming.unpreferredName('second'), width: 2), + ], name: 'payload'); + final structure = LogicStructure([ + Logic(name: 'header'), + nested, + ]); + final layout = SynthStructureLayout(structure); + + expect(layout.fieldNameAt(1, fallbackName: 'fallback'), 'payload_0'); + expect(layout.fieldNameAt(3, fallbackName: 'fallback'), 'payload_1'); + }); + + test('returns bit ranges for nested field paths', () { + final nested = LogicStructure([ + Logic(name: 'b', width: 2), + LogicStructure([ + Logic(name: 'd', width: 3), + ], name: 'c'), + ], name: 'a'); + final structure = LogicStructure([ + Logic(name: 'prefix'), + nested, + ]); + final layout = SynthStructureLayout(structure); + + expect(layout.bitRangeForPath('a'), (start: 1, end: 6)); + expect(layout.bitRangeForPath('a.b'), (start: 1, end: 3)); + expect(layout.bitRangeForPath('a.c'), (start: 3, end: 6)); + expect(layout.bitRangeForPath('a.c.d'), (start: 3, end: 6)); + expect(layout.bitRangeForPath('a.missing'), isNull); + }); + + test('supports unpack-specific anonymous field names', () { + final fieldName = Naming.unpreferredName('field'); + final structure = LogicStructure([Logic(name: fieldName)]); + final layout = SynthStructureLayout(structure); + + expect(layout.fieldNameAt(0, fallbackName: 'fallback'), fieldName); + expect( + layout.fieldNameAt( + 0, + fallbackName: 'fallback', + anonymousUnpreferred: true, + ), + 'anonymous_0', + ); + }); + + test('does not recurse into LogicArray elements', () { + final structure = LogicStructure([ + LogicArray([2], 3, name: 'entries'), + Logic(name: 'tail'), + ]); + final layout = SynthStructureLayout(structure); + + expect(layout.fieldNameAt(0, fallbackName: 'fallback'), 'entries'); + expect(layout.fieldNameAt(5, fallbackName: 'fallback'), 'entries'); + expect(layout.fieldNameAt(6, fallbackName: 'fallback'), 'tail'); + }); + }); +} diff --git a/tool/generate_gate_catalog.dart b/tool/generate_gate_catalog.dart new file mode 100644 index 000000000..41b0a9c89 --- /dev/null +++ b/tool/generate_gate_catalog.dart @@ -0,0 +1,69 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// generate_gate_catalog.dart +// Regenerates the checked-in gate-catalog netlist asset +// (`test/fixtures/gate_catalog.rohd.json`) from `GateCatalog` (see +// `test/fixtures/gate_catalog_module.dart`) using the default +// [NetlistSynthesizerConfiguration]. +// +// Usage: +// dart run tool/generate_gate_catalog.dart +// +// After regenerating, review the diff to `test/fixtures/gate_catalog.rohd.json` +// before committing it, and re-run `dart test test/gate_catalog_test.dart` to +// confirm the fixture, determinism, and coverage checks all pass. +// +// 2026 August 20 +// Author: Desmond Kirkpatrick + +import 'dart:io'; + +import 'package:rohd/rohd.dart'; + +import '../test/fixtures/gate_catalog_module.dart'; + +/// Relative (to the package root) output path for the generated fixture. +const _fixturePath = 'test/fixtures/gate_catalog.rohd.json'; + +/// Builds a fresh [GateCatalog] with deterministic, freshly-allocated input +/// signals. +/// +/// This must stay in sync with `_buildCatalog()` in +/// `test/gate_catalog_test.dart` so that the fixture this tool generates is +/// exactly what that test's byte-for-byte comparison expects. +GateCatalog _buildCatalog() => GateCatalog( + clk: Logic(name: 'clk'), + en: Logic(name: 'en'), + reset: Logic(name: 'reset'), + muxSel: Logic(name: 'muxSel'), + enableTri: Logic(name: 'enableTri'), + a4: Logic(name: 'a4', width: 4), + b4: Logic(name: 'b4', width: 4), + a8: Logic(name: 'a8', width: 8), + b8: Logic(name: 'b8', width: 8), + d4: Logic(name: 'd4', width: 4), + shamt4: Logic(name: 'shamt4', width: 4), + idx3: Logic(name: 'idx3', width: 3), + idx5: Logic(name: 'idx5', width: 5), + resetValueDyn4: Logic(name: 'resetValueDyn4', width: 4), + busNet: LogicNet(name: 'busNet', width: 8), + ); + +Future main() async { + final catalog = _buildCatalog(); + await catalog.build(); + + final synth = NetlistSynthesizer(); + // We will migrate to a new public API in a future PR + // ignore: invalid_use_of_visible_for_testing_member + final json = synth.synthesizeToJson(catalog); + + final fixtureFile = File(_fixturePath); + fixtureFile.parent.createSync(recursive: true); + fixtureFile.writeAsStringSync(json); + + stdout.writeln('Wrote $_fixturePath (${json.length} bytes).'); + + await Simulator.reset(); +}