diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index f2ed2cf65..b74924d3e 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -61,9 +61,18 @@ jobs: - name: Install software - Icarus Verilog run: tool/gh_actions/install_iverilog.sh + - name: Install software - Accellera SystemC + run: tool/gh_actions/install_systemc.sh + + - name: Pre-build SystemC PCH and Makefile + run: tool/gh_actions/setup_systemc_pch.sh + - name: Run project tests run: tool/gh_actions/run_tests.sh + - name: Clean SystemC temporary files + run: tool/gh_actions/cleanup_systemc_tmp.sh + - name: Check temporary test files run: tool/gh_actions/check_tmp_test.sh @@ -71,7 +80,10 @@ jobs: - name: Build dev container and run tests in it uses: devcontainers/ci@v0.3 with: - runCmd: tool/gh_actions/run_tests.sh + runCmd: | + tool/gh_actions/run_tests.sh + tool/gh_actions/cleanup_systemc_tmp.sh + tool/gh_actions/check_tmp_test.sh deploy-documentation: name: Deploy Documentation diff --git a/README.md b/README.md index a996ccccb..1863edac9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ [![Chat](https://img.shields.io/discord/1001179329411166267?label=Chat)](https://discord.gg/jubxF84yGw) [![License](https://img.shields.io/badge/License-BSD--3-blue)](https://github.com/intel/rohd/blob/main/LICENSE) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/intel/rohd/blob/main/CODE_OF_CONDUCT.md) -[![Coverage](https://raw.githubusercontent.com/intel/rohd/refs/heads/badges/coverage/main.svg)](https://github.com/intel/rohd/blob/main/.github/workflows/coverage.yml) ROHD (pronounced like "road") is a framework for describing and verifying hardware in the Dart programming language. @@ -45,7 +44,7 @@ You can also open this repository in a GitHub Codespace to run the example in yo - **Simple and fast build**, free of complex build systems and EDA vendor tools - Can use the excellent pub.dev **package manager** and all the packages it has to offer - Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform dumper** to .vcd file format -- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption +- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** and **SystemC** for integration or downstream tool consumption - **Run-time dynamic** module port definitions (numbers, names, widths, etc.) and internal module logic, including recursive module contents - Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components. - Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. @@ -69,5 +68,6 @@ One of ROHD's goals is to help grow an open-source community around reusable har ROHD is under active development. If you're interested in contributing, have feedback or a question, or found a bug, please see [CONTRIBUTING.md](https://github.com/intel/rohd/blob/main/CONTRIBUTING.md). ---------------- + Copyright (C) 2021-2026 Intel Corporation SPDX-License-Identifier: BSD-3-Clause diff --git a/dart_test.yaml b/dart_test.yaml new file mode 100644 index 000000000..30eaa9f01 --- /dev/null +++ b/dart_test.yaml @@ -0,0 +1,15 @@ +# Test configuration for ROHD. +# +# To exclude FFI-dependent tests (e.g. in CI without native code support): +# dart test --preset no-ffi +# +# To run all tests including FFI (requires native shared libraries): +# dart test + +tags: + ffi: + # Tests requiring dart:ffi and native shared libraries. + +presets: + no-ffi: + exclude_tags: ffi diff --git a/doc/architecture.md b/doc/architecture.md index cc1e775ae..aacf29c35 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -24,7 +24,7 @@ The `Simulator` acts as a statically accessible driver of the overall simulation ### Synthesizer -A separate type of object responsible for taking a `Module` and converting it to some output, such as SystemVerilog. +A separate type of object responsible for taking a `Module` and converting it to some output, such as SystemVerilog or SystemC. ## Organization @@ -44,7 +44,7 @@ Contains a collection of `Module` implementations that can be used as primitive ### Synthesizers -Contains logic for synthesizing `Module`s into some output. It is structured to maximize reusability across different output types (including those not yet supported). +Contains logic for synthesizing `Module`s into some output (e.g. SystemVerilog, SystemC). It is structured to maximize reusability across different output types. ### Utilities diff --git a/doc/user_guide/_docs/A21-generation.md b/doc/user_guide/_docs/A21-generation.md index 5c172eeb9..b8a2f0f6f 100644 --- a/doc/user_guide/_docs/A21-generation.md +++ b/doc/user_guide/_docs/A21-generation.md @@ -5,7 +5,7 @@ last_modified_at: 2023-11-13 toc: true --- -Hardware in ROHD is convertible to an output format via `Synthesizer`s, the most popular of which is SystemVerilog. Hardware in ROHD can be converted to logically equivalent, human-readable SystemVerilog with structure, hierarchy, ports, and names maintained. +Hardware in ROHD is convertible to an output format via `Synthesizer`s. The most popular output format is SystemVerilog, with SystemC also available. Hardware in ROHD can be converted to logically equivalent, human-readable SystemVerilog or SystemC with structure, hierarchy, ports, and names maintained. The simplest way to generate SystemVerilog is with the helper method `generateSynth` in `Module`: @@ -28,6 +28,26 @@ void main() async { The `generateSynth` function will return a `String` with the SystemVerilog `module` definitions for the top-level it is called on, as well as any sub-modules (recursively). You can dump the entire contents to a file and use it anywhere you would any other SystemVerilog. +## SystemC generation + +ROHD can also generate SystemC (C++ with the SystemC library) from the same hardware description. Use the `generateSystemC` helper method: + +```dart +void main() async { + final myModule = MyModule(); + await myModule.build(); + + final generatedSc = myModule.generateSystemC(); + + // write it to a file + File('myHardware.h').writeAsStringSync(generatedSc); +} +``` + +The generated SystemC uses `SC_MODULE`, `SC_METHOD`, and `SC_CTHREAD` constructs. Combinational logic becomes `SC_METHOD` processes, sequential logic (flip-flops and `Sequential` blocks) sharing the same clock and reset are consolidated into a single `SC_CTHREAD`, and sub-modules are instantiated with port bindings. All signal types map to SystemC equivalents (`bool`, `sc_uint`, `sc_biguint`). + +For more control over SystemC generation, use `SynthBuilder` with `SystemCSynthesizer()` directly. + ## Controlling port types Generated ports have explicit object and data types by default: inputs are `input wire logic`, outputs are `output var logic`, and inouts are `inout wire logic`. Use a `SystemVerilogSynthesizerConfiguration` to omit either category and rely on SystemVerilog's implicit types: diff --git a/doc/user_guide/_get-started/01-overview.md b/doc/user_guide/_get-started/01-overview.md index c1a98cdc1..c30c9f87f 100644 --- a/doc/user_guide/_get-started/01-overview.md +++ b/doc/user_guide/_get-started/01-overview.md @@ -19,7 +19,7 @@ Features of ROHD include: - **Simple and fast build**, free of complex build systems and EDA vendor tools - Can use the excellent pub.dev **package manager** and all the packages it has to offer - Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform dumper** to .vcd file format -- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption +- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** and **SystemC** for integration or downstream tool consumption - **Run-time dynamic** module port definitions (numbers, names, widths, etc.) and internal module logic, including recursive module contents - Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components. - Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. diff --git a/lib/src/module.dart b/lib/src/module.dart index a1cb8ec5c..f98d91fab 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1161,6 +1161,27 @@ abstract class Module { SystemVerilogSynthesizer(configuration: configuration), ).getSynthFileContents().join('\n\n////////////////////\n\n'); } + + /// Returns a synthesized SystemC version of this [Module]. + /// + /// Generates SystemC code that is equivalent to the hardware described by + /// this module, using the same naming strategy as [generateSynth]. + String generateSystemC() { + if (!_hasBuilt) { + throw ModuleNotBuiltException(this); + } + + final synthBuilder = SynthBuilder(this, SystemCSynthesizer()); + final moduleContents = + synthBuilder.getSynthFileContents().map((e) => e.contents).join('\n'); + return '// Generated by ROHD - www.github.com/intel/rohd\n' + '// Generation time: ${Timestamper.stamp()}\n' + '// ROHD Version: ${Config.version}\n' + '\n' + '#include \n' + '\n' + '$moduleContents'; + } } extension on LogicStructure { diff --git a/lib/src/modules/bus.dart b/lib/src/modules/bus.dart index 3a797fbf4..23ec0b05e 100644 --- a/lib/src/modules/bus.dart +++ b/lib/src/modules/bus.dart @@ -7,8 +7,6 @@ // 2021 August 2 // Author: Max Korbel -import 'dart:math' show max; - import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; @@ -18,7 +16,7 @@ import 'package:rohd/rohd.dart'; /// The output [subset] will have width equal to `|endIndex - startIndex| + 1`. /// /// This module also supports nets, allowing subsets to be bidirectional. -class BusSubset extends Module with InlineSystemVerilog { +class BusSubset extends Module with InlineLeaf { /// Name for the input port of this module. late final String _originalName; @@ -136,38 +134,6 @@ class BusSubset extends Module with InlineSystemVerilog { subset.put(original.value.getRange(startIndex, endIndex + 1)); } } - - /// A regular expression that will have matches if an expression is included. - static final RegExp _expressionRegex = RegExp("[()']"); - - @override - String inlineVerilog(Map inputs) { - assert(inputs.length == 1 || (inputs.length == 2 && _isNet), - 'BusSubset has exactly one input, but saw $inputs.'); - - final a = inputs[_originalName]!; - - assert(!a.contains(_expressionRegex), - 'Inputs to bus swizzle cannot contain any expressions.'); - - // When, input width is 1, ignore startIndex and endIndex - if (original.width == 1) { - return a; - } - - // SystemVerilog doesn't allow reverse-order select to reverse a bus, - // so do it manually - if (startIndex > endIndex) { - final swizzleContents = - List.generate(startIndex - endIndex + 1, (i) => '$a[${endIndex + i}]') - .join(','); - return '{$swizzleContents}'; - } - - final sliceString = - startIndex == endIndex ? '[$startIndex]' : '[$endIndex:$startIndex]'; - return '$a$sliceString'; - } } /// A [Module] that performs concatenation of signals into one bigger [Logic]. @@ -180,14 +146,9 @@ class BusSubset extends Module with InlineSystemVerilog { /// /// This module supports nets, allowing concatenation to be bidirectionally /// driven. -class Swizzle extends Module with InlineSystemVerilog { +class Swizzle extends Module with InlineLeaf { final String _out = Naming.unpreferredName('swizzled'); - /// A regular expression that will have matches if an expression is a single - /// bit select of a signal or packed array element. - static final RegExp _singleBitSelectRegex = - RegExp(r'^\(?([A-Za-z_][A-Za-z0-9_$]*(?:\[\d+\])*)\[(\d+)\]\)?$'); - /// The output port containing concatenated signals. late final Logic out; @@ -254,175 +215,4 @@ class Swizzle extends Module with InlineSystemVerilog { @override String get resultSignalName => _out; - - @override - String inlineVerilog(Map inputs) { - assert( - inputs.length == _swizzleInputs.length || - (inputs.length == _swizzleInputs.length + 1 && isNet), - 'This swizzle has ${_swizzleInputs.length} inputs,' - ' but saw $inputs with ${inputs.length} values.'); - - // Calculate all width descriptions upfront to determine alignment - final validInputs = - _swizzleInputs.reversed.where((e) => e.width > 0).toList(); - final operands = _collapseContiguousBitSelects(validInputs, inputs); - - // If there's only one element, no need for width descriptions - if (operands.length == 1) { - return operands.first.expression; - } - - final widthDescriptions = <({int upper, int? lower})>[]; - var upperIndex = out.width - 1; - - // First pass: calculate all width descriptions - for (final operand in operands) { - if (operand.width > 1) { - final lowerIndex = upperIndex - operand.width + 1; - widthDescriptions.add((upper: upperIndex, lower: lowerIndex)); - } else { - widthDescriptions.add((upper: upperIndex, lower: null)); - } - upperIndex -= operand.width; - } - - // Find maximum width for alignment - final maxUpperWidth = widthDescriptions.isEmpty - ? 0 - : widthDescriptions - .map((desc) => desc.upper.toString().length) - .reduce(max); - final maxLowerWidth = - widthDescriptions.where((desc) => desc.lower != null).isEmpty - ? 0 - : widthDescriptions - .where((desc) => desc.lower != null) - .map((desc) => desc.lower!.toString().length) - .reduce(max); - - // Second pass: generate aligned output - upperIndex = out.width - 1; - final inputLines = []; - var descIndex = 0; - - for (final operand in operands) { - final desc = widthDescriptions[descIndex++]; - - String alignedDesc; - if (desc.lower != null) { - final paddedUpper = desc.upper.toString().padLeft(maxUpperWidth); - final paddedLower = desc.lower!.toString().padLeft(maxLowerWidth); - alignedDesc = '$paddedUpper:$paddedLower'; - } else { - // For single bits, right-align to the total width (upper:lower format) - final totalWidth = - maxUpperWidth + (maxLowerWidth > 0 ? 1 + maxLowerWidth : 0); - alignedDesc = desc.upper.toString().padLeft(totalWidth); - } - - upperIndex -= operand.width; - final maybeComma = - upperIndex >= 0 ? ',' : ' '; // space at end for alignment - inputLines.add('${operand.expression}$maybeComma /* $alignedDesc */'); - } - - return ''' -{ -${inputLines.join('\n')} -}'''; - } - - /// Rewrites runs of adjacent descending single-bit selects from the same - /// packed signal into wider SystemVerilog slices. - /// - /// For example, `a[7], a[6], a[5]` becomes `a[7:5]`, and - /// `a[0][1], a[0][0]` becomes `a[0][1:0]`. Ascending runs are intentionally - /// left expanded because SystemVerilog slices cannot reverse bit order with - /// `lower:upper` syntax. - List<({String expression, int width})> _collapseContiguousBitSelects( - List validInputs, - Map inputs, - ) { - final operands = <({String expression, int width})>[]; - - var index = 0; - while (index < validInputs.length) { - final input = validInputs[index]; - final expression = inputs[input.name]!; - final selectedBit = _singleBitSelect(input, expression); - if (selectedBit == null) { - operands.add((expression: expression, width: input.width)); - index++; - continue; - } - - var lowerIndex = selectedBit.index; - var endIndex = index + 1; - while (endIndex < validInputs.length) { - final nextInput = validInputs[endIndex]; - final nextExpression = inputs[nextInput.name]!; - final nextSelectedBit = _singleBitSelect(nextInput, nextExpression); - if (nextSelectedBit == null || - nextSelectedBit.source != selectedBit.source || - nextSelectedBit.index != lowerIndex - 1) { - break; - } - - lowerIndex = nextSelectedBit.index; - endIndex++; - } - - if (endIndex == index + 1) { - operands.add((expression: expression, width: input.width)); - } else { - operands.add(( - expression: '${selectedBit.source}[${selectedBit.index}:$lowerIndex]', - width: endIndex - index, - )); - } - index = endIndex; - } - - return operands; - } - - /// Parses [expression] as a single-bit select of a packed signal when it is - /// safe to participate in slice collapsing. - /// - /// Returns `null` for multi-bit inputs, non-select expressions, or selects - /// sourced from unpacked arrays. - ({String source, int index})? _singleBitSelect( - Logic input, - String expression, - ) { - if (input.width != 1 || _hasUnpackedArraySource(input.srcConnection)) { - return null; - } - - final match = _singleBitSelectRegex.firstMatch(expression); - if (match == null) { - return null; - } - - return (source: match.group(1)!, index: int.parse(match.group(2)!)); - } - - /// Walks up [logic]'s containing structures to detect unpacked arrays. - /// - /// SystemVerilog packed slices are not interchangeable with unpacked array - /// indexing, so any unpacked array source disables bit-select collapsing. - bool _hasUnpackedArraySource(Logic? logic) { - var current = logic; - while (current?.parentStructure != null) { - final parentStructure = current!.parentStructure!; - if (parentStructure is LogicArray && - parentStructure.numUnpackedDimensions > 0) { - return true; - } - current = parentStructure; - } - - return false; - } } diff --git a/lib/src/modules/conditionals/always.dart b/lib/src/modules/conditionals/always.dart index 7c50a9eb6..834cb8668 100644 --- a/lib/src/modules/conditionals/always.dart +++ b/lib/src/modules/conditionals/always.dart @@ -11,6 +11,8 @@ import 'dart:collection'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_process_emitter.dart'; +import 'package:rohd/src/synthesizers/utilities/process_emission_plan.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; @@ -144,17 +146,6 @@ abstract class Always extends Module with SystemVerilog { } } - String _alwaysContents(Map inputsNameMap, - Map outputsNameMap, String assignOperator) { - final contents = StringBuffer(); - for (final conditional in conditionals) { - final subContents = conditional.verilogContents( - 1, inputsNameMap, outputsNameMap, assignOperator); - contents.write('$subContents\n'); - } - return contents.toString(); - } - /// The "always" part of the `always` block when generating SystemVerilog. /// /// For example, `always_comb` or `always_ff`. @@ -174,18 +165,10 @@ abstract class Always extends Module with SystemVerilog { String instanceType, String instanceName, Map ports, - ) { - // no `inouts` can be used in a `Conditional` - final inputs = Map.fromEntries( - ports.entries.where((element) => this.inputs.containsKey(element.key))); - final outputs = Map.fromEntries(ports.entries - .where((element) => this.outputs.containsKey(element.key))); - - var verilog = ''; - verilog += '// $instanceName\n'; - verilog += '${alwaysVerilogStatement(inputs)} begin\n'; - verilog += _alwaysContents(inputs, outputs, assignOperator()); - verilog += 'end\n'; - return verilog; - } + ) => + const SystemVerilogProcessEmitter().emit( + ProcessEmissionPlan.fromAlways(this), + instanceName, + ports, + ); } diff --git a/lib/src/modules/conditionals/case.dart b/lib/src/modules/conditionals/case.dart index 61b90fff3..1241444c4 100644 --- a/lib/src/modules/conditionals/case.dart +++ b/lib/src/modules/conditionals/case.dart @@ -11,6 +11,7 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/modules/conditionals/ssa.dart'; +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_conditional_emitter.dart'; /// Represents a single case within a [Case] block. class CaseItem { @@ -148,6 +149,10 @@ class Case extends Conditional { @protected String get caseType => 'case'; + /// Returns the case keyword used for code emission. + @internal + String get emissionCaseType => caseType; + @override void execute(Set? drivenSignals, [void Function(Logic)? guard]) { if (guard != null) { @@ -254,44 +259,12 @@ class Case extends Conditional { @override String verilogContents(int indent, Map inputsNameMap, - Map outputsNameMap, String assignOperator) { - final padding = Conditional.calcPadding(indent); - final expressionName = inputsNameMap[driverInput(expression).name]; - var caseHeader = caseType; - if (conditionalType == ConditionalType.priority) { - caseHeader = 'priority $caseType'; - } else if (conditionalType == ConditionalType.unique) { - caseHeader = 'unique $caseType'; - } - final verilog = StringBuffer('$padding$caseHeader ($expressionName) \n'); - final subPadding = Conditional.calcPadding(indent + 2); - for (final item in items) { - final conditionName = inputsNameMap[driverInput(item.value).name]; - final caseContents = item.then - .map((conditional) => conditional.verilogContents( - indent + 4, inputsNameMap, outputsNameMap, assignOperator)) - .join('\n'); - verilog.write(''' -$subPadding$conditionName : begin -$caseContents -${subPadding}end -'''); - } - if (defaultItem != null) { - final defaultCaseContents = defaultItem! - .map((conditional) => conditional.verilogContents( - indent + 4, inputsNameMap, outputsNameMap, assignOperator)) - .join('\n'); - verilog.write(''' -${subPadding}default : begin -$defaultCaseContents -${subPadding}end -'''); - } - verilog.write('${padding}endcase\n'); - - return verilog.toString(); - } + Map outputsNameMap, String assignOperator) => + SystemVerilogConditionalEmitter( + inputsNameMap: inputsNameMap, + outputsNameMap: outputsNameMap, + assignOperator: assignOperator, + ).emit(this, indent); @override Map processSsa(Map currentMappings, diff --git a/lib/src/modules/conditionals/combinational.dart b/lib/src/modules/conditionals/combinational.dart index ea8e8ccbe..548dfb8e9 100644 --- a/lib/src/modules/conditionals/combinational.dart +++ b/lib/src/modules/conditionals/combinational.dart @@ -130,8 +130,9 @@ class Combinational extends Always { signalToSsaDrivers.putIfAbsent(tpi, () => {}).add(ssaDriver); if (tpi.isInput && - // ignore: deprecated_member_use_from_same_package - ((tpi.parentModule! is CustomSystemVerilog) || + (tpi.parentModule! is InlineLeaf || + // ignore: deprecated_member_use_from_same_package + tpi.parentModule! is CustomSystemVerilog || tpi.parentModule! is SystemVerilog)) { toParse.addAll(tpi.parentModule!.outputs.values); } else { diff --git a/lib/src/modules/conditionals/conditional.dart b/lib/src/modules/conditionals/conditional.dart index 2a52a6b21..c0276b246 100644 --- a/lib/src/modules/conditionals/conditional.dart +++ b/lib/src/modules/conditionals/conditional.dart @@ -119,6 +119,14 @@ abstract class Conditional { Logic receiverOutput(Logic receiver) => _assignedReceiverToOutputMap[receiver]!; + /// Gets the registered input used to emit [driver]. + @internal + Logic emissionDriver(Logic driver) => driverInput(driver); + + /// Gets the registered output used to emit [receiver]. + @internal + Logic emissionReceiver(Logic receiver) => receiverOutput(receiver); + /// Executes the functionality of this [Conditional] and /// populates [drivenSignals] with all [Logic]s that were driven /// during execution. diff --git a/lib/src/modules/conditionals/conditional_assign.dart b/lib/src/modules/conditionals/conditional_assign.dart index 9c77787c8..a45435017 100644 --- a/lib/src/modules/conditionals/conditional_assign.dart +++ b/lib/src/modules/conditionals/conditional_assign.dart @@ -10,6 +10,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/modules/conditionals/ssa.dart'; +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_conditional_emitter.dart'; /// An assignment that only happens under certain conditions. /// @@ -77,12 +78,12 @@ class ConditionalAssign extends Conditional { @override String verilogContents(int indent, Map inputsNameMap, - Map outputsNameMap, String assignOperator) { - final padding = Conditional.calcPadding(indent); - final driverName = inputsNameMap[driverInput(driver).name]!; - final receiverName = outputsNameMap[receiverOutput(receiver).name]!; - return '$padding$receiverName $assignOperator $driverName;'; - } + Map outputsNameMap, String assignOperator) => + SystemVerilogConditionalEmitter( + inputsNameMap: inputsNameMap, + outputsNameMap: outputsNameMap, + assignOperator: assignOperator, + ).emit(this, indent); @override Map processSsa(Map currentMappings, diff --git a/lib/src/modules/conditionals/conditional_group.dart b/lib/src/modules/conditionals/conditional_group.dart index a8745eac6..9e223c94c 100644 --- a/lib/src/modules/conditionals/conditional_group.dart +++ b/lib/src/modules/conditionals/conditional_group.dart @@ -9,6 +9,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_conditional_emitter.dart'; /// Represents a group of [Conditional]s to be executed. class ConditionalGroup extends Conditional { @@ -52,12 +53,9 @@ class ConditionalGroup extends Conditional { @override String verilogContents(int indent, Map inputsNameMap, Map outputsNameMap, String assignOperator) => - conditionals - .map((c) => c.verilogContents( - indent, - inputsNameMap, - outputsNameMap, - assignOperator, - )) - .join('\n'); + SystemVerilogConditionalEmitter( + inputsNameMap: inputsNameMap, + outputsNameMap: outputsNameMap, + assignOperator: assignOperator, + ).emit(this, indent); } diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index cd9aa8750..3e3f4acdf 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -88,6 +88,11 @@ class FlipFlop extends Module with SystemVerilog { /// Only initialized if a constant value is provided. late LogicValue _resetValueConst; + /// Returns the constant reset value if one was provided, or null if the + /// reset value is a port or no reset exists. + LogicValue? get constantResetValue => + _reset != null && _resetValuePort == null ? _resetValueConst : null; + /// Indicates whether provided `reset` signals should be treated as an async /// reset. If no `reset` is provided, this will have no effect. final bool asyncReset; diff --git a/lib/src/modules/conditionals/if.dart b/lib/src/modules/conditionals/if.dart index f406abcef..aa38def9c 100644 --- a/lib/src/modules/conditionals/if.dart +++ b/lib/src/modules/conditionals/if.dart @@ -11,6 +11,7 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/modules/conditionals/ssa.dart'; +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_conditional_emitter.dart'; /// A conditional block to execute only if [condition] is satisfied. /// @@ -182,31 +183,12 @@ class If extends Conditional { @override String verilogContents(int indent, Map inputsNameMap, - Map outputsNameMap, String assignOperator) { - final padding = Conditional.calcPadding(indent); - final verilog = StringBuffer(); - for (final iff in iffs) { - final header = iff == iffs.first - ? 'if' - : iff is Else - ? 'else' - : 'else if'; - - final conditionName = inputsNameMap[driverInput(iff.condition).name]; - final ifContents = iff.then - .map((conditional) => conditional.verilogContents( - indent + 2, inputsNameMap, outputsNameMap, assignOperator)) - .join('\n'); - final condition = iff is! Else ? '($conditionName)' : ''; - verilog.write(''' -$padding$header$condition begin -$ifContents -${padding}end '''); - } - verilog.write('\n'); - - return verilog.toString(); - } + Map outputsNameMap, String assignOperator) => + SystemVerilogConditionalEmitter( + inputsNameMap: inputsNameMap, + outputsNameMap: outputsNameMap, + assignOperator: assignOperator, + ).emit(this, indent); @override Map processSsa(Map currentMappings, diff --git a/lib/src/modules/conditionals/sequential.dart b/lib/src/modules/conditionals/sequential.dart index 62a7c1129..6f789aba2 100644 --- a/lib/src/modules/conditionals/sequential.dart +++ b/lib/src/modules/conditionals/sequential.dart @@ -10,6 +10,7 @@ import 'dart:async'; import 'dart:collection'; +import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/duplicate_detection_set.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; @@ -135,6 +136,20 @@ class Sequential extends Always { /// The input edge triggers used in this block. final List<_SequentialTrigger> _triggers = []; + /// Returns the edge polarity for each trigger input port. + /// + /// Each entry pairs the trigger input port name with whether the trigger + /// fires on a positive edge (`true`) or negative edge (`false`). + List<({String portName, bool isPosedge})> get triggerEdges => _triggers + .map((t) => (portName: t.signal.name, isPosedge: t.isPosedge)) + .toList(); + + /// Returns the trigger signals and edge polarities used for code emission. + @internal + List<({Logic signal, bool isPosedge})> get emissionTriggers => _triggers + .map((trigger) => (signal: trigger.signal, isPosedge: trigger.isPosedge)) + .toList(); + /// When `false`, an [SignalRedrivenException] will be thrown during /// simulation if the same signal is driven multiple times within this /// [Sequential]. diff --git a/lib/src/modules/gates.dart b/lib/src/modules/gates.dart index 9bdc858eb..5cfcee938 100644 --- a/lib/src/modules/gates.dart +++ b/lib/src/modules/gates.dart @@ -11,7 +11,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; /// A gate [Module] that performs bit-wise inversion. -class NotGate extends Module with InlineSystemVerilog { +class NotGate extends Module with InlineLeaf { /// Name for the input of this inverter. late final String _inName; @@ -48,20 +48,12 @@ class NotGate extends Module with InlineSystemVerilog { void _execute() { out.put(~_in.value); } - - @override - String inlineVerilog(Map inputs) { - assert(inputs.length == 1, 'Gate has exactly one input.'); - - final in_ = inputs[_inName]!; - return '~$in_'; - } } /// A generic unary gate [Module]. /// /// It always takes one input, and the output width is always 1. -class _OneInputUnaryGate extends Module with InlineSystemVerilog { +class _OneInputUnaryGate extends Module with InlineLeaf { /// Name for the input port of this module. late final String _inName; @@ -81,15 +73,11 @@ class _OneInputUnaryGate extends Module with InlineSystemVerilog { Logic get y => out; final LogicValue Function(LogicValue a) _op; - final String _opStr; /// Constructs a unary gate for an arbitrary custom functional implementation. /// - /// The function [_op] is executed as the custom functional behavior. When - /// this [Module] is in-lined as SystemVerilog, it will use [_opStr] as the - /// prefix to the input signal name (e.g. if [_opStr] was "&", generated - /// SystemVerilog may look like "&a"). - _OneInputUnaryGate(this._op, this._opStr, Logic in_, {String name = 'ugate'}) + /// The function [_op] is executed as the custom functional behavior. + _OneInputUnaryGate(this._op, Logic in_, {String name = 'ugate'}) : super(name: name) { _inName = Naming.unpreferredName(in_.name); _outName = Naming.unpreferredName('${name}_${in_.name}'); @@ -111,22 +99,13 @@ class _OneInputUnaryGate extends Module with InlineSystemVerilog { void _execute() { out.put(_op(_in.value)); } - - @override - String inlineVerilog(Map inputs) { - if (inputs.length != 1) { - throw Exception('Gate has exactly one input.'); - } - final in_ = inputs[_inName]!; - return '$_opStr$in_'; - } } /// A generic two-input bitwise gate [Module]. /// /// It always takes two inputs and has one output. All ports have the /// same width. -abstract class _TwoInputBitwiseGate extends Module with InlineSystemVerilog { +abstract class _TwoInputBitwiseGate extends Module with InlineLeaf { /// Name for a first input port of this module. late final String _in0Name; @@ -157,9 +136,6 @@ abstract class _TwoInputBitwiseGate extends Module with InlineSystemVerilog { /// The functional operation to perform for this gate. final LogicValue Function(LogicValue in0, LogicValue in1) _op; - /// The `String` representing the operation to perform in generated code. - final String _opStr; - /// The width of the inputs and outputs for this operation. final int width; @@ -167,25 +143,15 @@ abstract class _TwoInputBitwiseGate extends Module with InlineSystemVerilog { /// width than the inputs, which should be considered in generated verilog. final int _outputSvWidthExpansion; - /// If true, it will wrap the expression in `{}` to try to force the - /// expression to behave as a self-determined width. - final bool _makeSelfDetermined; - /// Constructs a two-input bitwise gate for an arbitrary custom functional /// implementation. /// - /// The function [_op] is executed as the custom functional behavior. When - /// this [Module] is in-lined as SystemVerilog, it will use [_opStr] as a - /// String between the two input signal names (e.g. if [_opStr] was "&", - /// generated SystemVerilog may look like "a & b"). - _TwoInputBitwiseGate(this._op, this._opStr, Logic in0, dynamic in1, - {String name = 'gate2', - int outputSvWidthExpansion = 0, - bool makeSelfDetermined = false}) + /// The function [_op] is executed as the custom functional behavior. + _TwoInputBitwiseGate(this._op, Logic in0, dynamic in1, + {String name = 'gate2', int outputSvWidthExpansion = 0}) : width = in0.width, assert(!outputSvWidthExpansion.isNegative, 'Should not be negative.'), _outputSvWidthExpansion = outputSvWidthExpansion, - _makeSelfDetermined = makeSelfDetermined, super(name: name) { if (in1 is Logic && in0.width != in1.width) { throw PortWidthMismatchException.equalWidth(in0, in1); @@ -220,25 +186,12 @@ abstract class _TwoInputBitwiseGate extends Module with InlineSystemVerilog { void _execute() { out.put(_op(_in0.value, _in1.value)); } - - @override - String inlineVerilog(Map inputs) { - assert(inputs.length == 2, 'Gate has exactly two inputs.'); - - final in0 = inputs[_in0Name]!; - final in1 = inputs[_in1Name]!; - var sv = '$in0 $_opStr $in1'; - if (_makeSelfDetermined) { - sv = '{$sv}'; - } - return sv; - } } /// A generic two-input comparison gate [Module]. /// /// It always takes two inputs of the same width and has one 1-bit output. -abstract class _TwoInputComparisonGate extends Module with InlineSystemVerilog { +abstract class _TwoInputComparisonGate extends Module with InlineLeaf { /// Name for a first input port of this module. late final String _in0Name; @@ -266,17 +219,11 @@ abstract class _TwoInputComparisonGate extends Module with InlineSystemVerilog { /// The functional operation to perform for this gate. final LogicValue Function(LogicValue in0, LogicValue in1) _op; - /// The `String` representing the operation to perform in generated code. - final String _opStr; - /// Constructs a two-input comparison gate for an arbitrary custom functional /// implementation. /// - /// The function [_op] is executed as the custom functional behavior. When - /// this [Module] is in-lined as SystemVerilog, it will use [_opStr] as a - /// String between the two input signal names (e.g. if [_opStr] was ">", - /// generated SystemVerilog may look like "a > b"). - _TwoInputComparisonGate(this._op, this._opStr, Logic in0, dynamic in1, + /// The function [_op] is executed as the custom functional behavior. + _TwoInputComparisonGate(this._op, Logic in0, dynamic in1, {String name = 'cmp2'}) : super(name: name) { if (in1 is Logic && in0.width != in1.width) { @@ -312,22 +259,13 @@ abstract class _TwoInputComparisonGate extends Module with InlineSystemVerilog { void _execute() { out.put(_op(_in0.value, _in1.value)); } - - @override - String inlineVerilog(Map inputs) { - assert(inputs.length == 2, 'Gate has exactly two inputs.'); - - final in0 = inputs[_in0Name]!; - final in1 = inputs[_in1Name]!; - return '$in0 $_opStr $in1'; - } } /// A generic two-input shift gate [Module]. /// /// It always takes two inputs and has one output of equal width to the primary /// of the input. -abstract class _ShiftGate extends Module with InlineSystemVerilog { +abstract class _ShiftGate extends Module with InlineLeaf { /// Name for the main input port of this module. late final String _inName; @@ -352,12 +290,6 @@ abstract class _ShiftGate extends Module with InlineSystemVerilog { /// The functional operation to perform for this gate. final LogicValue Function(LogicValue in_, LogicValue shiftAmount) _op; - /// The `String` representing the operation to perform in generated code. - final String _opStr; - - /// Whether or not this gate operates on a signed number. - final bool signed; - /// The width of the output for this operation. final int width; @@ -379,14 +311,9 @@ abstract class _ShiftGate extends Module with InlineSystemVerilog { /// Constructs a two-input shift gate for an arbitrary custom functional /// implementation. /// - /// The function [_op] is executed as the custom functional behavior. When - /// this [Module] is in-lined as SystemVerilog, it will use [_opStr] as a - /// String between the two input signal names (e.g. if [_opStr] was ">>", - /// generated SystemVerilog may look like "a >> b"). - _ShiftGate(this._op, this._opStr, Logic in_, dynamic shiftAmount, - {String name = 'gate2', - this.signed = false, - bool outputSvWidthExpansion = false}) + /// The function [_op] is executed as the custom functional behavior. + _ShiftGate(this._op, Logic in_, dynamic shiftAmount, + {String name = 'gate2', bool outputSvWidthExpansion = false}) : width = in_.width, _outputSvWidthExpansion = outputSvWidthExpansion, _isNet = in_.isNet && @@ -460,45 +387,27 @@ abstract class _ShiftGate extends Module with InlineSystemVerilog { void _execute() { out.put(_op(_in.value, _shiftAmount.value)); } - - @override - String inlineVerilog(Map inputs) { - assert(inputs.length == 2, 'Gate has exactly two inputs.'); - - final in_ = inputs[_inName]!; - final shiftAmount = inputs[_shiftAmountName]!; - - String signWrap(String original) => - signed ? '\$signed($original)' : original; - - final aStr = signWrap(in_); - - final shiftStr = '$aStr $_opStr $shiftAmount'; - - // In case of signed, wrap in {} to make it self-determined. - return signed ? '{$shiftStr}' : shiftStr; - } } /// A two-input AND gate. class And2Gate extends _TwoInputBitwiseGate { /// Calculates the AND of [in0] and [in1]. And2Gate(Logic in0, Logic in1, {String name = 'and'}) - : super((a, b) => a & b, '&', in0, in1, name: name); + : super((a, b) => a & b, in0, in1, name: name); } /// A two-input OR gate. class Or2Gate extends _TwoInputBitwiseGate { /// Calculates the OR of [in0] and [in1]. Or2Gate(Logic in0, Logic in1, {String name = 'or'}) - : super((a, b) => a | b, '|', in0, in1, name: name); + : super((a, b) => a | b, in0, in1, name: name); } /// A two-input XOR gate. class Xor2Gate extends _TwoInputBitwiseGate { /// Calculates the XOR of [in0] and [in1]. Xor2Gate(Logic in0, Logic in1, {String name = 'xor'}) - : super((a, b) => a ^ b, '^', in0, in1, name: name); + : super((a, b) => a ^ b, in0, in1, name: name); } /// A two-input power module. @@ -508,8 +417,7 @@ class Power extends _TwoInputBitwiseGate { /// [in1] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. Power(Logic in0, dynamic in1, {String name = 'power'}) - : super((a, b) => a.pow(b), '**', in0, in1, - name: name, makeSelfDetermined: true); + : super((a, b) => a.pow(b), in0, in1, name: name); } /// A two-input addition module. @@ -627,7 +535,7 @@ class Subtract extends _TwoInputBitwiseGate { /// [in1] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. Subtract(Logic in0, dynamic in1, {String name = 'subtract'}) - : super((a, b) => a - b, '-', in0, in1, name: name); + : super((a, b) => a - b, in0, in1, name: name); } /// A two-input multiplication module. @@ -637,8 +545,7 @@ class Multiply extends _TwoInputBitwiseGate { /// [in1] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. Multiply(Logic in0, dynamic in1, {String name = 'multiply'}) - : super((a, b) => a * b, '*', in0, in1, - name: name, makeSelfDetermined: true); + : super((a, b) => a * b, in0, in1, name: name); } /// A two-input division module. @@ -648,7 +555,7 @@ class Divide extends _TwoInputBitwiseGate { /// [in1] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. Divide(Logic in0, dynamic in1, {String name = 'divide'}) - : super((a, b) => a / b, '/', in0, in1, name: name); + : super((a, b) => a / b, in0, in1, name: name); } /// A two-input modulo module. @@ -658,7 +565,7 @@ class Modulo extends _TwoInputBitwiseGate { /// [in1] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. Modulo(Logic in0, dynamic in1, {String name = 'modulo'}) - : super((a, b) => a % b, '%', in0, in1, name: name); + : super((a, b) => a % b, in0, in1, name: name); } /// A two-input equality comparison module. @@ -668,7 +575,7 @@ class Equals extends _TwoInputComparisonGate { /// [in1] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. Equals(Logic in0, dynamic in1, {String name = 'equals'}) - : super((a, b) => a.eq(b), '==', in0, in1, name: name); + : super((a, b) => a.eq(b), in0, in1, name: name); } /// A two-input inequality comparison module. @@ -678,7 +585,7 @@ class NotEquals extends _TwoInputComparisonGate { /// [in1] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. NotEquals(Logic in0, dynamic in1, {String name = 'notEquals'}) - : super((a, b) => a.neq(b), '!=', in0, in1, name: name); + : super((a, b) => a.neq(b), in0, in1, name: name); } /// A two-input comparison module for less-than. @@ -688,7 +595,7 @@ class LessThan extends _TwoInputComparisonGate { /// [in1] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. LessThan(Logic in0, dynamic in1, {String name = 'lessthan'}) - : super((a, b) => a < b, '<', in0, in1, name: name); + : super((a, b) => a < b, in0, in1, name: name); } /// A two-input comparison module for greater-than. @@ -698,7 +605,7 @@ class GreaterThan extends _TwoInputComparisonGate { /// [in1] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. GreaterThan(Logic in0, dynamic in1, {String name = 'greaterThan'}) - : super((a, b) => a > b, '>', in0, in1, name: name); + : super((a, b) => a > b, in0, in1, name: name); } /// A two-input comparison module for less-than-or-equal-to. @@ -708,7 +615,7 @@ class LessThanOrEqual extends _TwoInputComparisonGate { /// [in1] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. LessThanOrEqual(Logic in0, dynamic in1, {String name = 'lessThanOrEqual'}) - : super((a, b) => a <= b, '<=', in0, in1, name: name); + : super((a, b) => a <= b, in0, in1, name: name); } /// A two-input comparison module for greater-than-or-equal-to. @@ -719,28 +626,28 @@ class GreaterThanOrEqual extends _TwoInputComparisonGate { /// [LogicValue.of]. GreaterThanOrEqual(Logic in0, dynamic in1, {String name = 'greaterThanOrEqual'}) - : super((a, b) => a >= b, '>=', in0, in1, name: name); + : super((a, b) => a >= b, in0, in1, name: name); } /// A unary AND gate. class AndUnary extends _OneInputUnaryGate { /// Calculates whether all bits of [in_] are high. AndUnary(Logic in_, {String name = 'uand'}) - : super((a) => a.and(), '&', in_, name: name); + : super((a) => a.and(), in_, name: name); } /// A unary OR gate. class OrUnary extends _OneInputUnaryGate { /// Calculates whether any bits of [in_] are high. OrUnary(Logic in_, {String name = 'uor'}) - : super((a) => a.or(), '|', in_, name: name); + : super((a) => a.or(), in_, name: name); } /// A unary XOR gate. class XorUnary extends _OneInputUnaryGate { /// Calculates the parity of the bits of [in_]. XorUnary(Logic in_, {String name = 'uxor'}) - : super((a) => a.xor(), '^', in_, name: name); + : super((a) => a.xor(), in_, name: name); } /// A logical right-shift module. @@ -755,7 +662,7 @@ class RShift extends _ShiftGate { /// [LogicValue.of]. RShift(Logic in_, dynamic shiftAmount, {String name = 'rshift'}) : // Note: >>> vs >> is backwards for SystemVerilog and Dart - super((a, shamt) => a >>> shamt, '>>', in_, shiftAmount, name: name); + super((a, shamt) => a >>> shamt, in_, shiftAmount, name: name); @override void _netSetup(LogicNet internalOut) { @@ -777,8 +684,7 @@ class ARShift extends _ShiftGate { /// [LogicValue.of]. ARShift(Logic in_, dynamic shiftAmount, {String name = 'arshift'}) : // Note: >>> vs >> is backwards for SystemVerilog and Dart - super((a, shamt) => a >> shamt, '>>>', in_, shiftAmount, - name: name, signed: true); + super((a, shamt) => a >> shamt, in_, shiftAmount, name: name); @override void _netSetup(LogicNet internalOut) { @@ -799,7 +705,7 @@ class LShift extends _ShiftGate { /// [shiftAmount] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. LShift(Logic in_, dynamic shiftAmount, {String name = 'lshift'}) - : super((a, shamt) => a << shamt, '<<', in_, shiftAmount, + : super((a, shamt) => a << shamt, in_, shiftAmount, name: name, outputSvWidthExpansion: true); @override @@ -836,7 +742,7 @@ Logic mux(Logic control, Logic d1, Logic d0) { /// /// If [_control] has value `1`, then [out] gets [_d1]. /// If [_control] has value `0`, then [out] gets [_d0]. -class Mux extends Module with InlineSystemVerilog { +class Mux extends Module with InlineLeaf { /// Name for the control signal of this mux. late final String _controlName; @@ -916,22 +822,12 @@ class Mux extends Module with InlineSystemVerilog { out.put(_d1.value.isValid ? _d1.value : LogicValue.x); } } - - @override - String inlineVerilog(Map inputs) { - assert(inputs.length == 3, 'Mux2 has exactly three inputs.'); - - final d0 = inputs[_d0Name]!; - final d1 = inputs[_d1Name]!; - final control = inputs[_controlName]!; - return '$control ? $d1 : $d0'; - } } /// A two-input bit index gate [Module]. /// /// It always takes two inputs and has one output of width 1. -class IndexGate extends Module with InlineSystemVerilog { +class IndexGate extends Module with InlineLeaf { late final String _originalName; late final String _indexName; late final String _selectionName; @@ -988,20 +884,6 @@ class IndexGate extends Module with InlineSystemVerilog { selection.put(LogicValue.x); } } - - @override - String inlineVerilog(Map inputs) { - assert(inputs.length == 2, 'Gate has exactly two inputs.'); - - final target = inputs[_originalName]!; - - if (_original.width == 1) { - return target; - } - - final idx = inputs[_indexName]!; - return '$target[$idx]'; - } } /// A Replication Operator [Module]. @@ -1012,7 +894,7 @@ class IndexGate extends Module with InlineSystemVerilog { /// Note that many simulators do not support the SystemVerilog generated by this /// module when it operates on [LogicNet]s. The default [Logic.replicate] /// function will instead use swizzling to accomplish equivalent behavior. -class ReplicationOp extends Module with InlineSystemVerilog { +class ReplicationOp extends Module with InlineLeaf { /// Input name. final String _inputName; @@ -1086,13 +968,4 @@ class ReplicationOp extends Module with InlineSystemVerilog { @override String get resultSignalName => _outputName; - - @override - String inlineVerilog(Map inputs) { - assert(inputs.length == 1, 'Gate has exactly one input.'); - - final target = inputs[_inputName]!; - final width = _multiplier; - return '{$width{$target}}'; - } } diff --git a/lib/src/synthesizers/synthesizers.dart b/lib/src/synthesizers/synthesizers.dart index b8c8523ec..7eff9bdf9 100644 --- a/lib/src/synthesizers/synthesizers.dart +++ b/lib/src/synthesizers/synthesizers.dart @@ -1,8 +1,17 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +// +// synthesizers.dart +// Public exports for ROHD synthesis backends and synthesis APIs. +// +// 2026 July +// Author: Desmond A. Kirkpatrick export 'synth_builder.dart'; export 'synth_file_contents.dart'; export 'synthesis_result.dart'; export 'synthesizer.dart'; +export 'systemc/systemc.dart'; export 'systemverilog/systemverilog.dart'; +export 'utilities/backend_artifact.dart'; +export 'utilities/inline_leaf.dart'; diff --git a/lib/src/synthesizers/systemc/systemc.dart b/lib/src/synthesizers/systemc/systemc.dart new file mode 100644 index 000000000..7522a194d --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc.dart @@ -0,0 +1,35 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc.dart +// Definition for SystemC Synthesizer +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; + +export 'systemc_mixins.dart'; + +/// A [Synthesizer] which generates equivalent SystemC as the given [Module]. +/// +/// Attempts to maintain signal naming and structure as much as possible, +/// using the same naming strategy as the SystemVerilog synthesizer. +class SystemCSynthesizer extends Synthesizer { + @override + bool generatesDefinition(Module module) { + final generatesNoDefinition = module is InlineLeaf || + // ignore: deprecated_member_use_from_same_package + module is CustomSystemVerilog || + (module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none); + + return !generatesNoDefinition; + } + + @override + SynthesisResult synthesize(Module module, + String Function(Module module) getInstanceTypeOfModule) => + SystemCSynthesisResult(module, getInstanceTypeOfModule); +} diff --git a/lib/src/synthesizers/systemc/systemc_conditional_emitter.dart b/lib/src/synthesizers/systemc/systemc_conditional_emitter.dart new file mode 100644 index 000000000..8fdb33495 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_conditional_emitter.dart @@ -0,0 +1,150 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_conditional_emitter.dart +// SystemC renderer for backend-neutral conditional emission plans. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/conditional_emission_plan.dart'; +import 'package:rohd/src/synthesizers/utilities/conditional_emitter.dart'; + +/// Emits SystemC/C++ syntax for backend-neutral conditional emission plans. +class SystemCConditionalEmitter extends ConditionalEmitter { + /// Resolves a conditional driver to a SystemC expression. + final String Function(Logic driver) driverExpressionFor; + + /// Resolves a conditional receiver to a SystemC assignment target. + final String Function(Logic receiver) receiverExpressionFor; + + /// Whether a case-item value can be emitted as a C++ switch label. + final bool Function(Logic value) isConstCaseItem; + + /// Emits a comparison condition for a case item lowered to an if chain. + final String Function( + Logic value, + String expression, { + required bool isCaseZ, + }) caseItemConditionFor; + + /// Creates a SystemC conditional emitter. + const SystemCConditionalEmitter({ + required this.driverExpressionFor, + required this.receiverExpressionFor, + required this.isConstCaseItem, + required this.caseItemConditionFor, + }); + + @override + String driverFor(Conditional source, Logic driver) => + driverExpressionFor(driver); + + @override + String receiverFor(Conditional source, Logic receiver) => + receiverExpressionFor(receiver); + + @override + String emitAssignment(int indent, String receiver, String driver) => + '${Conditional.calcPadding(indent)}$receiver = $driver;\n'; + + @override + String emitIf(int indent, List branches) { + final padding = Conditional.calcPadding(indent); + final buffer = StringBuffer(); + for (final branch in branches) { + final header = branch == branches.first + ? 'if' + : branch.isElse + ? ' else' + : ' else if'; + final condition = branch.isElse ? '' : ' (${branch.condition})'; + buffer + ..write('$padding$header$condition {\n') + ..write(branch.contents) + ..write('$padding}'); + } + buffer.writeln(); + return buffer.toString(); + } + + @override + String emitCase( + ConditionalCaseEmissionPlan plan, + int indent, + String expression, + List items, + String? defaultContents, + ) { + if (!_usesSwitch(plan)) { + return _emitCaseAsIfElse( + plan, + indent, + expression, + items, + defaultContents, + ); + } + + final padding = Conditional.calcPadding(indent); + final buffer = StringBuffer()..writeln('${padding}switch ($expression) {'); + for (final item in items) { + buffer + ..writeln('$padding case ${item.match}:') + ..write(item.contents) + ..writeln('$padding break;'); + } + if (defaultContents != null) { + buffer + ..writeln('$padding default:') + ..write(defaultContents) + ..writeln('$padding break;'); + } + buffer.writeln('$padding}'); + return buffer.toString(); + } + + @override + int caseChildIndent(ConditionalCaseEmissionPlan plan, int indent) => + _usesSwitch(plan) ? indent + 2 : indent + 1; + + @override + String get childrenSeparator => ''; + + bool _usesSwitch(ConditionalCaseEmissionPlan plan) => + plan.caseBlock is! CaseZ && + plan.items.every((item) => isConstCaseItem(item.source.value)); + + String _emitCaseAsIfElse( + ConditionalCaseEmissionPlan plan, + int indent, + String expression, + List items, + String? defaultContents, + ) { + final padding = Conditional.calcPadding(indent); + final buffer = StringBuffer(); + for (var index = 0; index < items.length; index++) { + final item = items[index]; + final condition = caseItemConditionFor( + item.item.value, + expression, + isCaseZ: plan.caseBlock is CaseZ, + ); + final header = index == 0 ? 'if' : ' else if'; + buffer + ..write('$padding$header ($condition) {\n') + ..write(item.contents) + ..write('$padding}'); + } + if (defaultContents != null) { + buffer + ..write(' else {\n') + ..write(defaultContents) + ..write('$padding}'); + } + buffer.writeln(); + return buffer.toString(); + } +} diff --git a/lib/src/synthesizers/systemc/systemc_leaf_emitter.dart b/lib/src/synthesizers/systemc/systemc_leaf_emitter.dart new file mode 100644 index 000000000..2b33b1e39 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_leaf_emitter.dart @@ -0,0 +1,237 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_leaf_emitter.dart +// SystemC renderer for semantic leaf expression plans. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// Emits backend-specific SystemC/C++ expressions for semantic leaf gates. +class SystemCLeafEmitter implements InlineLeafEmitter { + /// Returns the SystemC type for a requested signal width. + final String Function(int width) typeForWidth; + + /// Creates a leaf emitter. + const SystemCLeafEmitter({required this.typeForWidth}); + + /// Emits a SystemC expression for an inline-style module [m]. + /// + /// [inputs] maps module input port names to SystemC read expressions. + @override + String expressionFor(InlineLeaf m, Map inputs) { + final plan = LeafExpressionPlan.fromInlineModule(m, inputs); + final op = plan.operation; + + // ── Single-output bitwise gates ── + if (op == LeafOperationKind.not || m is NotGate) { + final outputWidth = plan.meta('outputWidth') ?? + (m as Module).outputs.values.first.width; + if (outputWidth == 1) { + return '!${inputs.values.first}'; + } + return '~${inputs.values.first}'; + } + + // ── Binary operators ── + const binaryOps = { + LeafOperationKind.and: '&', + LeafOperationKind.or: '|', + LeafOperationKind.xor: '^', + LeafOperationKind.subtract: '-', + LeafOperationKind.multiply: '*', + }; + final binOp = binaryOps[op]; + if (binOp != null) { + final vals = plan.inputValues; + return '${vals[0]} $binOp ${vals[1]}'; + } + if (op == LeafOperationKind.divide || + op == LeafOperationKind.modulo || + m is Divide || + m is Modulo) { + final vals = plan.inputValues; + final divideOp = + (op == LeafOperationKind.divide || m is Divide) ? '/' : '%'; + return '(${vals[1]} != 0 ? ${vals[0]} $divideOp ${vals[1]} : 0)'; + } + if (op == LeafOperationKind.power || m is Power) { + final vals = plan.inputValues; + final w = plan.meta('inputWidth') ?? + (m as Module).inputs.values.first.width; + return '${typeForWidth(w)}' + '(static_cast' + '(pow(static_cast(${vals[0]}),' + ' static_cast(${vals[1]}))))'; + } + + // ── Comparisons ── + const cmpOps = { + LeafOperationKind.equals: '==', + LeafOperationKind.notEquals: '!=', + LeafOperationKind.lessThan: '<', + LeafOperationKind.greaterThan: '>', + LeafOperationKind.lessThanOrEqual: '<=', + LeafOperationKind.greaterThanOrEqual: '>=', + }; + final cmpOp = cmpOps[op]; + if (cmpOp != null) { + final vals = plan.inputValues; + return '${vals[0]} $cmpOp ${vals[1]}'; + } + + // ── Shifts ── + if (op == LeafOperationKind.shiftLeft || + op == LeafOperationKind.shiftRight || + op == LeafOperationKind.arithmeticShiftRight || + m is LShift || + m is RShift || + m is ARShift) { + final vals = plan.inputValues; + final w = plan.meta('inputWidth') ?? + (m as Module).inputs.values.first.width; + final outType = typeForWidth(w); + final shiftAmtWidth = plan.meta('shiftAmountWidth') ?? + (m as Module).inputs.values.toList()[1].width; + final shiftExpr = + shiftAmtWidth == 1 ? '(int)(${vals[1]})' : '(${vals[1]}).to_int()'; + final isArithmetic = + op == LeafOperationKind.arithmeticShiftRight || m is ARShift; + if (isArithmetic) { + final signedType = w <= 64 ? 'sc_int<$w>' : 'sc_bigint<$w>'; + final shiftOp = '$outType(($signedType(${vals[0]})) >> $shiftExpr)'; + if (shiftAmtWidth > 31) { + final overflow = '$outType(($signedType(${vals[0]})) >> ${w - 1})'; + return '(${vals[1]} >= $w) ? $overflow : $shiftOp'; + } + return shiftOp; + } + final shiftOpSymbol = + (op == LeafOperationKind.shiftLeft || m is LShift) ? '<<' : '>>'; + final shiftOp = '$outType(${vals[0]} $shiftOpSymbol $shiftExpr)'; + if (shiftAmtWidth > 31) { + return '(${vals[1]} >= $w) ? $outType(0) : $shiftOp'; + } + return shiftOp; + } + + // ── Unary reductions ── + if (op == LeafOperationKind.andUnary || + op == LeafOperationKind.orUnary || + op == LeafOperationKind.xorUnary || + m is AndUnary || + m is OrUnary || + m is XorUnary) { + final inputWidth = plan.meta('inputWidth') ?? + (m as Module).inputs.values.first.width; + if (inputWidth == 1) { + return 'static_cast(${inputs.values.first})'; + } + if (op == LeafOperationKind.andUnary || m is AndUnary) { + return '${inputs.values.first}.and_reduce()'; + } else if (op == LeafOperationKind.orUnary || m is OrUnary) { + return '${inputs.values.first}.or_reduce()'; + } else { + return '${inputs.values.first}.xor_reduce()'; + } + } + + // ── Bus subset ── + if (op == LeafOperationKind.busSubset || m is BusSubset) { + final inputWidth = plan.meta('inputWidth') ?? + (m as Module).inputs.values.first.width; + final startIndex = plan.meta('startIndex') ?? + (m is BusSubset ? m.startIndex : null); + final endIndex = + plan.meta('endIndex') ?? (m is BusSubset ? m.endIndex : null); + if (startIndex == null || endIndex == null) { + throw SynthException( + 'SystemC bus subset leaf requires startIndex and endIndex metadata.', + ); + } + + final a = inputs.values.first; + if (inputWidth == 1 && startIndex == 0 && endIndex == 0) { + return a; + } + if (startIndex == endIndex) { + return 'static_cast($a[$startIndex])'; + } + if (startIndex > endIndex) { + final bits = List.generate(startIndex - endIndex + 1, + (i) => 'sc_uint<1>($a[${endIndex + i}])'); + return '(${bits.join(', ')})'; + } + final w = endIndex - startIndex + 1; + final rangeType = w <= 64 ? 'sc_uint' : 'sc_biguint'; + return '$rangeType<$w>($a.range($endIndex, $startIndex))'; + } + + // ── Dynamic index ── + if (op == LeafOperationKind.bitIndex || m is IndexGate) { + final vals = plan.inputValues; + return 'static_cast(${vals[0]}[${vals[1]}])'; + } + + // ── Mux ── + if (op == LeafOperationKind.mux || m is Mux) { + final vals = plan.inputValues; + final w = plan.meta('outputWidth') ?? (m as Mux).out.width; + final utype = typeForWidth(w); + return '${vals[0]} ? $utype(${vals[2]}) : $utype(${vals[1]})'; + } + + // ── Replication ── + if (op == LeafOperationKind.replication || m is ReplicationOp) { + final a = inputs.values.first; + final inputWidth = plan.meta('inputWidth') ?? + (m as Module).inputs.values.first.width; + final outputWidth = plan.meta('outputWidth') ?? + (m as ReplicationOp).replicated.width; + final numReps = outputWidth ~/ inputWidth; + if (inputWidth == 1) { + final utype = typeForWidth(outputWidth); + return '$utype($a ? $utype(-1) : $utype(0))'; + } + final copies = List.filled(numReps, a); + return '(${copies.join(', ')})'; + } + + // ── Swizzle ── + if (op == LeafOperationKind.swizzle || m is Swizzle) { + final inputWidths = plan.meta>('inputWidths') ?? + (m as Module).inputs.values.map((input) => input.width).toList(); + final exprList = []; + var i = 0; + for (final expr in inputs.values) { + final w = inputWidths[i]; + if (w == 0) { + i++; + continue; + } + if (w == 1) { + exprList.add('sc_uint<1>($expr)'); + } else { + exprList.add(expr); + } + i++; + } + if (exprList.length == 1) { + return exprList.first; + } + return '(${exprList.reversed.join(', ')})'; + } + + if (m is SystemCInlineExpression) { + return (m as SystemCInlineExpression).inlineSystemC(inputs); + } + + throw SynthException( + 'SystemC cannot emit semantic leaf operation for ${m.runtimeType}. ' + 'Provide LeafCellProvider metadata or implement SystemCInlineExpression.', + ); + } +} diff --git a/lib/src/synthesizers/systemc/systemc_mixins.dart b/lib/src/synthesizers/systemc/systemc_mixins.dart new file mode 100644 index 000000000..ee46ee79f --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_mixins.dart @@ -0,0 +1,20 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_mixins.dart +// SystemC-specific module emission extension contracts. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Allows a module to provide an explicit SystemC expression implementation. +/// +/// Use this only when a module cannot be represented by a standard semantic +/// leaf operation. Portable leaf modules should provide semantic leaf metadata +/// instead. +mixin SystemCInlineExpression on Module { + /// Emits a SystemC/C++ expression for this module's inline result. + String inlineSystemC(Map inputs); +} diff --git a/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart b/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart new file mode 100644 index 000000000..e670279d4 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart @@ -0,0 +1,31 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synth_module_definition.dart +// Definition for SystemCSynthModuleDefinition +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// A special [SynthModuleDefinition] for SystemC modules. +class SystemCSynthModuleDefinition extends SynthModuleDefinition { + /// Creates a new [SystemCSynthModuleDefinition] for the given [module]. + SystemCSynthModuleDefinition(super.module); + + @override + void process() { + // For now, do not collapse inline modules. Each InlineSystemVerilog gate + // remains as a sub-module instantiation and gets emitted as an assign-style + // expression in the generated SystemC (similar to SV `assign x = a & b`). + // + // Future: implement chain-collapsing for compound expressions. + } + + @override + SynthSubModuleInstantiation createSubModuleInstantiation(Module m) => + SystemCSynthSubModuleInstantiation(m); +} diff --git a/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart new file mode 100644 index 000000000..9a0287234 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart @@ -0,0 +1,96 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synth_sub_module_instantiation.dart +// Definition for SystemCSynthSubModuleInstantiation +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_leaf_emitter.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// Represents a submodule instantiation for SystemC. +class SystemCSynthSubModuleInstantiation extends SynthSubModuleInstantiation { + static const _defaultLeafEmitter = + SystemCLeafEmitter(typeForWidth: _systemCType); + + static String _systemCType(int width) { + if (width == 1) { + return 'bool'; + } else if (width <= 64) { + return 'sc_uint<$width>'; + } else { + return 'sc_biguint<$width>'; + } + } + + /// Shared leaf emitter used for inline expression generation. + SystemCLeafEmitter leafEmitter = _defaultLeafEmitter; + + /// Creates a new [SystemCSynthSubModuleInstantiation] for the given + /// [module]. + SystemCSynthSubModuleInstantiation(super.module); + + /// Mapping from [SynthLogic]s which are outputs of inlineable modules to + /// those inlineable modules. + Map? + synthLogicToInlineableSynthSubmoduleMap; + + /// Provides the inline SystemC expression for this module. + /// + /// Should only be called if [module] is [InlineLeaf]. + String inlineSystemC() { + final portNameToValueMapping = modulePortsMapWithInline( + {...inputMapping, ...inOutMapping} + ..remove((module as InlineLeaf).resultSignalName), + synthLogicToInlineableSynthSubmoduleMap, + (submodule) => submodule.inlineSystemC(), + ); + + final inlineRepresentation = + _inlineSystemCExpression(portNameToValueMapping); + + return '($inlineRepresentation)'; + } + + /// Generates the inline SystemC expression for the gate module. + String _inlineSystemCExpression(Map inputs) { + final m = module; + + if (m is InlineLeaf) { + return leafEmitter.expressionFor(m, inputs); + } + + throw SynthException('Unsupported inline module type: ${m.runtimeType}'); + } + + /// Provides the full SystemC instantiation for this module as a member + /// declaration and port binding in the constructor. + /// + /// Returns null if this module does not need instantiation. + String? memberDeclaration(String instanceType) { + if (!needsInstantiation) { + return null; + } + return '$instanceType $name{"$name"};'; + } + + /// Generates port binding statements for the constructor body. + String? portBindings() { + if (!needsInstantiation) { + return null; + } + final bindings = []; + final allPorts = {...inputMapping, ...outputMapping, ...inOutMapping}; + for (final entry in allPorts.entries) { + final portName = entry.key; + final synthLogic = entry.value; + if (!synthLogic.declarationCleared) { + bindings.add('$name.$portName(${synthLogic.name});'); + } + } + return bindings.join('\n'); + } +} diff --git a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart new file mode 100644 index 000000000..fd89fbb07 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart @@ -0,0 +1,1497 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synthesis_result.dart +// Definition for SystemCSynthesisResult +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:collection/collection.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/conditionals/always.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_conditional_emitter.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_leaf_emitter.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// A [SynthesisResult] representing a conversion of a [Module] to SystemC. +class SystemCSynthesisResult extends SynthesisResult { + /// Shared SystemC leaf-expression emitter. + late final SystemCLeafEmitter _leafEmitter = + const SystemCLeafEmitter(typeForWidth: systemCType); + + /// A cached copy of the generated ports. + late final String _portsString; + + /// A cached copy of the generated module body (used for matching). + late final String _moduleBodyString; + + /// The main [SynthModuleDefinition] for this. + final SynthModuleDefinition _synthModuleDefinition; + + /// Backend-neutral resolved structure used by this renderer. + late final ModuleEmissionPlan _emissionPlan; + + @override + List get supportingModules => + _synthModuleDefinition.supportingModules; + + // Cached sections for final assembly + late final String _internalSigs; + late final String _subMembers; + late final String _ctorBody; + late final String _methodBodies; + + /// Creates a new [SystemCSynthesisResult] for the given [module]. + SystemCSynthesisResult(super.module, super.getInstanceTypeOfModule) + : _synthModuleDefinition = SystemCSynthModuleDefinition(module) { + _emissionPlan = ModuleEmissionPlan.fromDefinition(_synthModuleDefinition); + _findClockResetSignals(); + _portsString = _systemCPorts(); + _buildModuleBody(getInstanceTypeOfModule); + _moduleBodyString = '$_ctorBody|$_methodBodies'; + } + + @override + bool matchesImplementation(SynthesisResult other) => + other is SystemCSynthesisResult && + other._portsString == _portsString && + other._moduleBodyString == _moduleBodyString; + + @override + int get matchHashCode => _portsString.hashCode ^ _moduleBodyString.hashCode; + + @override + String toFileContents() => _toSystemC(); + + @override + List toSynthFileContents() => List.unmodifiable([ + SynthFileContents( + name: instanceTypeName, + description: 'SystemC module definition for $instanceTypeName', + contents: _toSystemC(), + ) + ]); + + // ──────────────────────────────────────────────────────────────────── + // Line/column position tracking for debug tracing + // ──────────────────────────────────────────────────────────────────── + + /// SystemC line map: signal/instance name → list of `'line:col'` positions + /// in the generated SystemC output (both 1-based). + /// + /// Each name's list contains the first occurrence (the declaration / port / + /// submodule member line) followed by each assignment LHS line where that + /// name appears on the left of `=` in a method body. Positions are in + /// textual (source) order; consumers that need the "assignments first, + /// declaration last" convention should reorder at emit time. + /// + /// Populated by [_buildScLineMap] after the final text is assembled. + /// Keys match the names used in the FLC trace data: canonical signal + /// names (from [SynthLogic.name]) for signals and + /// [Module.uniqueInstanceName] for submodule instances. + Map> get scLineMap => Map.unmodifiable( + _scLineMap.map((k, v) => MapEntry(k, List.unmodifiable(v)))); + final Map> _scLineMap = {}; + + /// Walks the already-generated [scText] counting newlines, and records + /// the 1-based `line:col` of each signal declaration, port, submodule + /// instance member, and assignment LHS. + /// + /// This mirrors the approach used by the SystemVerilog synthesizer's + /// `_buildSvLineMap` in the `source_debug` branch, enabling the + /// `SignalSourceTracer` to emit FLC data with both SV and SC positions. + void _buildScLineMap(String scText) { + _scLineMap.clear(); + + final targets = { + for (final sig in _emissionPlan.inputs) sig.name, + for (final sig in _emissionPlan.outputs) sig.name, + for (final sig in _emissionPlan.inOuts) sig.name, + for (final sig + in _emissionPlan.internalSignals.where((e) => e.needsDeclaration)) + sig.name, + for (final smi + in _emissionPlan.instances.where((s) => s.needsInstantiation)) + smi.name, + }; + + if (targets.isEmpty) { + return; + } + + // Single-pass: tokenize each line once, check tokens against target set. + // Record the first occurrence (declaration) and any subsequent occurrence + // that is an assignment LHS (identifier followed by `=` but not `==`). + final identRe = RegExp(r'[A-Za-z_]\w*'); + var lineNum = 1; + var lineStart = 0; + final len = scText.length; + + for (var i = 0; i <= len; i++) { + if (i == len || scText[i] == '\n') { + final lineText = scText.substring(lineStart, i); + for (final match in identRe.allMatches(lineText)) { + final word = match.group(0)!; + if (!targets.contains(word)) { + continue; + } + final pos = '$lineNum:${match.start + 1}'; + final list = _scLineMap[word]; + if (list == null) { + // First occurrence — declaration / port / sub-module member. + _scLineMap[word] = [pos]; + } else if (_isAssignmentLhs(lineText, match.end) && + !list.contains(pos)) { + // Subsequent occurrence on an assignment LHS — record it. + list.add(pos); + } + } + lineNum++; + lineStart = i + 1; + } + } + } + + /// Returns true if the identifier ending at [afterIdent] in [lineText] is + /// followed (after optional whitespace) by a single `=` (and not `==`). + static bool _isAssignmentLhs(String lineText, int afterIdent) { + var j = afterIdent; + while (j < lineText.length && + (lineText.codeUnitAt(j) == 0x20 || lineText.codeUnitAt(j) == 0x09)) { + j++; + } + if (j >= lineText.length || lineText[j] != '=') { + return false; + } + if (j + 1 < lineText.length && lineText[j + 1] == '=') { + return false; + } + return true; + } + + // ──────────────────────────────────────────────────────────────────── + // Clock/reset detection + // ──────────────────────────────────────────────────────────────────── + + /// Internal clock signals promoted to ports (from SimpleClockGenerator). + late final Set _promotedClockSignals; + + /// Pre-scans sub-module instantiations to identify clock/reset signals + /// and internal clocks that should be promoted to ports. + void _findClockResetSignals() { + final promotedClocks = {}; + for (final ssmi in _emissionPlan.instances) { + final m = ssmi.module; + // Detect SimpleClockGenerator and promote its output to a port + if (m is SimpleClockGenerator) { + for (final entry in ssmi.outputMapping.entries) { + promotedClocks.add(entry.value.name); + } + } + } + _promotedClockSignals = promotedClocks; + } + + // ──────────────────────────────────────────────────────────────────── + // Type mapping + // ──────────────────────────────────────────────────────────────────── + + /// Sanitize a signal/port name to be a valid C++ identifier. + /// Replaces `[N]` with `_N_` (LogicArray element indexing). + static String _scName(String name) => + name.replaceAllMapped(RegExp(r'\[(\d+)\]'), (m) => '_${m[1]}_'); + + /// Maps a signal width to the appropriate SystemC data type. + static String systemCType(int width) { + if (width == 1) { + return 'bool'; + } else if (width <= 64) { + return 'sc_uint<$width>'; + } else { + return 'sc_biguint<$width>'; + } + } + + /// SystemC input port type for a given width. + static String systemCInType(int width) => 'sc_in<${systemCType(width)}>'; + + /// SystemC output port type for a given width. + static String systemCOutType(int width) => 'sc_out<${systemCType(width)}>'; + + /// SystemC inout port type for a given width. + static String systemCInOutType(int width) => + 'sc_inout<${systemCType(width)}>'; + + /// SystemC signal type for a given width. + static String systemCSignalType(int width) => + 'sc_signal<${systemCType(width)}>'; + + // ──────────────────────────────────────────────────────────────────── + // Port declarations + // ──────────────────────────────────────────────────────────────────── + + String _systemCPorts() { + final lines = []; + for (final sig in _emissionPlan.inputs) { + final n = _scName(sig.name); + lines.add(' ${systemCInType(sig.width)} $n{"$n"};'); + } + // Promote internal clock signals (from SimpleClockGenerator) to ports + for (final clkName in _promotedClockSignals) { + final n = _scName(clkName); + lines.add(' ${systemCInType(1)} $n{"$n"};'); + } + for (final sig in _emissionPlan.outputs) { + final n = _scName(sig.name); + lines.add(' ${systemCOutType(sig.width)} $n{"$n"};'); + } + for (final sig in _emissionPlan.inOuts) { + final n = _scName(sig.name); + lines.add(' ${systemCInOutType(sig.width)} $n{"$n"};'); + } + return lines.join('\n'); + } + + // ──────────────────────────────────────────────────────────────────── + // Internal signals + // ──────────────────────────────────────────────────────────────────── + + String _buildInternalSignals() { + final declarations = []; + for (final sig in _emissionPlan.internalSignals + .where((e) => e.needsDeclaration) + .where((e) => !_promotedClockSignals.contains(e.name)) + .sorted((a, b) => a.name.compareTo(b.name))) { + final n = _scName(sig.name); + declarations.add(' ${systemCSignalType(sig.width)} $n{"$n"};'); + } + + // Declare individual signals for array elements that are written to + // (FlipFlop/Sequential outputs targeting array elements) + for (final elemName in _arrayElementsWritten.keys) { + final n = _scName(elemName); + final width = _arrayElementsWritten[elemName]!; + declarations.add(' ${systemCSignalType(width)} $n{"$n"};'); + } + return declarations.join('\n'); + } + + /// Maps array element names (e.g. "delayLine[0]") to their widths. + /// These need separate signal declarations because SystemC can't do + /// partial writes to sc_signal. + late final Map _arrayElementsWritten = + _findArrayElementsWritten(); + + /// Groups array elements by parent: parentName → list of (index, elemWidth). + late final Map> + _arrayElementsByParent = _groupArrayElementsByParent(); + + Map _findArrayElementsWritten() { + final result = {}; + + void addIfArrayElement(SynthLogic sl) { + if (sl is SynthLogicArrayElement) { + result[sl.name] = sl.logic.width; + } + } + + for (final ssmi in _emissionPlan.instances) { + final m = ssmi.module; + + // All submodule output mappings + ssmi.outputMapping.values.forEach(addIfArrayElement); + + // Inline gate result logics + if (ssmi is SystemCSynthSubModuleInstantiation) { + final rl = ssmi.inlineResultLogic; + if (rl != null) { + addIfArrayElement(rl); + } + } + + // Scan conditionals for nested array element receivers + if (m is Combinational) { + _collectArrayReceiversFromConditionals(m.conditionals, result); + } else if (m is Sequential) { + _collectArrayReceiversFromConditionals(m.conditionals, result); + } + } + + // Wire assignments targeting array elements + for (final assignment in _emissionPlan.assignments) { + addIfArrayElement(assignment.dst); + } + + return result; + } + + /// Recursively walks a conditionals tree to find all receivers that + /// are array elements and adds them to [result]. + void _collectArrayReceiversFromConditionals( + List conditionals, Map result) { + for (final c in conditionals) { + for (final receiver in c.receivers) { + final sl = _synthModuleDefinition.logicToSynthMap[receiver]; + if (sl is SynthLogicArrayElement && !result.containsKey(sl.name)) { + result[sl.name] = sl.logic.width; + } + } + // Recurse into sub-conditionals + _collectArrayReceiversFromConditionals(c.conditionals, result); + } + } + + /// Groups array elements by their root parent signal, + /// computing flat bit offsets for nested elements. + Map> + _groupArrayElementsByParent() { + final result = >{}; + + void addElement(SynthLogicArrayElement sl) { + // Walk up to root and compute flat bit offset + var flatOffset = 0; + SynthLogic current = sl; + while (current is SynthLogicArrayElement) { + final idx = current.logic.arrayIndex; + if (idx == null) { + return; // pruned element — skip + } + flatOffset += idx * current.logic.width; + current = current.parentArray.replacement ?? current.parentArray; + } + final rootName = current.name; + + final entry = ( + // Use flat bit offset as "index" for assembly ordering + index: flatOffset, + width: sl.logic.width, + elemName: sl.name, + ); + // Avoid duplicates + final list = result.putIfAbsent(rootName, () => []); + if (!list.any((e) => e.elemName == entry.elemName)) { + list.add(entry); + } + } + + // Use logicToSynthMap to find the SynthLogicArrayElement for each written + // element, rather than re-scanning submodule instantiations. + for (final sl in _synthModuleDefinition.logicToSynthMap.values) { + if (sl is SynthLogicArrayElement && sl.replacement == null) { + // Skip elements whose parent has been pruned or not named + final parent = sl.parentArray.replacement ?? sl.parentArray; + if (parent.declarationCleared) { + continue; + } + if (_arrayElementsWritten.containsKey(sl.name)) { + addElement(sl); + } + } + } + + // Sort each list by flat bit offset + for (final list in result.values) { + list.sort((a, b) => a.index.compareTo(b.index)); + } + return result; + } + + // ──────────────────────────────────────────────────────────────────── + // Inline gate expressions + // ──────────────────────────────────────────────────────────────────── + + /// Returns true if a module is a SystemVerilog gate that generates no + /// definition and should be inlined (like Add). + static bool _isInlinableSystemVerilogGate(Module m) => + m is SystemVerilog && + m is! InlineLeaf && + m is! Always && + m is! FlipFlop && + m.generatedDefinitionType == DefinitionGenerationType.none; + + /// Converts a [SynthLogic] to a SystemC read expression. + /// Constants become typed literals; signals get `.read()`. + /// Array elements become range expressions on their parent. + static String _synthLogicReadExpr(SynthLogic sl) { + if (sl.isConstant) { + final c = sl.logics.whereType().first; + return _typedConstExpr(c.value, c.width); + } + if (sl is SynthLogicArrayElement) { + return _arrayElementReadExpr(sl); + } + return '${_scName(sl.name)}.read()'; + } + + /// Generates a typed constant expression for SystemC. + /// Handles x/z values by treating them as 0. + static String _typedConstExpr(LogicValue val, int width) { + if (val.isValid) { + if (width == 0) { + return '0'; + } + final bigVal = val.toBigInt(); + if (width > 64) { + // Use hex string constructor for sc_biguint + var hex = bigVal.toUnsigned(width).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + return '${systemCType(width)}("0x$hex")'; + } + // For uint64 values above INT64_MAX, add ULL suffix + if (bigVal > (BigInt.one << 63) - BigInt.one) { + return '${systemCType(width)}' + '(${bigVal.toUnsigned(width)}ULL)'; + } + return '${systemCType(width)}(${bigVal.toUnsigned(width)})'; + } + // For values with x/z, use 0 (SystemC doesn't have x/z) + return '${systemCType(width)}(0)'; + } + + /// Generates a range read expression for an array element. e.g. + /// deserialized[0] (8-bit in 32-bit parent) → deserialized.read().range(7, 0) + /// Generates a range read expression for an array element, handling + /// arbitrary nesting depth. e.g. `laIn[2][1]` in a `[3,2]x8` array + /// → `laIn.read().range(47, 40)`. + static String _arrayElementReadExpr(SynthLogicArrayElement sl) { + final elemWidth = sl.logic.width; + + // Walk up the parent chain to find the root signal and accumulate + // the flat bit offset. + var flatOffset = 0; + SynthLogic current = sl; + while (current is SynthLogicArrayElement) { + final idx = current.logic.arrayIndex!; + final w = current.logic.width; + flatOffset += idx * w; + current = current.parentArray.replacement ?? current.parentArray; + } + final rootName = _scName(current.name); + final rootWidth = current.width; + + final lo = flatOffset; + final hi = lo + elemWidth - 1; + + // If the root is 1-bit (bool), subscript/range is not valid + if (rootWidth == 1) { + return '$rootName.read()'; + } + if (elemWidth == 1) { + return 'static_cast($rootName.read()[$lo])'; + } + final rangeType = elemWidth <= 64 ? 'sc_uint' : 'sc_biguint'; + return '$rangeType<$elemWidth>($rootName.read().range($hi, $lo))'; + } + + /// Returns the sensitivity signal name for a SynthLogic. + /// For array elements, walks up to the root (non-array-element) parent. + static String _sensitivityName(SynthLogic sl) { + var current = sl; + while (current is SynthLogicArrayElement) { + current = current.parentArray.replacement ?? current.parentArray; + } + return _scName(current.name); + } + + /// Generates an SC_METHOD for inline gates (like SV `assign` stmts). + _MethodResult? _buildInlineGates() { + final inlineGates = _synthModuleDefinition.subModuleInstantiations + .where((s) => + s.needsInstantiation && + (s.module is InlineLeaf || _isInlinableSystemVerilogGate(s.module))) + .cast() + .toList(); + + if (inlineGates.isEmpty) { + return null; + } + + final assignments = <_ScMethodAssignment>[]; + + for (final ssmi in inlineGates) { + final m = ssmi.module; + final sensitivities = {}; + final bodyLines = []; + final destinations = {}; + + // Collect inputs — constants become literals, signals get .read(). + // Inline modules connected to LogicNets can have source ports mapped as + // inouts, so include non-result inout mappings as inputs. + final inputExprs = {}; + final inputMappings = {...ssmi.inputMapping, ...ssmi.inOutMapping}; + if (m is InlineLeaf) { + inputMappings.remove(m.resultSignalName); + } + for (final entry in inputMappings.entries) { + final sl = entry.value; + if (!sl.isConstant) { + sensitivities.add(_sensitivityName(sl)); + } + inputExprs[entry.key] = _synthLogicReadExpr(sl); + } + + if (m is InlineLeaf) { + final resultSynthLogic = ssmi.inlineResultLogic; + if (resultSynthLogic == null || !resultSynthLogic.hasName) { + continue; + } + final expr = _gateExpression(m, inputExprs); + final dst = _scName(resultSynthLogic.name); + destinations.add(dst); + bodyLines.add(' $dst = $expr;'); + } else if (m is Add) { + // Add has two outputs: sum and carry. + // Emit inline expressions for each used output. + final vals = inputExprs.values.toList(); + final sumPortName = m.sum.name; + for (final entry in ssmi.outputMapping.entries) { + final portName = entry.key; + final dst = _scName(entry.value.name); + destinations.add(dst); + if (portName == sumPortName) { + bodyLines.add(' $dst = ${vals[0]} + ${vals[1]};'); + } else { + // carry: high bit of (width+1)-bit addition + final w = m.width; + final w1 = w + 1; + final utype = systemCType(w1); + final carryExpr = 'static_cast' + '($utype($utype(${vals[0]})' + ' + $utype(${vals[1]}))[$w])'; + bodyLines.add(' $dst = $carryExpr;'); + } + } + } + + if (bodyLines.isEmpty) { + continue; + } + + assignments.add(_ScMethodAssignment( + bodyLines: bodyLines, + sensitivities: sensitivities, + destinations: destinations, + )); + ssmi.clearInstantiation(); + } + + if (assignments.isEmpty) { + return null; + } + + return _emitGroupedAssignments('assign', assignments); + } + + /// Maps an InlineSystemVerilog gate to a C++ expression. + /// + /// Handles all gate types that have SV-specific syntax which needs + /// translation to valid SystemC/C++. + String _gateExpression(InlineLeaf m, Map inputs) => + _leafEmitter.expressionFor(m, inputs); + + // ──────────────────────────────────────────────────────────────────── + // Clock / trigger edge resolution + // ──────────────────────────────────────────────────────────────────── + + /// Resolves a trigger [SynthLogic] to the effective clock port and edge. + /// + /// If the trigger signal is a module input port, it can be used directly + /// with `SC_CTHREAD`. If it is an internal signal derived from a [NotGate], + /// the method traces through the inversion chain to find the original port + /// and flips the edge accordingly (`negedge(~clk) = posedge(clk)`). + ({String clockName, bool isPort, bool isPosedge}) _resolveClockAndEdge( + SynthLogic triggerSL, bool isPosedge) { + final sl = triggerSL.replacement ?? triggerSL; + + if (sl.isPort(_synthModuleDefinition.module)) { + return (clockName: sl.name, isPort: true, isPosedge: isPosedge); + } + + // Try to trace through a NotGate inversion + for (final logic in sl.logics) { + final src = logic.srcConnection; + if (src != null && src.parentModule is NotGate) { + final notInput = src.parentModule!.inputs.values.first; + final notInputSrc = notInput.srcConnection; + if (notInputSrc != null) { + final srcSL = _synthModuleDefinition.logicToSynthMap[notInputSrc]; + if (srcSL != null) { + // Inversion flips the edge + return _resolveClockAndEdge(srcSL, !isPosedge); + } + } + } + } + + // Fallback — use the signal as-is (SC_THREAD will be needed) + return (clockName: sl.name, isPort: false, isPosedge: isPosedge); + } + + // ──────────────────────────────────────────────────────────────────── + // Combinational / Sequential processes + // ──────────────────────────────────────────────────────────────────── + + _MethodResult? _buildProcesses() { + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + var idx = 0; + + // Collect clocked processes for consolidation by (clock, reset) pair. + // Sequentials and FlipFlops sharing the same clock/reset are merged + // into a single SC_CTHREAD, eliminating repeated async_reset_signal_is. + final clockedGroups = {}; + + for (final ssmi + in _synthModuleDefinition.subModuleInstantiations.toList()) { + ssmi as SystemCSynthSubModuleInstantiation; + final m = ssmi.module; + + if (m is Combinational) { + final name = 'comb_$idx'; + idx++; + + final sensitivities = ssmi.inputMapping.values + .where((sl) => !sl.declarationCleared && !sl.isConstant) + .map(_sensitivityName) + .toSet(); + + setupBuf.writeln(' SC_METHOD($name);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + final processPlan = ProcessEmissionPlan.fromAlways(m); + final inputsMap = ssmi.inputMapping + .map((k, sl) => MapEntry(k, _synthLogicReadExpr(sl))); + final outputsMap = + ssmi.outputMapping.map((k, sl) => MapEntry(k, _scName(sl.name))); + + bodyBuf + ..writeln(' void $name() {') + ..write(_emitSystemCProcessBody( + processPlan, + 2, + inputsMap, + outputsMap, + )) + ..writeln(' }') + ..writeln(); + ssmi.clearInstantiation(); + } else if (m is Sequential) { + final resetEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('reset')) + .firstOrNull; + + // Detect async reset: either explicitly via asyncReset flag, or + // implicitly when the reset signal is also listed as a trigger + // (e.g. Sequential.multi([clk, reset], reset: reset, ...)). + final isAsync = m.asyncReset || + (resetEntry != null && + ssmi.inputMapping.entries.any((e) => + e.key.contains('trigger') && + e.value.name == resetEntry.value.name)); + + // Resolve ALL trigger entries to (signalName, edge, isPort). + final triggerEdges = m.triggerEdges; + final triggerEntries = ssmi.inputMapping.entries + .where((e) => e.key.contains('trigger')) + .toList(); + + final resolvedTriggers = + <({String signalName, bool isPosedge, bool isPort})>[]; + + for (final te in triggerEntries) { + final triggerSL = te.value; + // Skip if this trigger is the async reset signal + if (resetEntry != null && triggerSL.name == resetEntry.value.name) { + continue; + } + // Skip constant triggers (e.g. clk <= Const(0) — never toggles) + if (triggerSL.isConstant) { + continue; + } + final isPosedge = triggerEdges + .where((t) => t.portName == te.key) + .firstOrNull + ?.isPosedge ?? + true; + final resolved = _resolveClockAndEdge(triggerSL, isPosedge); + // Skip if the resolved signal is constant + final resolvedSL = _synthModuleDefinition.logicToSynthMap.values + .where((sl) => sl.replacement == null && !sl.declarationCleared) + .where((sl) => sl.name == resolved.clockName) + .firstOrNull; + if (resolvedSL != null && resolvedSL.isConstant) { + continue; + } + resolvedTriggers.add(( + signalName: resolved.clockName, + isPosedge: resolved.isPosedge, + isPort: resolved.isPort, + )); + } + + // Deduplicate by (signalName, isPosedge) + final seen = {}; + final uniqueTriggers = + <({String signalName, bool isPosedge, bool isPort})>[]; + for (final t in resolvedTriggers) { + final key = '${t.signalName}|${t.isPosedge}'; + if (seen.add(key)) { + uniqueTriggers.add(t); + } + } + + // Build group key from all trigger signals + reset + final triggerKey = uniqueTriggers + .map((t) => '${t.signalName}:${t.isPosedge}') + .join(','); + final groupKey = '$triggerKey|${resetEntry?.value.name ?? '_none_'}'; + final group = clockedGroups.putIfAbsent( + groupKey, + () => _ClockedGroupData( + resetName: resetEntry?.value.name, + isAsyncReset: isAsync, + )); + // Add all triggers to the group (dedup handled by emission) + for (final t in uniqueTriggers) { + if (!group.triggers.any((existing) => + existing.signalName == t.signalName && + existing.isPosedge == t.isPosedge)) { + group.triggers.add(t); + } + } + if (isAsync) { + group.isAsyncReset = true; + } + + final inputsMap = ssmi.inputMapping + .map((k, sl) => MapEntry(k, _synthLogicReadExpr(sl))); + final outputsMap = + ssmi.outputMapping.map((k, sl) => MapEntry(k, _scName(sl.name))); + + for (final outName in outputsMap.values) { + group.resetLines.add(' $outName = 0;'); + } + final conditionalBody = _emitSystemCProcessBody( + ProcessEmissionPlan.fromAlways(m), + 3, + inputsMap, + outputsMap, + ); + group.whileBodyLines.add(conditionalBody); + ssmi.clearInstantiation(); + } else if (m is FlipFlop) { + // Resolve port signals via the input/output mapping + final clkSl = ssmi.inputMapping.entries + .firstWhere((e) => e.key.contains('clk')) + .value; + final dSl = ssmi.inputMapping.entries + .firstWhere((e) => e.key.contains('d')) + .value; + final resetEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('reset') && !e.key.contains('Value')) + .firstOrNull; + final enEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('en')) + .firstOrNull; + final resetValueEntry = ssmi.inputMapping.entries + .where( + (e) => e.key.contains('resetValue') || e.key.contains('Value')) + .firstOrNull; + final qSl = ssmi.outputMapping.values.first; + + final groupKey = + '${clkSl.name}:true|${resetEntry?.value.name ?? '_none_'}'; + final group = clockedGroups.putIfAbsent( + groupKey, + () => _ClockedGroupData( + resetName: resetEntry?.value.name, + isAsyncReset: m.asyncReset, + )); + // FlipFlop always posedge + if (!group.triggers + .any((t) => t.signalName == clkSl.name && t.isPosedge)) { + group.triggers.add(( + signalName: clkSl.name, + isPosedge: true, + isPort: clkSl.isPort(_synthModuleDefinition.module), + )); + } + if (m.asyncReset) { + group.isAsyncReset = true; + } + + // Reset value + String resetValExpr; + if (resetValueEntry != null) { + resetValExpr = _synthLogicReadExpr(resetValueEntry.value); + } else if (m.constantResetValue != null) { + resetValExpr = m.constantResetValue!.toBigInt().toString(); + } else { + resetValExpr = '0'; + } + group.resetLines.add(' ${_scName(qSl.name)} = $resetValExpr;'); + + // Build the data assignment (with optional enable gate) + final assignExpr = + ' ${_scName(qSl.name)} = ${_synthLogicReadExpr(dSl)};\n'; + final bodyLine = enEntry != null + ? ' if (${_synthLogicReadExpr(enEntry.value)}) {\n' + ' $assignExpr' + ' }\n' + : assignExpr; + + // Wrap in sync reset check if needed + if (resetEntry != null && !m.asyncReset) { + group.whileBodyLines + .add(' if (${_scName(resetEntry.value.name)}.read()) {\n' + ' ${_scName(qSl.name)} = $resetValExpr;\n' + ' } else {\n' + ' $bodyLine' + ' }\n'); + } else { + group.whileBodyLines.add(bodyLine); + } + ssmi.clearInstantiation(); + } + } + + // Emit one SC_CTHREAD or SC_THREAD per (clock, reset) group + for (final group in clockedGroups.values) { + final name = 'clocked_$idx'; + idx++; + + final triggers = group.triggers; + + if (triggers.isEmpty) { + // All triggers were constant — skip this group + continue; + } + + // Determine if we can use SC_CTHREAD: + // - exactly one trigger signal + // - that signal is a port (sc_in) + // - only one edge direction + final distinctSignals = triggers.map((t) => t.signalName).toSet(); + final useCthread = distinctSignals.length == 1 && + triggers.first.isPort && + triggers.length == 1; + + if (useCthread) { + final t = triggers.first; + final clockRef = _scName(t.signalName); + final edge = t.isPosedge ? '.pos()' : '.neg()'; + setupBuf.writeln(' SC_CTHREAD($name, $clockRef$edge);'); + if (group.resetName != null && group.isAsyncReset) { + setupBuf.writeln(' async_reset_signal_is(' + '${_scName(group.resetName!)}, true);'); + } + + bodyBuf.writeln(' void $name() {'); + group.resetLines.forEach(bodyBuf.writeln); + bodyBuf + ..writeln(' wait();') + ..writeln(' while (true) {'); + group.whileBodyLines.forEach(bodyBuf.write); + bodyBuf + ..writeln(' wait();') + ..writeln(' }') + ..writeln(' }') + ..writeln(); + } else { + // SC_THREAD with explicit wait on events + setupBuf.writeln(' SC_THREAD($name);'); + + // Build wait expression from all trigger events + String waitExpr; + if (distinctSignals.length == 1) { + // Same signal, but both edges + final sig = _scName(triggers.first.signalName); + final edges = triggers.map((t) => t.isPosedge).toSet(); + if (edges.length == 2) { + waitExpr = '$sig.value_changed_event()'; + } else if (edges.first) { + waitExpr = '$sig.posedge_event()'; + } else { + waitExpr = '$sig.negedge_event()'; + } + } else { + // Multiple distinct trigger signals — OR them together + final eventExprs = []; + for (final t in triggers) { + final sig = _scName(t.signalName); + eventExprs + .add('$sig.${t.isPosedge ? 'posedge' : 'negedge'}_event()'); + } + waitExpr = eventExprs.join(' | '); + } + + bodyBuf.writeln(' void $name() {'); + group.resetLines.forEach(bodyBuf.writeln); + bodyBuf + ..writeln(' while (true) {') + ..writeln(' wait($waitExpr);'); + group.whileBodyLines.forEach(bodyBuf.write); + bodyBuf + ..writeln(' }') + ..writeln(' }') + ..writeln(); + } + } + + if (setupBuf.isEmpty && bodyBuf.isEmpty) { + return null; + } + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Regular sub-module instantiations + // ──────────────────────────────────────────────────────────────────── + + /// Returns true if the sub-module is handled inline (not a real child + /// instantiation) — i.e. it is an inline gate, Always, FlipFlop, or clock. + static bool _isHandledInline(SystemCSynthSubModuleInstantiation ssmi) => + !ssmi.needsInstantiation || + ssmi.module is InlineLeaf || + ssmi.module is Always || + ssmi.module is FlipFlop || + ssmi.module is SimpleClockGenerator || + _isInlinableSystemVerilogGate(ssmi.module); + + String _buildSubModuleMembers( + String Function(Module module) getInstanceTypeOfModule) { + final lines = []; + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + ssmi as SystemCSynthSubModuleInstantiation; + if (_isHandledInline(ssmi)) { + continue; + } + final instanceType = getInstanceTypeOfModule(ssmi.module); + lines.add(' $instanceType ${ssmi.name}{"${ssmi.name}"};'); + } + return lines.join('\n'); + } + + /// Dummy signal declarations needed for unconnected submodule output ports. + /// Populated by [_buildSubModuleBindings]. + final List _unconnectedOutputSignals = []; + + /// Signal declarations for constants bound to submodule input ports. + /// Populated by [_buildSubModuleBindings]. + final List _constInputSignals = []; + + /// Initialization statements for constant signals (in constructor body). + /// Populated by [_buildSubModuleBindings]. + final List _constInputInits = []; + + String _buildSubModuleBindings( + String Function(Module module) getInstanceTypeOfModule) { + final lines = []; + var unconnIdx = 0; + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + ssmi as SystemCSynthSubModuleInstantiation; + if (_isHandledInline(ssmi)) { + continue; + } + + // Bind connected ports (inputs, outputs, inouts) + final allPorts = { + ...ssmi.inputMapping, + ...ssmi.outputMapping, + ...ssmi.inOutMapping, + }; + for (final entry in allPorts.entries) { + if (!entry.value.declarationCleared) { + if (entry.value.isConstant) { + // Constants can't be bound directly to sc_in ports; + // create a signal, initialize it, and bind that. + final constName = _scName('_const_${ssmi.name}' + '_${entry.key}_${_constInputSignals.length}'); + final w = entry.value.width; + final c = entry.value.logics.whereType().first; + final constVal = _typedConstExpr(c.value, c.width); + _constInputSignals + .add(' ${systemCSignalType(w)} $constName{"$constName"};'); + _constInputInits.add(' $constName.write($constVal);'); + lines.add(' ${ssmi.name}.${entry.key}($constName);'); + } else { + lines.add(' ' + '${ssmi.name}.${entry.key}(${_scName(entry.value.name)});'); + } + } + } + + // Bind unconnected ports to dummy signals + // (SystemC requires all sc_in/sc_out ports to be bound) + for (final entry in [ + ...ssmi.outputMapping.entries, + ...ssmi.inputMapping.entries, + ]) { + if (entry.value.declarationCleared) { + final dummyName = '_unused_${ssmi.name}_${entry.key}_$unconnIdx'; + final w = entry.value.width; + _unconnectedOutputSignals + .add(' ${systemCSignalType(w)} $dummyName{"$dummyName"};'); + lines.add(' ${ssmi.name}.${entry.key}($dummyName);'); + unconnIdx++; + } + } + } + return lines.join('\n'); + } + + // ──────────────────────────────────────────────────────────────────── + // Wire assignments + // ──────────────────────────────────────────────────────────────────── + + _MethodResult? _buildWireAssignments() { + if (_synthModuleDefinition.assignments.isEmpty) { + return null; + } + + final assignments = <_ScMethodAssignment>[]; + + // Group partial assignments by destination for concatenated writes + final partialsByDst = >{}; + + for (final assignment in _synthModuleDefinition.assignments) { + if (assignment is PartialSynthAssignment) { + partialsByDst + .putIfAbsent(_scName(assignment.dst.name), () => []) + .add(assignment); + } else { + final sensitivities = {}; + if (!assignment.src.isConstant) { + sensitivities.add(_sensitivityName(assignment.src)); + } + final bodyLine = ' ${_scName(assignment.dst.name)} = ' + '${_synthLogicReadExpr(assignment.src)};'; + assignments.add(_ScMethodAssignment( + bodyLines: [bodyLine], + sensitivities: sensitivities, + destinations: {_scName(assignment.dst.name)}, + )); + } + } + + // Emit grouped partial assignments as shift-or concatenation + for (final entry in partialsByDst.entries) { + final dstName = entry.key; + final partials = entry.value + ..sort((a, b) => a.dstLowerIndex.compareTo(b.dstLowerIndex)); + + // Find total width from the destination SynthLogic + final dstWidth = partials.last.dstUpperIndex + 1; + final utype = systemCType(dstWidth); + final parts = []; + final sensitivities = {}; + for (final p in partials) { + if (!p.src.isConstant) { + sensitivities.add(_sensitivityName(p.src)); + } + final srcExpr = _synthLogicReadExpr(p.src); + if (p.dstLowerIndex == 0) { + parts.add('$utype($srcExpr)'); + } else { + parts.add('($utype($srcExpr) << ${p.dstLowerIndex})'); + } + } + assignments.add(_ScMethodAssignment( + bodyLines: [' $dstName = ${parts.join(' | ')};'], + sensitivities: sensitivities, + destinations: {dstName}, + )); + } + + return _emitGroupedAssignments('wire_assign', assignments); + } + + _MethodResult _emitGroupedAssignments( + String methodPrefix, List<_ScMethodAssignment> assignments) { + final groups = <_ScMethodAssignmentGroup>[]; + + for (final assignment in assignments) { + final group = groups.firstWhereOrNull((g) => g.canAdd(assignment)); + if (group == null) { + groups.add(_ScMethodAssignmentGroup()..add(assignment)); + } else { + group.add(assignment); + } + } + + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + + for (var i = 0; i < groups.length; i++) { + final group = groups[i]; + final methodName = '${methodPrefix}_$i'; + setupBuf.writeln(' SC_METHOD($methodName);'); + for (final sig in group.sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + bodyBuf + ..writeln(' void $methodName() {') + ..writeln(group.bodyLines.join('\n')) + ..writeln(' }') + ..writeln(); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Conditional → SystemC + // ──────────────────────────────────────────────────────────────────── + + String _emitSystemCProcessBody( + ProcessEmissionPlan plan, + int indent, + Map inputsMap, + Map outputsMap, + ) { + final emitter = SystemCConditionalEmitter( + driverExpressionFor: (driver) => _resolveDriver(driver, inputsMap), + receiverExpressionFor: (receiver) => + _resolveReceiver(receiver, outputsMap), + isConstCaseItem: _isConstCaseItem, + caseItemConditionFor: (value, expression, {required isCaseZ}) => + _caseItemCondition( + value, + expression, + inputsMap, + isCaseZ: isCaseZ, + ), + ); + return plan.body + .map((statement) => emitter.emitPlan(statement, indent)) + .join(); + } + + /// Checks whether a case item value is a compile-time constant. + bool _isConstCaseItem(dynamic value) { + if (value is Const) { + return true; + } + if (value is LogicValue) { + return true; + } + if (value is Logic) { + if (value.srcConnection is Const) { + return true; + } + final sl = _synthModuleDefinition.logicToSynthMap[value]; + if (sl != null && sl.isConstant) { + return true; + } + return false; + } + return true; // int, string, etc. + } + + /// Generates the condition expression for a case item comparison. + String _caseItemCondition( + dynamic value, String expr, Map inputsMap, + {bool isCaseZ = false}) { + // Extract LogicValue from Const for CaseZ mask matching + LogicValue? lv; + if (value is Const) { + lv = value.value; + } else if (value is LogicValue) { + lv = value; + } + if (isCaseZ && lv != null && !lv.isValid) { + // CaseZ: create mask comparison (expr & mask) == pattern + // z bits become don't-care (mask out those bits) + final width = lv.width; + // z→0 in mask, 0/1→1 in mask + var maskStr = ''; + var patStr = ''; + for (var i = width - 1; i >= 0; i--) { + final bit = lv[i]; + if (bit == LogicValue.z || bit == LogicValue.x) { + maskStr += '0'; + patStr += '0'; + } else { + maskStr += '1'; + patStr += bit == LogicValue.one ? '1' : '0'; + } + } + final maskVal = BigInt.parse(maskStr, radix: 2); + final patVal = BigInt.parse(patStr, radix: 2); + return '($expr & $maskVal) == $patVal'; + } + if (value is Logic && value is! Const) { + final resolved = _resolveDriver(value, inputsMap); + return '$expr == $resolved'; + } + return '$expr == ${_constLit(value)}'; + } + + /// Resolves a driver Logic to a SystemC read expression using the + /// SynthModuleDefinition's logicToSynthMap to find the canonical name. + String _resolveDriver(Logic driver, Map inputsMap) { + if (driver is Const) { + return _constLit(driver); + } + // Look up via logicToSynthMap — the SynthLogic has the canonical name + final sl = _synthModuleDefinition.logicToSynthMap[driver]; + if (sl != null) { + return _synthLogicReadExpr(sl); + } + // Try to find via source connection chain — handles cases where + // the Logic object isn't directly in the map but its source is + var src = driver.srcConnection; + while (src != null) { + final srcSl = _synthModuleDefinition.logicToSynthMap[src]; + if (srcSl != null) { + return _synthLogicReadExpr(srcSl); + } + src = src.srcConnection; + } + // Fallback: try inputsMap by port name + if (inputsMap.containsKey(driver.name)) { + return inputsMap[driver.name]!; + } + return '${_scName(driver.name)}.read()'; + } + + /// Resolves a receiver Logic to a SystemC signal name using the + /// SynthModuleDefinition's logicToSynthMap to find the canonical name. + String _resolveReceiver(Logic receiver, Map outputsMap) { + // Look up via logicToSynthMap + final sl = _synthModuleDefinition.logicToSynthMap[receiver]; + if (sl != null) { + return _scName(sl.name); + } + // Fallback + if (outputsMap.containsKey(receiver.name)) { + return outputsMap[receiver.name]!; + } + return _scName(receiver.name); + } + + String _constLit(dynamic value) { + if (value is Const) { + if (value.value.isValid) { + return value.value.toBigInt().toString(); + } + return '0'; // x/z → 0 in SystemC + } else if (value is LogicValue) { + if (value.isValid) { + return value.toBigInt().toString(); + } + return '0'; // x/z → 0 in SystemC + } else if (value is Logic) { + // If the Logic is driven by a Const, resolve to integer literal + if (value.srcConnection is Const) { + final cv = (value.srcConnection! as Const).value; + return cv.isValid ? cv.toBigInt().toString() : '0'; + } + // Check logicToSynthMap for a constant SynthLogic + final sl = _synthModuleDefinition.logicToSynthMap[value]; + if (sl != null && sl.isConstant) { + final constLogic = sl.logics.whereType().firstOrNull; + if (constLogic != null) { + return constLogic.value.isValid + ? constLogic.value.toBigInt().toString() + : '0'; + } + } + // Fallback: use signal read expression + return '${value.name}.read()'; + } + return value.toString(); + } + + // ──────────────────────────────────────────────────────────────────── + // Build all sections + // ──────────────────────────────────────────────────────────────────── + + void _buildModuleBody( + String Function(Module module) getInstanceTypeOfModule) { + _subMembers = _buildSubModuleMembers(getInstanceTypeOfModule); + + final inlineGates = _buildInlineGates(); + final processes = _buildProcesses(); + final wireAssigns = _buildWireAssignments(); + final arrayAssembly = _buildArrayAssemblyMethod(); + final subBindings = _buildSubModuleBindings(getInstanceTypeOfModule); + + // Build internal signals, appending dummy signals for unconnected + // submodule outputs (populated by _buildSubModuleBindings above). + final baseSigs = _buildInternalSignals(); + _internalSigs = [ + baseSigs, + ..._unconnectedOutputSignals, + ..._constInputSignals, + ].where((s) => s.isNotEmpty).join('\n'); + + final ctorParts = [ + if (_constInputInits.isNotEmpty) _constInputInits.join('\n'), + if (inlineGates != null) inlineGates.setup, + if (processes != null) processes.setup, + if (wireAssigns != null) wireAssigns.setup, + if (arrayAssembly != null) arrayAssembly.setup, + if (subBindings.isNotEmpty) subBindings, + ]; + _ctorBody = ctorParts.join(); + + final bodyParts = [ + if (inlineGates != null) inlineGates.body, + if (processes != null) processes.body, + if (wireAssigns != null) wireAssigns.body, + if (arrayAssembly != null) arrayAssembly.body, + ]; + _methodBodies = bodyParts.where((s) => s.isNotEmpty).join('\n'); + } + + /// Builds an SC_METHOD that assembles individual array element signals + /// back into their parent signal via concatenation. + _MethodResult? _buildArrayAssemblyMethod() { + if (_arrayElementsByParent.isEmpty) { + return null; + } + + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + var methodIdx = 0; + + for (final entry in _arrayElementsByParent.entries) { + final parentName = _scName(entry.key); + final elements = entry.value; + final methodName = 'array_assemble_$methodIdx'; + methodIdx++; + + setupBuf.writeln(' SC_METHOD($methodName);'); + for (final elem in elements) { + setupBuf.writeln(' sensitive << ${_scName(elem.elemName)};'); + } + + // Build concatenation: (elem[N-1], ..., elem[1], elem[0]) + // SystemC concat is MSB-first, so highest index first + // Wrap 1-bit (bool) elements in sc_uint<1>() for proper concat + final concatParts = elements.reversed.map((e) { + final read = '${_scName(e.elemName)}.read()'; + return e.width == 1 ? 'sc_uint<1>($read)' : read; + }).toList(); + + bodyBuf + ..writeln(' void $methodName() {') + ..writeln(' $parentName = (${concatParts.join(', ')});') + ..writeln(' }') + ..writeln(); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Final assembly + // ──────────────────────────────────────────────────────────────────── + + String _toSystemC() { + final moduleName = getInstanceTypeOfModule(module); + final buf = StringBuffer()..writeln('SC_MODULE($moduleName) {'); + + if (_portsString.isNotEmpty) { + buf.writeln(_portsString); + } + if (_internalSigs.isNotEmpty) { + buf + ..writeln() + ..writeln(_internalSigs); + } + if (_subMembers.isNotEmpty) { + buf + ..writeln() + ..writeln(_subMembers); + } + + buf + ..writeln() + ..writeln(' SC_CTOR($moduleName) {'); + if (_ctorBody.isNotEmpty) { + buf.write(_ctorBody); + } + buf.writeln(' }'); + + if (_methodBodies.isNotEmpty) { + buf + ..writeln() + ..write(_methodBodies) + ..writeln(); + } + + buf.writeln('};'); + final text = buf.toString(); + + _buildScLineMap(text); + + return text; + } +} + +/// Helper to hold a constructor setup string and method body string. +class _MethodResult { + final String setup; + final String body; + const _MethodResult({required this.setup, required this.body}); +} + +class _ScMethodAssignment { + final List bodyLines; + final Set sensitivities; + final Set destinations; + + const _ScMethodAssignment({ + required this.bodyLines, + required this.sensitivities, + required this.destinations, + }); +} + +class _ScMethodAssignmentGroup { + final List bodyLines = []; + final Set sensitivities = {}; + final Set destinations = {}; + + bool canAdd(_ScMethodAssignment assignment) => + !assignment.sensitivities.any(destinations.contains) && + !assignment.destinations.any(sensitivities.contains); + + void add(_ScMethodAssignment assignment) { + bodyLines.addAll(assignment.bodyLines); + sensitivities.addAll(assignment.sensitivities); + destinations.addAll(assignment.destinations); + } +} + +/// Collects clocked process data for consolidation by (clock, reset) pair. +class _ClockedGroupData { + final String? resetName; + bool isAsyncReset; + + /// All distinct trigger events (signal name, edge, and whether it's a port). + final List<({String signalName, bool isPosedge, bool isPort})> triggers = []; + + final List resetLines = []; + final List whileBodyLines = []; + _ClockedGroupData({this.resetName, this.isAsyncReset = false}); +} diff --git a/lib/src/synthesizers/systemverilog/systemverilog_conditional_emitter.dart b/lib/src/synthesizers/systemverilog/systemverilog_conditional_emitter.dart new file mode 100644 index 000000000..6c5444d46 --- /dev/null +++ b/lib/src/synthesizers/systemverilog/systemverilog_conditional_emitter.dart @@ -0,0 +1,112 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemverilog_conditional_emitter.dart +// SystemVerilog renderer for backend-neutral conditional emission plans. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/conditional_emission_plan.dart'; +import 'package:rohd/src/synthesizers/utilities/conditional_emitter.dart'; + +/// Emits SystemVerilog for [Conditional] trees. +class SystemVerilogConditionalEmitter extends ConditionalEmitter { + /// Input-port names resolved for this conditional block. + final Map inputsNameMap; + + /// Output-port names resolved for this conditional block. + final Map outputsNameMap; + + /// Assignment operator for this conditional block. + final String assignOperator; + + /// Creates a SystemVerilog conditional emitter. + const SystemVerilogConditionalEmitter({ + required this.inputsNameMap, + required this.outputsNameMap, + required this.assignOperator, + }); + + @override + String driverFor(Conditional source, Logic driver) => + inputsNameMap[source.emissionDriver(driver).name]!; + + @override + String receiverFor(Conditional source, Logic receiver) => + outputsNameMap[source.emissionReceiver(receiver).name]!; + + /// Emits a single assignment statement. + @override + String emitAssignment(int indent, String receiverName, String driverName) { + final padding = Conditional.calcPadding(indent); + return '$padding$receiverName $assignOperator $driverName;'; + } + + /// Emits an if/else-if/else chain. + @override + String emitIf(int indent, List branches) { + final padding = Conditional.calcPadding(indent); + final verilog = StringBuffer(); + for (final branch in branches) { + final header = branch == branches.first + ? 'if' + : branch.isElse + ? 'else' + : 'else if'; + final condition = branch.isElse ? '' : '(${branch.condition})'; + verilog.write(''' +$padding$header$condition begin +${branch.contents} +${padding}end '''); + } + verilog.write('\n'); + + return verilog.toString(); + } + + /// Emits a case statement. + @override + String emitCase( + ConditionalCaseEmissionPlan plan, + int indent, + String expressionName, + List items, + String? defaultContents, + ) { + var caseHeader = plan.caseBlock.emissionCaseType; + if (plan.caseBlock.conditionalType == ConditionalType.priority) { + caseHeader = 'priority $caseHeader'; + } else if (plan.caseBlock.conditionalType == ConditionalType.unique) { + caseHeader = 'unique $caseHeader'; + } + final padding = Conditional.calcPadding(indent); + final verilog = StringBuffer('$padding$caseHeader ($expressionName) \n'); + final subPadding = Conditional.calcPadding(indent + 2); + for (final item in items) { + verilog.write(''' +$subPadding${item.match} : begin +${item.contents} +${subPadding}end +'''); + } + if (defaultContents != null) { + verilog.write(''' +${subPadding}default : begin +$defaultContents +${subPadding}end +'''); + } + verilog.write('${padding}endcase\n'); + + return verilog.toString(); + } + + @override + int ifChildIndent(int indent) => indent + 2; + + @override + int caseChildIndent(ConditionalCaseEmissionPlan plan, int indent) => + indent + 4; +} diff --git a/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart b/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart new file mode 100644 index 000000000..1d9f08b35 --- /dev/null +++ b/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart @@ -0,0 +1,316 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemverilog_leaf_emitter.dart +// SystemVerilog renderer for semantic leaf expression plans. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// Emits inline SystemVerilog expressions for semantic leaf gates. +class SystemVerilogLeafEmitter implements InlineLeafEmitter { + static final RegExp _singleBitSelectRegex = + RegExp(r'^\(?([A-Za-z_][A-Za-z0-9_$]*(?:\[\d+\])*)\[(\d+)\]\)?$'); + static final RegExp _sliceSelectRegex = + RegExp(r'^\(?([A-Za-z_][A-Za-z0-9_$]*)\[(\d+):(\d+)\]\)?$'); + + /// Creates a leaf emitter. + const SystemVerilogLeafEmitter(); + + static ({String? target, int? index}) _singleBitSelect(String expression) { + final match = _singleBitSelectRegex.firstMatch(expression.trim()); + if (match == null) { + return (target: null, index: null); + } + + return (target: match.group(1), index: int.parse(match.group(2)!)); + } + + static String _bitSelect(String expression, int index) { + final match = _sliceSelectRegex.firstMatch(expression.trim()); + if (match == null) { + return '$expression[$index]'; + } + + final target = match.group(1)!; + final upper = int.parse(match.group(2)!); + final lower = int.parse(match.group(3)!); + final selectedIndex = upper >= lower ? lower + index : lower - index; + return '$target[$selectedIndex]'; + } + + static List<({String expression, int width})> _collapseBitSelects( + List<({bool canCollapse, String expression, int width})> operands, + ) { + final collapsed = <({String expression, int width})>[]; + + var index = 0; + while (index < operands.length) { + final first = operands[index]; + final firstSel = first.width == 1 && first.canCollapse + ? _singleBitSelect(first.expression) + : (target: null, index: null); + + if (firstSel.target == null || firstSel.index == null) { + collapsed.add((expression: first.expression, width: first.width)); + index++; + continue; + } + + var lastIndex = index; + var expectedBit = firstSel.index! - 1; + while (lastIndex + 1 < operands.length) { + final next = operands[lastIndex + 1]; + final nextSelect = next.width == 1 && next.canCollapse + ? _singleBitSelect(next.expression) + : (target: null, index: null); + if (nextSelect.target != firstSel.target || + nextSelect.index != expectedBit) { + break; + } + lastIndex++; + expectedBit--; + } + + if (lastIndex == index) { + collapsed.add(( + expression: '${firstSel.target}[${firstSel.index}]', + width: 1, + )); + } else { + final lowerSelect = _singleBitSelect(operands[lastIndex].expression); + collapsed.add(( + expression: + '${firstSel.target}[${firstSel.index}:${lowerSelect.index}]', + width: lastIndex - index + 1, + )); + } + + index = lastIndex + 1; + } + + return collapsed; + } + + @override + String expressionFor(InlineLeaf module, Map inputs) { + final plan = LeafExpressionPlan.fromInlineModule(module, inputs); + + final op = plan.operation; + final vals = plan.inputValues; + + if (op == LeafOperationKind.not && vals.length == 1) { + return '~${vals[0]}'; + } + + const binaryOps = { + LeafOperationKind.and: '&', + LeafOperationKind.or: '|', + LeafOperationKind.xor: '^', + LeafOperationKind.subtract: '-', + LeafOperationKind.multiply: '*', + LeafOperationKind.divide: '/', + LeafOperationKind.modulo: '%', + LeafOperationKind.equals: '==', + LeafOperationKind.notEquals: '!=', + LeafOperationKind.lessThan: '<', + LeafOperationKind.greaterThan: '>', + LeafOperationKind.lessThanOrEqual: '<=', + LeafOperationKind.greaterThanOrEqual: '>=', + LeafOperationKind.shiftLeft: '<<', + LeafOperationKind.shiftRight: '>>', + }; + final binaryOp = binaryOps[op]; + if (binaryOp != null && vals.length >= 2) { + return '${vals[0]} $binaryOp ${vals[1]}'; + } + + if (op == LeafOperationKind.arithmeticShiftRight && vals.length >= 2) { + return '{\$signed(${vals[0]}) >>> ${vals[1]}}'; + } + + if (op == LeafOperationKind.andUnary && vals.length == 1) { + return '&${vals[0]}'; + } + if (op == LeafOperationKind.orUnary && vals.length == 1) { + return '|${vals[0]}'; + } + if (op == LeafOperationKind.xorUnary && vals.length == 1) { + return '^${vals[0]}'; + } + + if (op == LeafOperationKind.mux && vals.length >= 3) { + return '${vals[0]} ? ${vals[2]} : ${vals[1]}'; + } + + if (op == LeafOperationKind.power && vals.length >= 2) { + final expr = '${vals[0]} ** ${vals[1]}'; + final selfDetermined = plan.meta('makeSelfDetermined') ?? true; + return selfDetermined ? '{$expr}' : expr; + } + + if (op == LeafOperationKind.busSubset && vals.length == 1) { + final inputWidth = plan.meta('inputWidth') ?? + plan.sourceModule.inputs.values.first.width; + final startIndex = plan.meta('startIndex'); + final endIndex = plan.meta('endIndex'); + if (startIndex == null || endIndex == null) { + throw SynthException( + 'SystemVerilog bus subset leaf requires startIndex and endIndex ' + 'metadata.', + ); + } + + final a = vals[0]; + if (inputWidth == 1) { + return a; + } + if (startIndex > endIndex) { + final swizzleContents = List.generate( + startIndex - endIndex + 1, + (i) => _bitSelect(a, endIndex + i), + ).join(','); + return '{$swizzleContents}'; + } + + final sliceString = + startIndex == endIndex ? '[$startIndex]' : '[$endIndex:$startIndex]'; + return '$a$sliceString'; + } + + if (op == LeafOperationKind.replication && vals.length == 1) { + final count = plan.meta('replicationCount') ?? + ((plan.meta('outputWidth') ?? 0) ~/ + (plan.meta('inputWidth') ?? 1)); + return '{$count{${vals[0]}}}'; + } + + if (op == LeafOperationKind.bitIndex && vals.length >= 2) { + final originalWidth = plan.meta('originalWidth') ?? + plan.sourceModule.inputs.values.first.width; + if (originalWidth == 1) { + return vals[0]; + } + return '${vals[0]}[${vals[1]}]'; + } + + if (op == LeafOperationKind.swizzle) { + final inputWidths = plan.meta>('inputWidths'); + final inputCount = plan.meta('inputCount'); + final inputIsArrayMember = + plan.meta>('inputIsArrayMember') ?? const []; + final inputHasUnpackedArraySource = + plan.meta>('inputHasUnpackedArraySource') ?? + const []; + if (inputWidths == null || inputCount == null) { + throw SynthException( + 'SystemVerilog swizzle leaf requires inputWidths and inputCount ' + 'metadata.', + ); + } + + if (inputCount == 0 && vals.isEmpty) { + return ''; + } + + if (vals.length != inputCount && vals.length != inputCount + 1) { + throw SynthException( + 'SystemVerilog swizzle leaf expected $inputCount inputs, but saw ' + '${vals.length}.', + ); + } + + final filtered = <({bool canCollapse, String expression, int width})>[]; + final inputExpressions = vals.take(inputCount).toList(); + for (var i = inputWidths.length - 1; i >= 0; i--) { + if (i >= inputExpressions.length) { + continue; + } + final width = inputWidths[i]; + if (width > 0) { + final isArrayMember = + i < inputIsArrayMember.length && inputIsArrayMember[i]; + final hasUnpackedArraySource = + i < inputHasUnpackedArraySource.length && + inputHasUnpackedArraySource[i]; + filtered.add(( + canCollapse: !isArrayMember && !hasUnpackedArraySource, + expression: inputExpressions[i].isEmpty + ? "$width'b${'z' * width}" + : inputExpressions[i], + width: width, + )); + } + } + + if (filtered.isEmpty) { + throw SynthException( + 'SystemVerilog swizzle leaf requires at least one non-zero-width ' + 'input.', + ); + } + + final operands = _collapseBitSelects(filtered); + if (operands.length == 1) { + return operands.single.expression; + } + + final outWidth = operands.fold(0, (sum, entry) => sum + entry.width); + final widthDescriptions = <({int upper, int? lower})>[]; + var upperIndex = outWidth - 1; + for (final entry in operands) { + if (entry.width > 1) { + final lowerIndex = upperIndex - entry.width + 1; + widthDescriptions.add((upper: upperIndex, lower: lowerIndex)); + } else { + widthDescriptions.add((upper: upperIndex, lower: null)); + } + upperIndex -= entry.width; + } + + var maxUpperWidth = 0; + var maxLowerWidth = 0; + for (final desc in widthDescriptions) { + final upperLen = desc.upper.toString().length; + if (upperLen > maxUpperWidth) { + maxUpperWidth = upperLen; + } + if (desc.lower != null) { + final lowerLen = desc.lower!.toString().length; + if (lowerLen > maxLowerWidth) { + maxLowerWidth = lowerLen; + } + } + } + + final inputLines = []; + var lineUpper = outWidth - 1; + for (var i = 0; i < operands.length; i++) { + final entry = operands[i]; + final desc = widthDescriptions[i]; + + final alignedDesc = desc.lower != null + ? '${desc.upper.toString().padLeft(maxUpperWidth)}:' + '${desc.lower!.toString().padLeft(maxLowerWidth)}' + : desc.upper.toString().padLeft( + maxUpperWidth + (maxLowerWidth > 0 ? 1 + maxLowerWidth : 0), + ); + + lineUpper -= entry.width; + final maybeComma = lineUpper >= 0 ? ',' : ' '; + inputLines.add('${entry.expression}$maybeComma /* $alignedDesc */'); + } + + return '{\n${inputLines.join('\n')}\n}'; + } + + throw SynthException( + 'SystemVerilog cannot emit semantic leaf operation for ' + '${module.runtimeType}. Provide LeafCellProvider metadata or a ' + 'backend-specific leaf emitter extension.', + ); + } +} diff --git a/lib/src/synthesizers/systemverilog/systemverilog_mixins.dart b/lib/src/synthesizers/systemverilog/systemverilog_mixins.dart index 20de51535..98675f583 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_mixins.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_mixins.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // systemverilog_mixins.dart @@ -28,9 +28,47 @@ class SystemVerilogParameterDefinition { {required this.type, required this.defaultValue}); } +BackendArtifact? _systemVerilogArtifactFor( + SystemVerilog module, BackendArtifactContext context) { + if (context.backend != EmissionBackend.systemVerilog) { + return null; + } + + switch (context.kind) { + case BackendArtifactKind.definition: + final contents = module.definitionVerilog(context.definitionType!); + return contents == null || contents.isEmpty + ? null + : BackendArtifact( + backend: context.backend, + kind: context.kind, + contents: contents, + ); + case BackendArtifactKind.instantiation: + final contents = module.instantiationVerilog( + context.instanceType!, + context.instanceName!, + context.ports, + ); + return contents == null + ? null + : BackendArtifact( + backend: context.backend, + kind: context.kind, + contents: contents, + ); + case BackendArtifactKind.simulationProcess: + return null; + } +} + /// Allows a [Module] to control the instantiation and/or definition of /// generated SystemVerilog for that module. -mixin SystemVerilog on Module { +mixin SystemVerilog on Module implements BackendArtifactProvider { + @override + BackendArtifact? artifactFor(BackendArtifactContext context) => + _systemVerilogArtifactFor(this, context); + /// Generates custom SystemVerilog to be injected in place of a `module` /// instantiation. /// @@ -123,7 +161,11 @@ enum DefinitionGenerationType { /// /// The inline SystemVerilog will get parentheses wrapped around it and then /// dropped into other code in the same way a variable name is. -mixin InlineSystemVerilog on Module implements SystemVerilog { +mixin InlineSystemVerilog on Module implements SystemVerilog, InlineLeaf { + @override + BackendArtifact? artifactFor(BackendArtifactContext context) => + _systemVerilogArtifactFor(this, context); + /// Generates custom SystemVerilog to be injected in place of the output /// port's corresponding signal name. /// @@ -142,6 +184,7 @@ mixin InlineSystemVerilog on Module implements SystemVerilog { /// /// By default, this assumes one [output] port. This should be overridden in /// classes which have an [inOut] port as the in-lined symbol. + @override String get resultSignalName { if (outputs.keys.length != 1) { throw Exception('Inline verilog expected to have exactly one output,' @@ -170,6 +213,7 @@ mixin InlineSystemVerilog on Module implements SystemVerilog { return 'assign $result = $inline; // $instanceName'; } + @override @override @protected final List expressionlessInputs = const []; @@ -186,6 +230,7 @@ mixin InlineSystemVerilog on Module implements SystemVerilog { @internal @override + @override bool get isWiresOnly => false; } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_process_emitter.dart b/lib/src/synthesizers/systemverilog/systemverilog_process_emitter.dart new file mode 100644 index 000000000..088673fdc --- /dev/null +++ b/lib/src/synthesizers/systemverilog/systemverilog_process_emitter.dart @@ -0,0 +1,66 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemverilog_process_emitter.dart +// SystemVerilog renderer for backend-neutral process emission plans. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_conditional_emitter.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// Emits SystemVerilog procedural blocks from [ProcessEmissionPlan]s. +class SystemVerilogProcessEmitter { + /// Creates a SystemVerilog process emitter. + const SystemVerilogProcessEmitter(); + + /// Emits [plan] using resolved module [ports]. + String emit( + ProcessEmissionPlan plan, + String instanceName, + Map ports, + ) { + final inputs = Map.fromEntries( + ports.entries.where((entry) => plan.source.inputs.containsKey(entry.key)), + ); + final outputs = Map.fromEntries( + ports.entries + .where((entry) => plan.source.outputs.containsKey(entry.key)), + ); + final conditionalEmitter = SystemVerilogConditionalEmitter( + inputsNameMap: inputs, + outputsNameMap: outputs, + assignOperator: + plan.assignmentKind == ProcessAssignmentKind.blocking ? '=' : '<=', + ); + + final contents = StringBuffer(); + for (final statement in plan.body) { + contents + ..write(conditionalEmitter.emitPlan(statement, 1)) + ..write('\n'); + } + + return ''' +// $instanceName +${_header(plan, inputs)} begin +${contents}end +'''; + } + + String _header(ProcessEmissionPlan plan, Map inputs) { + switch (plan.kind) { + case ProcessEmissionKind.combinational: + return 'always_comb'; + case ProcessEmissionKind.clocked: + final triggers = plan.triggers + .map( + (trigger) => '${trigger.isPosedge ? 'posedge' : 'negedge'} ' + '${inputs[trigger.signal.name]}', + ) + .join(' or '); + return 'always_ff @($triggers)'; + } + } +} diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 18ff4caed..7fa8bf473 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -13,8 +13,14 @@ import 'package:rohd/src/synthesizers/utilities/utilities.dart'; /// A special [SynthModuleDefinition] for SystemVerilog modules. class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { + /// Configuration controlling generated SystemVerilog. + final SystemVerilogSynthesizerConfiguration configuration; + /// Creates a new [SystemVerilogSynthModuleDefinition] for the given [module]. - SystemVerilogSynthModuleDefinition(super.module); + SystemVerilogSynthModuleDefinition( + super.module, { + this.configuration = const SystemVerilogSynthesizerConfiguration(), + }); /// A shared mapping from [SynthLogic]s which are the result of an inlineable /// submodule to the instantiation that produces them. @@ -34,6 +40,56 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { _replaceNetConnections(); _collapseMarkedChainableModules(); _replaceInOutConnectionInlineableModules(); + _removeUnusedConstantIntermediates(); + } + + void _removeUnusedConstantIntermediates() { + final usedSignals = {}; + for (final instantiation in subModuleInstantiations) { + if (instantiation.needsInstantiation) { + usedSignals + ..addAll(instantiation.inputMapping.values.map((e) => e.resolved)) + ..addAll(instantiation.outputMapping.values.map((e) => e.resolved)) + ..addAll(instantiation.inOutMapping.values.map((e) => e.resolved)); + } else if (instantiation.module case final InlineLeaf inlineLeaf) { + usedSignals.addAll( + instantiation.inOutMapping.values.map((e) => e.resolved), + ); + for (final inputName in inlineLeaf.expressionlessInputs) { + final mapped = instantiation.inputMapping[inputName] ?? + instantiation.inOutMapping[inputName]; + if (mapped != null) { + usedSignals.add(mapped.resolved); + } + } + } + } + for (final assignment in assignments) { + usedSignals.add(assignment.src.resolved); + } + usedSignals + ..addAll(inputs.map((e) => e.resolved)) + ..addAll(outputs.map((e) => e.resolved)) + ..addAll(inOuts.map((e) => e.resolved)); + + final removedSignals = {}; + assignments.removeWhere((assignment) { + final destination = assignment.dst.resolved; + final remove = assignment.src.resolved.isConstant && + !usedSignals.contains(destination) && + destination.isClearable && + !destination.isPort(module) && + internalSignals.contains(destination); + if (remove) { + removedSignals.add(destination); + } + return remove; + }); + + for (final signal in removedSignals) { + signal.clearDeclaration(); + internalSignals.remove(signal); + } } /// Inlines a fully covered packed bus into its sole submodule input. @@ -57,7 +113,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { for (final instantiation in subModuleInstantiations) { instantiation as SystemVerilogSynthSubModuleInstantiation; for (final entry in instantiation.inputMapping.entries) { - if (instantiation.module is! InlineSystemVerilog) { + if (instantiation.module is! InlineLeaf) { allMappedSignals.add(entry.value.resolved); } inputUses.putIfAbsent(entry.value.resolved, () => []).add(( @@ -69,7 +125,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { ...instantiation.outputMapping.values, ...instantiation.inOutMapping.values, ]) { - if (instantiation.module is! InlineSystemVerilog) { + if (instantiation.module is! InlineLeaf) { allMappedSignals.add(signal.resolved); outputOrInOutMappedSignals.add(signal.resolved); } @@ -103,7 +159,11 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { outputOrInOutMappedSignals.contains(bus) || !bus.isClearable || !internalSignals.contains(bus) || - use.instantiation.module is InlineSystemVerilog || + use.instantiation.module is InlineLeaf || + (use.instantiation.module is InlineLeaf && + (use.instantiation.module as InlineLeaf) + .expressionlessInputs + .contains(use.portName)) || (use.instantiation.module is SystemVerilog && (use.instantiation.module as SystemVerilog) .expressionlessInputs @@ -169,7 +229,9 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { continue; } - _addSwizzleConnect(bus, sources); + if (!_addSwizzleConnect(bus, sources)) { + continue; + } removedAssignments ..addAll(drivers) ..addAll(constantDrivers); @@ -294,6 +356,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { // along with where (a submodule port mapping is the only use we can // currently inline into). final aggregateUseCount = {}; + final mappedSignals = {}; final aggregatePortUse = >{}; final assignmentsBySignal = _assignmentsBySignal(); + final assignmentsByDestination = >{}; + final assignmentsBySource = >{}; + for (final assignment in assignments) { + assignmentsByDestination + .putIfAbsent(assignment.dst.resolved, () => []) + .add(assignment); + assignmentsBySource + .putIfAbsent(assignment.src.resolved, () => []) + .add(assignment); + } final elementUseCount = {}; void noteElementUse(SynthLogic? synthLogic) { if (synthLogic is SynthLogicArrayElement) { @@ -369,6 +447,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } final removedAssignments = {}; + final removedSignals = {}; for (final aggEntry in aggregatePortUse.entries) { final agg = aggEntry.key; final use = aggEntry.value; @@ -394,6 +473,10 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { // The consuming port must accept expressions. final consumer = use.instantiation.module; + if (consumer is InlineLeaf && + consumer.expressionlessInputs.contains(use.portName)) { + continue; + } if (consumer is SystemVerilog && consumer.expressionlessInputs.contains(use.portName)) { continue; @@ -439,6 +522,10 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { final elementSources = []; var allElementsSingleSourced = true; + final aggregateElementAssignments = { + for (final element in elementLogics.nonNulls) + ...elementAssignments[element] ?? const [], + }; // Net [BusSubset]s consumed while tracing element sources through // pass-through buses; their instantiations are cleared and the buses @@ -470,7 +557,52 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { allElementsSingleSourced = false; break; } - elementSources.add(source); + var elementSource = source; + final sourceDrivers = + assignmentsByDestination[source] ?? const []; + final sourceConsumers = + assignmentsBySource[source] ?? const []; + final removableConstantIntermediate = sourceDrivers.length == 1 && + sourceConsumers.isNotEmpty && + sourceConsumers.every(aggregateElementAssignments.contains) && + sourceDrivers.single is! PartialSynthAssignment && + sourceDrivers.single.src.resolved.isConstant && + !source.hasPreservedName && + !mappedSignals.contains(source) && + internalSignals.contains(source); + if (removableConstantIntermediate) { + removedAssignments.add(sourceDrivers.single); + removedSignals.add(source); + elementSource = sourceDrivers.single.src.resolved; + } else if (source is SynthLogicArrayElement) { + final parentArray = source.parentArray.resolved; + final parentDrivers = assignmentsByDestination[parentArray] ?? + const []; + final parentElementAssignments = { + for (final parentElement in parentArray.logics + .whereType() + .expand((logicArray) => logicArray.elements) + .map(getSynthLogic) + .nonNulls + .map((e) => e.resolved)) + ...assignmentsBySignal[parentElement] ?? + const [], + }; + final removableConstantArray = parentDrivers.length == 1 && + parentElementAssignments.isNotEmpty && + parentElementAssignments + .every(aggregateElementAssignments.contains) && + parentDrivers.single is! PartialSynthAssignment && + parentDrivers.single.src.resolved.isConstant && + !parentArray.hasPreservedName && + !mappedSignals.contains(parentArray) && + internalSignals.contains(parentArray); + if (removableConstantArray) { + removedAssignments.add(parentDrivers.single); + removedSignals.add(parentArray); + } + } + elementSources.add(elementSource); continue; } @@ -531,7 +663,9 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { // real hardware) whose concatenation reproduces the aggregate, and // register it so the single aggregate use renders as the inline // concatenation. - _addSwizzleConnect(agg, elementSources); + if (!_addSwizzleConnect(agg, elementSources)) { + continue; + } // Remove the now-inlined element assignments and clear the aggregate // declaration. @@ -556,6 +690,10 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { changed = true; } assignments.removeWhere(removedAssignments.contains); + for (final signal in removedSignals) { + signal.clearDeclaration(); + internalSignals.remove(signal); + } } } @@ -874,6 +1012,10 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { // The consuming port must accept expressions. final consumer = use.instantiation.module; + if (consumer is InlineLeaf && + consumer.expressionlessInputs.contains(use.portName)) { + continue; + } if (consumer is SystemVerilog && consumer.expressionlessInputs.contains(use.portName)) { continue; @@ -942,7 +1084,9 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { // Inline the bus as a concatenation of its per-bit nets and drop the // bus plus its definer [BusSubset]s. - _addSwizzleConnect(bus, elementSources); + if (!_addSwizzleConnect(bus, elementSources)) { + continue; + } for (final view in definers) { view.inst.clearInstantiation(); @@ -1106,7 +1250,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } for (final inst in activeInstantiations) { - final isInlineable = inst.module is InlineSystemVerilog; + final isInlineable = inst.module is InlineLeaf; final resultLogic = isInlineable ? inst.inlineResultLogic?.resolved : null; @@ -1249,8 +1393,11 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { /// Fabricates a [_SwizzleConnect] that represents [agg] as the inline /// concatenation of [elementSources] (ordered with index 0 as the LSB), and /// registers it in [_inlineableSubmoduleMap]. - void _addSwizzleConnect(SynthLogic agg, List elementSources) { + bool _addSwizzleConnect(SynthLogic agg, List elementSources) { final isNet = agg.isNet; + if (isNet && elementSources.any((source) => source.declarationCleared)) { + return false; + } // The [Swizzle] concatenates its `signals` with `signals[0]` as the MSB, // i.e. `out = {signals[0], signals[1], ..., signals[last]}`. Our @@ -1292,6 +1439,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { supportingModules.add(swizzle); _inlineableSubmoduleMap[agg] = swizzleInst; + return true; } /// Collapses chainable, inlineable modules after naming. @@ -1386,13 +1534,13 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } } - /// Finds all [InlineSystemVerilog] modules where all ports are [LogicNet]s + /// Finds all [InlineLeaf] modules where all ports are [LogicNet]s /// and which have not had their declarations cleared and replaces them with a /// [_NetConnect] assignment instead of a normal assignment. void _replaceInOutConnectionInlineableModules() { for (final subModuleInstantiation in subModuleInstantiations.toList().where( (e) => - e.module is InlineSystemVerilog && + e.module is InlineLeaf && e.needsInstantiation && e.outputMapping.isEmpty && e.inOutMapping.isNotEmpty, @@ -1404,13 +1552,48 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { subModuleInstantiation as SystemVerilogSynthSubModuleInstantiation; - subModuleInstantiation.clearInstantiation(); - - final resultName = (subModuleInstantiation.module as InlineSystemVerilog) - .resultSignalName; + final resultName = + (subModuleInstantiation.module as InlineLeaf).resultSignalName; final subModResult = subModuleInstantiation.inOutMapping[resultName]!; + if (subModuleInstantiation.module is Swizzle && + subModuleInstantiation.inOutMapping.entries.any((entry) => + entry.key != resultName && entry.value.declarationCleared)) { + final portNameToValueMapping = + subModuleInstantiation.modulePortsMapWithInline( + {...subModuleInstantiation.inOutMapping}..remove( + (subModuleInstantiation.module as InlineLeaf).resultSignalName, + ), + subModuleInstantiation.synthLogicToInlineableSynthSubmoduleMap, + (submodule) => submodule.inlineVerilog(), + ); + + var offset = 0; + final sortedInputs = subModuleInstantiation.inOutMapping.entries + .where((entry) => entry.key != resultName) + .toList() + ..sort((a, b) => _swizzlePortIndex(a.key).compareTo( + _swizzlePortIndex(b.key), + )); + for (final entry in sortedInputs) { + final source = portNameToValueMapping[entry.key]; + final width = entry.value.width; + if (source != null && source.isNotEmpty) { + final destination = width == subModResult.width + ? subModResult.name + : '${subModResult.name}[${offset + width - 1}:$offset]'; + _addRawNetConnect(destination, source, width); + } + offset += width; + } + + subModuleInstantiation.clearInstantiation(); + continue; + } + + subModuleInstantiation.clearInstantiation(); + // use a dummy as a placeholder, it will not really be used since we are // updating the inlineable map final dummy = SynthLogic( @@ -1427,6 +1610,17 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { subModuleInstantiation; } } + + static int _swizzlePortIndex(String portName) => + int.parse(portName.replaceFirst(RegExp('^_?in'), '')); + + void _addRawNetConnect(String dst, String src, int width) { + final netConnect = _RawNetConnect(dst, src, width); + final inst = getSynthSubModuleInstantiation(netConnect) + as SystemVerilogSynthSubModuleInstantiation; + supportingModules.add(netConnect); + inst.pickName(module); + } } /// A resolved view of a net [BusSubset] instantiation: the resolved `original` @@ -1505,6 +1699,44 @@ inout wire[WIDTH-1:0] w; endmodule'''; } +class _RawNetConnect extends Module with SystemVerilog { + final String dst; + final String src; + final int width; + + @override + bool get hasBuilt => true; + + _RawNetConnect(this.dst, this.src, this.width) + : super( + definitionName: _NetConnect._definitionName, + name: _NetConnect._definitionName, + ); + + @override + String instantiationVerilog( + String instanceType, + String instanceName, + Map ports, + ) { + assert( + instanceType == _NetConnect._definitionName, + 'Instance type selected should match the definition name.', + ); + return '$instanceType' + ' #(.WIDTH($width))' + ' $instanceName' + ' ($dst, $src);'; + } + + @override + String? definitionVerilog(String definitionType) => ''' +// A special module for connecting two nets bidirectionally +module $definitionType #(parameter int WIDTH=1) (w, w); +inout wire[WIDTH-1:0] w; +endmodule'''; +} + /// A [Swizzle] fabricated post-build purely as instrumentation for /// SystemVerilog generation; it is never connected to the real hardware /// hierarchy or used in simulation. diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart index eb1a0cd45..7cb5a49ec 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // systemverilog_synth_sub_module_instantiation.dart @@ -9,17 +9,13 @@ import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; /// Represents a submodule instantiation for SystemVerilog. class SystemVerilogSynthSubModuleInstantiation extends SynthSubModuleInstantiation { - /// If [module] is [InlineSystemVerilog], this will be the [SynthLogic] that - /// is the `result` of that module. Otherwise, `null`. - SynthLogic? get inlineResultLogic => module is! InlineSystemVerilog - ? null - : (outputMapping[(module as InlineSystemVerilog).resultSignalName] ?? - inOutMapping[(module as InlineSystemVerilog).resultSignalName]); + static const _leafEmitter = SystemVerilogLeafEmitter(); /// Creates a new [SystemVerilogSynthSubModuleInstantiation] for the given /// [module]. @@ -30,37 +26,31 @@ class SystemVerilogSynthSubModuleInstantiation Map? synthLogicToInlineableSynthSubmoduleMap; - /// Provides a mapping from ports of this module to a string that can be fed - /// into that port, which may include inline SV modules as well. - Map _modulePortsMapWithInline( - Map plainPorts) => - plainPorts.map((name, synthLogic) => MapEntry( - name, - synthLogicToInlineableSynthSubmoduleMap?[synthLogic] - ?.inlineVerilog() ?? - // if cleared, then empty port - (synthLogic.declarationCleared ? '' : synthLogic.name))); - /// Provides the inline SV representation for this module. /// - /// Should only be called if [module] is [InlineSystemVerilog]. + /// Should only be called if [module] is [InlineLeaf]. String inlineVerilog() { - final portNameToValueMapping = _modulePortsMapWithInline( + final portNameToValueMapping = modulePortsMapWithInline( {...inputMapping, ...inOutMapping} - ..remove((module as InlineSystemVerilog).resultSignalName), + ..remove((module as InlineLeaf).resultSignalName), + synthLogicToInlineableSynthSubmoduleMap, + (submodule) => submodule.inlineVerilog(), ); assert( (module is SystemVerilog && (module as SystemVerilog).acceptsEmptyPortConnections) || + module is Swizzle || portNameToValueMapping.values.none((e) => e.isEmpty), 'Inline modules should not ever receive empty port values,' ' only module instantiations can get something like `.port_name()`.'); - final inlineSvRepresentation = - (module as InlineSystemVerilog).inlineVerilog(portNameToValueMapping); + final inlineSvRepresentation = _leafEmitter.expressionFor( + module as InlineLeaf, + portNameToValueMapping, + ); - return '($inlineSvRepresentation)'; + return inlineSvRepresentation.isEmpty ? '' : '($inlineSvRepresentation)'; } /// Provides the full SV instantiation for this module. @@ -68,14 +58,84 @@ class SystemVerilogSynthSubModuleInstantiation if (!needsInstantiation) { return null; } + final ports = modulePortsMapWithInline({ + ...inputMapping, + ...outputMapping, + ...inOutMapping, + }, synthLogicToInlineableSynthSubmoduleMap, + (submodule) => submodule.inlineVerilog()); + + for (final entry in inOutMapping.entries) { + final portValue = ports[entry.key]; + final inlineSubModule = + synthLogicToInlineableSynthSubmoduleMap?[entry.value] ?? + synthLogicToInlineableSynthSubmoduleMap?[entry.value.resolved]; + final aggregateLvalue = inlineSubModule == null + ? null + : _declaredSwizzleAggregateReference(inlineSubModule); + if (portValue != null && + portValue.contains("'bz") && + inlineSubModule?.module is Swizzle && + aggregateLvalue != null) { + ports[entry.key] = aggregateLvalue; + } + } + + if (module is InlineLeaf) { + final resultName = (module as InlineLeaf).resultSignalName; + final resultLogic = inlineResultLogic; + if (resultLogic == null || !resultLogic.hasName) { + return null; + } + ports[resultName] = resultLogic.name; + } return SystemVerilogSynthesizer.instantiationVerilogFor( module: module, instanceType: instanceType, instanceName: name, - ports: _modulePortsMapWithInline({ - ...inputMapping, - ...outputMapping, - ...inOutMapping, - })); + ports: ports); + } + + String? _declaredSwizzleAggregateReference( + SystemVerilogSynthSubModuleInstantiation swizzle, + ) { + final result = swizzle.inlineResultLogic; + if (result == null) { + return null; + } + + final mappedInputs = [ + ...swizzle.inputMapping.values, + ...swizzle.inOutMapping.entries + .where( + (entry) => + entry.key != (swizzle.module as InlineLeaf).resultSignalName, + ) + .map((entry) => entry.value), + ]; + final parentArrays = {}; + for (final input in mappedInputs) { + final element = input is SynthLogicArrayElement + ? input + : input.resolved is SynthLogicArrayElement + ? input.resolved as SynthLogicArrayElement + : null; + if (element == null) { + return null; + } + parentArrays.add(element.parentArray.resolved); + } + final parentArray = parentArrays.singleOrNull; + if (parentArray == null || + parentArray.width != result.width || + !parentArray.needsDeclaration || + !parentArray.parentSynthModuleDefinition.internalSignals + .contains(parentArray)) { + return null; + } + + return parentArray.width > 1 + ? '(${parentArray.name}[${parentArray.width - 1}:0])' + : parentArray.name; } } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart index b86fc4d34..219f6c9b7 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart @@ -22,6 +22,15 @@ extension on SynthLogic { /// A [SynthesisResult] representing a [Module] that provides a custom /// SystemVerilog definition. class SystemVerilogCustomDefinitionSynthesisResult extends SynthesisResult { + /// Returns the custom definition artifact for [definitionType]. + BackendArtifact _definitionArtifactFor(String definitionType) => + (module as BackendArtifactProvider).artifactFor( + BackendArtifactContext.definition( + backend: EmissionBackend.systemVerilog, + definitionType: definitionType, + ), + )!; + /// Creates a new [SystemVerilogCustomDefinitionSynthesisResult] for the given /// [module]. SystemVerilogCustomDefinitionSynthesisResult( @@ -34,24 +43,24 @@ class SystemVerilogCustomDefinitionSynthesisResult extends SynthesisResult { @override int get matchHashCode => - (module as SystemVerilog).definitionVerilog('*PLACEHOLDER*')!.hashCode; + _definitionArtifactFor('*PLACEHOLDER*').contents.hashCode; @override bool matchesImplementation(SynthesisResult other) => other is SystemVerilogCustomDefinitionSynthesisResult && - (module as SystemVerilog).definitionVerilog('*PLACEHOLDER*')! == - (other.module as SystemVerilog).definitionVerilog('*PLACEHOLDER*')!; + _definitionArtifactFor('*PLACEHOLDER*').contents == + other._definitionArtifactFor('*PLACEHOLDER*').contents; @override - String toFileContents() => (module as SystemVerilog) - .definitionVerilog(getInstanceTypeOfModule(module))!; + String toFileContents() => + _definitionArtifactFor(getInstanceTypeOfModule(module)).contents; @override List toSynthFileContents() => List.unmodifiable([ SynthFileContents( name: instanceTypeName, - contents: (module as SystemVerilog) - .definitionVerilog(getInstanceTypeOfModule(module))!) + contents: _definitionArtifactFor(getInstanceTypeOfModule(module)) + .contents) ]); } @@ -73,6 +82,9 @@ class SystemVerilogSynthesisResult extends SynthesisResult { /// The main [SynthModuleDefinition] for this. final SynthModuleDefinition _synthModuleDefinition; + /// Backend-neutral resolved structure used by this renderer. + late final ModuleEmissionPlan _emissionPlan; + @override List get supportingModules => _synthModuleDefinition.supportingModules; @@ -82,7 +94,11 @@ class SystemVerilogSynthesisResult extends SynthesisResult { super.module, super.getInstanceTypeOfModule, { this.configuration = const SystemVerilogSynthesizerConfiguration(), - }) : _synthModuleDefinition = SystemVerilogSynthModuleDefinition(module) { + }) : _synthModuleDefinition = SystemVerilogSynthModuleDefinition( + module, + configuration: configuration, + ) { + _emissionPlan = ModuleEmissionPlan.fromDefinition(_synthModuleDefinition); _portsString = _verilogPorts(); _moduleContentsString = _verilogModuleContents(getInstanceTypeOfModule); _parameterString = _verilogParameters(module); @@ -114,22 +130,21 @@ class SystemVerilogSynthesisResult extends SynthesisResult { ]); /// Representation of all input port declarations in generated SV. - Iterable _verilogInputs() => _synthModuleDefinition.inputs.map((sig) { + Iterable _verilogInputs() => _emissionPlan.inputs.map((sig) { assert(module.tryInput(sig.name) != null, 'Named input ${sig.name} not found in module ${module.name}.'); return _verilogPort('input', 'wire', sig); }); /// Representation of all output port declarations in generated SV. - Iterable _verilogOutputs() => - _synthModuleDefinition.outputs.map((sig) { + Iterable _verilogOutputs() => _emissionPlan.outputs.map((sig) { assert(module.tryOutput(sig.name) != null, 'Named output ${sig.name} not found in module ${module.name}.'); return _verilogPort('output', 'var', sig); }); /// Representation of all inout port declarations in generated SV. - Iterable _verilogInOuts() => _synthModuleDefinition.inOuts.map((sig) { + Iterable _verilogInOuts() => _emissionPlan.inOuts.map((sig) { assert(module.tryInOut(sig.name) != null, 'Named inOut ${sig.name} not found in module ${module.name}.'); return _verilogPort('inout', 'wire', sig); @@ -146,10 +161,10 @@ class SystemVerilogSynthesisResult extends SynthesisResult { ].join(' '); /// Representation of all internal net declarations in generated SV. - String _verilogInternalSignals() { + String _verilogInternalSignals({Set excludedSignals = const {}}) { final declarations = []; - for (final sig in _synthModuleDefinition.internalSignals - .where((e) => e.needsDeclaration) + for (final sig in _emissionPlan.internalSignals + .where((e) => e.needsDeclaration && !excludedSignals.contains(e)) .sorted((a, b) => a.name.compareTo(b.name))) { declarations.add('${sig.definitionType()} ${sig.definitionName()};'); } @@ -157,14 +172,20 @@ class SystemVerilogSynthesisResult extends SynthesisResult { } /// Representation of all assignments in generated SV. - String _verilogAssignments() { + String _verilogAssignments({Set excludedSignals = const {}}) { final assignmentLines = []; String rangeString(int upperIndex, int lowerIndex) => upperIndex == lowerIndex ? '[$upperIndex]' : '[$upperIndex:$lowerIndex]'; - for (final assignment in _synthModuleDefinition.assignments) { + for (final assignment in _emissionPlan.assignments) { + if (assignment.src.declarationCleared || + assignment.dst.declarationCleared || + excludedSignals.contains(assignment.dst.resolved)) { + continue; + } + assert( !(assignment.src.isNet && assignment.dst.isNet), 'Net connections should have been implemented as' @@ -198,8 +219,7 @@ class SystemVerilogSynthesisResult extends SynthesisResult { String _verilogSubModuleInstantiations( String Function(Module module) getInstanceTypeOfModule) { final subModuleLines = []; - for (final subModuleInstantiation - in _synthModuleDefinition.subModuleInstantiations) { + for (final subModuleInstantiation in _emissionPlan.instances) { final instanceType = getInstanceTypeOfModule(subModuleInstantiation.module); @@ -217,12 +237,45 @@ class SystemVerilogSynthesisResult extends SynthesisResult { /// The contents of this module converted to SystemVerilog without module /// declaration, ports, etc. String _verilogModuleContents( - String Function(Module module) getInstanceTypeOfModule) => - [ - _verilogInternalSignals(), - _verilogAssignments(), // order matters! - _verilogSubModuleInstantiations(getInstanceTypeOfModule), - ].where((element) => element.isNotEmpty).join('\n'); + String Function(Module module) getInstanceTypeOfModule, + ) { + final subModuleInstantiations = + _verilogSubModuleInstantiations(getInstanceTypeOfModule); + final unusedConstantIntermediates = + _unusedConstantIntermediates(subModuleInstantiations); + + return [ + _verilogInternalSignals(excludedSignals: unusedConstantIntermediates), + _verilogAssignments(excludedSignals: unusedConstantIntermediates), + subModuleInstantiations, + ].where((element) => element.isNotEmpty).join('\n'); + } + + Set _unusedConstantIntermediates(String emittedInstances) { + final assignmentSources = {}; + for (final assignment in _emissionPlan.assignments) { + assignmentSources.add(assignment.src.resolved); + } + + return { + for (final assignment in _emissionPlan.assignments) + if (assignment.src.resolved.isConstant && + assignment.dst.resolved is! SynthLogicArrayElement && + _emissionPlan.internalSignals.contains(assignment.dst.resolved) && + !assignment.dst.resolved.isPort(module) && + !assignmentSources.contains(assignment.dst.resolved) && + !_emittedTextReferences( + emittedInstances, + assignment.dst.resolved.name, + )) + assignment.dst.resolved, + }; + } + + bool _emittedTextReferences(String text, String signalName) => RegExp( + '(^|[^A-Za-z0-9_])${RegExp.escape(signalName)}(?=[^A-Za-z0-9_]|\$)', + multiLine: true, + ).hasMatch(text); /// The representation of all port declarations. String _verilogPorts() => [ diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index 5aea3cf58..203b95158 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart @@ -8,6 +8,7 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart'; import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart'; /// A [Synthesizer] which generates equivalent SystemVerilog as the @@ -24,11 +25,15 @@ class SystemVerilogSynthesizer extends Synthesizer { }); @override - bool generatesDefinition(Module module) => - // ignore: deprecated_member_use_from_same_package - !((module is CustomSystemVerilog) || - (module is SystemVerilog && - module.generatedDefinitionType == DefinitionGenerationType.none)); + bool generatesDefinition(Module module) { + final generatesNoDefinition = module is InlineLeaf || + // ignore: deprecated_member_use_from_same_package + module is CustomSystemVerilog || + (module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none); + + return !generatesNoDefinition; + } /// Creates a line of SystemVerilog that instantiates [module]. /// @@ -57,18 +62,50 @@ class SystemVerilogSynthesizer extends Synthesizer { Map? parameters, bool forceStandardInstantiation = false}) { if (!forceStandardInstantiation) { - if (module is SystemVerilog) { - return module.instantiationVerilog( - instanceType, - instanceName, - ports, - ) ?? - instantiationVerilogFor( - module: module, - instanceType: instanceType, - instanceName: instanceName, - ports: ports, - forceStandardInstantiation: true); + if (module is InlineLeaf) { + const leafEmitter = SystemVerilogLeafEmitter(); + final result = ports[module.resultSignalName]; + if (result == null) { + throw SynthException( + 'Inline leaf ${module.runtimeType} has no mapped result port ' + '${module.resultSignalName}.', + ); + } + + final inputPorts = Map.fromEntries( + ports.entries.where( + (element) => + module.inputs.containsKey(element.key) || + (module.inOuts.containsKey(element.key) && + element.key != module.resultSignalName), + ), + ); + final inline = leafEmitter.expressionFor(module, inputPorts); + return 'assign $result = $inline; // $instanceName'; + } + + if (module is BackendArtifactProvider) { + final artifactProvider = module as BackendArtifactProvider; + final artifact = artifactProvider.artifactFor( + BackendArtifactContext.instantiation( + backend: EmissionBackend.systemVerilog, + instanceType: instanceType, + instanceName: instanceName, + ports: ports, + ), + ); + if (artifact != null) { + return artifact.contents; + } + if (module is SystemVerilog) { + return instantiationVerilogFor( + module: module, + instanceType: instanceType, + instanceName: instanceName, + ports: ports, + forceStandardInstantiation: true, + ); + } } // ignore: deprecated_member_use_from_same_package else if (module is CustomSystemVerilog) { diff --git a/lib/src/synthesizers/utilities/backend_artifact.dart b/lib/src/synthesizers/utilities/backend_artifact.dart new file mode 100644 index 000000000..a5ad5706c --- /dev/null +++ b/lib/src/synthesizers/utilities/backend_artifact.dart @@ -0,0 +1,109 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// backend_artifact.dart +// Backend-specific artifact contracts for language emission escape hatches. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +/// A code-generation backend that can provide a language-specific artifact. +enum EmissionBackend { + /// SystemVerilog output. + systemVerilog, + + /// SystemC/C++ output. + systemC, +} + +/// The emission location for a backend-specific artifact. +enum BackendArtifactKind { + /// A complete module definition supplied by a backend-specific module. + definition, + + /// Source injected in place of a standard module instantiation. + instantiation, + + /// A simulation-only process, such as a timed clock source. + simulationProcess, +} + +/// Context supplied when resolving a [BackendArtifact]. +class BackendArtifactContext { + /// Requested backend. + final EmissionBackend backend; + + /// Requested artifact location. + final BackendArtifactKind kind; + + /// Definition type for a definition artifact. + final String? definitionType; + + /// Instance type for an instantiation artifact. + final String? instanceType; + + /// Instance name for an instantiation artifact. + final String? instanceName; + + /// Resolved port expressions for an instantiation artifact. + final Map ports; + + /// Creates a backend artifact context. + const BackendArtifactContext({ + required this.backend, + required this.kind, + this.definitionType, + this.instanceType, + this.instanceName, + this.ports = const {}, + }); + + /// Creates a definition artifact context. + const BackendArtifactContext.definition({ + required EmissionBackend backend, + required String definitionType, + }) : this( + backend: backend, + kind: BackendArtifactKind.definition, + definitionType: definitionType, + ); + + /// Creates an instantiation artifact context. + const BackendArtifactContext.instantiation({ + required EmissionBackend backend, + required String instanceType, + required String instanceName, + required Map ports, + }) : this( + backend: backend, + kind: BackendArtifactKind.instantiation, + instanceType: instanceType, + instanceName: instanceName, + ports: ports, + ); +} + +/// Backend-specific source emitted for one [BackendArtifactContext]. +class BackendArtifact { + /// The backend this artifact targets. + final EmissionBackend backend; + + /// The location where this artifact is emitted. + final BackendArtifactKind kind; + + /// Complete backend source for this artifact. + final String contents; + + /// Creates a backend-specific source artifact. + const BackendArtifact({ + required this.backend, + required this.kind, + required this.contents, + }); +} + +/// Optional mixin for modules with backend-specific emission artifacts. +mixin BackendArtifactProvider { + /// Returns a backend-specific artifact for [context], if one is available. + BackendArtifact? artifactFor(BackendArtifactContext context) => null; +} diff --git a/lib/src/synthesizers/utilities/conditional_emission_plan.dart b/lib/src/synthesizers/utilities/conditional_emission_plan.dart new file mode 100644 index 000000000..92de9a7bc --- /dev/null +++ b/lib/src/synthesizers/utilities/conditional_emission_plan.dart @@ -0,0 +1,156 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// conditional_emission_plan.dart +// Backend-neutral semantic plans for conditional emission trees. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Backend-neutral description of a [Conditional] emission tree. +abstract class ConditionalEmissionPlan { + /// The source conditional represented by this plan. + final Conditional source; + + /// Creates an emission plan for [source]. + const ConditionalEmissionPlan(this.source); + + /// Creates a plan for [conditional] and all of its children. + factory ConditionalEmissionPlan.fromConditional(Conditional conditional) { + if (conditional is ConditionalAssign) { + return ConditionalAssignmentEmissionPlan(conditional); + } + if (conditional is If) { + return ConditionalIfEmissionPlan( + conditional, + [ + for (final branch in conditional.iffs) + ConditionalIfBranchEmissionPlan( + branch, + branch is Else ? null : branch.condition, + [ + for (final child in branch.then) + ConditionalEmissionPlan.fromConditional(child), + ], + ), + ], + ); + } + if (conditional is Case) { + return ConditionalCaseEmissionPlan( + conditional, + [ + for (final item in conditional.items) + ConditionalCaseItemEmissionPlan( + item, + [ + for (final child in item.then) + ConditionalEmissionPlan.fromConditional(child), + ], + ), + ], + conditional.defaultItem == null + ? null + : [ + for (final child in conditional.defaultItem!) + ConditionalEmissionPlan.fromConditional(child), + ], + ); + } + if (conditional is ConditionalGroup) { + return ConditionalGroupEmissionPlan( + conditional, + [ + for (final child in conditional.conditionals) + ConditionalEmissionPlan.fromConditional(child), + ], + ); + } + + throw UnsupportedError( + 'Unsupported Conditional type for emission: ${conditional.runtimeType}', + ); + } +} + +/// Plan for a [ConditionalAssign]. +class ConditionalAssignmentEmissionPlan extends ConditionalEmissionPlan { + /// The assignment represented by this plan. + ConditionalAssign get assignment => source as ConditionalAssign; + + /// Creates an assignment plan. + const ConditionalAssignmentEmissionPlan(super.source); +} + +/// Plan for an [If] tree. +class ConditionalIfEmissionPlan extends ConditionalEmissionPlan { + /// The ordered branches of this if tree. + final List branches; + + /// Creates an if plan. + const ConditionalIfEmissionPlan(super.source, this.branches); +} + +/// Plan for one branch of an [If]. +class ConditionalIfBranchEmissionPlan { + /// The original branch. + final Iff source; + + /// The branch condition, or null for an else branch. + final Logic? condition; + + /// Plans emitted when this branch is selected. + final List children; + + /// Creates an if branch plan. + const ConditionalIfBranchEmissionPlan( + this.source, + this.condition, + this.children, + ); + + /// Whether this is the final else branch. + bool get isElse => source is Else; +} + +/// Plan for a [Case] tree. +class ConditionalCaseEmissionPlan extends ConditionalEmissionPlan { + /// The ordered case items. + final List items; + + /// Plans emitted when no case item matches. + final List? defaultChildren; + + /// The case represented by this plan. + Case get caseBlock => source as Case; + + /// Creates a case plan. + const ConditionalCaseEmissionPlan( + super.source, + this.items, + this.defaultChildren, + ); +} + +/// Plan for one [CaseItem]. +class ConditionalCaseItemEmissionPlan { + /// The original case item. + final CaseItem source; + + /// Plans emitted when [source] matches. + final List children; + + /// Creates a case item plan. + const ConditionalCaseItemEmissionPlan(this.source, this.children); +} + +/// Plan for a [ConditionalGroup]. +class ConditionalGroupEmissionPlan extends ConditionalEmissionPlan { + /// Plans emitted in source order. + final List children; + + /// Creates a group plan. + const ConditionalGroupEmissionPlan(super.source, this.children); +} diff --git a/lib/src/synthesizers/utilities/conditional_emitter.dart b/lib/src/synthesizers/utilities/conditional_emitter.dart new file mode 100644 index 000000000..3f3d97bb9 --- /dev/null +++ b/lib/src/synthesizers/utilities/conditional_emitter.dart @@ -0,0 +1,153 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// conditional_emitter.dart +// Shared conditional-plan traversal contract for backend renderers. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/conditional_emission_plan.dart'; + +/// Shared traversal for rendering [ConditionalEmissionPlan]s in a backend. +abstract class ConditionalEmitter { + /// Creates a conditional emitter. + const ConditionalEmitter(); + + /// Emits [conditional] at [indent]. + String emit(Conditional conditional, int indent) => + emitPlan(ConditionalEmissionPlan.fromConditional(conditional), indent); + + /// Emits [plan] at [indent]. + String emitPlan(ConditionalEmissionPlan plan, int indent) { + if (plan is ConditionalAssignmentEmissionPlan) { + final assignment = plan.assignment; + return emitAssignment( + indent, + receiverFor(plan.source, assignment.receiver), + driverFor(plan.source, assignment.driver), + ); + } + if (plan is ConditionalIfEmissionPlan) { + return emitIf( + indent, + [ + for (final branch in plan.branches) + ConditionalIfBranchEmission( + isElse: branch.isElse, + condition: branch.condition == null + ? null + : driverFor(plan.source, branch.condition!), + contents: emitChildren(branch.children, ifChildIndent(indent)), + ), + ], + ); + } + if (plan is ConditionalCaseEmissionPlan) { + return emitCase( + plan, + indent, + driverFor(plan.source, plan.caseBlock.expression), + [ + for (final item in plan.items) + ConditionalCaseItemEmission( + item: item.source, + match: driverFor(plan.source, item.source.value), + contents: emitChildren( + item.children, + caseChildIndent(plan, indent), + ), + ), + ], + plan.defaultChildren == null + ? null + : emitChildren( + plan.defaultChildren!, + caseChildIndent(plan, indent), + ), + ); + } + if (plan is ConditionalGroupEmissionPlan) { + return emitGroup(indent, emitChildren(plan.children, indent)); + } + + throw UnsupportedError('Unsupported conditional emission plan: $plan'); + } + + /// Resolves [driver] to a backend expression for [source]. + String driverFor(Conditional source, Logic driver); + + /// Resolves [receiver] to a backend assignment target for [source]. + String receiverFor(Conditional source, Logic receiver); + + /// Emits one assignment. + String emitAssignment(int indent, String receiver, String driver); + + /// Emits an if tree. + String emitIf(int indent, List branches); + + /// Emits a case tree. + String emitCase( + ConditionalCaseEmissionPlan plan, + int indent, + String expression, + List items, + String? defaultContents, + ); + + /// Emits a linear conditional group. + String emitGroup(int indent, String contents) => contents; + + /// Returns the child indentation for an if branch. + int ifChildIndent(int indent) => indent + 1; + + /// Returns the child indentation for [plan]'s case items. + int caseChildIndent(ConditionalCaseEmissionPlan plan, int indent) => + indent + 1; + + /// Emits [children] with [indent]. + String emitChildren(List children, int indent) => + children.map((child) => emitPlan(child, indent)).join(childrenSeparator); + + /// Separator used between rendered sibling conditionals. + String get childrenSeparator => '\n'; +} + +/// A resolved if branch ready for backend-specific syntax rendering. +class ConditionalIfBranchEmission { + /// Whether this is an else branch. + final bool isElse; + + /// Resolved backend condition, or null for an else branch. + final String? condition; + + /// Rendered child contents. + final String contents; + + /// Creates a resolved if branch. + const ConditionalIfBranchEmission({ + required this.isElse, + required this.condition, + required this.contents, + }); +} + +/// A resolved case item ready for backend-specific syntax rendering. +class ConditionalCaseItemEmission { + /// Source case item, retained for backend semantic choices. + final CaseItem item; + + /// Resolved backend case-item expression. + final String match; + + /// Rendered child contents. + final String contents; + + /// Creates a resolved case item. + const ConditionalCaseItemEmission({ + required this.item, + required this.match, + required this.contents, + }); +} diff --git a/lib/src/synthesizers/utilities/inline_leaf.dart b/lib/src/synthesizers/utilities/inline_leaf.dart new file mode 100644 index 000000000..2a65a1d74 --- /dev/null +++ b/lib/src/synthesizers/utilities/inline_leaf.dart @@ -0,0 +1,36 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// inline_leaf.dart +// Backend-neutral contract for inline leaf modules. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// Indicates that a [Module] can be rendered as an inline leaf expression by +/// synthesis backends. +mixin InlineLeaf on Module { + /// The name of the [output] or [inOut] port which can be inlined. + /// + /// By default, this assumes one [output] port. Override this for modules + /// whose inline result is an [inOut] or one of multiple outputs. + String get resultSignalName { + if (outputs.keys.length != 1) { + throw Exception('Inline leaf expected to have exactly one output,' + ' but saw $outputs.'); + } + + return outputs.keys.first; + } + + /// Input names that cannot be represented as inline expressions. + List get expressionlessInputs => const []; + + /// Indicates that this module is only wires, no logic inside, which can be + /// leveraged for pruning. + @internal + bool get isWiresOnly => false; +} diff --git a/lib/src/synthesizers/utilities/inline_leaf_emitter.dart b/lib/src/synthesizers/utilities/inline_leaf_emitter.dart new file mode 100644 index 000000000..90f0cab3d --- /dev/null +++ b/lib/src/synthesizers/utilities/inline_leaf_emitter.dart @@ -0,0 +1,23 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// inline_leaf_emitter.dart +// Backend renderer contract for inline leaf expressions. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Backend renderer contract for inline leaf expressions. +class InlineLeafEmitter { + /// Creates an inline leaf emitter contract base. + const InlineLeafEmitter(); + + /// Emits a backend-specific expression for [module] given [inputs]. + String expressionFor(InlineLeaf module, Map inputs) { + throw UnimplementedError( + 'InlineLeafEmitter.expressionFor must be implemented by subclasses.', + ); + } +} diff --git a/lib/src/synthesizers/utilities/leaf_cell_spec.dart b/lib/src/synthesizers/utilities/leaf_cell_spec.dart new file mode 100644 index 000000000..4eeb30a1e --- /dev/null +++ b/lib/src/synthesizers/utilities/leaf_cell_spec.dart @@ -0,0 +1,106 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// leaf_cell_spec.dart +// Backend-neutral semantic metadata for primitive leaf modules. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +// ignore_for_file: public_member_api_docs + +import 'package:meta/meta.dart'; + +/// Direction for a leaf-cell port. +enum LeafPortDirection { + /// Input-only port. + input, + + /// Output-only port. + output, + + /// Bidirectional port. + inOut, +} + +/// Semantic operation kind for a leaf cell. +/// +/// This is backend-neutral metadata that renderers can consume to emit +/// SystemVerilog, SystemC, or netlist primitive forms. +enum LeafOperationKind { + not, + and, + or, + xor, + add, + subtract, + multiply, + divide, + modulo, + power, + equals, + notEquals, + lessThan, + greaterThan, + lessThanOrEqual, + greaterThanOrEqual, + andUnary, + orUnary, + xorUnary, + shiftLeft, + shiftRight, + arithmeticShiftRight, + mux, + bitIndex, + busSubset, + swizzle, + replication, + custom, +} + +/// A port declaration in a semantic leaf-cell spec. +@immutable +class LeafPortSpec { + /// Name of the port in the source module. + final String name; + + /// Bit-width of the port. + final int width; + + /// Direction of the port. + final LeafPortDirection direction; + + /// Creates a port spec. + const LeafPortSpec(this.name, this.width, this.direction); +} + +/// Semantic description of a primitive/leaf module operation. +@immutable +class LeafCellSpec { + /// Operation kind represented by this leaf. + final LeafOperationKind operation; + + /// All ports (inputs, outputs, inouts) for this leaf. + final List ports; + + /// Extra operation-specific parameters. + /// + /// Keys are renderer-defined (for example: `startIndex`, `endIndex`, + /// `signed`, `resultSignalName`, etc). + final Map metadata; + + /// Creates a semantic leaf-cell spec. + const LeafCellSpec({ + required this.operation, + this.ports = const [], + this.metadata = const {}, + }); +} + +/// Optional interface for modules that can provide semantic leaf metadata. +/// +/// This enables backend renderers to avoid backend-specific type switches. +abstract interface class LeafCellProvider { + /// Semantic description of this leaf cell. + LeafCellSpec get leafCellSpec; +} diff --git a/lib/src/synthesizers/utilities/leaf_cell_spec_inference.dart b/lib/src/synthesizers/utilities/leaf_cell_spec_inference.dart new file mode 100644 index 000000000..3fb87e578 --- /dev/null +++ b/lib/src/synthesizers/utilities/leaf_cell_spec_inference.dart @@ -0,0 +1,209 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// leaf_cell_spec_inference.dart +// Inference bridge from existing inline modules to leaf-cell metadata. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/leaf_cell_spec.dart'; + +/// Infers a semantic [LeafCellSpec] for existing inline modules. +/// +/// This bridges current type-based inline modules into backend-neutral leaf +/// metadata without requiring those modules to implement [LeafCellProvider] +/// immediately. +LeafCellSpec? leafCellSpecForInlineModule(InlineLeaf module) { + if (module is LeafCellProvider) { + return (module as LeafCellProvider).leafCellSpec; + } + + if (module is NotGate) { + return LeafCellSpec( + operation: LeafOperationKind.not, + metadata: { + 'outputWidth': module.outputs.values.first.width, + }, + ); + } + + if (module is And2Gate) { + return const LeafCellSpec(operation: LeafOperationKind.and); + } + if (module is Or2Gate) { + return const LeafCellSpec(operation: LeafOperationKind.or); + } + if (module is Xor2Gate) { + return const LeafCellSpec(operation: LeafOperationKind.xor); + } + + if (module is Subtract) { + return const LeafCellSpec(operation: LeafOperationKind.subtract); + } + if (module is Multiply) { + return const LeafCellSpec(operation: LeafOperationKind.multiply); + } + if (module is Divide) { + return const LeafCellSpec(operation: LeafOperationKind.divide); + } + if (module is Modulo) { + return const LeafCellSpec(operation: LeafOperationKind.modulo); + } + if (module is Power) { + return LeafCellSpec( + operation: LeafOperationKind.power, + metadata: { + 'inputWidth': module.inputs.values.first.width, + 'makeSelfDetermined': true, + }, + ); + } + + if (module is Equals) { + return const LeafCellSpec(operation: LeafOperationKind.equals); + } + if (module is NotEquals) { + return const LeafCellSpec(operation: LeafOperationKind.notEquals); + } + if (module is LessThan) { + return const LeafCellSpec(operation: LeafOperationKind.lessThan); + } + if (module is GreaterThan) { + return const LeafCellSpec(operation: LeafOperationKind.greaterThan); + } + if (module is LessThanOrEqual) { + return const LeafCellSpec(operation: LeafOperationKind.lessThanOrEqual); + } + if (module is GreaterThanOrEqual) { + return const LeafCellSpec(operation: LeafOperationKind.greaterThanOrEqual); + } + + if (module is AndUnary) { + return LeafCellSpec( + operation: LeafOperationKind.andUnary, + metadata: { + 'inputWidth': module.inputs.values.first.width, + }, + ); + } + if (module is OrUnary) { + return LeafCellSpec( + operation: LeafOperationKind.orUnary, + metadata: { + 'inputWidth': module.inputs.values.first.width, + }, + ); + } + if (module is XorUnary) { + return LeafCellSpec( + operation: LeafOperationKind.xorUnary, + metadata: { + 'inputWidth': module.inputs.values.first.width, + }, + ); + } + + if (module is LShift) { + return LeafCellSpec( + operation: LeafOperationKind.shiftLeft, + metadata: { + 'inputWidth': module.inputs.values.first.width, + 'shiftAmountWidth': module.inputs.values.toList()[1].width, + }, + ); + } + if (module is RShift) { + return LeafCellSpec( + operation: LeafOperationKind.shiftRight, + metadata: { + 'inputWidth': module.inputs.values.first.width, + 'shiftAmountWidth': module.inputs.values.toList()[1].width, + }, + ); + } + if (module is ARShift) { + return LeafCellSpec( + operation: LeafOperationKind.arithmeticShiftRight, + metadata: { + 'inputWidth': module.inputs.values.first.width, + 'shiftAmountWidth': module.inputs.values.toList()[1].width, + }, + ); + } + + if (module is Mux) { + return LeafCellSpec( + operation: LeafOperationKind.mux, + metadata: { + 'outputWidth': module.out.width, + }, + ); + } + if (module is IndexGate) { + return LeafCellSpec( + operation: LeafOperationKind.bitIndex, + metadata: { + 'originalWidth': module.inputs.values.first.width, + }, + ); + } + + if (module is BusSubset) { + return LeafCellSpec( + operation: LeafOperationKind.busSubset, + metadata: { + 'inputWidth': module.original.width, + 'startIndex': module.startIndex, + 'endIndex': module.endIndex, + }, + ); + } + + if (module is Swizzle) { + final inputPorts = { + ...module.inputs, + ...module.inOuts, + }..remove(module.resultSignalName); + return LeafCellSpec( + operation: LeafOperationKind.swizzle, + metadata: { + 'inputCount': inputPorts.length, + 'inputWidths': inputPorts.values.map((input) => input.width).toList(), + 'inputIsArrayMember': + inputPorts.values.map((input) => input.isArrayMember).toList(), + 'inputHasUnpackedArraySource': + inputPorts.values.map(_hasUnpackedArraySource).toList(), + }, + ); + } + if (module is ReplicationOp) { + final inputWidth = module.inputs.values.first.width; + final outputWidth = module.replicated.width; + return LeafCellSpec( + operation: LeafOperationKind.replication, + metadata: { + 'inputWidth': inputWidth, + 'outputWidth': outputWidth, + 'replicationCount': outputWidth ~/ inputWidth, + }, + ); + } + + return null; +} + +bool _hasUnpackedArraySource(Logic input) { + var current = input.srcConnection; + while (current?.parentStructure != null) { + final parentStructure = current!.parentStructure!; + if (parentStructure is LogicArray && + parentStructure.numUnpackedDimensions > 0) { + return true; + } + current = parentStructure; + } + + return false; +} diff --git a/lib/src/synthesizers/utilities/leaf_expression_plan.dart b/lib/src/synthesizers/utilities/leaf_expression_plan.dart new file mode 100644 index 000000000..cdb1c4e72 --- /dev/null +++ b/lib/src/synthesizers/utilities/leaf_expression_plan.dart @@ -0,0 +1,60 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// leaf_expression_plan.dart +// Normalized semantic plans for inline leaf expression rendering. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/leaf_cell_spec.dart'; +import 'package:rohd/src/synthesizers/utilities/leaf_cell_spec_inference.dart'; + +/// Normalized planning data for rendering an inline leaf expression. +class LeafExpressionPlan { + /// Module whose leaf semantics are being rendered. + final Module sourceModule; + + /// Semantic operation kind when available. + final LeafOperationKind? operation; + + /// Semantic metadata when available. + final Map metadata; + + /// Ordered input expressions. + final List inputValues; + + /// Input expressions keyed by port name. + final Map inputsByPort; + + /// Creates a new [LeafExpressionPlan]. + const LeafExpressionPlan({ + required this.sourceModule, + required this.operation, + required this.metadata, + required this.inputValues, + required this.inputsByPort, + }); + + /// Builds a plan for [module] and [inputs]. + factory LeafExpressionPlan.fromInlineModule( + InlineLeaf module, + Map inputs, + ) { + final spec = leafCellSpecForInlineModule(module); + return LeafExpressionPlan( + sourceModule: module as Module, + operation: spec?.operation, + metadata: spec?.metadata ?? const {}, + inputValues: inputs.values.toList(), + inputsByPort: Map.unmodifiable(inputs), + ); + } + + /// Returns metadata [key] cast as [T], or `null` if absent/mismatched. + T? meta(String key) { + final value = metadata[key]; + return value is T ? value : null; + } +} diff --git a/lib/src/synthesizers/utilities/module_emission_plan.dart b/lib/src/synthesizers/utilities/module_emission_plan.dart new file mode 100644 index 000000000..0e1182a7c --- /dev/null +++ b/lib/src/synthesizers/utilities/module_emission_plan.dart @@ -0,0 +1,104 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_emission_plan.dart +// Backend-neutral structural plans for resolved module emission. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_assignment.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_logic.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_sub_module_instantiation.dart'; + +/// Direction of a port in a [ModuleEmissionPlan]. +enum ModuleEmissionPortDirection { + /// An input port. + input, + + /// An output port. + output, + + /// A bidirectional port. + inOut, +} + +/// A resolved module port ready for backend-specific declaration rendering. +class ModuleEmissionPortPlan { + /// Port direction. + final ModuleEmissionPortDirection direction; + + /// Resolved signal for the port. + final SynthLogic signal; + + /// Creates a resolved port plan. + const ModuleEmissionPortPlan(this.direction, this.signal); +} + +/// Backend-neutral structural view of a resolved module definition. +/// +/// This plan intentionally retains resolved synthesis objects. Backends choose +/// their own declaration syntax, type mapping, and instance lowering while +/// sharing one structural source of truth. +class ModuleEmissionPlan { + /// The source module. + final Module sourceModule; + + /// Resolved ports in declaration order. + final List ports; + + /// Resolved internal signals before backend declaration filtering. + final List internalSignals; + + /// Resolved structural connections. + final List assignments; + + /// Resolved child module instances. + /// + /// Renderers must check [SynthSubModuleInstantiation.needsInstantiation], + /// since backend lowering may consume an instance while producing output. + final List instances; + + /// Creates a structural module emission plan. + const ModuleEmissionPlan({ + required this.sourceModule, + required this.ports, + required this.internalSignals, + required this.assignments, + required this.instances, + }); + + /// Creates a plan from an already-resolved [definition]. + factory ModuleEmissionPlan.fromDefinition(SynthModuleDefinition definition) => + ModuleEmissionPlan( + sourceModule: definition.module, + ports: List.unmodifiable([ + for (final signal in definition.inputs) + ModuleEmissionPortPlan(ModuleEmissionPortDirection.input, signal), + for (final signal in definition.outputs) + ModuleEmissionPortPlan(ModuleEmissionPortDirection.output, signal), + for (final signal in definition.inOuts) + ModuleEmissionPortPlan(ModuleEmissionPortDirection.inOut, signal), + ]), + internalSignals: List.unmodifiable(definition.internalSignals), + assignments: List.unmodifiable(definition.assignments), + instances: List.unmodifiable(definition.subModuleInstantiations), + ); + + /// Input ports. + Iterable get inputs => ports + .where((port) => port.direction == ModuleEmissionPortDirection.input) + .map((port) => port.signal); + + /// Output ports. + Iterable get outputs => ports + .where((port) => port.direction == ModuleEmissionPortDirection.output) + .map((port) => port.signal); + + /// Bidirectional ports. + Iterable get inOuts => ports + .where((port) => port.direction == ModuleEmissionPortDirection.inOut) + .map((port) => port.signal); +} diff --git a/lib/src/synthesizers/utilities/process_emission_plan.dart b/lib/src/synthesizers/utilities/process_emission_plan.dart new file mode 100644 index 000000000..63d2fee03 --- /dev/null +++ b/lib/src/synthesizers/utilities/process_emission_plan.dart @@ -0,0 +1,113 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// process_emission_plan.dart +// Backend-neutral semantic plans for procedural process emission. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/conditionals/always.dart'; +import 'package:rohd/src/synthesizers/utilities/conditional_emission_plan.dart'; + +/// The semantic class of a procedural process. +enum ProcessEmissionKind { + /// A process reevaluated when its inputs change. + combinational, + + /// A process reevaluated on one or more clock edges. + clocked, +} + +/// The assignment semantics used inside a procedural process. +enum ProcessAssignmentKind { + /// Assign immediately within the process. + blocking, + + /// Assign at the end of the current time step. + nonBlocking, +} + +/// A clock-edge trigger in a [ProcessEmissionPlan]. +class ProcessTriggerEmissionPlan { + /// The trigger signal. + final Logic signal; + + /// Whether this trigger is a rising edge. + final bool isPosedge; + + /// Creates a clock-edge trigger plan. + const ProcessTriggerEmissionPlan(this.signal, {required this.isPosedge}); +} + +/// Backend-neutral description of an [Always] procedural block. +class ProcessEmissionPlan { + /// The source process. + final Always source; + + /// Whether this is combinational or clocked logic. + final ProcessEmissionKind kind; + + /// Assignment semantics for statements in [body]. + final ProcessAssignmentKind assignmentKind; + + /// Clock-edge triggers for a clocked process. + final List triggers; + + /// Whether a clocked process has an asynchronous reset trigger. + final bool hasAsyncReset; + + /// Backend-neutral statement plans in source order. + final List body; + + /// Creates a procedural process plan. + const ProcessEmissionPlan({ + required this.source, + required this.kind, + required this.assignmentKind, + required this.triggers, + required this.hasAsyncReset, + required this.body, + }); + + /// Builds a normalized plan for [always]. + factory ProcessEmissionPlan.fromAlways(Always always) { + if (always is Combinational) { + return ProcessEmissionPlan( + source: always, + kind: ProcessEmissionKind.combinational, + assignmentKind: ProcessAssignmentKind.blocking, + triggers: const [], + hasAsyncReset: false, + body: [ + for (final conditional in always.conditionals) + ConditionalEmissionPlan.fromConditional(conditional), + ], + ); + } + if (always is Sequential) { + return ProcessEmissionPlan( + source: always, + kind: ProcessEmissionKind.clocked, + assignmentKind: ProcessAssignmentKind.nonBlocking, + triggers: [ + for (final trigger in always.emissionTriggers) + ProcessTriggerEmissionPlan( + trigger.signal, + isPosedge: trigger.isPosedge, + ), + ], + hasAsyncReset: always.asyncReset, + body: [ + for (final conditional in always.conditionals) + ConditionalEmissionPlan.fromConditional(conditional), + ], + ); + } + + throw UnsupportedError( + 'Unsupported procedural process for emission: ${always.runtimeType}', + ); + } +} diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index d29cc84f3..093b011bf 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -114,6 +114,9 @@ class SynthLogic { bool get constNameDisallowed => _constNameDisallowed; bool _constNameDisallowed; + /// Whether a synthesized name has been picked for this signal. + bool get hasName => _name != null; + /// Whether this signal should be declared. bool get needsDeclaration => !(isConstant && !_constNameDisallowed) && !declarationCleared; diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 5cd689882..67b0d4a13 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -269,6 +269,10 @@ class SynthModuleDefinition { .contains(logic.name)) || (logic.parentModule is SystemVerilog && (logic.parentModule! as SystemVerilog) + .expressionlessInputs + .contains(logic.name)) || + (logic.parentModule is InlineLeaf && + (logic.parentModule! as InlineLeaf) .expressionlessInputs .contains(logic.name))); @@ -390,9 +394,10 @@ class SynthModuleDefinition { /// Creates a new definition representation for this [module]. SynthModuleDefinition(this.module) : assert( - !(module is SystemVerilog && - module.generatedDefinitionType == - DefinitionGenerationType.none), + module is! InlineLeaf && + !(module is SystemVerilog && + module.generatedDefinitionType == + DefinitionGenerationType.none), 'Do not build a definition for a module' ' which generates no definition!') { // start by traversing output signals @@ -669,8 +674,7 @@ class SynthModuleDefinition { /// Finds chainable, inlineable modules. Iterable _findChainableModulesToCollapse() { final inlineableSubmoduleInstantiations = subModuleInstantiations.where( - (submoduleInstantiation) => - submoduleInstantiation.module is InlineSystemVerilog, + (submoduleInstantiation) => submoduleInstantiation.module is InlineLeaf, ); final signalUsage = {}; @@ -795,7 +799,14 @@ class SynthModuleDefinition { for (final instantiation in subModuleInstantiations) { final subModule = instantiation.module; - if (subModule is SystemVerilog) { + if (subModule is InlineLeaf) { + singleUseSignals.removeAll( + subModule.expressionlessInputs.map( + (e) => + instantiation.inputMapping[e] ?? instantiation.inOutMapping[e], + ), + ); + } else if (subModule is SystemVerilog) { singleUseSignals.removeAll( subModule.expressionlessInputs.map( (e) => @@ -840,7 +851,7 @@ class SynthModuleDefinition { activePath.add(candidate); final resultSignalName = - (candidate.module as InlineSystemVerilog).resultSignalName; + (candidate.module as InlineLeaf).resultSignalName; for (final input in [ ...candidate.inputMapping.values, ...candidate.inOutMapping.entries @@ -876,7 +887,7 @@ class SynthModuleDefinition { SynthLogic? _inlineResultLogic(SynthSubModuleInstantiation instantiation) { final subModule = instantiation.module; - if (subModule is! InlineSystemVerilog) { + if (subModule is! InlineLeaf) { return null; } @@ -971,7 +982,10 @@ class SynthModuleDefinition { (logic) => logic.isPort && isSubmoduleAndPresent(logic.parentModule) && - ((logic.parentModule! is SystemVerilog && + ((logic.parentModule! is InlineLeaf && + !(logic.parentModule! as InlineLeaf).isWiresOnly && + internalSignal is! SynthLogicArrayElement) || + (logic.parentModule! is SystemVerilog && !(logic.parentModule! as SystemVerilog) .acceptsEmptyPortConnections) || // ignore: deprecated_member_use_from_same_package @@ -1088,7 +1102,12 @@ class SynthModuleDefinition { )) { final subModule = subModuleInstantiation.module; - if (subModule is SystemVerilog && subModule.isWiresOnly) { + final isWiresOnly = switch (subModule) { + InlineLeaf() => subModule.isWiresOnly, + SystemVerilog() => subModule.isWiresOnly, + _ => false, + }; + if (isWiresOnly) { final inputs = { ...subModuleInstantiation.inputMapping, ...subModuleInstantiation.inOutMapping, @@ -1218,6 +1237,23 @@ class SynthModuleDefinition { _submoduleMappingReferences(includeInputs: true, includeOutputs: false); final submoduleOutputMappingReferences = _submoduleMappingReferences(includeInputs: false, includeOutputs: true); + final inlineOperationInputReferences = {}; + for (final instantiation in subModuleInstantiations) { + final inlineModule = instantiation.module; + if (inlineModule is! InlineLeaf || inlineModule.isWiresOnly) { + continue; + } + for (final mapped in [ + ...instantiation.inputMapping.values, + ...instantiation.inOutMapping.entries + .where((entry) => entry.key != inlineModule.resultSignalName) + .map((entry) => entry.value), + ]) { + inlineOperationInputReferences + ..add(mapped.resolved) + ..add(_referenceBase(mapped.resolved)); + } + } final assignmentConnectedSignals = {}; var foundInlineDependency = true; @@ -1267,6 +1303,11 @@ class SynthModuleDefinition { final destinationReferenceBase = _referenceBase(destinationBase); final sourceIsMappedOutput = submoduleOutputMappingReferences.contains(sourceBase); + final destinationFeedsInlineOperation = + inlineOperationInputReferences.contains(destinationBase) || + inlineOperationInputReferences.contains( + destinationReferenceBase, + ); if (submoduleInputMappingReferences.contains(destinationBase) && sourceIsMappedOutput) { @@ -1279,6 +1320,11 @@ class SynthModuleDefinition { if (sourceIsMappedOutput) { assignmentConnectedSignals.add(sourceBase); } + if (destinationFeedsInlineOperation) { + assignmentConnectedSignals + ..add(destinationBase) + ..add(sourceBase); + } } return assignmentConnectedSignals; @@ -1305,7 +1351,7 @@ class SynthModuleDefinition { for (final submoduleInstantiation in subModuleInstantiations) { if (!submoduleInstantiation.needsInstantiation || - submoduleInstantiation.module is InlineSystemVerilog) { + submoduleInstantiation.module is InlineLeaf) { continue; } @@ -1352,7 +1398,7 @@ class SynthModuleDefinition { for (final submoduleInstantiation in subModuleInstantiations) { if (!submoduleInstantiation.needsInstantiation || - submoduleInstantiation.module is InlineSystemVerilog) { + submoduleInstantiation.module is InlineLeaf) { continue; } @@ -2783,7 +2829,7 @@ class SynthModuleDefinition { })>>{}; for (final instantiation in subModuleInstantiations) { if (!instantiation.needsInstantiation || - instantiation.module is InlineSystemVerilog) { + instantiation.module is InlineLeaf) { continue; } for (final entry in instantiation.outputMapping.entries) { diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 1eccf9da9..89d12349a 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -114,6 +114,38 @@ class SynthSubModuleInstantiation { bool get needsInstantiation => _needsInstantiation; bool _needsInstantiation = true; + /// If [module] is [InlineLeaf], this is the [SynthLogic] mapped from its + /// [InlineLeaf.resultSignalName]. + SynthLogic? get inlineResultLogic { + final m = module; + if (m is! InlineLeaf) { + return null; + } + return outputMapping[m.resultSignalName] ?? + inOutMapping[m.resultSignalName]; + } + + /// Creates a port-name to expression map, optionally inlining source + /// submodule expressions for mapped [SynthLogic] values. + Map modulePortsMapWithInline< + T extends SynthSubModuleInstantiation>( + Map plainPorts, + Map? synthLogicToInlineableSynthSubmoduleMap, + String Function(T subModuleInstantiation) inlineExpressionFor, + ) => + plainPorts.map((name, synthLogic) { + final resolved = synthLogic.resolved; + final inlineSubModule = + synthLogicToInlineableSynthSubmoduleMap?[synthLogic] ?? + synthLogicToInlineableSynthSubmoduleMap?[resolved]; + if (inlineSubModule != null) { + return MapEntry(name, inlineExpressionFor(inlineSubModule)); + } + + // Cleared declarations map to empty port connections. + return MapEntry(name, resolved.declarationCleared ? '' : resolved.name); + }); + /// Removes the need for this module to be declared (via /// [needsInstantiation]). void clearInstantiation() { diff --git a/lib/src/synthesizers/utilities/utilities.dart b/lib/src/synthesizers/utilities/utilities.dart index c3cccdf32..fffb315a9 100644 --- a/lib/src/synthesizers/utilities/utilities.dart +++ b/lib/src/synthesizers/utilities/utilities.dart @@ -1,6 +1,22 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +// +// utilities.dart +// Exports shared synthesis utility contracts and resolved synthesis models. +// +// 2026 July +// Author: Desmond A. Kirkpatrick +export 'backend_artifact.dart'; +export 'conditional_emission_plan.dart'; +export 'conditional_emitter.dart'; +export 'inline_leaf.dart'; +export 'inline_leaf_emitter.dart'; +export 'leaf_cell_spec.dart'; +export 'leaf_cell_spec_inference.dart'; +export 'leaf_expression_plan.dart'; +export 'module_emission_plan.dart'; +export 'process_emission_plan.dart'; export 'synth_assignment.dart'; export 'synth_logic.dart'; export 'synth_module_definition.dart'; diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index f8c07eb71..8d52eebde 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -2,22 +2,25 @@ // SPDX-License-Identifier: BSD-3-Clause // // simcompare.dart -// Helper functionality for unit testing (sv testbench generation, iverilog simulation, vectors, checking/comparison, etc.) +// Helper functionality for unit testing (sv testbench generation, +// iverilog simulation, vectors, checking/comparison, etc.) // // 2021 May 7 // Author: Max Korbel -// ignore_for_file: avoid_print - import 'dart:async'; import 'dart:io'; import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; import 'package:rohd/src/utilities/web.dart'; import 'package:test/test.dart'; +part 'systemverilog_simcompare.dart'; +part 'systemc_simcompare.dart'; + /// Represents a single test case to check in a single clock cycle. /// /// Useful for testing equivalent behavior in different simulation environments. @@ -43,96 +46,9 @@ class Vector { @override String toString() => '$inputValues => $expectedOutputValues'; - /// Computes a SystemVerilog code string that checks in a SystemVerilog - /// simulation whether a signal [sigName] has the [expected] value given - /// the [inputValues]. - static String _errorCheckString(String sigName, dynamic expected, - LogicValue expectedVal, String inputValues) { - if (expected is! int && - expected is! LogicValue && - expected is! BigInt && - expected is! String) { - throw NonSupportedTypeException(expected); - } - - String expectedHexStr; - if (expected is int) { - expectedHexStr = - BigInt.from(expected).toUnsigned(expectedVal.width).toRadixString(16); - expectedHexStr = '0x$expectedHexStr'; - } else if (expected is BigInt) { - expectedHexStr = expected.toUnsigned(expectedVal.width).toRadixString(16); - expectedHexStr = '0x$expectedHexStr'; - } else { - expectedHexStr = expected.toString(); - } - - final expectedValStr = expectedVal.toString(); - - return 'if($sigName !== $expectedValStr) ' - '\$error(\$sformatf("Expected $sigName=$expectedHexStr,' - ' but found $sigName=0x%x (0b%b) with inputs $inputValues",' - ' $sigName, $sigName));'; - } - /// Converts this vector into a SystemVerilog check. - String toTbVerilog(Module module) { - final assignments = inputValues.keys.map((signalName) { - final signal = module.tryInOut(signalName) ?? module.input(signalName); - - if (signal is LogicArray) { - final arrAssigns = StringBuffer(); - var index = 0; - final fullVal = - LogicValue.of(inputValues[signalName], width: signal.width); - for (final leaf in signal.leafElements) { - final subVal = fullVal.getRange(index, index + leaf.width); - arrAssigns.writeln('${leaf.structureName} = $subVal;'); - index += leaf.width; - } - return arrAssigns.toString(); - } else { - final signalVal = - LogicValue.of(inputValues[signalName], width: signal.width); - return '$signalName = $signalVal;'; - } - }).join('\n'); - - final checksList = []; - for (final expectedOutput in expectedOutputValues.entries) { - final outputName = expectedOutput.key; - final outputPort = - module.tryInOut(outputName) ?? module.output(outputName); - final expected = expectedOutput.value; - final expectedValue = LogicValue.of( - expected, - width: outputPort.width, - ); - final inputStimulus = inputValues.toString(); - - if (outputPort is LogicArray) { - var index = 0; - for (final leaf in outputPort.leafElements) { - final subVal = expectedValue.getRange(index, index + leaf.width); - checksList.add(_errorCheckString( - leaf.structureName, subVal, subVal, inputStimulus)); - index += leaf.width; - } - } else { - checksList.add(_errorCheckString( - outputName, expected, expectedValue, inputStimulus)); - } - } - final checks = checksList.join('\n'); - - final tbVerilog = [ - assignments, - '#$_offset', - checks, - '#${_period - _offset}', - ].join('\n'); - return tbVerilog; - } + String toTbVerilog(Module module) => + _SystemVerilogVectorTestbench(this, module).toTbVerilog(); } /// A utility class for checking a collection of [Vector]s against @@ -202,12 +118,10 @@ abstract class SimCompare { throw NonSupportedTypeException(value); } } - }).catchError( - test: (error) => error is Exception, - (Object err, StackTrace stackTrace) { - Simulator.throwException(err as Exception, stackTrace); - }, - )); + }).catchError(test: (error) => error is Exception, + (Object err, StackTrace stackTrace) { + Simulator.throwException(err as Exception, stackTrace); + })); } }); timestamp += Vector._period; @@ -218,15 +132,6 @@ abstract class SimCompare { await Simulator.run(); } - /// A collection of warnings that are fine to ignore usually. - static final List _knownWarnings = [ - RegExp('sorry: Case unique/unique0 qualities are ignored.'), - RegExp(r'sorry: constant selects in always_\* processes' - ' are not currently supported'), - RegExp('warning: always_comb process has no sensitivities'), - RegExp('finish called at'), - ]; - /// Executes [vectors] against the Icarus Verilog simulator and checks /// that it passes. static void checkIverilogVector( @@ -242,20 +147,20 @@ abstract class SimCompare { bool buildOnly = false, SystemVerilogSynthesizerConfiguration synthesizerConfiguration = const SystemVerilogSynthesizerConfiguration(), - }) { - final result = iverilogVector(module, vectors, + }) => + _SystemVerilogSimCompare.checkIverilogVector( + module, + vectors, moduleName: moduleName, dontDeleteTmpFiles: dontDeleteTmpFiles, dumpWaves: dumpWaves, iverilogExtraArgs: iverilogExtraArgs, allowWarnings: allowWarnings, maskKnownWarnings: maskKnownWarnings, + enableChecking: enableChecking, buildOnly: buildOnly, - synthesizerConfiguration: synthesizerConfiguration); - if (enableChecking) { - expect(result, true); - } - } + synthesizerConfiguration: synthesizerConfiguration, + ); /// Executes [vectors] against the Icarus Verilog simulator. static bool iverilogVector( @@ -270,177 +175,173 @@ abstract class SimCompare { bool buildOnly = false, SystemVerilogSynthesizerConfiguration synthesizerConfiguration = const SystemVerilogSynthesizerConfiguration(), - }) { - if (kIsWeb) { - // if running in web mode, then we can't run icarus verilog - return true; - } - - String signalDeclaration(String signalName, - {String Function(String original)? adjust, - String? signalTypeOverride}) { - final signal = module.signals.firstWhere((e) => e.name == signalName); - - final signalType = signalTypeOverride ?? - ((signal is LogicNet || (signal is LogicArray && signal.isNet)) - ? 'wire' - : 'logic'); - - if (adjust != null) { - signalName = adjust(signalName); - } - - if (signal is LogicArray) { - final unpackedDims = - signal.dimensions.getRange(0, signal.numUnpackedDimensions); - final packedDims = signal.dimensions - .getRange(signal.numUnpackedDimensions, signal.dimensions.length); - // ignore: prefer_interpolation_to_compose_strings - return signalType + - ' ' + - // ignore: prefer_interpolation_to_compose_strings - packedDims.map((d) => '[${d - 1}:0]').join() + - ' [${signal.elementWidth - 1}:0] $signalName' + - unpackedDims.map((d) => '[${d - 1}:0]').join(); - } else if (signal.width != 1) { - return '$signalType [${signal.width - 1}:0] $signalName'; - } else { - return '$signalType $signalName'; - } - } - - final topModule = moduleName ?? module.definitionName; - final allSignals = { - for (final v in vectors) ...v.inputValues.keys, - for (final v in vectors) ...v.expectedOutputValues.keys, - }; - - late final tbWireUniquifier = Uniquifier(); - late final alreadyMappedLogicToWires = {}; - String toTbWireName(String name) => alreadyMappedLogicToWires.putIfAbsent( - name, () => tbWireUniquifier.getUniqueName(initialName: 'wire__$name')); - - final logicToWireMapping = Map.fromEntries(vectors - .map((v) => v.inputValues.keys) - .flattened - .where((name) => module.tryInOut(name) != null) - .map((name) => MapEntry(name, toTbWireName(name)))); - - final localDeclarations = [ - ...allSignals.map((e) { - final sigDecl = signalDeclaration(e, - signalTypeOverride: - logicToWireMapping.containsKey(e) ? 'logic' : null); - return '$sigDecl;'; - }), - ...logicToWireMapping.entries.map((e) { - final logicName = e.key; - final wireName = e.value; - - final sigDecl = signalDeclaration(logicName, - adjust: toTbWireName, signalTypeOverride: 'wire'); - return '$sigDecl; assign $wireName = $logicName;'; - }), - ].join('\n'); - - final moduleConnections = - allSignals.map((e) => '.$e(${logicToWireMapping[e] ?? e})').join(', '); - final moduleInstance = '$topModule dut($moduleConnections);'; - final stimulus = vectors.map((e) => e.toTbVerilog(module)).join('\n'); - final generatedVerilog = module.generateSynth( - configuration: synthesizerConfiguration, - ); - - // so that when they run in parallel, they dont step on each other - final uniqueId = - (generatedVerilog + localDeclarations + stimulus + moduleInstance) - .hashCode; - - const dir = 'tmp_test'; - final tmpTestFile = '$dir/tmp_test$uniqueId.sv'; - final tmpOutput = '$dir/tmp_out$uniqueId'; - final tmpVcdFile = '$dir/tmp_waves_$uniqueId.vcd'; - - final waveDumpCode = ''' -\$dumpfile("$tmpVcdFile"); -\$dumpvars(0,dut); -'''; - - final testbench = [ - generatedVerilog, - 'module tb;', - localDeclarations, - moduleInstance, - 'initial begin', - if (dumpWaves) waveDumpCode, - '#1', - stimulus, - r'$finish;', // so the test doesn't run forever if there's a clock gen - 'end', - 'endmodule', - ].join('\n'); + }) => + _SystemVerilogSimCompare.iverilogVector( + module, + vectors, + moduleName: moduleName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + dumpWaves: dumpWaves, + iverilogExtraArgs: iverilogExtraArgs, + allowWarnings: allowWarnings, + maskKnownWarnings: maskKnownWarnings, + buildOnly: buildOnly, + synthesizerConfiguration: synthesizerConfiguration, + ); - Directory(dir).createSync(recursive: true); - File(tmpTestFile).writeAsStringSync(testbench); - final compileResult = Process.runSync('iverilog', - ['-g2012', '-o', tmpOutput, ...iverilogExtraArgs, tmpTestFile]); - bool printIfContentsAndCheckError(dynamic output) { - final maskedOutput = output - .toString() - .split('\n') - .where((element) => element.isNotEmpty) - .map((line) { - for (final knownWarning in _knownWarnings) { - if (knownWarning.hasMatch(line)) { - return null; - } - } - return line; - }) - .nonNulls - .join('\n'); - if (maskedOutput.isNotEmpty) { - print(maskedOutput); - } + /// Cleans up all cached SystemC executables and the precompiled header. + /// Call from `tearDownAll` in tests. + /// + /// If [keepPch] is true (the default), the precompiled header is preserved + /// for faster subsequent runs. Pass `keepPch: false` to remove everything. + static void cleanupSystemCCache({bool keepPch = true}) => + _SystemCSimCompare.cleanupSystemCCache(keepPch: keepPch); - return output.toString().contains(RegExp( - [ - 'error', - 'unable', - if (!allowWarnings) 'warning', - ].join('|'), - caseSensitive: false)); - } + /// Compiles a SystemC module into a reusable stdin-driven vector-testbench + /// executable. + /// + /// Returns a [SystemCVectorExecutable] that can be used to run multiple + /// vector sets without recompilation. Use in `setUpAll` for test groups. + /// Results are cached — calling this with the same module definition + /// returns the previously compiled binary. + static SystemCVectorExecutable? buildSystemCVectorExecutable( + Module module, { + String? moduleName, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + }) => + _SystemCSimCompare.buildSystemCVectorExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); - if (printIfContentsAndCheckError(compileResult.stdout)) { - return false; - } - if (printIfContentsAndCheckError(compileResult.stderr)) { - return false; - } + /// Legacy name for [buildSystemCVectorExecutable]. + @Deprecated('Use buildSystemCVectorExecutable instead.') + static SystemCVectorExecutable? buildSystemCExecutable( + Module module, { + String? moduleName, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + }) => + buildSystemCVectorExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); - if (!buildOnly) { - final simResult = Process.runSync('vvp', [tmpOutput]); - if (printIfContentsAndCheckError(simResult.stdout)) { - return false; - } - if (printIfContentsAndCheckError(simResult.stderr)) { - return false; - } - } + /// Runs [vectors] against a pre-compiled [SystemCVectorExecutable]. + /// + /// Returns `true` if all vectors pass. + static bool runSystemCVectors( + SystemCVectorExecutable exe, List vectors) => + _SystemCSimCompare.runSystemCVectors(exe, vectors); + + /// Convenience: runs [vectors] against a pre-compiled executable and + /// asserts the result. + static void checkSystemCVectors( + SystemCVectorExecutable exe, List vectors) => + _SystemCSimCompare.checkSystemCVectors(exe, vectors); + + /// Executes [vectors] against a SystemC simulator compiled with g++ and + /// checks that it passes (single-shot, compiles each time). + static void checkSystemCVector(Module module, List vectors, + {String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false}) => + _SystemCSimCompare.checkSystemCVector(module, vectors, + moduleName: moduleName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + buildOnly: buildOnly); + + /// Legacy API — returns bool. + static bool systemcVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false, + }) => + _SystemCSimCompare.systemcVector( + module, + vectors, + moduleName: moduleName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + buildOnly: buildOnly, + ); - if (!dontDeleteTmpFiles) { - try { - File(tmpOutput).deleteSync(); - File(tmpTestFile).deleteSync(); - if (dumpWaves) { - File(tmpVcdFile).deleteSync(); - } - } on Exception catch (e) { - print("Couldn't delete: $e"); - return false; - } - } - return true; - } + /// Runs the ROHD simulation using [stimulus], records input/output values + /// at every posedge of [clk], then replays the captured vectors through + /// the SystemC-synthesized version of [module] and compares results. + /// + /// [stimulus] is an async function that sets up and drives the simulation + /// (inject signals, register actions, etc.) but does NOT call + /// [Simulator.run] — that is done internally. + /// + /// [inputNames] and [outputNames] specify which ports to record. If null, + /// all module inputs (excluding clock) and all module outputs are used. + /// + /// Example usage with an existing test: + /// ```dart + /// await SimCompare.systemcSimCompare( + /// counter, + /// clk, + /// stimulus: () async { + /// reset.inject(1); + /// en.inject(0); + /// Simulator.registerAction(25, () { reset.put(0); en.put(1); }); + /// Simulator.setMaxSimTime(100); + /// }, + /// ); + /// ``` + static Future systemcSimCompare( + Module module, + Logic clk, { + required Future Function() stimulus, + List? inputNames, + List? outputNames, + String? clockName, + String? resetName, + bool dontDeleteTmpFiles = false, + String? systemcHome, + String? systemcLib, + }) => + _SystemCSimCompare.systemcSimCompare( + module, + clk, + stimulus: stimulus, + inputNames: inputNames, + outputNames: outputNames, + clockName: clockName, + resetName: resetName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); } diff --git a/lib/src/utilities/systemc_cosim_ffi.dart b/lib/src/utilities/systemc_cosim_ffi.dart new file mode 100644 index 000000000..9e55e5567 --- /dev/null +++ b/lib/src/utilities/systemc_cosim_ffi.dart @@ -0,0 +1,979 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_cosim_ffi.dart +// FFI-based real-time co-simulation with a SystemC compiled module. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; +import 'package:rohd/src/utilities/synchronous_propagator.dart'; +import 'package:rohd/src/utilities/web.dart'; + +// ============================================================================ +// FFI Type Definitions (using only dart:ffi built-in types) +// ============================================================================ + +typedef _DestroyDart = void Function(Pointer); + +typedef _SetInputDart = void Function(Pointer, Pointer, int); + +typedef _SetInputWideDart = void Function( + Pointer, Pointer, Pointer); + +typedef _GetOutputDart = int Function(Pointer, Pointer); + +typedef _GetOutputWideDart = Pointer Function( + Pointer, Pointer); + +typedef _AdvanceDart = void Function(Pointer, int); + +// ============================================================================ +// SystemCFfiCosim — Real-time FFI co-simulation with SystemC +// ============================================================================ + +/// A co-simulation wrapper that compiles an ROHD module's SystemC output to a +/// shared library and drives it in lock-step with the ROHD [Simulator]. +/// +/// ## How it works +/// +/// 1. The ROHD module is synthesized to SystemC C++ via +/// [Module.generateSystemC] +/// 2. A C-linkage FFI wrapper is generated around the SystemC module +/// 3. The wrapper is compiled to a `.so` shared library +/// 4. On each [Simulator] tick (at the `clkStable` phase): +/// - Current ROHD input values are pushed to SystemC via FFI +/// - SystemC is advanced by one half clock period (`sc_start`) +/// - SystemC output values are pulled back and `put()` onto ROHD outputs +/// +/// ## Timing compatibility with existing tests +/// +/// The synchronization point at `clkStable` means: +/// - `inject()` calls have already executed (mainTick phase) +/// - Clock has already toggled (mainTick) +/// - `previousValue` was snapshot at preTick (before this tick started) +/// - After clkStable, outputs are updated, then `postTick` fires +/// - `await clk.nextPosedge` resumes after postTick +/// +/// This preserves the same timing semantics as native ROHD Sequential blocks. +/// +/// ## Example +/// +/// ```dart +/// final counter = SimpleCounter(clk, reset, en); +/// await counter.build(); +/// +/// final cosim = await SystemCFfiCosim.create(counter, clk: clk); +/// if (cosim == null) return; // SystemC not installed +/// +/// // Now run your test exactly as before: +/// unawaited(Simulator.run()); +/// reset.inject(1); +/// await clk.nextPosedge; +/// // ... counter.output('val') is driven by SystemC +/// ``` +class SystemCFfiCosim { + /// The ROHD module whose SystemC synthesis is being co-simulated. + final Module module; + + /// The clock signal (null for combinational/clockless mode). + final Logic? clk; + + /// Clock period in nanoseconds (matches SimpleClockGenerator's period). + /// Ignored in combinational mode. + final int clockPeriodNs; + + /// Whether this cosim operates in combinational (clockless) mode. + /// In this mode, inputs are propagated immediately via delta cycles + /// (sc_start(SC_ZERO_TIME)) whenever any input changes. + bool get isCombinational => clk == null; + + /// Handle to the loaded shared library. + DynamicLibrary? _lib; + + /// Opaque handle to the SystemC simulation context. + Pointer _handle = nullptr; + + /// Path to the compiled .so file. + late String? _soPath; + + /// Input port names and widths. + final Map _inputWidths = {}; + + /// Output port names and widths. + final Map _outputWidths = {}; + + /// Clock port name(s) to skip when driving inputs. + final Set _clockNames = {}; + + /// Whether the cosim is actively stepping. + bool _active = false; + + /// Subscription to Simulator.clkStable for per-tick stepping (clocked mode). + StreamSubscription? _clkStableSubscription; + + /// Synchronous subscriptions on input glitches (combinational mode). + final List> + _inputGlitchSubscriptions = []; + + /// Whether a combinational step is already pending in this propagation wave. + /// Prevents re-entrant stepping when multiple inputs change in the same + /// event (e.g. Swizzle feeding a bus). + bool _combStepPending = false; + + // FFI function handles + late final _SetInputDart _setInput; + late final _SetInputWideDart _setInputWide; + late final _GetOutputDart _getOutput; + late final _GetOutputWideDart _getOutputWide; + late final _AdvanceDart _advance; + late final _DestroyDart _destroy; + + // Cached C-string pointers for signal names (allocated once, reused every + // step) + final Map> _inputNamePtrs = {}; + final Map> _outputNamePtrs = {}; + + // Cached signal references (avoid module.input/output map lookups per step) + final Map _inputSignals = {}; + final Map _outputSignals = {}; + + // Pre-allocated buffer for wide hex strings (avoids malloc/free per step). + // 512 chars covers up to 2048-bit signals. + Pointer _hexBuf = nullptr; + static const _hexBufSize = 512; + + // Native memory management + static final _free = DynamicLibrary.process().lookupFunction< + Void Function(Pointer), void Function(Pointer)>('free'); + static final _malloc = DynamicLibrary.process().lookupFunction< + Pointer Function(IntPtr), Pointer Function(int)>('malloc'); + + /// Cache of loaded libraries and handles keyed by .so path. + /// Prevents re-loading a .so that's already in the process, which + /// would crash SystemC's singleton kernel (E113). + static final _loadedLibs = {}; + + /// Deletes all `cosim_ffi_*` source and shared-library files from + /// `tmp_test/` and clears the in-process cache. + /// + /// Call from `tearDownAll` in tests to satisfy `check_tmp_test.sh`. + static void cleanupCache() { + _loadedLibs.clear(); + const dir = 'tmp_test'; + final d = Directory(dir); + if (!d.existsSync()) { + return; + } + for (final entity in d.listSync()) { + final name = entity.uri.pathSegments.last; + if (name.startsWith('cosim_ffi_') || name.startsWith('libcosim_ffi_')) { + try { + entity.deleteSync(recursive: true); + } on Exception catch (_) { + // ignore deletion errors (file may be locked or already removed) + } + } + } + } + + SystemCFfiCosim._(this.module, this.clk, {required this.clockPeriodNs}); + + /// Compiles the module's SystemC output to a shared library, loads it, + /// and begins co-simulation. + /// + /// Returns `null` if SystemC is not installed or compilation fails. + /// + /// If [clk] is provided, the cosim operates in clocked mode — stepping + /// SystemC at each clock edge via `Simulator.clkStable`. + /// + /// If [clk] is omitted (null), the cosim operates in combinational mode — + /// propagating inputs through SystemC delta cycles immediately whenever + /// any input signal changes. This gives the same semantics as native ROHD + /// [Combinational] blocks. + static Future create( + Module module, { + Logic? clk, + int clockPeriodNs = 10, + String? systemcHome, + String? systemcLib, + }) async { + if (kIsWeb) { + return null; + } + + final cosim = SystemCFfiCosim._(module, clk, clockPeriodNs: clockPeriodNs); + + if (!cosim._compileAndLoad( + systemcHome: systemcHome ?? '', + systemcLib: systemcLib ?? '', + )) { + return null; + } + + cosim + .._cachePortInfo() + .._start(); + return cosim; + } + + /// Pre-elaborates the module's SystemC code without starting co-simulation. + /// + /// Call this in `setUpAll` for every module configuration that will be + /// cosim-tested in the file. This ensures all SystemC module types are + /// instantiated during the elaboration phase (before `sc_start`), which + /// avoids E113 errors when multiple configurations are tested. + /// + /// Returns `false` if SystemC is not installed or compilation fails. + static Future preElaborate( + Module module, { + Logic? clk, + int clockPeriodNs = 10, + String? systemcHome, + String? systemcLib, + }) async { + if (kIsWeb) { + return false; + } + + final cosim = SystemCFfiCosim._(module, clk, clockPeriodNs: clockPeriodNs); + + return cosim._compileAndLoad( + systemcHome: systemcHome ?? '', + systemcLib: systemcLib ?? '', + ); + } + + /// Compiles the SystemC wrapper to .so and loads it. + /// Uses a static cache to avoid re-loading the same .so (which would + /// crash SystemC's singleton kernel with E113). + bool _compileAndLoad({ + required String systemcHome, + required String systemcLib, + }) { + final resolvedHome = _resolveHome(systemcHome); + final resolvedLib = _resolveLib(systemcLib); + if (resolvedHome == null || resolvedLib == null) { + // ignore: avoid_print + print('SystemC FFI cosim: SystemC installation not found'); + return false; + } + + // Collect port widths — treat clocks as regular 1-bit inputs driven + // manually via sc_signal (avoids sc_clock phase alignment issues + // when reusing the cached SystemC kernel across tests). + for (final entry in module.inputs.entries) { + final name = entry.key; + if (name == 'clk' || name.contains('clock')) { + _clockNames.add(name); + } + _inputWidths[name] = entry.value.width; + } + for (final entry in module.outputs.entries) { + _outputWidths[entry.key] = entry.value.width; + } + + // Generate wrapper C++ source + final generatedSC = module.generateSystemC(); + + // Compute a content hash to distinguish modules with the same + // definitionName but different logic (e.g., DAZ/FTZ variants). + // Strip non-deterministic lines (e.g. timestamps) before hashing so + // that repeated instantiations of the same module share one .so. + final stableCode = generatedSC + .split('\n') + .where((line) => !line.contains('Generation time:')) + .join('\n'); + final contentHash = stableCode.hashCode.toUnsigned(32).toRadixString(16); + final uniqueName = '${module.definitionName}_$contentHash'; + + // Rename the top-level SC_MODULE in the generated code to the unique name + // so that different logic variants don't collide in the SystemC linker. + final renamedSC = generatedSC.replaceAll(module.definitionName, uniqueName); + final wrapperSrc = _generateWrapper(renamedSC, uniqueName); + + const dir = 'tmp_test'; + Directory(dir).createSync(recursive: true); + final cacheKey = uniqueName; + final cppFile = '$dir/cosim_ffi_$cacheKey.cpp'; + _soPath = '$dir/libcosim_ffi_$cacheKey.so'; + + // Check cache — if already loaded in this process, reuse it + if (_loadedLibs.containsKey(cacheKey)) { + final cached = _loadedLibs[cacheKey]!; + _lib = cached.lib; + _handle = cached.handle; + _setInput = cached.setInput; + _setInputWide = cached.setInputWide; + _getOutput = cached.getOutput; + _getOutputWide = cached.getOutputWide; + _advance = cached.advance; + _destroy = cached.destroy; + + // Reset all inputs to 0 (including clock) so the DUT starts fresh. + // The writes are committed by the first sc_start in _step(). + // Note: _cachePortInfo() is called after this, so use temp pointers here. + for (final name in _inputWidths.keys) { + if (_inputNamePtrs.containsKey(name)) { + _setInput(_handle, _inputNamePtrs[name]!.cast(), 0); + } else { + final namePtr = _toCString(name); + _setInput(_handle, namePtr.cast(), 0); + _free(namePtr); + } + } + + return true; + } + + // Compile (only if .so doesn't exist on disk) + if (!File(_soPath!).existsSync()) { + File(cppFile).writeAsStringSync(wrapperSrc); + + final cxxStd = _detectCxxStd(resolvedLib); + final result = Process.runSync('g++', [ + '-std=$cxxStd', + '-shared', + '-fPIC', + '-O2', + '-I$resolvedHome', + '-L$resolvedLib', + '-Wl,-rpath,$resolvedLib', + '-o', + _soPath!, + cppFile, + '-lsystemc', + ]); + + if (result.exitCode != 0) { + // ignore: avoid_print + print('SystemC FFI: compilation failed:\n${result.stderr}'); + return false; + } + } + + // Load the shared library + _lib = DynamicLibrary.open(_soPath!); + + // Bind function pointers + final create = _lib!.lookupFunction Function(Pointer), + Pointer Function(Pointer)>('sc_cosim_create'); + _setInput = _lib!.lookupFunction< + Void Function(Pointer, Pointer, Uint64), + _SetInputDart>('sc_cosim_set_input'); + _setInputWide = _lib!.lookupFunction< + Void Function(Pointer, Pointer, Pointer), + _SetInputWideDart>('sc_cosim_set_input_wide'); + _getOutput = _lib!.lookupFunction< + Uint64 Function(Pointer, Pointer), + _GetOutputDart>('sc_cosim_get_output'); + _getOutputWide = _lib!.lookupFunction< + Pointer Function(Pointer, Pointer), + _GetOutputWideDart>('sc_cosim_get_output_wide'); + _advance = _lib! + .lookupFunction, Uint64), _AdvanceDart>( + 'sc_cosim_advance'); + _destroy = _lib!.lookupFunction), _DestroyDart>( + 'sc_cosim_destroy'); + + // Create the SystemC context (elaborates the design) + final namePtr = _toCString(module.definitionName); + _handle = create(namePtr.cast()); + _free(namePtr); + + if (_handle == nullptr) { + // ignore: avoid_print + print('SystemC FFI: sc_cosim_create returned null'); + return false; + } + + // Cache for reuse + _loadedLibs[cacheKey] = _LoadedCosimLib( + lib: _lib!, + handle: _handle, + setInput: _setInput, + setInputWide: _setInputWide, + getOutput: _getOutput, + getOutputWide: _getOutputWide, + advance: _advance, + destroy: _destroy, + ); + + return true; + } + + /// Pre-allocates cached C-string pointers and signal references. + /// Call once after _compileAndLoad succeeds. + void _cachePortInfo() { + for (final entry in _inputWidths.entries) { + _inputNamePtrs[entry.key] = _toCString(entry.key); + _inputSignals[entry.key] = module.input(entry.key); + } + for (final entry in _outputWidths.entries) { + _outputNamePtrs[entry.key] = _toCString(entry.key); + _outputSignals[entry.key] = module.output(entry.key); + } + // Pre-allocate hex buffer for wide signals + _hexBuf = _malloc(_hexBufSize); + } + + /// Whether an edge occurred in this tick (set by glitch listener). + bool _edgePending = false; + + /// Subscription to clock glitch for edge detection. + SynchronousSubscription? _glitchSubscription; + + /// Starts the co-simulation by hooking into the clock's glitch and + /// Simulator.clkStable — mirroring how ROHD's Sequential works. + /// + /// In clocked mode: Steps SystemC on BOTH posedge and negedge, advancing + /// by half-period each time. This keeps the SystemC clock perfectly aligned + /// with ROHD's: + /// + /// ROHD posedge → sc_start(T/2) → SystemC posedge occurs → read outputs + /// ROHD negedge → sc_start(T/2) → SystemC negedge occurs → read outputs + /// + /// In combinational mode: Listens to input signal glitches and immediately + /// propagates through SystemC via delta cycles (sc_start(0)). This gives + /// the same timing semantics as native ROHD [Combinational] blocks. + void _start() { + _active = true; + + if (isCombinational) { + _startCombinational(); + } else { + _startClocked(); + } + } + + /// Starts clocked mode — step at each clock edge via clkStable. + void _startClocked() { + // Detect any clock edge (0→1 or 1→0) by listening to the glitch stream. + _glitchSubscription = clk!.glitch.listen((event) { + if (!_active) { + return; + } + // Any valid transition on the clock (posedge or negedge) + final isPosedge = event.previousValue == LogicValue.zero && + event.newValue == LogicValue.one; + final isNegedge = event.previousValue == LogicValue.one && + event.newValue == LogicValue.zero; + if ((isPosedge || isNegedge) && !_edgePending) { + _edgePending = true; + // Wait for clkStable (all inputs settled) then step SystemC. + unawaited(Simulator.clkStable.first.then((_) { + if (!_active) { + return; + } + _edgePending = false; + _step(); + })); + } + }); + } + + /// Starts combinational mode — step on any input change (synchronous). + /// + /// Uses synchronous glitch subscriptions so that output values are + /// available immediately after `put()` — matching native ROHD behavior. + /// + /// Does NOT call sc_start here — the kernel transition from ELABORATION + /// to RUNNING is deferred to the first actual `_stepCombinational()` call. + /// This allows multiple module variants to be pre-elaborated before the + /// kernel starts (avoiding E113 errors). + void _startCombinational() { + for (final entry in _inputWidths.entries) { + final name = entry.key; + // Skip clock-like signals (shouldn't exist in combinational mode, + // but guard against it) + if (_clockNames.contains(name)) { + continue; + } + + final signal = module.input(name); + final sub = signal.glitch.listen((event) { + if (!_active) { + return; + } + if (_combStepPending) { + return; + } + _combStepPending = true; + + // Push all current inputs, advance by 1 ps (triggers delta cycles), + // and pull outputs. The _combStepPending flag prevents re-entrant + // calls during the same propagation wave. + _stepCombinational(); + _combStepPending = false; + }); + _inputGlitchSubscriptions.add(sub); + } + } + + /// One co-simulation step: push inputs, advance time, pull outputs. + void _step() { + _pushInputs(); + + // Advance SystemC to process the signal writes (delta cycle). + // We advance T/2 per edge for timing consistency. The clock signal + // is driven manually (not sc_clock), so posedge/negedge detection + // in SystemC relies on the sc_signal transitions we just wrote. + _advance(_handle, clockPeriodNs * 1000 ~/ 2); + + _pullOutputs(); + } + + /// Combinational step: push inputs, advance minimally, pull outputs. + /// + /// Advances by 1 ps — the minimum non-zero time to trigger the full + /// SystemC evaluate→update→notify loop. Per IEEE 1666 §4.3.4.2, + /// sc_start(SC_ZERO_TIME) explicitly does NOT process delta notifications, + /// so external signal writes cannot trigger SC_METHOD evaluation without + /// a non-zero time advancement. + void _stepCombinational() { + _pushInputs(); + _advance(_handle, 1); // 1 ps — minimum to trigger full eval loop + _pullOutputs(); + } + + /// Pushes all current ROHD input values to the SystemC model via FFI. + void _pushInputs() { + for (final entry in _inputWidths.entries) { + final name = entry.key; + final width = entry.value; + final signal = _inputSignals[name]!; + final val = signal.value; + + if (width <= 64) { + final intVal = val.isValid ? val.toInt() : 0; + _setInput(_handle, _inputNamePtrs[name]!.cast(), intVal); + } else { + final bigVal = + val.isValid ? val.toBigInt().toUnsigned(width) : BigInt.zero; + var hex = bigVal.toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + // Write hex into pre-allocated buffer (no malloc/free per step) + final fullHex = '0x$hex'; + final bytes = utf8.encode(fullHex); + final buf = _hexBuf.cast(); + for (var i = 0; i < bytes.length && i < _hexBufSize - 1; i++) { + (buf + i).value = bytes[i]; + } + (buf + bytes.length).value = 0; + _setInputWide(_handle, _inputNamePtrs[name]!.cast(), _hexBuf.cast()); + } + } + } + + /// Pulls all SystemC output values back to ROHD signals. + void _pullOutputs() { + for (final entry in _outputWidths.entries) { + final name = entry.key; + final width = entry.value; + final signal = _outputSignals[name]!; + + if (width <= 64) { + final intVal = _getOutput(_handle, _outputNamePtrs[name]!.cast()); + signal.put(LogicValue.ofInt(intVal, width)); + } else { + final hexCharPtr = + _getOutputWide(_handle, _outputNamePtrs[name]!.cast()); + final hexStr = _fromCString(hexCharPtr); + final bigVal = BigInt.parse( + hexStr.startsWith('0x') ? hexStr.substring(2) : hexStr, + radix: 16); + signal.put(LogicValue.of(bigVal.toUnsigned(width), width: width)); + } + } + } + + /// Stops co-simulation and releases all resources. + Future dispose() async { + _active = false; + await _clkStableSubscription?.cancel(); + _clkStableSubscription = null; + _glitchSubscription?.cancel(); + _glitchSubscription = null; + for (final sub in _inputGlitchSubscriptions) { + sub.cancel(); + } + _inputGlitchSubscriptions.clear(); + // Free cached name pointers + _inputNamePtrs.values.forEach(_free); + _inputNamePtrs.clear(); + _outputNamePtrs.values.forEach(_free); + _outputNamePtrs.clear(); + if (_hexBuf != nullptr) { + _free(_hexBuf); + _hexBuf = nullptr; + } + _inputSignals.clear(); + _outputSignals.clear(); + if (_handle != nullptr) { + _destroy(_handle); + _handle = nullptr; + } + _lib = null; + } + + // ══════════════════════════════════════════════════════════════════════ + // C++ Code Generation + // ══════════════════════════════════════════════════════════════════════ + + static void _writeCode(StringBuffer sb, String code, {String prefix = ''}) { + final lines = code.substring(1).split('\n'); + final nonEmptyLines = lines.where((line) => line.trim().isNotEmpty); + final indent = nonEmptyLines.isEmpty + ? 0 + : nonEmptyLines + .map((line) => line.length - line.trimLeft().length) + .reduce((current, next) => current < next ? current : next); + + sb.write(lines + .map((line) => + line.length < indent ? '' : '$prefix${line.substring(indent)}') + .join('\n')); + } + + /// Generates the C++ wrapper with extern "C" API around the ROHD-generated + /// SystemC module code. + String _generateWrapper(String generatedSystemC, String topModule) { + final sb = StringBuffer(); + + _writeCode(sb, ''' + // Auto-generated SystemC FFI Cosim Wrapper + // Module: $topModule + + #include + #include + #include + #include + using namespace std; + + // ═══ ROHD-Generated SystemC Module(s) ═══ + + '''); + sb.writeln(generatedSystemC); + _writeCode(sb, ''' + // ═══ FFI Cosim Context ═══ + + struct CosimContext { + '''); + + // All input signal declarations (including clocks as sc_signal) + for (final entry in _inputWidths.entries) { + final type = SystemCSynthesisResult.systemCType(entry.value); + sb.writeln(' sc_signal<$type> ${entry.key};'); + } + // Output signal declarations + for (final entry in _outputWidths.entries) { + final type = SystemCSynthesisResult.systemCType(entry.value); + sb.writeln(' sc_signal<$type> ${entry.key};'); + } + + _writeCode(sb, ''' + $topModule* dut; + }; + + extern "C" { + + // Required by SystemC linker — we never call it directly + int sc_main(int, char*[]) { return 0; } + + // Track whether the kernel has been initialized + static CosimContext* _active_ctx = nullptr; + + void* sc_cosim_create(const char* name) { + // If a context already exists (same process, new test), + // just return the existing one after resetting signals. + if (_active_ctx != nullptr) { + // Reset all input signals to 0 + '''); + + for (final entry in _inputWidths.entries) { + final type = SystemCSynthesisResult.systemCType(entry.value); + sb.writeln(' _active_ctx->${entry.key}.write($type(0));'); + } + + _writeCode(sb, ''' + return static_cast(_active_ctx); + } + + // Guard: cannot create sc_signal after kernel starts + if (sc_get_status() != SC_ELABORATION && sc_get_status() != SC_BEFORE_END_OF_ELABORATION) { + return nullptr; // E113 prevention + } + + auto* ctx = new CosimContext(); + ctx->dut = new $topModule("dut"); + '''); + + // Bind all inputs (including clocks — driven via sc_signal) + for (final name in _inputWidths.keys) { + sb.writeln(' ctx->dut->$name(ctx->$name);'); + } + // Bind outputs + for (final name in _outputWidths.keys) { + sb.writeln(' ctx->dut->$name(ctx->$name);'); + } + + _writeCode(sb, ''' + // Store context — do NOT call sc_start here. + // Deferring sc_start to the first advance allows + // multiple module types to be elaborated before + // the kernel starts (avoids E113). + _active_ctx = ctx; + return static_cast(ctx); + } + + void sc_cosim_set_input(void* handle, const char* name, uint64_t value) { + auto* ctx = static_cast(handle); + '''); + + _generateInputDispatch(sb, narrow: true); + + _writeCode(sb, ''' + } + + void sc_cosim_set_input_wide(void* handle, const char* name, const char* hex_value) { + auto* ctx = static_cast(handle); + '''); + + _generateInputDispatch(sb, narrow: false); + + _writeCode(sb, ''' + } + + uint64_t sc_cosim_get_output(void* handle, const char* name) { + auto* ctx = static_cast(handle); + '''); + + _generateOutputDispatch(sb, narrow: true); + + _writeCode(sb, ''' + return 0; + } + + const char* sc_cosim_get_output_wide(void* handle, const char* name) { + auto* ctx = static_cast(handle); + static char _buf[512]; + '''); + + _generateOutputDispatch(sb, narrow: false); + + _writeCode(sb, ''' + _buf[0] = '0'; _buf[1] = 0; + return _buf; + } + + void sc_cosim_advance(void* handle, uint64_t time_ps) { + // End elaboration on first advance (allows multiple + // module types to be instantiated before starting). + if (sc_get_status() == SC_ELABORATION) { + sc_start(SC_ZERO_TIME); + } + if (time_ps == 0) { + // Zero-time advance: process delta cycles only. + // Use SC_ZERO_TIME explicitly (some implementations + // treat sc_time(0,SC_PS) differently). + sc_start(SC_ZERO_TIME); + } else { + sc_start(sc_time(static_cast(time_ps), SC_PS)); + } + } + + void sc_cosim_destroy(void* handle) { + // Do NOT delete or sc_stop — the SystemC kernel is a + // process-wide singleton. The context is reused if + // sc_cosim_create is called again (same module). + // This avoids E113 "insert primitive channel failed". + } + + } // extern "C" + '''); + + return sb.toString(); + } + + /// Generates the if-else chain for setting input signals. + void _generateInputDispatch(StringBuffer sb, {required bool narrow}) { + var first = true; + for (final entry in _inputWidths.entries) { + final name = entry.key; + final width = entry.value; + + if (narrow && width > 64) { + continue; + } + if (!narrow && width <= 64) { + continue; + } + + final ifStr = first ? ' if' : ' } else if'; + first = false; + + sb.writeln('$ifStr (strcmp(name, "$name") == 0) {'); + if (narrow) { + final type = SystemCSynthesisResult.systemCType(width); + sb.writeln(' ctx->$name.write(static_cast<$type>(value));'); + } else { + _writeCode( + sb, + ''' + sc_biguint<$width> v(hex_value); + ctx->$name.write(v); + ''', + prefix: ' '); + } + } + if (!first) { + sb.writeln(' }'); + } + } + + /// Generates the if-else chain for reading output signals. + void _generateOutputDispatch(StringBuffer sb, {required bool narrow}) { + var first = true; + for (final entry in _outputWidths.entries) { + final name = entry.key; + final width = entry.value; + + if (narrow && width > 64) { + continue; + } + if (!narrow && width <= 64) { + continue; + } + + final ifStr = first ? ' if' : ' } else if'; + first = false; + + sb.writeln('$ifStr (strcmp(name, "$name") == 0) {'); + if (narrow) { + sb.writeln(' return static_cast(ctx->$name.read());'); + } else { + _writeCode( + sb, + ''' + sc_biguint<$width> v = ctx->$name.read(); + string s = v.to_string(SC_HEX_US); + strncpy(_buf, s.c_str(), sizeof(_buf)-1); + _buf[sizeof(_buf)-1] = 0; + return _buf; + ''', + prefix: ' '); + } + } + if (!first) { + sb.writeln(' }'); + } + } + + // ══════════════════════════════════════════════════════════════════════ + // String/Memory Utilities (no package:ffi dependency) + // ══════════════════════════════════════════════════════════════════════ + + /// Allocates a null-terminated C string from a Dart string. + static Pointer _toCString(String s) { + final bytes = utf8.encode(s); + final ptr = _malloc(bytes.length + 1); + final charPtr = ptr.cast(); + for (var i = 0; i < bytes.length; i++) { + (charPtr + i).value = bytes[i]; + } + (charPtr + bytes.length).value = 0; + return ptr; + } + + /// Reads a null-terminated C string into a Dart string. + static String _fromCString(Pointer ptr) { + final bytes = []; + var i = 0; + while (true) { + final byte = (ptr.cast() + i).value; + if (byte == 0) { + break; + } + bytes.add(byte); + i++; + } + return utf8.decode(bytes); + } + + // ══════════════════════════════════════════════════════════════════════ + // SystemC Path Resolution (mirrors SimCompare) + // ══════════════════════════════════════════════════════════════════════ + + static const _defaultHome = '/opt/systemc/include'; + static const _defaultLib = '/opt/systemc/lib'; + + static String? _resolveHome(String scHome) { + if (scHome.isNotEmpty && Directory(scHome).existsSync()) { + return scHome; + } + if (Directory(_defaultHome).existsSync()) { + return _defaultHome; + } + return null; + } + + static String? _resolveLib(String scLib) { + if (scLib.isNotEmpty && Directory(scLib).existsSync()) { + return scLib; + } + if (Directory(_defaultLib).existsSync()) { + return _defaultLib; + } + return null; + } + + static String _detectCxxStd(String scLib) { + try { + final r = Process.runSync('nm', ['-D', '$scLib/libsystemc.so']); + if (r.exitCode == 0) { + final out = r.stdout as String; + if (out.contains('cxx202002L')) { + return 'c++20'; + } + if (out.contains('cxx201703L')) { + return 'c++17'; + } + } + } on Object { + // ignore + } + return 'c++20'; + } +} + +/// Cached state for a loaded SystemC cosim shared library. +class _LoadedCosimLib { + final DynamicLibrary lib; + final Pointer handle; + final _SetInputDart setInput; + final _SetInputWideDart setInputWide; + final _GetOutputDart getOutput; + final _GetOutputWideDart getOutputWide; + final _AdvanceDart advance; + final _DestroyDart destroy; + + _LoadedCosimLib({ + required this.lib, + required this.handle, + required this.setInput, + required this.setInputWide, + required this.getOutput, + required this.getOutputWide, + required this.advance, + required this.destroy, + }); +} diff --git a/lib/src/utilities/systemc_simcompare.dart b/lib/src/utilities/systemc_simcompare.dart new file mode 100644 index 000000000..650d6be75 --- /dev/null +++ b/lib/src/utilities/systemc_simcompare.dart @@ -0,0 +1,841 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_simcompare.dart +// SystemC simulation comparison support for SimCompare. +// +// 2026 July 20 +// Author: Desmond A. Kirkpatrick + +// ignore_for_file: avoid_print + +part of 'simcompare.dart'; + +class _SystemCSimCompare { + /// The default SystemC installation path (Accellera). + static const _systemCDefaultHome = '/opt/systemc/include'; + static const _systemCDefaultLib = '/opt/systemc/lib'; + + /// Cache of compiled SystemC vector-testbench executables keyed by generated + /// code hash. + static final _compilationCache = {}; + + /// Prefix for SystemC artifacts owned by this test process. + static final String tempPrefix = + 'tmp_sc_${pid}_${DateTime.now().microsecondsSinceEpoch}_' + '${Object().hashCode}'; + + /// Path to the precompiled header, built lazily on first compilation. + static String? _pchPath; + + /// Builds the precompiled header for systemc.h if not already done. + /// Returns the directory containing systemc.h.gch, or null on failure. + /// + /// In CI, the PCH is pre-built by `tool/gh_actions/setup_systemc_pch.sh` + /// before tests run, so this just finds it on disk. Locally it builds + /// on first use (safe because local runs are typically sequential). + static String? _ensurePch(String scHome, String cxxStd) { + if (_pchPath != null) { + return _pchPath; + } + + const dir = 'tmp_test'; + const pchDir = '$dir/pch'; + const gchFile = '$pchDir/systemc.h.gch'; + + // Reuse if already on disk (pre-built by CI or a previous run) + if (File(gchFile).existsSync()) { + return _pchPath = pchDir; + } + + Directory(pchDir).createSync(recursive: true); + + // Copy the original header next to the .gch so g++ matches them + File('$scHome/systemc.h').copySync('$pchDir/systemc.h'); + + final args = [ + '-std=$cxxStd', + '-I$scHome', + '-x', + 'c++-header', + '-o', + gchFile, + '$scHome/systemc.h', + ]; + final result = Process.runSync('g++', args); + if (result.exitCode != 0) { + print('PCH compilation failed (falling back to normal headers):'); + print(result.stderr); + return null; + } + + return _pchPath = pchDir; + } + + /// Resolves SystemC home/lib paths. If explicit paths are given, uses them. + /// Otherwise uses the default Accellera install paths. + static (String?, String?) _resolveSystemCPaths(String scHome, String scLib) { + if (scHome.isNotEmpty && scLib.isNotEmpty) { + if (Directory(scHome).existsSync()) { + return (scHome, scLib); + } + return (null, null); + } + if (Directory(_systemCDefaultHome).existsSync()) { + return (_systemCDefaultHome, _systemCDefaultLib); + } + return (null, null); + } + + /// Detects the C++ standard the SystemC library was compiled with + /// by inspecting the `sc_api_version` symbol in libsystemc.so. + static String _detectCxxStandard(String scLib) { + try { + final result = Process.runSync('nm', ['-D', '$scLib/libsystemc.so']); + if (result.exitCode == 0) { + final output = result.stdout as String; + if (output.contains('cxx202002L')) { + return 'c++20'; + } + if (output.contains('cxx201703L')) { + return 'c++17'; + } + } + } on Object { + // Fall through to default + } + return 'c++20'; + } + + /// Cleans up all cached SystemC executables and the precompiled header. + /// Call from `tearDownAll` in tests. + /// + /// If [keepPch] is true (the default), the precompiled header is preserved + /// for faster subsequent runs. Pass `keepPch: false` to remove everything. + static void cleanupSystemCCache({bool keepPch = true}) { + _compilationCache.clear(); + _pchPath = null; + if (kIsWeb) { + return; + } + try { + final dir = Directory('tmp_test'); + if (dir.existsSync()) { + for (final entity in dir.listSync()) { + // Use entity.path (not entity.uri) to get the basename: Directory.uri + // always appends a trailing slash, making pathSegments.last == "". + final name = entity.path.split('/').last; + + // Remove only SystemC artifacts owned by this test process. Other + // test isolates may be compiling or running from the same tmp_test + // directory concurrently. + if (name.startsWith(tempPrefix) || name == 'Makefile_sc') { + entity.deleteSync(recursive: true); + continue; + } + + // Remove pch/ directory only when keepPch is false + if (!keepPch && entity is Directory && entity.path.endsWith('/pch')) { + entity.deleteSync(recursive: true); + continue; + } + + // Leave everything else (iverilog files from parallel tests) alone + } + } + } on Exception catch (_) {} + } + + /// Compiles a SystemC module into a reusable stdin-driven vector-testbench + /// executable. + /// + /// Returns a [SystemCVectorExecutable] that can be used to run multiple + /// vector sets without recompilation. Use in `setUpAll` for test groups. + /// Results are cached — calling this with the same module definition + /// returns the previously compiled binary. + static SystemCVectorExecutable? buildSystemCVectorExecutable( + Module module, { + String? moduleName, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + }) { + if (kIsWeb) { + return null; + } + + final scHome = systemcHome ?? ''; + final scLib = systemcLib ?? ''; + final (resolvedHome, resolvedLib) = _resolveSystemCPaths(scHome, scLib); + + if (resolvedHome == null || resolvedLib == null) { + print('SystemC installation not found'); + return null; + } + + final topModule = moduleName ?? module.definitionName; + final generatedSystemC = module.generateSystemC(); + + // Check compilation cache + final cacheKey = generatedSystemC.hashCode; + if (_compilationCache.containsKey(cacheKey)) { + final cached = _compilationCache[cacheKey]!; + if (File(cached.binaryPath).existsSync()) { + return cached; + } + // Binary was removed; recompile. + _compilationCache.remove(cacheKey); + } + + // Identify clock signals + final clockSignals = {}; + if (clockName != null) { + clockSignals.add(clockName); + } + for (final input in module.inputs.entries) { + final name = input.key; + if (clockSignals.isEmpty && (name == 'clk' || name.contains('clock'))) { + clockSignals.add(name); + } + } + final promotedClocks = {}; + for (final sub in module.subModules) { + if (sub is SimpleClockGenerator) { + final clkSigName = sub.clk.name; + promotedClocks.add(clkSigName); + clockSignals.add(clkSigName); + } + } + + // Collect ALL module ports for the stdin-driven harness + final inputPorts = {}; + for (final input in module.inputs.entries) { + if (promotedClocks.contains(input.key)) { + continue; + } + inputPorts[input.key] = input.value.width; + } + final outputPorts = {}; + for (final output in module.outputs.entries) { + outputPorts[output.key] = output.value.width; + } + final inOutPorts = {}; + for (final inOut in module.inOuts.entries) { + inOutPorts[inOut.key] = inOut.value.width; + } + + // Generate stdin-driven testbench + final tb = StringBuffer() + ..write(''' +#include +#include +#include +#include +#include +#include +using namespace std; + +''') + ..writeln(generatedSystemC) + ..write(''' +int sc_main(int argc, char* argv[]) { +'''); + + // Clock + for (final clkName in clockSignals) { + tb.writeln( + ' sc_clock $clkName("$clkName", ${Vector._period}, SC_NS);', + ); + } + + // Signals for all non-clock input ports + for (final entry in inputPorts.entries) { + if (clockSignals.contains(entry.key)) { + continue; + } + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};', + ); + } + + // Signals for all output ports + for (final entry in outputPorts.entries) { + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};', + ); + } + + // Signals for all inout ports + for (final entry in inOutPorts.entries) { + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};', + ); + } + + tb + ..writeln() + // DUT instantiation and port binding + ..writeln(' $topModule dut("dut");'); + for (final name in inputPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + for (final clkName in clockSignals) { + if (!inputPorts.containsKey(clkName)) { + tb.writeln(' dut.$clkName($clkName);'); + } + } + for (final name in outputPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + for (final name in inOutPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + + tb.write(''' + int _tb_errors = 0; + + // Initial offset + sc_start(sc_time(1, SC_NS)); + + // Read number of vectors + int _tb_nvec; + cin >> _tb_nvec; + + for (int _tb_v = 0; _tb_v < _tb_nvec; _tb_v++) { +'''); + + // Read and drive each non-clock input + final drivableInputs = + inputPorts.keys.where((k) => !clockSignals.contains(k)).toList(); + for (final name in drivableInputs) { + final w = inputPorts[name]!; + if (w > 64) { + // BigInt — read as hex string + tb + ..writeln(' { string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _v(_h.c_str());') + ..writeln(' $name.write(_v); }'); + } else { + tb + ..writeln(' { uint64_t _v; cin >> _v;') + ..writeln(' $name.write(_v); }'); + } + } + for (final entry in inOutPorts.entries) { + final name = entry.key; + final w = entry.value; + tb.writeln(' { int _drive; cin >> _drive;'); + if (w > 64) { + tb + ..writeln(' if (_drive) { string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _v(_h.c_str());') + ..writeln(' $name.write(_v); } }'); + } else { + tb + ..writeln(' if (_drive) { uint64_t _v; cin >> _v;') + ..writeln(' $name.write(_v); } }'); + } + } + + // Advance to check point + tb.write(''' + sc_start(sc_time(${Vector._offset}, SC_NS)); + + // Read number of outputs to check + int _tb_nchk; + cin >> _tb_nchk; + + for (int _tb_c = 0; _tb_c < _tb_nchk; _tb_c++) { + string _tb_pn; + cin >> _tb_pn; +'''); + + // Generate if-else chain for each output and inout port + var first = true; + final checkablePorts = {...outputPorts, ...inOutPorts}; + for (final entry in checkablePorts.entries) { + final name = entry.key; + final w = entry.value; + final ifKey = first ? 'if' : '} else if'; + first = false; + tb.writeln(' $ifKey (_tb_pn == "$name") {'); + if (w > 64) { + tb + ..writeln(' string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _tb_exp(_h.c_str());') + ..writeln(' if ($name.read() != _tb_exp) {'); + } else { + tb + ..writeln(' uint64_t _tb_exp; cin >> _tb_exp;') + ..writeln(' if ($name.read() != _tb_exp) {'); + } + tb + ..writeln( + ' cout << "ERROR vector " << _tb_v' + ' << ": expected $name=" << _tb_exp' + ' << ", got " << $name.read() << endl;', + ) + ..writeln(' _tb_errors++;') + ..writeln(' }'); + } + if (checkablePorts.isNotEmpty) { + tb + ..writeln(' } else {') + ..writeln(' string _d; cin >> _d; // skip unknown') + ..writeln(' }'); + } + + tb.write(''' + } + + sc_start(sc_time(${Vector._period - Vector._offset}, SC_NS)); + } + + if (_tb_errors == 0) { + cout << "PASS" << endl; + } else { + cout << "FAIL: " << _tb_errors << " errors" << endl; + } + return _tb_errors > 0 ? 1 : 0; +} +'''); + + final testbenchCode = tb.toString(); + + // Write and compile + const dir = 'tmp_test'; + Directory(dir).createSync(recursive: true); + final compileDir = Directory( + dir, + ).createTempSync('${tempPrefix}_${generatedSystemC.hashCode}_'); + final tmpCppFile = '${compileDir.path}/main.cpp'; + final tmpOutput = '${compileDir.path}/sim'; + File(tmpCppFile).writeAsStringSync(testbenchCode); + + // Detect C++ standard for this installation + final cxxStd = _detectCxxStandard(resolvedLib); + + // Build precompiled header on first use + final pchDir = _ensurePch(resolvedHome, cxxStd); + final pchArgs = pchDir != null ? ['-I$pchDir'] : []; + + final compileResult = Process.runSync('g++', [ + '-std=$cxxStd', + '-pipe', + ...pchArgs, + '-I$resolvedHome', + '-o', + tmpOutput, + tmpCppFile, + '-L$resolvedLib', + '-lsystemc', + ]); + if (compileResult.exitCode != 0) { + print('SystemC compilation failed:'); + print(compileResult.stdout); + print(compileResult.stderr); + return null; + } + + final exe = SystemCVectorExecutable._( + binaryPath: tmpOutput, + cppFile: tmpCppFile, + scLib: resolvedLib, + clockSignals: clockSignals, + inputPorts: inputPorts, + outputPorts: outputPorts, + inOutPorts: inOutPorts, + ); + _compilationCache[cacheKey] = exe; + return exe; + } + + /// Runs [vectors] against a pre-compiled [SystemCVectorExecutable]. + /// + /// Returns `true` if all vectors pass. + static bool runSystemCVectors( + SystemCVectorExecutable exe, List vectors) { + if (!File(exe.binaryPath).existsSync()) { + print('SystemC binary not found: ${exe.binaryPath}'); + return false; + } + + // Build stdin data + final sb = StringBuffer()..writeln(vectors.length); + + final drivableInputs = exe.inputPorts.keys + .where((k) => !exe.clockSignals.contains(k)) + .toList(); + + // Track last-driven values (persist across vectors like iverilog) + final lastValues = { + for (final name in drivableInputs) name: '0', + }; + + for (final vector in vectors) { + // Update last-driven values with this vector's inputs + for (final name in drivableInputs) { + final value = vector.inputValues[name]; + if (value != null) { + final w = exe.inputPorts[name]!; + if (w > 64) { + final lv = LogicValue.of(value, width: w); + var hex = lv.toBigInt().toUnsigned(w).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + lastValues[name] = '0x$hex'; + } else { + lastValues[name] = '${_systemcIntValue(value, w)}'; + } + } + } + // Write all input values (using persisted values for unspecified) + for (final name in drivableInputs) { + sb.write('${lastValues[name]} '); + } + for (final name in exe.inOutPorts.keys) { + final value = vector.inputValues[name]; + if (value != null) { + final w = exe.inOutPorts[name]!; + final formattedValue = w > 64 + ? _systemcHexValue(value, w) + : '${_systemcIntValue(value, w)}'; + lastValues[name] = formattedValue; + } + final lastValue = lastValues[name]; + if (lastValue == null) { + sb.write('0 '); + } else { + sb.write('1 $lastValue '); + } + } + sb.writeln(); + + // Write expected outputs: count then name/value pairs + // Skip x/z outputs + final checks = {}; + for (final entry in vector.expectedOutputValues.entries) { + final name = entry.key; + final checkablePorts = {...exe.outputPorts, ...exe.inOutPorts}; + final w = checkablePorts[name]!; + final expectedLV = LogicValue.of(entry.value, width: w); + if (expectedLV.toString().contains('x') || + expectedLV.toString().contains('z')) { + continue; + } + if (w > 64) { + checks[name] = _systemcHexValue(entry.value, w); + } else { + checks[name] = '${_systemcIntValue(entry.value, w)}'; + } + } + sb.write('${checks.length} '); + for (final entry in checks.entries) { + sb.write('${entry.key} ${entry.value} '); + } + sb.writeln(); + } + + // Write vectors to a unique temp file, redirect as stdin. + final stdinDir = Directory('tmp_test').createTempSync('sc_input_'); + final stdinFile = '${stdinDir.path}/input.txt'; + late final ProcessResult result; + try { + File(stdinFile).writeAsStringSync(sb.toString()); + + result = Process.runSync( + 'sh', + ['-c', '${exe.binaryPath} < $stdinFile'], + environment: { + 'LD_LIBRARY_PATH': exe.scLib, + 'SC_COPYRIGHT_MESSAGE': 'DISABLE', + }, + ); + } finally { + if (stdinDir.existsSync()) { + stdinDir.deleteSync(recursive: true); + } + } + + final stdout = result.stdout.toString(); + final stderr = result.stderr.toString(); + + if (stdout.isNotEmpty && !stdout.contains('PASS')) { + print(stdout); + } + if (stderr.isNotEmpty && !stderr.contains('Info:')) { + print(stderr); + } + + return stdout.contains('PASS') && !stdout.contains('FAIL'); + } + + /// Convenience: runs [vectors] against a pre-compiled executable and + /// asserts the result. + static void checkSystemCVectors( + SystemCVectorExecutable exe, List vectors) { + expect(runSystemCVectors(exe, vectors), true); + } + + /// Converts a value to an integer for stdin. + static int _systemcIntValue(dynamic value, int width) { + if (value is int) { + return value; + } + if (value is LogicValue) { + if (!value.isValid) { + return 0; + } + return value.toBigInt().toUnsigned(width).toInt(); + } + if (value is BigInt) { + return value.toUnsigned(width).toInt(); + } + if (value is String) { + final lv = LogicValue.of(value, width: width); + if (!lv.isValid) { + return 0; + } + return lv.toBigInt().toUnsigned(width).toInt(); + } + return 0; + } + + /// Converts a value to a hex string for stdin. + static String _systemcHexValue(dynamic value, int width) { + final lv = LogicValue.of(value, width: width); + var hex = lv.toBigInt().toUnsigned(width).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + return '0x$hex'; + } + + /// Executes [vectors] against a SystemC simulator compiled with g++ and + /// checks that it passes (single-shot, compiles each time). + static void checkSystemCVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false, + }) { + if (buildOnly) { + // Just verify SystemC code generation succeeds + module.generateSystemC(); + return; + } + final exe = buildSystemCVectorExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + if (exe == null) { + // SystemC not available — skip gracefully. + return; + } + final passed = runSystemCVectors(exe, vectors); + if (!dontDeleteTmpFiles) { + // Single-shot path: clean up this process's compiled artifacts now so + // tests that call checkSystemCVector do not require a tearDownAll. + // The PCH is kept to avoid rebuilding it for subsequent calls. + cleanupSystemCCache(); + } + expect(passed, true); + } + + /// Legacy API — returns bool. + static bool systemcVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false, + }) { + if (kIsWeb) { + return true; + } + final exe = buildSystemCVectorExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + if (exe == null) { + return false; + } + if (buildOnly) { + return true; + } + return runSystemCVectors(exe, vectors); + } + + /// Runs the ROHD simulation using [stimulus], records input/output values + /// at every posedge of [clk], then replays the captured vectors through + /// the SystemC-synthesized version of [module] and compares results. + static Future systemcSimCompare( + Module module, + Logic clk, { + required Future Function() stimulus, + List? inputNames, + List? outputNames, + String? clockName, + String? resetName, + bool dontDeleteTmpFiles = false, + String? systemcHome, + String? systemcLib, + }) async { + // Determine which signals to record + final clkName = clockName ?? + module.inputs.keys.firstWhere( + (n) => n == 'clk' || n.contains('clock'), + orElse: () => 'clk', + ); + + final inputs = + inputNames ?? module.inputs.keys.where((n) => n != clkName).toList(); + final outputs = outputNames ?? module.outputs.keys.toList(); + + // Record snapshots at each posedge. + // Use previousValue for outputs — this gives us the output state from + // BEFORE the clock edge, which matches what the SystemC testbench sees + // when it checks at offset (before the posedge). + // Use current value for inputs — these are the values being presented + // to the DUT when the clock edge fires. + final recordings = []; + + clk.posedge.listen((_) { + // Sample inputs (current value — what's being driven now) + final inputValues = {}; + for (final name in inputs) { + final sig = module.input(name); + final val = sig.value; + inputValues[name] = val.isValid ? val.toBigInt().toInt() : 0; + } + + // Sample outputs using previousValue — the settled output + // from before this tick started, which is what a testbench + // checking before the clock edge would observe. + final outputValues = {}; + for (final name in outputs) { + final sig = module.output(name); + final prev = sig.previousValue; + if (prev != null && prev.isValid) { + outputValues[name] = prev.toBigInt().toInt(); + } + // Skip null/x/z — no check for this output + } + + recordings.add(Vector(inputValues, outputValues)); + }); + + // Run the user's stimulus setup + await stimulus(); + + // Run the ROHD simulation + await Simulator.run(); + + if (recordings.length < 2) { + print( + 'Warning: only ${recordings.length} clock edges recorded,' + ' need at least 2 for comparison', + ); + return true; + } + + // No shifting needed — previousValue already gives us the output + // state from before the posedge, which matches systemcVector's + // check-before-edge timing. Just pass recordings directly as vectors. + + // Run through SystemC + return systemcVector( + module, + recordings, + clockName: clkName, + resetName: resetName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + } +} + +/// Holds the compiled state of a native SystemC vector-testbench executable for +/// reuse across tests. +class SystemCVectorExecutable { + /// Path to the compiled binary. + final String binaryPath; + + /// Path to the generated C++ source. + final String cppFile; + + /// Path to the SystemC library (for LD_LIBRARY_PATH). + final String scLib; + + /// Clock signal names. + final Set clockSignals; + + /// Input port names and widths (excluding promoted clocks). + final Map inputPorts; + + /// Output port names and widths. + final Map outputPorts; + + /// Inout port names and widths. + final Map inOutPorts; + + SystemCVectorExecutable._({ + required this.binaryPath, + required this.cppFile, + required this.scLib, + required this.clockSignals, + required this.inputPorts, + required this.outputPorts, + required this.inOutPorts, + }); + + /// Deletes the compiled binary and source. + void cleanup() { + void tryDelete(String path) { + final f = File(path); + if (f.existsSync()) { + f.deleteSync(); + } + } + + try { + final compileDir = File(cppFile).parent; + if (compileDir.existsSync() && + compileDir.uri.pathSegments.last.startsWith( + _SystemCSimCompare.tempPrefix, + )) { + compileDir.deleteSync(recursive: true); + return; + } + tryDelete(cppFile); + tryDelete(binaryPath); + } on Exception catch (_) {} + } +} + +/// Legacy name for [SystemCVectorExecutable]. +@Deprecated('Use SystemCVectorExecutable instead.') +typedef SystemCExecutable = SystemCVectorExecutable; diff --git a/lib/src/utilities/systemverilog_simcompare.dart b/lib/src/utilities/systemverilog_simcompare.dart new file mode 100644 index 000000000..2151d4046 --- /dev/null +++ b/lib/src/utilities/systemverilog_simcompare.dart @@ -0,0 +1,380 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemverilog_simcompare.dart +// SystemVerilog testbench generation and simulation comparison support for +// SimCompare. +// +// 2026 July 20 +// Author: Desmond A. Kirkpatrick + +// ignore_for_file: avoid_print + +part of 'simcompare.dart'; + +class _SystemVerilogVectorTestbench { + final Vector vector; + final Module module; + + _SystemVerilogVectorTestbench(this.vector, this.module); + + /// Computes a SystemVerilog code string that checks in a SystemVerilog + /// simulation whether a signal [sigName] has the [expected] value given + /// the [inputValues]. + static String _errorCheckString( + String sigName, + dynamic expected, + LogicValue expectedVal, + String inputValues, + ) { + if (expected is! int && + expected is! LogicValue && + expected is! BigInt && + expected is! String) { + throw NonSupportedTypeException(expected); + } + + String expectedHexStr; + if (expected is int) { + expectedHexStr = BigInt.from( + expected, + ).toUnsigned(expectedVal.width).toRadixString(16); + expectedHexStr = '0x$expectedHexStr'; + } else if (expected is BigInt) { + expectedHexStr = expected.toUnsigned(expectedVal.width).toRadixString(16); + expectedHexStr = '0x$expectedHexStr'; + } else { + expectedHexStr = expected.toString(); + } + + final expectedValStr = expectedVal.toString(); + + return 'if($sigName !== $expectedValStr) ' + '\$error(\$sformatf("Expected $sigName=$expectedHexStr,' + ' but found $sigName=0x%x (0b%b) with inputs $inputValues",' + ' $sigName, $sigName));'; + } + + String toTbVerilog() { + final assignments = vector.inputValues.keys.map((signalName) { + final signal = module.tryInOut(signalName) ?? module.input(signalName); + + if (signal is LogicArray) { + final arrAssigns = StringBuffer(); + var index = 0; + final fullVal = LogicValue.of( + vector.inputValues[signalName], + width: signal.width, + ); + for (final leaf in signal.leafElements) { + final subVal = fullVal.getRange(index, index + leaf.width); + arrAssigns.writeln('${leaf.structureName} = $subVal;'); + index += leaf.width; + } + return arrAssigns.toString(); + } else { + final signalVal = LogicValue.of( + vector.inputValues[signalName], + width: signal.width, + ); + return '$signalName = $signalVal;'; + } + }).join('\n'); + + final checksList = []; + for (final expectedOutput in vector.expectedOutputValues.entries) { + final outputName = expectedOutput.key; + final outputPort = + module.tryInOut(outputName) ?? module.output(outputName); + final expected = expectedOutput.value; + final expectedValue = LogicValue.of(expected, width: outputPort.width); + final inputStimulus = vector.inputValues.toString(); + + if (outputPort is LogicArray) { + var index = 0; + for (final leaf in outputPort.leafElements) { + final subVal = expectedValue.getRange(index, index + leaf.width); + checksList.add( + _errorCheckString( + leaf.structureName, + subVal, + subVal, + inputStimulus, + ), + ); + index += leaf.width; + } + } else { + checksList.add( + _errorCheckString(outputName, expected, expectedValue, inputStimulus), + ); + } + } + final checks = checksList.join('\n'); + + return [ + assignments, + '#${Vector._offset}', + checks, + '#${Vector._period - Vector._offset}', + ].join('\n'); + } +} + +class _SystemVerilogSimCompare { + /// A collection of warnings that are fine to ignore usually. + static final List _knownWarnings = [ + RegExp('sorry: Case unique/unique0 qualities are ignored.'), + RegExp( + r'sorry: constant selects in always_\* processes' + ' are not currently supported', + ), + RegExp('warning: always_comb process has no sensitivities'), + RegExp('finish called at'), + ]; + + static void checkIverilogVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + bool dumpWaves = false, + List iverilogExtraArgs = const [], + bool allowWarnings = false, + bool maskKnownWarnings = true, + bool enableChecking = true, + bool buildOnly = false, + SystemVerilogSynthesizerConfiguration synthesizerConfiguration = + const SystemVerilogSynthesizerConfiguration(), + }) { + final result = iverilogVector( + module, + vectors, + moduleName: moduleName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + dumpWaves: dumpWaves, + iverilogExtraArgs: iverilogExtraArgs, + allowWarnings: allowWarnings, + maskKnownWarnings: maskKnownWarnings, + buildOnly: buildOnly, + synthesizerConfiguration: synthesizerConfiguration, + ); + if (enableChecking) { + expect(result, true); + } + } + + static bool iverilogVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + bool dumpWaves = false, + List iverilogExtraArgs = const [], + bool allowWarnings = false, + bool maskKnownWarnings = true, + bool buildOnly = false, + SystemVerilogSynthesizerConfiguration synthesizerConfiguration = + const SystemVerilogSynthesizerConfiguration(), + }) { + if (kIsWeb) { + // if running in web mode, then we can't run icarus verilog + return true; + } + + String signalDeclaration( + String signalName, { + String Function(String original)? adjust, + String? signalTypeOverride, + }) { + final signal = module.signals.firstWhere((e) => e.name == signalName); + + final signalType = signalTypeOverride ?? + ((signal is LogicNet || (signal is LogicArray && signal.isNet)) + ? 'wire' + : 'logic'); + + if (adjust != null) { + signalName = adjust(signalName); + } + + if (signal is LogicArray) { + final unpackedDims = signal.dimensions.getRange( + 0, + signal.numUnpackedDimensions, + ); + final packedDims = signal.dimensions.getRange( + signal.numUnpackedDimensions, + signal.dimensions.length, + ); + // ignore: prefer_interpolation_to_compose_strings + return signalType + + ' ' + + // ignore: prefer_interpolation_to_compose_strings + packedDims.map((d) => '[${d - 1}:0]').join() + + ' [${signal.elementWidth - 1}:0] $signalName' + + unpackedDims.map((d) => '[${d - 1}:0]').join(); + } else if (signal.width != 1) { + return '$signalType [${signal.width - 1}:0] $signalName'; + } else { + return '$signalType $signalName'; + } + } + + final topModule = moduleName ?? module.definitionName; + final allSignals = { + for (final v in vectors) ...v.inputValues.keys, + for (final v in vectors) ...v.expectedOutputValues.keys, + }; + + late final tbWireUniquifier = Uniquifier(); + late final alreadyMappedLogicToWires = {}; + String toTbWireName(String name) => alreadyMappedLogicToWires.putIfAbsent( + name, + () => tbWireUniquifier.getUniqueName(initialName: 'wire__$name'), + ); + + final logicToWireMapping = Map.fromEntries( + vectors + .map((v) => v.inputValues.keys) + .flattened + .where((name) => module.tryInOut(name) != null) + .map((name) => MapEntry(name, toTbWireName(name))), + ); + + final localDeclarations = [ + ...allSignals.map((e) { + final sigDecl = signalDeclaration( + e, + signalTypeOverride: + logicToWireMapping.containsKey(e) ? 'logic' : null, + ); + return '$sigDecl;'; + }), + ...logicToWireMapping.entries.map((e) { + final logicName = e.key; + final wireName = e.value; + + final sigDecl = signalDeclaration( + logicName, + adjust: toTbWireName, + signalTypeOverride: 'wire', + ); + return '$sigDecl; assign $wireName = $logicName;'; + }), + ].join('\n'); + + final moduleConnections = + allSignals.map((e) => '.$e(${logicToWireMapping[e] ?? e})').join(', '); + final moduleInstance = '$topModule dut($moduleConnections);'; + final stimulus = vectors.map((e) => e.toTbVerilog(module)).join('\n'); + final generatedVerilog = module.generateSynth( + configuration: synthesizerConfiguration, + ); + + // so that when they run in parallel, they dont step on each other + final uniqueId = + (generatedVerilog + localDeclarations + stimulus + moduleInstance) + .hashCode; + + const dir = 'tmp_test'; + final tmpTestFile = '$dir/tmp_test$uniqueId.sv'; + final tmpOutput = '$dir/tmp_out$uniqueId'; + final tmpVcdFile = '$dir/tmp_waves_$uniqueId.vcd'; + + final waveDumpCode = ''' +\$dumpfile("$tmpVcdFile"); +\$dumpvars(0,dut); +'''; + + final testbench = [ + generatedVerilog, + 'module tb;', + localDeclarations, + moduleInstance, + 'initial begin', + if (dumpWaves) waveDumpCode, + '#1', + stimulus, + r'$finish;', // so the test doesn't run forever if there's a clock gen + 'end', + 'endmodule', + ].join('\n'); + + Directory(dir).createSync(recursive: true); + File(tmpTestFile).writeAsStringSync(testbench); + final compileResult = Process.runSync('iverilog', [ + '-g2012', + '-o', + tmpOutput, + ...iverilogExtraArgs, + tmpTestFile, + ]); + bool printIfContentsAndCheckError(dynamic output) { + final maskedOutput = output + .toString() + .split('\n') + .where((element) => element.isNotEmpty) + .map((line) { + for (final knownWarning in _knownWarnings) { + if (knownWarning.hasMatch(line)) { + return null; + } + } + return line; + }) + .nonNulls + .join('\n'); + if (maskedOutput.isNotEmpty) { + print(maskedOutput); + } + + return output.toString().contains( + RegExp( + ['error', 'unable', if (!allowWarnings) 'warning'].join('|'), + caseSensitive: false, + ), + ); + } + + if (printIfContentsAndCheckError(compileResult.stdout)) { + return false; + } + if (printIfContentsAndCheckError(compileResult.stderr)) { + return false; + } + + if (!buildOnly) { + final simResult = Process.runSync('vvp', [tmpOutput]); + if (printIfContentsAndCheckError(simResult.stdout)) { + return false; + } + if (printIfContentsAndCheckError(simResult.stderr)) { + return false; + } + } + + if (!dontDeleteTmpFiles) { + try { + final outFile = File(tmpOutput); + if (outFile.existsSync()) { + outFile.deleteSync(); + } + final testFile = File(tmpTestFile); + if (testFile.existsSync()) { + testFile.deleteSync(); + } + if (dumpWaves) { + final vcdFile = File(tmpVcdFile); + if (vcdFile.existsSync()) { + vcdFile.deleteSync(); + } + } + } on Exception catch (e) { + print("Couldn't delete: $e"); + return false; + } + } + return true; + } +} diff --git a/test/arithmetic_shift_right_test.dart b/test/arithmetic_shift_right_test.dart index 1d31f4a51..1ba137d1f 100644 --- a/test/arithmetic_shift_right_test.dart +++ b/test/arithmetic_shift_right_test.dart @@ -41,5 +41,6 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); } diff --git a/test/assignment_test.dart b/test/assignment_test.dart index 712ebd9ee..e9566f518 100644 --- a/test/assignment_test.dart +++ b/test/assignment_test.dart @@ -95,6 +95,7 @@ void main() { allowWarnings: true, // since always_comb has no sensitivities ); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(exampleModule, vectors); }); group('assign subset', () { @@ -110,6 +111,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('multiple bits', () async { diff --git a/test/backend_artifact_test.dart b/test/backend_artifact_test.dart new file mode 100644 index 000000000..2e6f8060e --- /dev/null +++ b/test/backend_artifact_test.dart @@ -0,0 +1,72 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// backend_artifact_test.dart +// Tests for backend-specific artifact resolution contracts. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +class _CustomArtifactModule extends Module with SystemVerilog { + @override + String? definitionVerilog(String definitionType) => + 'module $definitionType; endmodule'; + + @override + String instantiationVerilog( + String instanceType, + String instanceName, + Map ports, + ) => + '$instanceType $instanceName(.signal(${ports['signal']}));'; +} + +void main() { + group('SystemVerilog backend artifacts', () { + test('adapt custom definition and instantiation hooks', () { + final module = _CustomArtifactModule(); + + final definition = module.artifactFor( + const BackendArtifactContext.definition( + backend: EmissionBackend.systemVerilog, + definitionType: 'custom_definition', + ), + ); + final instantiation = module.artifactFor( + const BackendArtifactContext.instantiation( + backend: EmissionBackend.systemVerilog, + instanceType: 'custom_definition', + instanceName: 'custom_instance', + ports: {'signal': 'source_signal'}, + ), + ); + + expect(definition, isNotNull); + expect(definition!.backend, EmissionBackend.systemVerilog); + expect(definition.kind, BackendArtifactKind.definition); + expect(definition.contents, 'module custom_definition; endmodule'); + expect(instantiation, isNotNull); + expect(instantiation!.backend, EmissionBackend.systemVerilog); + expect(instantiation.kind, BackendArtifactKind.instantiation); + expect(instantiation.contents, + 'custom_definition custom_instance(.signal(source_signal));'); + }); + + test('do not provide SystemVerilog source to SystemC', () { + final module = _CustomArtifactModule(); + + expect( + module.artifactFor( + const BackendArtifactContext.definition( + backend: EmissionBackend.systemC, + definitionType: 'custom_definition', + ), + ), + isNull, + ); + }); + }); +} diff --git a/test/bus_test.dart b/test/bus_test.dart index 08ccb4c9b..ca5658096 100644 --- a/test/bus_test.dart +++ b/test/bus_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // bus_test.dart @@ -379,6 +379,23 @@ void main() { }); group('simcompare', () { + SystemCVectorExecutable? busSystemCExe; + + setUpAll(() async { + final gtm = BusTestModule(Logic(width: 8), Logic(width: 8)); + await gtm.build(); + busSystemCExe = SimCompare.buildSystemCVectorExecutable(gtm); + }); + + tearDownAll(SimCompare.cleanupSystemCCache); + + void checkBusSystemC(List vectors) { + final exe = busSystemCExe; + if (exe != null) { + SimCompare.checkSystemCVectors(exe, vectors); + } + } + group('const sv gen', () { test('Subset of a const', () async { final mod = ConstBusModule(0xabcd, subset: true); @@ -389,6 +406,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors, dontDeleteTmpFiles: true); }); test('Assignment of a const', () async { @@ -400,6 +418,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors, dontDeleteTmpFiles: true); final sv = mod.generateSynth(); expect(sv.contains("assign const_subset = 16'habcd;"), true); @@ -418,6 +437,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('And2Gate bus', () async { @@ -434,6 +454,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Operator indexing', () async { @@ -450,6 +471,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + checkBusSystemC(vectors); }); test('Bus shrink', () async { @@ -487,6 +509,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus reverse slice', () async { @@ -524,6 +547,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus reversed', () async { @@ -537,6 +561,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus range', () async { @@ -582,6 +607,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus swizzle', () async { @@ -597,6 +623,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus bit', () async { @@ -610,6 +637,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('add busses', () async { @@ -625,6 +653,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('expression bit select', () async { @@ -635,6 +664,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + checkBusSystemC(vectors); }); test('selectFrom and selectIndex', () async { @@ -652,6 +682,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(gtm, vectors, dontDeleteTmpFiles: true); }); test('selectFrom with default Value', () async { @@ -666,6 +697,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(gtm, vectors, dontDeleteTmpFiles: true); }); }); } diff --git a/test/collapse_test.dart b/test/collapse_test.dart index 8128eea2d..90b5dc5e2 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -61,6 +61,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('collapse pretty', () async { diff --git a/test/comparison_test.dart b/test/comparison_test.dart index 1a3e5e98c..a3e24ff25 100644 --- a/test/comparison_test.dart +++ b/test/comparison_test.dart @@ -136,6 +136,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(gtm, vectors); }); }); } diff --git a/test/conditionals_test.dart b/test/conditionals_test.dart index d35a8e5bb..37044cb38 100644 --- a/test/conditionals_test.dart +++ b/test/conditionals_test.dart @@ -490,6 +490,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -564,6 +565,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('iffblock comb', () async { @@ -578,6 +580,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('if invalid ', () async { @@ -600,6 +603,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('elseifblock comb', () async { @@ -614,6 +618,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('Conditional assign module with invalid inputs', () async { @@ -654,6 +659,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('case comb', () async { @@ -668,6 +674,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('Unique case', () async { @@ -696,6 +703,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('should return exception if a conditional is used multiple times.', @@ -717,6 +725,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test( @@ -731,6 +740,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test( @@ -745,6 +755,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test( @@ -780,6 +791,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test( diff --git a/test/counter_test.dart b/test/counter_test.dart index 8f59b58d7..3a5aedf2b 100644 --- a/test/counter_test.dart +++ b/test/counter_test.dart @@ -69,6 +69,7 @@ void main() { await SimCompare.checkFunctionalVector(counter, vectors); final simResult = SimCompare.iverilogVector(counter, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(counter, vectors); }); }); } diff --git a/test/extend_test.dart b/test/extend_test.dart index 54f608598..771d12661 100644 --- a/test/extend_test.dart +++ b/test/extend_test.dart @@ -52,6 +52,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); } test('zero extend with same width returns same thing', () async { @@ -117,6 +118,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); } test('setting with bigger number throws exception', () async { diff --git a/test/flop_test.dart b/test/flop_test.dart index 4e3def505..85d41958c 100644 --- a/test/flop_test.dart +++ b/test/flop_test.dart @@ -53,6 +53,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bit with enable', () async { @@ -74,6 +75,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus', () async { @@ -88,6 +90,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus with enable', () async { @@ -111,6 +114,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, no reset value', () async { @@ -124,6 +128,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, const reset value', () async { @@ -141,6 +146,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, logic reset value', () async { @@ -158,6 +164,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus no reset, const reset value', () async { @@ -174,6 +181,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus, enable, reset, const reset value', () async { @@ -194,6 +202,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); }); } diff --git a/test/leaf_backend_conformance_test.dart b/test/leaf_backend_conformance_test.dart new file mode 100644 index 000000000..dce98c0f4 --- /dev/null +++ b/test/leaf_backend_conformance_test.dart @@ -0,0 +1,309 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// leaf_backend_conformance_test.dart +// Tests for backend conformance of planned leaf expression emission. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_leaf_emitter.dart'; +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart'; +import 'package:rohd/src/synthesizers/utilities/leaf_cell_spec.dart'; +import 'package:test/test.dart'; + +class _BackendConformanceModule extends Module { + _BackendConformanceModule(Logic a, Logic b, Logic sel, Logic idx) { + a = addInput('a', a, width: 4); + b = addInput('b', b, width: 4); + sel = addInput('sel', sel); + idx = addInput('idx', idx, width: 2); + + final yAnd = addOutput('y_and', width: 4); + final yMux = addOutput('y_mux', width: 4); + final yPow = addOutput('y_pow', width: 4); + final yIdx = addOutput('y_idx'); + + yAnd <= a & b; + yMux <= mux(sel, a, b); + yPow <= Power(a, b).out; + yIdx <= IndexGate(a, idx).selection; + } +} + +class _InlineUnknownNand extends Module with InlineSystemVerilog { + late final Logic out; + + _InlineUnknownNand(Logic a, Logic b) { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + out = addOutput('out', width: a.width); + + // Functional behavior is arbitrary for this test; semantic leaf emission + // rejects this module because it has no backend-neutral metadata. + out <= a & b; + } + + @override + String inlineVerilog(Map inputs) => + '~(${inputs['a']} & ${inputs['b']})'; +} + +class _InlineUnknownUnaryInvert extends Module with InlineSystemVerilog { + late final Logic out; + + _InlineUnknownUnaryInvert(Logic a) { + a = addInput('a', a, width: a.width); + out = addOutput('out', width: a.width); + out <= ~a; + } + + @override + String inlineVerilog(Map inputs) => '~${inputs['a']}'; +} + +class _InlineUnknownMuxLike extends Module with InlineSystemVerilog { + late final Logic out; + + _InlineUnknownMuxLike(Logic sel, Logic a, Logic b) { + sel = addInput('sel', sel); + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + out = addOutput('out', width: a.width); + out <= mux(sel, a, b); + } + + @override + String inlineVerilog(Map inputs) => + '${inputs['sel']} ? ${inputs['b']} : ${inputs['a']}'; +} + +class _InlineSystemCOnly extends Module + with InlineSystemVerilog, SystemCInlineExpression { + late final Logic out; + + _InlineSystemCOnly(Logic dataIn) { + dataIn = addInput('dataIn', dataIn, width: dataIn.width); + out = addOutput('out', width: dataIn.width); + out <= dataIn; + } + + @override + String inlineVerilog(Map inputs) => inputs['dataIn']!; + + @override + String inlineSystemC(Map inputs) => + 'systemc_extension(${inputs['dataIn']})'; +} + +class _IncompleteBusSubsetLeaf extends Module + with InlineSystemVerilog + implements LeafCellProvider { + _IncompleteBusSubsetLeaf(Logic dataIn) { + dataIn = addInput('dataIn', dataIn, width: dataIn.width); + final out = addOutput('out', width: dataIn.width); + out <= dataIn; + } + + @override + LeafCellSpec get leafCellSpec => + const LeafCellSpec(operation: LeafOperationKind.busSubset); + + @override + String inlineVerilog(Map inputs) => + 'not_systemc(${inputs['dataIn']})'; +} + +class _BackendFallbackModule extends Module { + _BackendFallbackModule(Logic a, Logic b) { + a = addInput('a', a, width: 4); + b = addInput('b', b, width: 4); + + final y = addOutput('y', width: 4); + y <= _InlineUnknownNand(a, b).out; + } +} + +void main() { + group('Leaf backend conformance', () { + test('SystemC and planned SystemVerilog preserve key leaf semantics', + () async { + final emitter = SystemCLeafEmitter( + typeForWidth: (width) => + width <= 64 ? 'sc_uint<$width>' : 'sc_biguint<$width>', + ); + + final andGate = + And2Gate(Logic(name: 'a', width: 4), Logic(name: 'b', width: 4)); + final andExpr = emitter.expressionFor(andGate, { + andGate.inputs.keys.elementAt(0): 'a_expr', + andGate.inputs.keys.elementAt(1): 'b_expr', + }); + + final mux = Mux( + Logic(name: 'sel'), + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + ); + final muxExpr = emitter.expressionFor(mux, { + mux.inputs.keys.elementAt(0): 'sel_expr', + mux.inputs.keys.elementAt(1): 'b_expr', + mux.inputs.keys.elementAt(2): 'a_expr', + }); + + final power = + Power(Logic(name: 'a', width: 4), Logic(name: 'b', width: 4)); + final powerExpr = emitter.expressionFor(power, { + power.inputs.keys.elementAt(0): 'a_expr', + power.inputs.keys.elementAt(1): 'b_expr', + }); + + final index = + IndexGate(Logic(name: 'a', width: 4), Logic(name: 'idx', width: 2)); + final indexExpr = emitter.expressionFor(index, { + index.inputs.keys.elementAt(0): 'a_expr', + index.inputs.keys.elementAt(1): 'idx_expr', + }); + + expect(andExpr, equals('a_expr & b_expr')); + expect(muxExpr, contains('sel_expr ?')); + expect(muxExpr, contains('sc_uint<4>(a_expr)')); + expect(muxExpr, contains('sc_uint<4>(b_expr)')); + expect(powerExpr, contains('pow(')); + expect(indexExpr, equals('static_cast(a_expr[idx_expr])')); + + final mod = _BackendConformanceModule( + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + Logic(name: 'sel'), + Logic(name: 'idx', width: 2), + ); + await mod.build(); + + final planned = mod.generateSynth(); + + expect(planned, contains('assign y_and = a & b;')); + expect(planned, contains('assign y_mux = sel ? a : b;')); + expect(planned, contains('assign y_pow = {a ** b};')); + expect(planned, contains('assign y_idx = a[idx];')); + }); + + test('backends reject unknown SystemVerilog-only inline module', () async { + final systemCEmitter = SystemCLeafEmitter( + typeForWidth: (width) => + width <= 64 ? 'sc_uint<$width>' : 'sc_biguint<$width>', + ); + const systemVerilogEmitter = SystemVerilogLeafEmitter(); + + final unknown = _InlineUnknownNand( + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + ); + expect( + () => systemCEmitter.expressionFor( + unknown, + {'a': 'a_expr', 'b': 'b_expr'}, + ), + throwsA(isA()), + ); + expect( + () => systemVerilogEmitter.expressionFor( + unknown, + {'a': 'a_expr', 'b': 'b_expr'}, + ), + throwsA(isA()), + ); + + final mod = _BackendFallbackModule( + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + ); + await mod.build(); + + expect( + mod.generateSynth, + throwsA(isA()), + ); + }); + + test('unknown inline module is rejected by SystemVerilog synthesis', + () async { + final mod = _BackendFallbackModule( + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + ); + await mod.build(); + + expect( + mod.generateSynth, + throwsA(isA()), + ); + }); + + test('SystemC rejects unknown inline module matrix', () { + final emitter = SystemCLeafEmitter( + typeForWidth: (width) => + width <= 64 ? 'sc_uint<$width>' : 'sc_biguint<$width>', + ); + + final scenarios = <({ + InlineLeaf module, + Map inputs, + })>[ + ( + module: _InlineUnknownNand( + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + ), + inputs: {'a': 'a_expr', 'b': 'b_expr'}, + ), + ( + module: _InlineUnknownUnaryInvert(Logic(name: 'u', width: 5)), + inputs: {'a': 'u_expr'}, + ), + ( + module: _InlineUnknownMuxLike( + Logic(name: 'sel'), + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + ), + inputs: {'sel': 'sel_expr', 'a': 'a_expr', 'b': 'b_expr'}, + ), + ]; + + for (final scenario in scenarios) { + expect( + () => emitter.expressionFor(scenario.module, scenario.inputs), + throwsA(isA()), + ); + } + }); + + test('SystemC uses an explicit backend extension for unknown leaves', () { + final emitter = SystemCLeafEmitter( + typeForWidth: (width) => + width <= 64 ? 'sc_uint<$width>' : 'sc_biguint<$width>', + ); + final module = _InlineSystemCOnly(Logic(name: 'dataIn', width: 4)); + + expect( + emitter.expressionFor(module, {'dataIn': 'input_expr'}), + equals('systemc_extension(input_expr)'), + ); + }); + + test('SystemC rejects incomplete bus subset metadata', () { + final emitter = SystemCLeafEmitter( + typeForWidth: (width) => + width <= 64 ? 'sc_uint<$width>' : 'sc_biguint<$width>', + ); + final module = _IncompleteBusSubsetLeaf(Logic(name: 'dataIn', width: 4)); + + expect( + () => emitter.expressionFor(module, {'dataIn': 'input_expr'}), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/leaf_cell_spec_inference_test.dart b/test/leaf_cell_spec_inference_test.dart new file mode 100644 index 000000000..0a84e32de --- /dev/null +++ b/test/leaf_cell_spec_inference_test.dart @@ -0,0 +1,215 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// leaf_cell_spec_inference_test.dart +// Tests for semantic leaf-cell metadata inference. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +import 'leaf_test_module_factories.dart'; + +void main() { + group('leafCellSpecForInlineModule', () { + test('infers operation kind for simple gate', () { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final gate = And2Gate(a, b); + + final spec = leafCellSpecForInlineModule(gate); + + expect(spec, isNotNull); + expect(spec!.operation, LeafOperationKind.and); + }); + + test('includes bus subset metadata', () { + final bus = Logic(name: 'bus', width: 12); + final subset = BusSubset(bus, 9, 4); + + final spec = leafCellSpecForInlineModule(subset); + + expect(spec, isNotNull); + expect(spec!.operation, LeafOperationKind.busSubset); + expect(spec.metadata['inputWidth'], 12); + expect(spec.metadata['startIndex'], 9); + expect(spec.metadata['endIndex'], 4); + }); + + test('includes mux and replication width metadata', () { + final control = Logic(name: 'control'); + final d0 = Logic(name: 'd0', width: 5); + final d1 = Logic(name: 'd1', width: 5); + final mux = Mux(control, d1, d0); + final replication = ReplicationOp(Logic(name: 'in', width: 3), 4); + + final muxSpec = leafCellSpecForInlineModule(mux); + final replicationSpec = leafCellSpecForInlineModule(replication); + + expect(muxSpec, isNotNull); + expect(muxSpec!.operation, LeafOperationKind.mux); + expect(muxSpec.metadata['outputWidth'], 5); + + expect(replicationSpec, isNotNull); + expect(replicationSpec!.operation, LeafOperationKind.replication); + expect(replicationSpec.metadata['inputWidth'], 3); + expect(replicationSpec.metadata['outputWidth'], 12); + expect(replicationSpec.metadata['replicationCount'], 4); + }); + + test('includes width metadata for unary, shift, and swizzle', () { + final unary = AndUnary(Logic(name: 'u', width: 6)); + final shift = + LShift(Logic(name: 's', width: 9), Logic(name: 'sh', width: 4)); + final swizzle = Swizzle([ + Logic(name: 'a', width: 2), + Logic(name: 'b'), + Logic(name: 'c', width: 3), + ]); + + final unarySpec = leafCellSpecForInlineModule(unary); + final shiftSpec = leafCellSpecForInlineModule(shift); + final swizzleSpec = leafCellSpecForInlineModule(swizzle); + + expect(unarySpec, isNotNull); + expect(unarySpec!.metadata['inputWidth'], 6); + + expect(shiftSpec, isNotNull); + expect(shiftSpec!.metadata['inputWidth'], 9); + expect(shiftSpec.metadata['shiftAmountWidth'], 4); + + expect(swizzleSpec, isNotNull); + expect(swizzleSpec!.metadata['inputCount'], 3); + expect( + swizzleSpec.metadata['inputWidths'], + swizzle.inputs.values.map((input) => input.width).toList(), + ); + expect( + swizzleSpec.metadata['inputIsArrayMember'], + swizzle.inputs.values.map((input) => input.isArrayMember).toList(), + ); + expect( + swizzleSpec.metadata['inputHasUnpackedArraySource'], + [false, false, false], + ); + }); + + test('inference contract matrix across representative leaf operations', () { + final swizzle = Swizzle([ + Logic(name: 'a', width: 2), + Logic(name: 'b'), + Logic(name: 'c', width: 3), + ]); + + final scenarios = <({ + InlineLeaf module, + LeafOperationKind operation, + Map metadata, + })>[ + ( + module: NotGate(Logic(name: 'n', width: 7)), + operation: LeafOperationKind.not, + metadata: {'outputWidth': 7}, + ), + ( + module: Mux( + Logic(name: 'sel'), + Logic(name: 'd1', width: 4), + Logic(name: 'd0', width: 4), + ), + operation: LeafOperationKind.mux, + metadata: {'outputWidth': 4}, + ), + ( + module: LShift( + Logic(name: 'lhs', width: 9), + Logic(name: 'sh', width: 4), + ), + operation: LeafOperationKind.shiftLeft, + metadata: { + 'inputWidth': 9, + 'shiftAmountWidth': 4, + }, + ), + ( + module: BusSubset(Logic(name: 'bus', width: 12), 9, 4), + operation: LeafOperationKind.busSubset, + metadata: { + 'inputWidth': 12, + 'startIndex': 9, + 'endIndex': 4, + }, + ), + ( + module: ReplicationOp(Logic(name: 'in', width: 3), 4), + operation: LeafOperationKind.replication, + metadata: { + 'inputWidth': 3, + 'outputWidth': 12, + 'replicationCount': 4, + }, + ), + ( + module: Power( + Logic(name: 'base', width: 5), + Logic(name: 'exp', width: 5), + ), + operation: LeafOperationKind.power, + metadata: { + 'inputWidth': 5, + 'makeSelfDetermined': true, + }, + ), + ( + module: IndexGate( + Logic(name: 'word', width: 8), + Logic(name: 'idx', width: 3), + ), + operation: LeafOperationKind.bitIndex, + metadata: {'originalWidth': 8}, + ), + ( + module: swizzle, + operation: LeafOperationKind.swizzle, + metadata: { + 'inputCount': 3, + 'inputWidths': + swizzle.inputs.values.map((input) => input.width).toList(), + 'inputIsArrayMember': swizzle.inputs.values + .map((input) => input.isArrayMember) + .toList(), + 'inputHasUnpackedArraySource': [false, false, false], + }, + ), + ]; + + for (final scenario in scenarios) { + final spec = leafCellSpecForInlineModule(scenario.module); + + expect(spec, isNotNull); + expect(spec!.operation, scenario.operation); + + for (final entry in scenario.metadata.entries) { + expect(spec.metadata[entry.key], entry.value); + } + } + }); + + test('all known built-in inline leaf modules are inferable', () { + final modules = allKnownInlineLeafModules(); + + for (final module in modules) { + final spec = leafCellSpecForInlineModule(module); + expect( + spec, + isNotNull, + reason: 'Missing inference mapping for built-in inline module ' + '${module.runtimeType}.', + ); + } + }); + }); +} diff --git a/test/leaf_expression_plan_test.dart b/test/leaf_expression_plan_test.dart new file mode 100644 index 000000000..e7c51442f --- /dev/null +++ b/test/leaf_expression_plan_test.dart @@ -0,0 +1,183 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// leaf_expression_plan_test.dart +// Tests for normalized semantic leaf expression plans. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +import 'leaf_test_module_factories.dart'; + +void main() { + group('LeafExpressionPlan', () { + test('captures operation, metadata and ordered inputs', () { + final control = Logic(name: 'control'); + final d1 = Logic(name: 'd1', width: 4); + final d0 = Logic(name: 'd0', width: 4); + final mux = Mux(control, d1, d0); + + final plan = LeafExpressionPlan.fromInlineModule(mux, { + mux.inputs.keys.elementAt(0): 'ctrl_expr', + mux.inputs.keys.elementAt(1): 'd0_expr', + mux.inputs.keys.elementAt(2): 'd1_expr', + }); + + expect(plan.operation, LeafOperationKind.mux); + expect(plan.meta('outputWidth'), 4); + expect(plan.inputValues, ['ctrl_expr', 'd0_expr', 'd1_expr']); + }); + + test('returns null metadata for missing keys or wrong types', () { + final gate = And2Gate(Logic(name: 'a'), Logic(name: 'b')); + final plan = + LeafExpressionPlan.fromInlineModule(gate, {'in0': 'a', 'in1': 'b'}); + + expect(plan.meta('missingKey'), isNull); + expect(plan.meta('missingKey'), isNull); + }); + + test('planner contract matrix across representative leaf operations', () { + Map orderedInputs( + InlineLeaf module, + List values, + ) { + final keys = module.inputs.keys.toList(); + expect(values.length, keys.length); + return Map.fromIterables(keys, values); + } + + final mux = Mux( + Logic(name: 'sel'), + Logic(name: 'd1', width: 4), + Logic(name: 'd0', width: 4), + ); + final busSubset = BusSubset(Logic(name: 'bus', width: 12), 9, 4); + final shift = LShift( + Logic(name: 'lhs', width: 9), + Logic(name: 'sh', width: 4), + ); + final replication = ReplicationOp(Logic(name: 'rep_in', width: 3), 4); + final swizzle = Swizzle([ + Logic(name: 'a', width: 2), + Logic(name: 'b'), + Logic(name: 'c', width: 3), + ]); + + final scenarios = <({ + InlineLeaf module, + LeafOperationKind op, + List inputs, + Map metadata, + })>[ + ( + module: mux, + op: LeafOperationKind.mux, + inputs: ['sel_expr', 'd0_expr', 'd1_expr'], + metadata: {'outputWidth': 4}, + ), + ( + module: busSubset, + op: LeafOperationKind.busSubset, + inputs: ['bus_expr'], + metadata: { + 'inputWidth': 12, + 'startIndex': 9, + 'endIndex': 4, + }, + ), + ( + module: shift, + op: LeafOperationKind.shiftLeft, + inputs: ['lhs_expr', 'sh_expr'], + metadata: { + 'inputWidth': 9, + 'shiftAmountWidth': 4, + }, + ), + ( + module: replication, + op: LeafOperationKind.replication, + inputs: ['rep_expr'], + metadata: { + 'inputWidth': 3, + 'outputWidth': 12, + 'replicationCount': 4, + }, + ), + ( + module: swizzle, + op: LeafOperationKind.swizzle, + inputs: ['a_expr', 'b_expr', 'c_expr'], + metadata: { + 'inputCount': 3, + 'inputWidths': [3, 1, 2], + 'inputIsArrayMember': [false, false, false], + 'inputHasUnpackedArraySource': [false, false, false], + }, + ), + ]; + + for (final scenario in scenarios) { + final plan = LeafExpressionPlan.fromInlineModule( + scenario.module, + orderedInputs(scenario.module, scenario.inputs), + ); + + expect(plan.operation, scenario.op); + expect(plan.inputValues, scenario.inputs); + expect(plan.inputsByPort.values.toList(), scenario.inputs); + + for (final entry in scenario.metadata.entries) { + expect(plan.metadata[entry.key], entry.value); + } + } + }); + + test('plan mirrors inferred leaf spec across module matrix', () { + Map taggedInputs(InlineLeaf module) { + final mapping = {}; + for (final port in module.inputs.keys) { + mapping[port] = '${port}_expr'; + } + return mapping; + } + + final modules = representativeInlineLeafModules(); + + for (final module in modules) { + final inferred = leafCellSpecForInlineModule(module); + expect(inferred, isNotNull, + reason: 'Expected inference for ${module.runtimeType}.'); + + final inputs = taggedInputs(module); + final plan = LeafExpressionPlan.fromInlineModule(module, inputs); + + expect( + plan.operation, + inferred!.operation, + reason: 'Operation mismatch for ${module.runtimeType}.', + ); + expect( + plan.metadata, + inferred.metadata, + reason: 'Metadata mismatch for ${module.runtimeType}.', + ); + expect( + plan.inputsByPort, + inputs, + reason: 'Input map mismatch for ${module.runtimeType}.', + ); + expect( + plan.inputValues, + inputs.values.toList(), + reason: 'Input ordering mismatch for ${module.runtimeType}.', + ); + } + }); + }); +} diff --git a/test/leaf_test_module_factories.dart b/test/leaf_test_module_factories.dart new file mode 100644 index 000000000..f94f9a951 --- /dev/null +++ b/test/leaf_test_module_factories.dart @@ -0,0 +1,69 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// leaf_test_module_factories.dart +// Shared representative leaf modules for synthesis contract tests. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Representative inline leaf modules for focused contract tests. +List representativeInlineLeafModules() => [ + NotGate(Logic(name: 'n', width: 3)), + And2Gate(Logic(name: 'a', width: 4), Logic(name: 'b', width: 4)), + LShift(Logic(name: 'lhs', width: 9), Logic(name: 'sh', width: 4)), + Mux( + Logic(name: 'sel'), + Logic(name: 'd1', width: 4), + Logic(name: 'd0', width: 4), + ), + BusSubset(Logic(name: 'bus', width: 12), 9, 4), + ReplicationOp(Logic(name: 'in', width: 3), 4), + IndexGate(Logic(name: 'word', width: 8), Logic(name: 'idx', width: 3)), + Swizzle([ + Logic(name: 's0', width: 2), + Logic(name: 's1'), + Logic(name: 's2', width: 3), + ]), + ]; + +/// All known built-in inline leaf modules expected to have inference coverage. +List allKnownInlineLeafModules() => [ + NotGate(Logic(name: 'n', width: 3)), + And2Gate(Logic(name: 'a', width: 4), Logic(name: 'b', width: 4)), + Or2Gate(Logic(name: 'a', width: 4), Logic(name: 'b', width: 4)), + Xor2Gate(Logic(name: 'a', width: 4), Logic(name: 'b', width: 4)), + Subtract(Logic(name: 'a', width: 5), Logic(name: 'b', width: 5)), + Multiply(Logic(name: 'a', width: 5), Logic(name: 'b', width: 5)), + Divide(Logic(name: 'a', width: 5), Logic(name: 'b', width: 5)), + Modulo(Logic(name: 'a', width: 5), Logic(name: 'b', width: 5)), + Power(Logic(name: 'a', width: 5), Logic(name: 'b', width: 5)), + Equals(Logic(name: 'a', width: 4), Logic(name: 'b', width: 4)), + NotEquals(Logic(name: 'a', width: 4), Logic(name: 'b', width: 4)), + LessThan(Logic(name: 'a', width: 4), Logic(name: 'b', width: 4)), + GreaterThan(Logic(name: 'a', width: 4), Logic(name: 'b', width: 4)), + LessThanOrEqual(Logic(name: 'a', width: 4), Logic(name: 'b', width: 4)), + GreaterThanOrEqual( + Logic(name: 'a', width: 4), Logic(name: 'b', width: 4)), + AndUnary(Logic(name: 'u', width: 6)), + OrUnary(Logic(name: 'u', width: 6)), + XorUnary(Logic(name: 'u', width: 6)), + LShift(Logic(name: 'lhs', width: 9), Logic(name: 'sh', width: 4)), + RShift(Logic(name: 'lhs', width: 9), Logic(name: 'sh', width: 4)), + ARShift(Logic(name: 'lhs', width: 9), Logic(name: 'sh', width: 4)), + Mux( + Logic(name: 'sel'), + Logic(name: 'd1', width: 4), + Logic(name: 'd0', width: 4), + ), + IndexGate(Logic(name: 'word', width: 8), Logic(name: 'idx', width: 3)), + BusSubset(Logic(name: 'bus', width: 12), 9, 4), + Swizzle([ + Logic(name: 's0', width: 2), + Logic(name: 's1'), + Logic(name: 's2', width: 3), + ]), + ReplicationOp(Logic(name: 'in', width: 3), 4), + ]; diff --git a/test/module_emission_plan_test.dart b/test/module_emission_plan_test.dart new file mode 100644 index 000000000..c20355cf9 --- /dev/null +++ b/test/module_emission_plan_test.dart @@ -0,0 +1,45 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_emission_plan_test.dart +// Tests for backend-neutral resolved module emission plans. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +class _ModuleEmissionPlanChild extends Module { + _ModuleEmissionPlanChild(Logic input) { + input = addInput('inputValue', input, width: input.width); + final output = addOutput('outputValue', width: input.width); + output <= input; + } +} + +class _ModuleEmissionPlanFixture extends Module { + _ModuleEmissionPlanFixture(Logic input) { + input = addInput('inputValue', input, width: input.width); + final output = addOutput('outputValue', width: input.width); + output <= _ModuleEmissionPlanChild(input).output('outputValue'); + } +} + +void main() { + test('captures resolved ports and instances', () async { + final module = _ModuleEmissionPlanFixture(Logic(name: 'input', width: 4)); + await module.build(); + + final plan = + ModuleEmissionPlan.fromDefinition(SynthModuleDefinition(module)); + + expect(plan.sourceModule, same(module)); + expect(plan.inputs.map((signal) => signal.name), ['inputValue']); + expect(plan.outputs.map((signal) => signal.name), ['outputValue']); + expect(plan.inOuts, isEmpty); + expect(plan.instances, hasLength(1)); + expect(plan.instances.single.module, isA<_ModuleEmissionPlanChild>()); + }); +} diff --git a/test/net_test.dart b/test/net_test.dart index c8d15b7d7..7efe1dd4e 100644 --- a/test/net_test.dart +++ b/test/net_test.dart @@ -689,6 +689,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('build fails with missing inout port', () async { diff --git a/test/process_emission_plan_test.dart b/test/process_emission_plan_test.dart new file mode 100644 index 000000000..124438aa1 --- /dev/null +++ b/test/process_emission_plan_test.dart @@ -0,0 +1,55 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// process_emission_plan_test.dart +// Tests for backend-neutral procedural process emission plans. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +void main() { + group('ProcessEmissionPlan', () { + test('normalizes combinational process semantics', () { + final source = Logic(name: 'source', width: 4); + final destination = Logic(name: 'destination', width: 4); + final process = Combinational([destination < source]); + + final plan = ProcessEmissionPlan.fromAlways(process); + + expect(plan.kind, ProcessEmissionKind.combinational); + expect(plan.assignmentKind, ProcessAssignmentKind.blocking); + expect(plan.triggers, isEmpty); + expect(plan.hasAsyncReset, isFalse); + expect(plan.body, hasLength(1)); + expect(plan.body.single, isA()); + }); + + test('normalizes sequential edge and assignment semantics', () { + final risingClock = Logic(name: 'risingClock'); + final fallingClock = Logic(name: 'fallingClock'); + final source = Logic(name: 'source', width: 4); + final destination = Logic(name: 'destination', width: 4); + final process = Sequential.multi( + [risingClock], + [destination < source], + negedgeTriggers: [fallingClock], + ); + + final plan = ProcessEmissionPlan.fromAlways(process); + + expect(plan.kind, ProcessEmissionKind.clocked); + expect(plan.assignmentKind, ProcessAssignmentKind.nonBlocking); + expect(plan.hasAsyncReset, isFalse); + expect( + plan.triggers.map((trigger) => trigger.isPosedge), + [true, false], + ); + expect(plan.body, hasLength(1)); + expect(plan.body.single, isA()); + }); + }); +} diff --git a/test/synth_test_helpers.dart b/test/synth_test_helpers.dart new file mode 100644 index 000000000..d9f87c750 --- /dev/null +++ b/test/synth_test_helpers.dart @@ -0,0 +1,12 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_test_helpers.dart +// Shared helpers for stable synthesized-output test comparisons. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +/// Normalizes timestamped synth headers so full-output comparisons are stable. +String normalizeSynthHeader(String synth) => synth.replaceAll( + RegExp(r'Generation time:.*\n'), 'Generation time: \n'); diff --git a/test/systemc_ffi_cosim_test.dart b/test/systemc_ffi_cosim_test.dart new file mode 100644 index 000000000..5381d8f50 --- /dev/null +++ b/test/systemc_ffi_cosim_test.dart @@ -0,0 +1,266 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_ffi_cosim_test.dart +// Demonstrates FFI-based SystemC co-simulation with existing ROHD tests. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +@TestOn('vm') +@Tags(['ffi']) +library; +// ignore_for_file: avoid_print + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/systemc_cosim_ffi.dart'; +import 'package:test/test.dart'; + +// ═══════════════════════════════════════════════════════════════════════════ +// DUT: A simple counter (same as systemc_simcompare_test.dart) +// ═══════════════════════════════════════════════════════════════════════════ + +class SimpleCounter extends Module { + Logic get val => output('val'); + + SimpleCounter(Logic clk, Logic reset, Logic en) + : super(name: 'SimpleCounter') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + en = addInput('en', en); + final val = addOutput('val', width: 8); + + final nextVal = Logic(name: 'nextVal', width: 8); + + Sequential(clk, reset: reset, [ + If(en, then: [nextVal < nextVal + 1], orElse: [nextVal < nextVal]), + ]); + + val <= nextVal; + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Test that runs identically against both ROHD sim and SystemC FFI cosim +// ═══════════════════════════════════════════════════════════════════════════ + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + tearDownAll(SystemCFfiCosim.cleanupCache); + + /// The core test logic — parametrized so it can run against either the + /// native ROHD module or the SystemC FFI co-simulated module. + /// + /// [getVal] provides the output signal to check (from ROHD or cosim). + Future counterTest({ + required Logic Function() getVal, + required Logic clk, + required Logic reset, + required Logic en, + }) async { + Simulator.setMaxSimTime(200); + unawaited(Simulator.run()); + + // Reset + reset.inject(1); + en.inject(0); + await clk.nextPosedge; + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + + // Enable counting + en.inject(1); + await clk.nextPosedge; + + // After first posedge with en=1, counter should have incremented + // previousValue = 0 (value before this edge) + // value = 1 (updated at this edge) + expect(getVal().previousValue!.toInt(), 0); + expect(getVal().value.toInt(), 1); + + await clk.nextPosedge; + expect(getVal().previousValue!.toInt(), 1); + expect(getVal().value.toInt(), 2); + + await clk.nextPosedge; + expect(getVal().value.toInt(), 3); + + // Disable — counter should freeze + en.inject(0); + await clk.nextPosedge; + expect(getVal().value.toInt(), 3); + + await clk.nextPosedge; + expect(getVal().value.toInt(), 3); + + // Re-enable + en.inject(1); + await clk.nextPosedge; + expect(getVal().value.toInt(), 4); + + await Simulator.endSimulation(); + } + + // ───────────────────────────────────────────────────────────────────── + // Test 1: Pure ROHD simulation (baseline) + // ───────────────────────────────────────────────────────────────────── + + test('counter - ROHD native simulation', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final counter = SimpleCounter(clk, reset, en); + await counter.build(); + + await counterTest( + getVal: () => counter.val, + clk: clk, + reset: reset, + en: en, + ); + }); + + // ───────────────────────────────────────────────────────────────────── + // Test 2: SystemC FFI co-simulation (same test logic!) + // ───────────────────────────────────────────────────────────────────── + + test('counter - SystemC FFI cosimulation', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final counter = SimpleCounter(clk, reset, en); + await counter.build(); + + // Create the FFI cosim — this compiles the SystemC .so and hooks + // into the Simulator's clkStable phase. + final cosim = await SystemCFfiCosim.create( + counter, + clk: clk, + ); + + // If SystemC isn't installed, skip gracefully + if (cosim == null) { + print('SystemC not available — skipping FFI cosim test'); + return; + } + + try { + await counterTest( + // Use the same output signal — the cosim module puts() values + // onto it at clkStable, overriding the ROHD-computed values. + getVal: () => counter.val, + clk: clk, + reset: reset, + en: en, + ); + } finally { + await cosim.dispose(); + } + }); + + // ───────────────────────────────────────────────────────────────────── + // Test 3: Negedge checking (inject → await negedge → expect pattern) + // ───────────────────────────────────────────────────────────────────── + + /// Test logic that uses negedge for combinational settling checks. + /// Pattern: inject at posedge → await negedge (immediate next edge) → check + Future counterNegedgeTest({ + required Logic Function() getVal, + required Logic clk, + required Logic reset, + required Logic en, + }) async { + Simulator.setMaxSimTime(200); + unawaited(Simulator.run()); + + // Reset + reset.inject(1); + en.inject(0); + await clk.nextPosedge; + await clk.nextPosedge; + + // De-assert reset at posedge, check settled at negedge + reset.inject(0); + await clk.nextNegedge; // immediate next edge — no posedge in between + expect(getVal().value.toInt(), 0); // counter still 0 + + // Enable at posedge: inject en=1 at the posedge tick itself + await clk.nextPosedge; // posedge fires with en=0 (inject hasn't happened) + en.inject(1); // will take effect at NEXT mainTick + await clk.nextNegedge; // settle — en is now 1 but Sequential already + // fired at this posedge with en=0 + expect(getVal().value.toInt(), 0); // still 0 + + // Next posedge: Sequential sees en=1 + await clk.nextPosedge; + expect(getVal().value.toInt(), 1); // incremented! + + // Check at negedge: value stable between edges + await clk.nextNegedge; + expect(getVal().value.toInt(), 1); // unchanged + + // Another posedge + await clk.nextPosedge; + expect(getVal().value.toInt(), 2); + + // Disable at posedge, check at negedge + en.inject(0); + await clk.nextNegedge; + expect(getVal().value.toInt(), 2); // still 2 + + // Confirm stays 2 after next posedge with en=0 + await clk.nextPosedge; + expect(getVal().value.toInt(), 2); + + await Simulator.endSimulation(); + } + + test('counter negedge - ROHD native simulation', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final counter = SimpleCounter(clk, reset, en); + await counter.build(); + + await counterNegedgeTest( + getVal: () => counter.val, + clk: clk, + reset: reset, + en: en, + ); + }); + + test('counter negedge - SystemC FFI cosimulation', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final counter = SimpleCounter(clk, reset, en); + await counter.build(); + + final cosim = await SystemCFfiCosim.create( + counter, + clk: clk, + ); + + if (cosim == null) { + print('SystemC not available — skipping FFI cosim test'); + return; + } + + try { + await counterNegedgeTest( + getVal: () => counter.val, + clk: clk, + reset: reset, + en: en, + ); + } finally { + await cosim.dispose(); + } + }); +} diff --git a/test/systemc_leaf_emitter_test.dart b/test/systemc_leaf_emitter_test.dart new file mode 100644 index 000000000..11bf58126 --- /dev/null +++ b/test/systemc_leaf_emitter_test.dart @@ -0,0 +1,68 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_leaf_emitter_test.dart +// Tests for SystemC semantic leaf expression emission. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_leaf_emitter.dart'; +import 'package:test/test.dart'; + +class _UnknownInlineLeaf extends Module with InlineSystemVerilog { + _UnknownInlineLeaf(Logic dataIn) { + dataIn = addInput('dataIn', dataIn, width: dataIn.width); + final out = addOutput('out', width: dataIn.width); + out <= dataIn; + } + + @override + String inlineVerilog(Map inputs) => + 'not_systemc(${inputs['dataIn']})'; +} + +void main() { + final emitter = SystemCLeafEmitter( + typeForWidth: (width) => + width <= 64 ? 'sc_uint<$width>' : 'sc_biguint<$width>', + ); + + group('SystemCLeafEmitter', () { + test('renders inferred semantic operations', () { + final andGate = + And2Gate(Logic(name: 'a', width: 4), Logic(name: 'b', width: 4)); + final mux = Mux( + Logic(name: 'sel'), + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + ); + final subset = BusSubset(Logic(name: 'data', width: 8), 2, 5); + + expect(emitter.expressionFor(andGate, {'a': 'a_expr', 'b': 'b_expr'}), + equals('a_expr & b_expr')); + expect( + emitter.expressionFor(mux, { + 'sel': 'sel_expr', + 'd0': 'a_expr', + 'd1': 'b_expr', + }), + equals('sel_expr ? sc_uint<4>(b_expr) : sc_uint<4>(a_expr)'), + ); + expect( + emitter.expressionFor(subset, {'original_data': 'data_expr'}), + equals('sc_uint<4>(data_expr.range(5, 2))'), + ); + }); + + test('rejects an inline SystemVerilog-only leaf', () { + final module = _UnknownInlineLeaf(Logic(name: 'dataIn', width: 4)); + + expect( + () => emitter.expressionFor(module, {'dataIn': 'input_expr'}), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/systemc_simcompare_test.dart b/test/systemc_simcompare_test.dart new file mode 100644 index 000000000..9374b479a --- /dev/null +++ b/test/systemc_simcompare_test.dart @@ -0,0 +1,259 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_simcompare_test.dart +// Tests for SystemC synthesis and simulation comparison. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +/// A simple module with basic gates for testing SystemC synthesis. +class GateModule extends Module { + GateModule(Logic a, Logic b) : super(name: 'GateModule') { + a = addInput('a', a); + b = addInput('b', b); + final aAndB = addOutput('a_and_b'); + final aOrB = addOutput('a_or_b'); + final notA = addOutput('not_a'); + + aAndB <= a & b; + aOrB <= a | b; + notA <= ~a; + } +} + +/// A simple counter for testing sequential SystemC synthesis. +class SimpleCounter extends Module { + SimpleCounter(Logic clk, Logic reset, Logic en) : super(name: 'Counter') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + en = addInput('en', en); + final val = addOutput('val', width: 8); + + final nextVal = Logic(name: 'nextVal', width: 8); + + Sequential(clk, reset: reset, [ + If(en, then: [nextVal < nextVal + 1], orElse: [nextVal < nextVal]), + ]); + + val <= nextVal; + } +} + +/// A flip-flop module for testing. +class FlopModule extends Module { + FlopModule(Logic clk, Logic reset, Logic d) : super(name: 'FlopModule') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + d = addInput('d', d, width: 8); + final q = addOutput('q', width: 8); + q <= flop(clk, d, reset: reset); + } +} + +/// A flip-flop with enable. +class FlopEnModule extends Module { + FlopEnModule(Logic clk, Logic reset, Logic en, Logic d) + : super(name: 'FlopEnModule') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + en = addInput('en', en); + d = addInput('d', d, width: 8); + final q = addOutput('q', width: 8); + q <= flop(clk, d, reset: reset, en: en); + } +} + +/// A chained combinational module for checking generated sensitivity lists. +class ChainedGateModule extends Module { + ChainedGateModule(Logic a, Logic b, Logic c) + : super(name: 'ChainedGateModule') { + a = addInput('a', a); + b = addInput('b', b); + c = addInput('c', c); + final y = addOutput('y'); + + final mid = (a & b).named('mid'); + y <= mid | c; + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + tearDownAll(SimCompare.cleanupSystemCCache); + + group('SimCompare SystemC', () { + test('gate module passes vectors', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = GateModule(a, b); + await mod.build(); + + final vectors = [ + Vector({'a': 0, 'b': 0}, {'a_and_b': 0, 'a_or_b': 0, 'not_a': 1}), + Vector({'a': 1, 'b': 0}, {'a_and_b': 0, 'a_or_b': 1, 'not_a': 0}), + Vector({'a': 0, 'b': 1}, {'a_and_b': 0, 'a_or_b': 1, 'not_a': 1}), + Vector({'a': 1, 'b': 1}, {'a_and_b': 1, 'a_or_b': 1, 'not_a': 0}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('counter module passes vectors', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final mod = SimpleCounter(clk, reset, en); + await mod.build(); + + // Same vectors as counter_test.dart (iverilog-compatible timing) + final vectors = [ + Vector({'en': 0, 'reset': 0}, {}), + Vector({'en': 0, 'reset': 1}, {'val': 0}), + Vector({'en': 1, 'reset': 1}, {'val': 0}), + Vector({'en': 1, 'reset': 0}, {'val': 0}), + Vector({'en': 1, 'reset': 0}, {'val': 1}), + Vector({'en': 1, 'reset': 0}, {'val': 2}), + Vector({'en': 1, 'reset': 0}, {'val': 3}), + Vector({'en': 0, 'reset': 0}, {'val': 4}), + Vector({'en': 0, 'reset': 0}, {'val': 4}), + Vector({'en': 1, 'reset': 0}, {'val': 4}), + Vector({'en': 0, 'reset': 0}, {'val': 5}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('flip-flop module passes vectors', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final d = Logic(name: 'd', width: 8); + final mod = FlopModule(clk, reset, d); + await mod.build(); + + // Flop: output follows input with 1-cycle latency + final vectors = [ + Vector({'d': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0xAA, 'reset': 0}, {'q': 0}), + Vector({'d': 0xBB, 'reset': 0}, {'q': 0xAA}), + Vector({'d': 0xCC, 'reset': 0}, {'q': 0xBB}), + Vector({'d': 0xDD, 'reset': 0}, {'q': 0xCC}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('flip-flop with enable passes vectors', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final d = Logic(name: 'd', width: 8); + final mod = FlopEnModule(clk, reset, en, d); + await mod.build(); + + // When en=0, q holds; when en=1, q follows d with 1-cycle latency + final vectors = [ + Vector({'d': 0, 'en': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0, 'en': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0x42, 'en': 1, 'reset': 0}, {'q': 0}), + Vector({'d': 0x55, 'en': 1, 'reset': 0}, {'q': 0x42}), + Vector({'d': 0xFF, 'en': 0, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0x00, 'en': 0, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0x99, 'en': 1, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0xAA, 'en': 1, 'reset': 0}, {'q': 0x99}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('independent inline gates share a method', () async { + final mod = GateModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final systemc = mod.generateSystemC(); + final assignMethods = + RegExp(r'SC_METHOD\(assign_\d+\);').allMatches(systemc).toList(); + + expect(assignMethods, hasLength(1)); + expect(systemc, contains('sensitive << a;')); + expect(systemc, contains('sensitive << b;')); + expect(systemc, contains('a_and_b = a.read() & b.read();')); + expect(systemc, contains('a_or_b = a.read() | b.read();')); + expect(systemc, contains('not_a = !a.read();')); + }); + + test('chained inline gates use minimal sensitivity lists', () async { + final mod = ChainedGateModule( + Logic(name: 'a'), + Logic(name: 'b'), + Logic(name: 'c'), + ); + await mod.build(); + + final systemc = mod.generateSystemC(); + final sensitivityBlocks = + RegExp(r'SC_METHOD\(assign_\d+\);\n((?: sensitive << .+;\n)+)') + .allMatches(systemc) + .map((match) => match.group(1)!) + .toList(); + + expect(sensitivityBlocks, hasLength(2)); + expect( + sensitivityBlocks, + contains(allOf( + contains('sensitive << a;'), + contains('sensitive << b;'), + isNot(contains('sensitive << mid;')), + )), + ); + expect( + sensitivityBlocks, + contains(allOf( + contains('sensitive << mid;'), + contains('sensitive << c;'), + isNot(contains('sensitive << a;')), + isNot(contains('sensitive << b;')), + )), + ); + }); + + test('counter trace-based comparison', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final mod = SimpleCounter(clk, reset, en); + await mod.build(); + + // Use the trace-based approach: just write normal simulation code, + // no vectors needed. The method records all I/O at every clock edge + // and replays through SystemC. + final result = await SimCompare.systemcSimCompare( + mod, + clk, + stimulus: () async { + reset.inject(1); + en.inject(0); + Simulator.registerAction(25, () { + reset.put(0); + en.put(1); + }); + Simulator.registerAction(65, () { + en.put(0); + }); + Simulator.registerAction(85, () { + en.put(1); + }); + Simulator.setMaxSimTime(120); + }, + ); + expect(result, isTrue); + }); + }); +} diff --git a/test/systemverilog_leaf_plan_option_test.dart b/test/systemverilog_leaf_plan_option_test.dart new file mode 100644 index 000000000..4c86addd8 --- /dev/null +++ b/test/systemverilog_leaf_plan_option_test.dart @@ -0,0 +1,520 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemverilog_leaf_plan_option_test.dart +// Tests for SystemVerilog leaf expression plan rendering configuration. +// +// 2026 July +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import 'synth_test_helpers.dart'; + +class _InlineOpsModule extends Module { + _InlineOpsModule(Logic a, Logic b, Logic control) { + a = addInput('a', a, width: 4); + b = addInput('b', b, width: 4); + control = addInput('control', control); + + final yAnd = addOutput('y_and', width: 4); + final yNot = addOutput('y_not', width: 4); + final yMux = addOutput('y_mux', width: 4); + + yAnd <= a & b; + yNot <= ~a; + yMux <= mux(control, a, b); + } +} + +class _InlineRangeReplicationModule extends Module { + _InlineRangeReplicationModule(Logic a) { + a = addInput('a', a, width: 8); + + final ySubset = addOutput('y_subset', width: 4); + final yRep = addOutput('y_rep', width: 12); + final ySwizzle = addOutput('y_swizzle', width: 8); + + final subsetUpper = BusSubset(a, 5, 2).subset; + final subsetLower = BusSubset(a, 3, 0).subset; + ySubset <= subsetUpper; + yRep <= ReplicationOp(subsetLower, 3).replicated; + ySwizzle <= Swizzle([subsetUpper, subsetLower]).out; + } +} + +class _InlinePowerIndexModule extends Module { + _InlinePowerIndexModule(Logic a, Logic b, Logic idx) { + a = addInput('a', a, width: 4); + b = addInput('b', b, width: 4); + idx = addInput('idx', idx, width: 2); + + final yPow = addOutput('y_pow', width: 4); + final yIdx = addOutput('y_idx'); + + yPow <= Power(a, b).out; + yIdx <= IndexGate(a, idx).selection; + } +} + +class _InlineSingleBitEdgesModule extends Module { + _InlineSingleBitEdgesModule(Logic a, Logic scalar, Logic idx) { + a = addInput('a', a, width: 4); + scalar = addInput('scalar', scalar); + idx = addInput('idx', idx, width: 2); + + final ySubsetSingle = addOutput('y_subset_single'); + final yIdxSingle = addOutput('y_idx_single'); + + ySubsetSingle <= BusSubset(a, 2, 2).subset; + yIdxSingle <= IndexGate(scalar, idx).selection; + } +} + +class _InlineSwizzleZeroWidthModule extends Module { + _InlineSwizzleZeroWidthModule(Logic a) { + a = addInput('a', a, width: 4); + + final ySwizzleZero = addOutput('y_swizzle_zero', width: 4); + ySwizzleZero <= Swizzle([a, Const(0, width: 0)]).out; + } +} + +class _InlineSwizzleCollapsedSelectsModule extends Module { + _InlineSwizzleCollapsedSelectsModule(Logic a) { + a = addInput('a', a, width: 8); + + final ySwizzleCollapse = addOutput('y_swizzle_collapse', width: 3); + ySwizzleCollapse <= + Swizzle([ + BusSubset(a, 7, 7).subset, + BusSubset(a, 6, 6).subset, + BusSubset(a, 5, 5).subset, + ]).out; + } +} + +class _InlineSwizzlePartialCollapseModule extends Module { + _InlineSwizzlePartialCollapseModule(Logic a) { + a = addInput('a', a, width: 8); + + final ySwizzlePartial = addOutput('y_swizzle_partial', width: 3); + ySwizzlePartial <= + Swizzle([ + BusSubset(a, 7, 7).subset, + BusSubset(a, 6, 6).subset, + BusSubset(a, 4, 4).subset, + ]).out; + } +} + +class _InlineSwizzleAscendingSelectsModule extends Module { + _InlineSwizzleAscendingSelectsModule(Logic a) { + a = addInput('a', a, width: 8); + + final ySwizzleAscending = addOutput('y_swizzle_ascending', width: 3); + ySwizzleAscending <= + Swizzle([ + BusSubset(a, 5, 5).subset, + BusSubset(a, 6, 6).subset, + BusSubset(a, 7, 7).subset, + ]).out; + } +} + +class _InlineSwizzleMultiSourceCollapseModule extends Module { + _InlineSwizzleMultiSourceCollapseModule(Logic a, Logic b) { + a = addInput('a', a, width: 8); + b = addInput('b', b, width: 8); + + final ySwizzleMultiSource = addOutput('y_swizzle_multi_source', width: 4); + ySwizzleMultiSource <= + Swizzle([ + BusSubset(a, 7, 7).subset, + BusSubset(a, 6, 6).subset, + BusSubset(b, 3, 3).subset, + BusSubset(b, 2, 2).subset, + ]).out; + } +} + +class _InlineSwizzleUnpackedArrayElementsModule extends Module { + _InlineSwizzleUnpackedArrayElementsModule(LogicArray arr) { + final inArr = addInputArray( + 'arr', + arr, + dimensions: [4], + numUnpackedDimensions: 1, + ); + addOutput('y_swizzle_unpacked', width: 4) <= + inArr.elements.reversed.toList().swizzle(); + } +} + +class _InlineMixedOptionGateModule extends Module { + _InlineMixedOptionGateModule(Logic a, Logic b, Logic control, Logic idx) { + a = addInput('a', a, width: 8); + b = addInput('b', b, width: 8); + control = addInput('control', control); + idx = addInput('idx', idx, width: 3); + + final yAnd = addOutput('y_and', width: 8); + final yMux = addOutput('y_mux', width: 8); + final yPow = addOutput('y_pow', width: 8); + final yIdx = addOutput('y_idx'); + final ySwizzle = addOutput('y_swizzle', width: 8); + + yAnd <= a & b; + yMux <= mux(control, a, b); + yPow <= Power(a, b).out; + yIdx <= IndexGate(a, idx).selection; + ySwizzle <= Swizzle([a.slice(7, 4), a.slice(3, 0)]).out; + } +} + +void main() { + test('leaf-expression-plan inline rendering is the default', () async { + final mod = _InlineOpsModule( + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + Logic(name: 'control'), + ); + await mod.build(); + + final baseline = mod.generateSynth(); + final planned = mod.generateSynth(); + + expect(baseline, contains('assign y_and = a & b;')); + expect(baseline, contains('assign y_not = ~a;')); + expect(baseline, contains('assign y_mux = control ? a : b;')); + + expect(planned, contains('assign y_and = a & b;')); + expect(planned, contains('assign y_not = ~a;')); + expect(planned, contains('assign y_mux = control ? a : b;')); + }); + + test('planned path preserves inline output for range/replication/swizzle', + () async { + final mod = _InlineRangeReplicationModule(Logic(name: 'a', width: 8)); + await mod.build(); + + final baseline = mod.generateSynth(); + final planned = mod.generateSynth(); + + expect( + baseline, + contains(RegExp(r'assign y_subset = \{a\[2\],a\[3\],a\[4\],a\[5\]\};')), + ); + expect( + baseline, + contains(RegExp(r'assign y_rep = \{3\{_subset_0_3_a\}\};')), + ); + expect( + baseline, + contains(RegExp(r'assign y_swizzle = \{\s*y_subset, /\* 7:4 \*/')), + ); + + expect( + planned, + contains(RegExp(r'assign y_subset = \{a\[2\],a\[3\],a\[4\],a\[5\]\};')), + ); + expect( + planned, + contains(RegExp(r'assign y_rep = \{3\{_subset_0_3_a\}\};')), + ); + expect( + planned, + contains(RegExp(r'assign y_swizzle = \{\s*y_subset, /\* 7:4 \*/')), + ); + expect( + normalizeSynthHeader(planned), + equals(normalizeSynthHeader(baseline)), + ); + }); + + test('planned path preserves inline output for power/index', () async { + final mod = _InlinePowerIndexModule( + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + Logic(name: 'idx', width: 2), + ); + await mod.build(); + + final baseline = mod.generateSynth(); + final planned = mod.generateSynth(); + + expect(baseline, contains(RegExp(r'assign y_pow = \{a \*\* b\};'))); + expect(baseline, contains(RegExp(r'assign y_idx = a\[idx\];'))); + + expect(planned, contains(RegExp(r'assign y_pow = \{a \*\* b\};'))); + expect(planned, contains(RegExp(r'assign y_idx = a\[idx\];'))); + expect( + normalizeSynthHeader(planned), + equals(normalizeSynthHeader(baseline)), + ); + }); + + test('planned path preserves inline output for single-bit edge cases', + () async { + final mod = _InlineSingleBitEdgesModule( + Logic(name: 'a', width: 4), + Logic(name: 'scalar'), + Logic(name: 'idx', width: 2), + ); + await mod.build(); + + final baseline = mod.generateSynth(); + final planned = mod.generateSynth(); + + expect(baseline, contains('assign y_subset_single = a[2];')); + expect(baseline, contains('assign y_idx_single = scalar;')); + + expect(planned, contains('assign y_subset_single = a[2];')); + expect(planned, contains('assign y_idx_single = scalar;')); + expect( + normalizeSynthHeader(planned), + equals(normalizeSynthHeader(baseline)), + ); + }); + + test('planned path preserves swizzle output with zero-width input', () async { + final mod = _InlineSwizzleZeroWidthModule(Logic(name: 'a', width: 4)); + await mod.build(); + + final baseline = mod.generateSynth(); + final planned = mod.generateSynth(); + + expect(baseline, contains('assign y_swizzle_zero = a;')); + expect(planned, contains('assign y_swizzle_zero = a;')); + expect( + normalizeSynthHeader(planned), + equals(normalizeSynthHeader(baseline)), + ); + }); + + test('planned path preserves swizzle contiguous-select collapsing', () async { + final mod = _InlineSwizzleCollapsedSelectsModule( + Logic(name: 'a', width: 8), + ); + await mod.build(); + + final baseline = mod.generateSynth(); + final planned = mod.generateSynth(); + + expect(baseline, contains('assign y_swizzle_collapse = a[7:5];')); + expect(planned, contains('assign y_swizzle_collapse = a[7:5];')); + expect( + normalizeSynthHeader(planned), + equals(normalizeSynthHeader(baseline)), + ); + }); + + test('planned path preserves swizzle partial contiguous-collapse', () async { + final mod = _InlineSwizzlePartialCollapseModule( + Logic(name: 'a', width: 8), + ); + await mod.build(); + + final baseline = mod.generateSynth(); + final planned = mod.generateSynth(); + + expect(baseline, contains('a[7:6]')); + expect(baseline, contains('a[4]')); + expect(planned, contains('a[7:6]')); + expect(planned, contains('a[4]')); + expect( + normalizeSynthHeader(planned), + equals(normalizeSynthHeader(baseline)), + ); + }); + + test('planned path preserves non-collapsible ascending swizzle order', + () async { + final mod = _InlineSwizzleAscendingSelectsModule( + Logic(name: 'a', width: 8), + ); + await mod.build(); + + final baseline = mod.generateSynth(); + final planned = mod.generateSynth(); + + expect(baseline, contains('a[5]')); + expect(baseline, contains('a[6]')); + expect(baseline, contains('a[7]')); + expect(baseline, isNot(contains('a[7:5]'))); + + expect(planned, contains('a[5]')); + expect(planned, contains('a[6]')); + expect(planned, contains('a[7]')); + expect(planned, isNot(contains('a[7:5]'))); + expect( + normalizeSynthHeader(planned), + equals(normalizeSynthHeader(baseline)), + ); + }); + + test('planned path preserves per-source swizzle collapsing', () async { + final mod = _InlineSwizzleMultiSourceCollapseModule( + Logic(name: 'a', width: 8), + Logic(name: 'b', width: 8), + ); + await mod.build(); + + final baseline = mod.generateSynth(); + final planned = mod.generateSynth(); + + expect(baseline, contains('a[7:6]')); + expect(baseline, contains('b[3:2]')); + expect(planned, contains('a[7:6]')); + expect(planned, contains('b[3:2]')); + expect( + normalizeSynthHeader(planned), + equals(normalizeSynthHeader(baseline)), + ); + }); + + test('planned path preserves unpacked-array swizzle non-collapse', () async { + final mod = _InlineSwizzleUnpackedArrayElementsModule( + LogicArray([4], 1, numUnpackedDimensions: 1), + ); + await mod.build(); + + final baseline = mod.generateSynth(); + final planned = mod.generateSynth(); + + expect(baseline, contains('arr[3]')); + expect(baseline, contains('arr[2]')); + expect(baseline, contains('arr[1]')); + expect(baseline, contains('arr[0]')); + expect(baseline, isNot(contains('arr[3:0]'))); + + expect(planned, contains('arr[3]')); + expect(planned, contains('arr[2]')); + expect(planned, contains('arr[1]')); + expect(planned, contains('arr[0]')); + expect(planned, isNot(contains('arr[3:0]'))); + expect( + normalizeSynthHeader(planned), + equals(normalizeSynthHeader(baseline)), + ); + }); + + test('planned path preserves swizzle parity matrix', () async { + final scenarios = <({ + String name, + Module Function() build, + List contains, + List notContains, + })>[ + ( + name: 'zero-width filtered', + build: () => _InlineSwizzleZeroWidthModule( + Logic(name: 'a', width: 4), + ), + contains: ['assign y_swizzle_zero = a;'], + notContains: const [], + ), + ( + name: 'contiguous collapse', + build: () => _InlineSwizzleCollapsedSelectsModule( + Logic(name: 'a', width: 8), + ), + contains: ['assign y_swizzle_collapse = a[7:5];'], + notContains: const [], + ), + ( + name: 'partial collapse', + build: () => _InlineSwizzlePartialCollapseModule( + Logic(name: 'a', width: 8), + ), + contains: ['a[7:6]', 'a[4]'], + notContains: const [], + ), + ( + name: 'ascending non-collapsible', + build: () => _InlineSwizzleAscendingSelectsModule( + Logic(name: 'a', width: 8), + ), + contains: ['a[5]', 'a[6]', 'a[7]'], + notContains: ['a[7:5]'], + ), + ( + name: 'multi-source collapse', + build: () => _InlineSwizzleMultiSourceCollapseModule( + Logic(name: 'a', width: 8), + Logic(name: 'b', width: 8), + ), + contains: ['a[7:6]', 'b[3:2]'], + notContains: const [], + ), + ( + name: 'unpacked-array non-collapse', + build: () => _InlineSwizzleUnpackedArrayElementsModule( + LogicArray([4], 1, numUnpackedDimensions: 1), + ), + contains: ['arr[3]', 'arr[2]', 'arr[1]', 'arr[0]'], + notContains: ['arr[3:0]'], + ), + ]; + + for (final scenario in scenarios) { + final mod = scenario.build(); + await mod.build(); + + final baseline = mod.generateSynth(); + final planned = mod.generateSynth(); + + for (final expected in scenario.contains) { + expect( + baseline, + contains(expected), + reason: 'baseline missing "$expected" for ${scenario.name}', + ); + expect( + planned, + contains(expected), + reason: 'planned missing "$expected" for ${scenario.name}', + ); + } + + for (final disallowed in scenario.notContains) { + expect( + baseline, + isNot(contains(disallowed)), + reason: 'baseline unexpectedly had "$disallowed" for ' + '${scenario.name}', + ); + expect( + planned, + isNot(contains(disallowed)), + reason: 'planned unexpectedly had "$disallowed" for ' + '${scenario.name}', + ); + } + + expect( + normalizeSynthHeader(planned), + equals(normalizeSynthHeader(baseline)), + reason: 'full synth mismatch for ${scenario.name}', + ); + } + }); + + test('semantic leaf emission handles mixed ops', () async { + final mod = _InlineMixedOptionGateModule( + Logic(name: 'a', width: 8), + Logic(name: 'b', width: 8), + Logic(name: 'control'), + Logic(name: 'idx', width: 3), + ); + await mod.build(); + + final synth = mod.generateSynth(); + + expect(synth, contains('assign y_and = a & b;')); + expect(synth, contains('assign y_mux = control ? a : b;')); + expect(synth, contains('assign y_pow = {a ** b};')); + expect(synth, contains('assign y_idx = a[idx];')); + }); +} diff --git a/tool/gh_actions/check_tmp_test.sh b/tool/gh_actions/check_tmp_test.sh index a21154d8d..f0e5e92bc 100755 --- a/tool/gh_actions/check_tmp_test.sh +++ b/tool/gh_actions/check_tmp_test.sh @@ -13,9 +13,13 @@ set -euo pipefail declare -r folder_name='tmp_test' -# The "tmp_test" folder after performing the tests should be empty. +# The "tmp_test" folder after performing the tests should be empty, +# except for the precompiled-header cache (pch/) which is intentionally +# persistent and pre-built by CI before the test run. if [ -d "${folder_name}" ]; then - output=$(find ${folder_name} | wc --lines | tee) + output=$(find ${folder_name} -not -path "${folder_name}/pch" \ + -not -path "${folder_name}/pch/*" \ + | wc --lines | tee) if [ "${output}" -eq 1 ]; then echo "Success: directory \"${folder_name}\" is empty!" else diff --git a/tool/gh_actions/cleanup_systemc_tmp.sh b/tool/gh_actions/cleanup_systemc_tmp.sh new file mode 100755 index 000000000..d91149adb --- /dev/null +++ b/tool/gh_actions/cleanup_systemc_tmp.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# cleanup_systemc_tmp.sh +# GitHub Actions step helper: remove SystemC temporary build caches. +# +# 2026 June +# Author: Desmond Kirkpatrick + +set -euo pipefail + +declare -r folder_name='tmp_test' + +if [ ! -d "${folder_name}" ]; then + exit 0 +fi + +find "${folder_name}" -mindepth 1 \ + \( \ + -name 'pch' -o \ + -name 'pch.lock' -o \ + -name 'tmp_sc_*' -o \ + -name 'sc_input_*' -o \ + -name 'Makefile_sc' \ + \) \ + -exec rm -rf {} + + +mkdir -p "${folder_name}" \ No newline at end of file diff --git a/tool/gh_actions/install_systemc.sh b/tool/gh_actions/install_systemc.sh new file mode 100755 index 000000000..5c3fb82bf --- /dev/null +++ b/tool/gh_actions/install_systemc.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# Copyright (C) 2024-2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# install_systemc.sh +# GitHub Actions step: Install Accellera SystemC library. +# +# Downloads, builds, and installs SystemC to /opt/systemc. +# Uses a cache-friendly layout so the install directory can be +# cached across CI runs. +# +# 2026 May +# Author: Desmond Kirkpatrick + +set -euo pipefail + +SYSTEMC_VERSION="${SYSTEMC_VERSION:-3.0.2}" +INSTALL_PREFIX="${SYSTEMC_INSTALL_PREFIX:-/opt/systemc}" + +if [ "$(id -u)" -eq 0 ]; then + SUDO=() +else + SUDO=(sudo) +fi + +# Skip if already installed (e.g. from cache) +if [ -f "$INSTALL_PREFIX/lib/libsystemc.so" ]; then + echo "SystemC already installed at $INSTALL_PREFIX — skipping build." + exit 0 +fi + +echo "Installing Accellera SystemC $SYSTEMC_VERSION to $INSTALL_PREFIX ..." + +# Install build dependencies +"${SUDO[@]}" apt-get update -qq +"${SUDO[@]}" apt-get install --yes --no-install-recommends cmake g++ make + +# Download source +TARBALL="systemc-$SYSTEMC_VERSION.tar.gz" +DOWNLOAD_URL="https://github.com/accellera-official/systemc/archive/refs/tags/$SYSTEMC_VERSION.tar.gz" + +cd /tmp +curl -fsSL -o "$TARBALL" "$DOWNLOAD_URL" +tar xzf "$TARBALL" +cd "systemc-$SYSTEMC_VERSION" + +# Build with CMake +mkdir -p build && cd build +cmake .. \ + -DCMAKE_INSTALL_PREFIX="$INSTALL_PREFIX" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_STANDARD=17 \ + -DBUILD_SHARED_LIBS=ON \ + -DENABLE_EXAMPLES=OFF \ + -DENABLE_REGRESSION=OFF \ + -DDISABLE_COPYRIGHT_MESSAGE=ON + +make -j"$(nproc)" +"${SUDO[@]}" make install + +echo "SystemC $SYSTEMC_VERSION installed to $INSTALL_PREFIX" diff --git a/tool/gh_actions/run_tests.sh b/tool/gh_actions/run_tests.sh index 160352cd2..5e2804edc 100755 --- a/tool/gh_actions/run_tests.sh +++ b/tool/gh_actions/run_tests.sh @@ -11,8 +11,9 @@ set -euo pipefail -dart test +# Exclude FFI-dependent tests (dart:ffi unavailable on some CI platforms). +dart test $(find test -name '*_test.dart' ! -name 'systemc_ffi_cosim_test.dart' | sort) # run tests in JS (increase heap size also) export NODE_OPTIONS="--max-old-space-size=8192" -dart test --platform node \ No newline at end of file +dart test --platform node $(find test -name '*_test.dart' ! -name 'systemc_ffi_cosim_test.dart' | sort) \ No newline at end of file diff --git a/tool/gh_actions/setup_systemc_pch.sh b/tool/gh_actions/setup_systemc_pch.sh new file mode 100755 index 000000000..25d1ff93f --- /dev/null +++ b/tool/gh_actions/setup_systemc_pch.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Copyright (C) 2024-2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# setup_systemc_pch.sh +# GitHub Actions step: Pre-build SystemC precompiled header and Makefile. +# +# Run this after install_systemc.sh and before tests to avoid race +# conditions when multiple test isolates run in parallel. +# +# 2026 May +# Author: Desmond Kirkpatrick + +set -euo pipefail + +SC_HOME="${SYSTEMC_INCLUDE:-/opt/systemc/include}" +SC_LIB="${SYSTEMC_LIB:-/opt/systemc/lib}" + +if [ ! -d "$SC_HOME" ]; then + echo "SystemC not found at $SC_HOME — skipping PCH setup." + exit 0 +fi + +# Detect C++ standard from the installed library +CXX_STD="c++17" +if command -v nm &>/dev/null && [ -f "$SC_LIB/libsystemc.so" ]; then + if nm -D "$SC_LIB/libsystemc.so" 2>/dev/null | grep -q 'cxx202002L'; then + CXX_STD="c++20" + fi +fi + +echo "Setting up SystemC PCH ($CXX_STD) ..." + +# Build precompiled header +PCH_DIR="tmp_test/pch" +mkdir -p "$PCH_DIR" +cp "$SC_HOME/systemc.h" "$PCH_DIR/systemc.h" +g++ -std="$CXX_STD" -I"$SC_HOME" -x c++-header \ + -o "$PCH_DIR/systemc.h.gch" "$SC_HOME/systemc.h" + +echo "PCH built: $PCH_DIR/systemc.h.gch" diff --git a/tool/run_checks.sh b/tool/run_checks.sh index 6a0ea0080..939750a47 100755 --- a/tool/run_checks.sh +++ b/tool/run_checks.sh @@ -63,6 +63,10 @@ fi print_step 'Run project tests' tool/gh_actions/run_tests.sh +# Clean SystemC temporary files +print_step 'Clean SystemC temporary files' +tool/gh_actions/cleanup_systemc_tmp.sh + # Check temporary test files print_step 'Check temporary test files' tool/gh_actions/check_tmp_test.sh