From 701bb348d4f0e0ff5ed9cfb406fe9f786ef2605c Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 6 Jul 2026 06:45:36 -0700 Subject: [PATCH 01/14] Add base SystemC support --- .github/workflows/general.yml | 18 +- README.md | 4 +- dart_test.yaml | 15 + doc/architecture.md | 4 +- doc/user_guide/_docs/A21-generation.md | 22 +- doc/user_guide/_get-started/01-overview.md | 2 +- lib/src/module.dart | 22 + lib/src/modules/conditionals/flop.dart | 5 + lib/src/modules/conditionals/sequential.dart | 8 + lib/src/synthesizers/systemc/systemc.dart | 29 + .../systemc_synth_module_definition.dart | 31 + ...ystemc_synth_sub_module_instantiation.dart | 113 ++ .../systemc/systemc_synthesis_result.dart | 1710 +++++++++++++++++ lib/src/utilities/simcompare.dart | 847 +++++++- lib/src/utilities/systemc_cosim_ffi.dart | 961 +++++++++ test/systemc_ffi_cosim_test.dart | 266 +++ test/systemc_simcompare_test.dart | 243 +++ test/systemc_vector_test.dart | 1279 ++++++++++++ tool/gh_actions/check_tmp_test.sh | 8 +- tool/gh_actions/cleanup_systemc_tmp.sh | 30 + tool/gh_actions/install_systemc.sh | 62 + tool/gh_actions/run_tests.sh | 5 +- tool/gh_actions/setup_systemc_pch.sh | 42 + tool/run_checks.sh | 4 + 24 files changed, 5667 insertions(+), 63 deletions(-) create mode 100644 dart_test.yaml create mode 100644 lib/src/synthesizers/systemc/systemc.dart create mode 100644 lib/src/synthesizers/systemc/systemc_synth_module_definition.dart create mode 100644 lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart create mode 100644 lib/src/synthesizers/systemc/systemc_synthesis_result.dart create mode 100644 lib/src/utilities/systemc_cosim_ffi.dart create mode 100644 test/systemc_ffi_cosim_test.dart create mode 100644 test/systemc_simcompare_test.dart create mode 100644 test/systemc_vector_test.dart create mode 100755 tool/gh_actions/cleanup_systemc_tmp.sh create mode 100755 tool/gh_actions/install_systemc.sh create mode 100755 tool/gh_actions/setup_systemc_pch.sh diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 673f550d3..f5739e3d9 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -61,9 +61,21 @@ jobs: - name: Install software - Icarus Verilog run: tool/gh_actions/install_iverilog.sh + - name: Install software - Accellera SystemC + run: tool/gh_actions/install_systemc.sh + + - name: Pre-build SystemC PCH and Makefile + run: tool/gh_actions/setup_systemc_pch.sh + - name: Run project tests run: tool/gh_actions/run_tests.sh + - name: Run SystemC tests + run: dart test test/systemc_vector_test.dart + + - name: 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 +83,11 @@ jobs: - name: Build dev container and run tests in it uses: devcontainers/ci@v0.3 with: - runCmd: tool/gh_actions/run_tests.sh + runCmd: | + tool/gh_actions/run_tests.sh + dart test test/systemc_vector_test.dart + tool/gh_actions/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 00d3d25bb..27135a53f 100644 --- a/doc/user_guide/_docs/A21-generation.md +++ b/doc/user_guide/_docs/A21-generation.md @@ -5,7 +5,7 @@ last_modified_at: 2023-11-13 toc: true --- -Hardware in ROHD is convertible to an output format via `Synthesizer`s, the most popular of which is SystemVerilog. Hardware in ROHD can be converted to logically equivalent, human-readable SystemVerilog with structure, hierarchy, ports, and names maintained. +Hardware in ROHD is convertible to an output format via `Synthesizer`s. The most popular output format is SystemVerilog, with SystemC also available. Hardware in ROHD can be converted to logically equivalent, human-readable SystemVerilog or SystemC with structure, hierarchy, ports, and names maintained. The simplest way to generate SystemVerilog is with the helper method `generateSynth` in `Module`: @@ -28,6 +28,26 @@ void main() async { The `generateSynth` function will return a `String` with the SystemVerilog `module` definitions for the top-level it is called on, as well as any sub-modules (recursively). You can dump the entire contents to a file and use it anywhere you would any other SystemVerilog. +## SystemC generation + +ROHD can also generate SystemC (C++ with the SystemC library) from the same hardware description. Use the `generateSystemC` helper method: + +```dart +void main() async { + final myModule = MyModule(); + await myModule.build(); + + final generatedSc = myModule.generateSystemC(); + + // write it to a file + File('myHardware.h').writeAsStringSync(generatedSc); +} +``` + +The generated SystemC uses `SC_MODULE`, `SC_METHOD`, and `SC_CTHREAD` constructs. Combinational logic becomes `SC_METHOD` processes, sequential logic (flip-flops and `Sequential` blocks) sharing the same clock and reset are consolidated into a single `SC_CTHREAD`, and sub-modules are instantiated with port bindings. All signal types map to SystemC equivalents (`bool`, `sc_uint`, `sc_biguint`). + +For more control over SystemC generation, use `SynthBuilder` with `SystemCSynthesizer()` directly. + ## Controlling naming ### Modules diff --git a/doc/user_guide/_get-started/01-overview.md b/doc/user_guide/_get-started/01-overview.md index c1a98cdc1..c30c9f87f 100644 --- a/doc/user_guide/_get-started/01-overview.md +++ b/doc/user_guide/_get-started/01-overview.md @@ -19,7 +19,7 @@ Features of ROHD include: - **Simple and fast build**, free of complex build systems and EDA vendor tools - Can use the excellent pub.dev **package manager** and all the packages it has to offer - Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform dumper** to .vcd file format -- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption +- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** and **SystemC** for integration or downstream tool consumption - **Run-time dynamic** module port definitions (numbers, names, widths, etc.) and internal module logic, including recursive module contents - Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components. - Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. diff --git a/lib/src/module.dart b/lib/src/module.dart index ffeff9fc8..da94669b8 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -14,6 +14,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; import 'package:rohd/src/diagnostics/inspector_service.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc.dart'; import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; @@ -1155,6 +1156,27 @@ abstract class Module { .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/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index cd9aa8750..3e3f4acdf 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -88,6 +88,11 @@ class FlipFlop extends Module with SystemVerilog { /// Only initialized if a constant value is provided. late LogicValue _resetValueConst; + /// Returns the constant reset value if one was provided, or null if the + /// reset value is a port or no reset exists. + LogicValue? get constantResetValue => + _reset != null && _resetValuePort == null ? _resetValueConst : null; + /// Indicates whether provided `reset` signals should be treated as an async /// reset. If no `reset` is provided, this will have no effect. final bool asyncReset; diff --git a/lib/src/modules/conditionals/sequential.dart b/lib/src/modules/conditionals/sequential.dart index 62a7c1129..8871202fd 100644 --- a/lib/src/modules/conditionals/sequential.dart +++ b/lib/src/modules/conditionals/sequential.dart @@ -135,6 +135,14 @@ class Sequential extends Always { /// The input edge triggers used in this block. final List<_SequentialTrigger> _triggers = []; + /// Returns the edge polarity for each trigger input port. + /// + /// Each entry pairs the trigger input port name with whether the trigger + /// fires on a positive edge (`true`) or negative edge (`false`). + List<({String portName, bool isPosedge})> get triggerEdges => _triggers + .map((t) => (portName: t.signal.name, isPosedge: t.isPosedge)) + .toList(); + /// When `false`, an [SignalRedrivenException] will be thrown during /// simulation if the same signal is driven multiple times within this /// [Sequential]. diff --git a/lib/src/synthesizers/systemc/systemc.dart b/lib/src/synthesizers/systemc/systemc.dart new file mode 100644 index 000000000..7bf0f1211 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc.dart @@ -0,0 +1,29 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synthesizer.dart +// Definition for SystemC Synthesizer +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; + +/// A [Synthesizer] which generates equivalent SystemC as the given [Module]. +/// +/// Attempts to maintain signal naming and structure as much as possible, +/// using the same naming strategy as the SystemVerilog synthesizer. +class SystemCSynthesizer extends Synthesizer { + @override + bool generatesDefinition(Module module) => + // ignore: deprecated_member_use_from_same_package + !((module is CustomSystemVerilog) || + (module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none)); + + @override + SynthesisResult synthesize(Module module, + String Function(Module module) getInstanceTypeOfModule) => + SystemCSynthesisResult(module, getInstanceTypeOfModule); +} diff --git a/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart b/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart new file mode 100644 index 000000000..e670279d4 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart @@ -0,0 +1,31 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synth_module_definition.dart +// Definition for SystemCSynthModuleDefinition +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// A special [SynthModuleDefinition] for SystemC modules. +class SystemCSynthModuleDefinition extends SynthModuleDefinition { + /// Creates a new [SystemCSynthModuleDefinition] for the given [module]. + SystemCSynthModuleDefinition(super.module); + + @override + void process() { + // For now, do not collapse inline modules. Each InlineSystemVerilog gate + // remains as a sub-module instantiation and gets emitted as an assign-style + // expression in the generated SystemC (similar to SV `assign x = a & b`). + // + // Future: implement chain-collapsing for compound expressions. + } + + @override + SynthSubModuleInstantiation createSubModuleInstantiation(Module m) => + SystemCSynthSubModuleInstantiation(m); +} diff --git a/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart new file mode 100644 index 000000000..7e692ff8a --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart @@ -0,0 +1,113 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synth_sub_module_instantiation.dart +// Definition for SystemCSynthSubModuleInstantiation +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// Represents a submodule instantiation for SystemC. +class SystemCSynthSubModuleInstantiation extends SynthSubModuleInstantiation { + /// Creates a new [SystemCSynthSubModuleInstantiation] for the given + /// [module]. + SystemCSynthSubModuleInstantiation(super.module); + + /// If [module] is [InlineSystemVerilog], this will be the [SynthLogic] that + /// is the `result` of that module. Otherwise, `null`. + SynthLogic? get inlineResultLogic => module is! InlineSystemVerilog + ? null + : (outputMapping[(module as InlineSystemVerilog).resultSignalName] ?? + inOutMapping[(module as InlineSystemVerilog).resultSignalName]); + + /// Mapping from [SynthLogic]s which are outputs of inlineable modules to + /// those inlineable modules. + Map? + synthLogicToInlineableSynthSubmoduleMap; + + /// Provides a mapping from ports of this module to a string that can be fed + /// into that port, which may include inline expressions. + Map _modulePortsMapWithInline( + Map plainPorts) => + plainPorts.map((name, synthLogic) => MapEntry( + name, + synthLogicToInlineableSynthSubmoduleMap?[synthLogic] + ?.inlineSystemC() ?? + (synthLogic.declarationCleared ? '' : synthLogic.name))); + + /// Provides the inline SystemC expression for this module. + /// + /// Should only be called if [module] is [InlineSystemVerilog]. + String inlineSystemC() { + final portNameToValueMapping = _modulePortsMapWithInline( + {...inputMapping, ...inOutMapping} + ..remove((module as InlineSystemVerilog).resultSignalName), + ); + + final inlineRepresentation = + _inlineSystemCExpression(portNameToValueMapping); + + return '($inlineRepresentation)'; + } + + /// Generates the inline SystemC expression for the gate module. + String _inlineSystemCExpression(Map inputs) { + final m = module; + + if (m is NotGate) { + final inVal = inputs.values.first; + return '~$inVal'; + } else if (m is And2Gate) { + return '${inputs.values.first} & ${inputs.values.last}'; + } else if (m is Or2Gate) { + return '${inputs.values.first} | ${inputs.values.last}'; + } else if (m is Xor2Gate) { + return '${inputs.values.first} ^ ${inputs.values.last}'; + } else if (m is Mux) { + // Mux has inputs: control, d0, d1 → output: y + // In SystemC: control ? d1 : d0 + final entries = inputs.entries.toList(); + final control = entries[0].value; + final d0 = entries[1].value; + final d1 = entries[2].value; + return '$control ? $d1 : $d0'; + } else if (m is InlineSystemVerilog) { + // Fallback: use the verilog inline expression as a reasonable + // approximation (many operators are identical between SV and C++) + return m.inlineVerilog(inputs); + } + + throw SynthException('Unsupported inline module type: ${m.runtimeType}'); + } + + /// Provides the full SystemC instantiation for this module as a member + /// declaration and port binding in the constructor. + /// + /// Returns null if this module does not need instantiation. + String? memberDeclaration(String instanceType) { + if (!needsInstantiation) { + return null; + } + return '$instanceType $name{"$name"};'; + } + + /// Generates port binding statements for the constructor body. + String? portBindings() { + if (!needsInstantiation) { + return null; + } + final bindings = []; + final allPorts = {...inputMapping, ...outputMapping, ...inOutMapping}; + for (final entry in allPorts.entries) { + final portName = entry.key; + final synthLogic = entry.value; + if (!synthLogic.declarationCleared) { + bindings.add('$name.$portName(${synthLogic.name});'); + } + } + return bindings.join('\n'); + } +} diff --git a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart new file mode 100644 index 000000000..7097b63b0 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart @@ -0,0 +1,1710 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synthesis_result.dart +// Definition for SystemCSynthesisResult +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:collection/collection.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/conditionals/always.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// A [SynthesisResult] representing a conversion of a [Module] to SystemC. +class SystemCSynthesisResult extends SynthesisResult { + /// A cached copy of the generated ports. + late final String _portsString; + + /// A cached copy of the generated module body (used for matching). + late final String _moduleBodyString; + + /// The main [SynthModuleDefinition] for this. + final SynthModuleDefinition _synthModuleDefinition; + + @override + List get supportingModules => + _synthModuleDefinition.supportingModules; + + // Cached sections for final assembly + late final String _internalSigs; + late final String _subMembers; + late final String _ctorBody; + late final String _methodBodies; + + /// Creates a new [SystemCSynthesisResult] for the given [module]. + SystemCSynthesisResult(super.module, super.getInstanceTypeOfModule) + : _synthModuleDefinition = SystemCSynthModuleDefinition(module) { + _findClockResetSignals(); + _portsString = _systemCPorts(); + _buildModuleBody(getInstanceTypeOfModule); + _moduleBodyString = '$_ctorBody|$_methodBodies'; + } + + @override + bool matchesImplementation(SynthesisResult other) => + other is SystemCSynthesisResult && + other._portsString == _portsString && + other._moduleBodyString == _moduleBodyString; + + @override + int get matchHashCode => _portsString.hashCode ^ _moduleBodyString.hashCode; + + @override + String toFileContents() => _toSystemC(); + + @override + List toSynthFileContents() => List.unmodifiable([ + SynthFileContents( + name: instanceTypeName, + description: 'SystemC module definition for $instanceTypeName', + contents: _toSystemC(), + ) + ]); + + // ──────────────────────────────────────────────────────────────────── + // 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 _synthModuleDefinition.inputs) sig.name, + for (final sig in _synthModuleDefinition.outputs) sig.name, + for (final sig in _synthModuleDefinition.inOuts) sig.name, + for (final sig in _synthModuleDefinition.internalSignals + .where((e) => e.needsDeclaration)) + sig.name, + for (final smi in _synthModuleDefinition.subModuleInstantiations + .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 _synthModuleDefinition.subModuleInstantiations) { + final m = ssmi.module; + // Detect SimpleClockGenerator and promote its output to a port + if (m is SimpleClockGenerator) { + for (final entry in ssmi.outputMapping.entries) { + promotedClocks.add(entry.value.name); + } + } + } + _promotedClockSignals = promotedClocks; + } + + // ──────────────────────────────────────────────────────────────────── + // Type mapping + // ──────────────────────────────────────────────────────────────────── + + /// Sanitize a signal/port name to be a valid C++ identifier. + /// Replaces `[N]` with `_N_` (LogicArray element indexing). + static String _scName(String name) => + name.replaceAllMapped(RegExp(r'\[(\d+)\]'), (m) => '_${m[1]}_'); + + /// Maps a signal width to the appropriate SystemC data type. + static String systemCType(int width) { + if (width == 1) { + return 'bool'; + } else if (width <= 64) { + return 'sc_uint<$width>'; + } else { + return 'sc_biguint<$width>'; + } + } + + /// SystemC input port type for a given width. + static String systemCInType(int width) => 'sc_in<${systemCType(width)}>'; + + /// SystemC output port type for a given width. + static String systemCOutType(int width) => 'sc_out<${systemCType(width)}>'; + + /// SystemC signal type for a given width. + static String systemCSignalType(int width) => + 'sc_signal<${systemCType(width)}>'; + + // ──────────────────────────────────────────────────────────────────── + // Port declarations + // ──────────────────────────────────────────────────────────────────── + + String _systemCPorts() { + final lines = []; + for (final sig in _synthModuleDefinition.inputs) { + final n = _scName(sig.name); + lines.add(' ${systemCInType(sig.width)} $n{"$n"};'); + } + // Promote internal clock signals (from SimpleClockGenerator) to ports + for (final clkName in _promotedClockSignals) { + final n = _scName(clkName); + lines.add(' ${systemCInType(1)} $n{"$n"};'); + } + for (final sig in _synthModuleDefinition.outputs) { + final n = _scName(sig.name); + lines.add(' ${systemCOutType(sig.width)} $n{"$n"};'); + } + return lines.join('\n'); + } + + // ──────────────────────────────────────────────────────────────────── + // Internal signals + // ──────────────────────────────────────────────────────────────────── + + String _buildInternalSignals() { + final declarations = []; + for (final sig in _synthModuleDefinition.internalSignals + .where((e) => e.needsDeclaration) + .where((e) => !_promotedClockSignals.contains(e.name)) + .sorted((a, b) => a.name.compareTo(b.name))) { + final n = _scName(sig.name); + declarations.add(' ${systemCSignalType(sig.width)} $n{"$n"};'); + } + + // Declare individual signals for array elements that are written to + // (FlipFlop/Sequential outputs targeting array elements) + for (final elemName in _arrayElementsWritten.keys) { + final n = _scName(elemName); + final width = _arrayElementsWritten[elemName]!; + declarations.add(' ${systemCSignalType(width)} $n{"$n"};'); + } + return declarations.join('\n'); + } + + /// Maps array element names (e.g. "delayLine[0]") to their widths. + /// These need separate signal declarations because SystemC can't do + /// partial writes to sc_signal. + late final Map _arrayElementsWritten = + _findArrayElementsWritten(); + + /// Groups array elements by parent: parentName → list of (index, elemWidth). + late final Map> + _arrayElementsByParent = _groupArrayElementsByParent(); + + Map _findArrayElementsWritten() { + final result = {}; + + void addIfArrayElement(SynthLogic sl) { + if (sl is SynthLogicArrayElement) { + result[sl.name] = sl.logic.width; + } + } + + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + final m = ssmi.module; + + // All submodule output mappings + ssmi.outputMapping.values.forEach(addIfArrayElement); + + // Inline gate result logics + if (ssmi is SystemCSynthSubModuleInstantiation) { + final rl = ssmi.inlineResultLogic; + if (rl != null) { + addIfArrayElement(rl); + } + } + + // Scan conditionals for nested array element receivers + if (m is Combinational) { + _collectArrayReceiversFromConditionals(m.conditionals, result); + } else if (m is Sequential) { + _collectArrayReceiversFromConditionals(m.conditionals, result); + } + } + + // Wire assignments targeting array elements + for (final assignment in _synthModuleDefinition.assignments) { + addIfArrayElement(assignment.dst); + } + + return result; + } + + /// Recursively walks a conditionals tree to find all receivers that + /// are array elements and adds them to [result]. + void _collectArrayReceiversFromConditionals( + List conditionals, Map result) { + for (final c in conditionals) { + for (final receiver in c.receivers) { + final sl = _synthModuleDefinition.logicToSynthMap[receiver]; + if (sl is SynthLogicArrayElement && !result.containsKey(sl.name)) { + result[sl.name] = sl.logic.width; + } + } + // Recurse into sub-conditionals + _collectArrayReceiversFromConditionals(c.conditionals, result); + } + } + + /// Groups array elements by their root parent signal, + /// computing flat bit offsets for nested elements. + Map> + _groupArrayElementsByParent() { + final result = >{}; + + void addElement(SynthLogicArrayElement sl) { + // Walk up to root and compute flat bit offset + var flatOffset = 0; + SynthLogic current = sl; + while (current is SynthLogicArrayElement) { + final idx = current.logic.arrayIndex; + if (idx == null) { + return; // pruned element — skip + } + flatOffset += idx * current.logic.width; + current = current.parentArray.replacement ?? current.parentArray; + } + final rootName = current.name; + + final entry = ( + // Use flat bit offset as "index" for assembly ordering + index: flatOffset, + width: sl.logic.width, + elemName: sl.name, + ); + // Avoid duplicates + final list = result.putIfAbsent(rootName, () => []); + if (!list.any((e) => e.elemName == entry.elemName)) { + list.add(entry); + } + } + + // Use logicToSynthMap to find the SynthLogicArrayElement for each written + // element, rather than re-scanning submodule instantiations. + for (final sl in _synthModuleDefinition.logicToSynthMap.values) { + if (sl is SynthLogicArrayElement && sl.replacement == null) { + // Skip elements whose parent has been pruned or not named + final parent = sl.parentArray.replacement ?? sl.parentArray; + if (parent.declarationCleared) { + continue; + } + if (_arrayElementsWritten.containsKey(sl.name)) { + addElement(sl); + } + } + } + + // Sort each list by flat bit offset + for (final list in result.values) { + list.sort((a, b) => a.index.compareTo(b.index)); + } + return result; + } + + // ──────────────────────────────────────────────────────────────────── + // Inline gate expressions + // ──────────────────────────────────────────────────────────────────── + + /// Returns true if a module is a SystemVerilog gate that generates no + /// definition and should be inlined (like Add). + static bool _isInlinableSystemVerilogGate(Module m) => + m is SystemVerilog && + m is! InlineSystemVerilog && + m is! Always && + m is! FlipFlop && + m.generatedDefinitionType == DefinitionGenerationType.none; + + /// Converts a [SynthLogic] to a SystemC read expression. + /// Constants become typed literals; signals get `.read()`. + /// Array elements become range expressions on their parent. + static String _synthLogicReadExpr(SynthLogic sl) { + if (sl.isConstant) { + final c = sl.logics.whereType().first; + return _typedConstExpr(c.value, c.width); + } + if (sl is SynthLogicArrayElement) { + return _arrayElementReadExpr(sl); + } + return '${_scName(sl.name)}.read()'; + } + + /// Generates a typed constant expression for SystemC. + /// Handles x/z values by treating them as 0. + static String _typedConstExpr(LogicValue val, int width) { + if (val.isValid) { + if (width == 0) { + return '0'; + } + final bigVal = val.toBigInt(); + if (width > 64) { + // Use hex string constructor for sc_biguint + var hex = bigVal.toUnsigned(width).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + return '${systemCType(width)}("0x$hex")'; + } + // For uint64 values above INT64_MAX, add ULL suffix + if (bigVal > (BigInt.one << 63) - BigInt.one) { + return '${systemCType(width)}' + '(${bigVal.toUnsigned(width)}ULL)'; + } + return '${systemCType(width)}(${bigVal.toUnsigned(width)})'; + } + // For values with x/z, use 0 (SystemC doesn't have x/z) + return '${systemCType(width)}(0)'; + } + + /// Generates a range read expression for an array element. e.g. + /// deserialized[0] (8-bit in 32-bit parent) → deserialized.read().range(7, 0) + /// Generates a range read expression for an array element, handling + /// arbitrary nesting depth. e.g. `laIn[2][1]` in a `[3,2]x8` array + /// → `laIn.read().range(47, 40)`. + static String _arrayElementReadExpr(SynthLogicArrayElement sl) { + final elemWidth = sl.logic.width; + + // Walk up the parent chain to find the root signal and accumulate + // the flat bit offset. + var flatOffset = 0; + SynthLogic current = sl; + while (current is SynthLogicArrayElement) { + final idx = current.logic.arrayIndex!; + final w = current.logic.width; + flatOffset += idx * w; + current = current.parentArray.replacement ?? current.parentArray; + } + final rootName = _scName(current.name); + final rootWidth = current.width; + + final lo = flatOffset; + final hi = lo + elemWidth - 1; + + // If the root is 1-bit (bool), subscript/range is not valid + if (rootWidth == 1) { + return '$rootName.read()'; + } + if (elemWidth == 1) { + return 'static_cast($rootName.read()[$lo])'; + } + final rangeType = elemWidth <= 64 ? 'sc_uint' : 'sc_biguint'; + return '$rangeType<$elemWidth>($rootName.read().range($hi, $lo))'; + } + + /// Returns the sensitivity signal name for a SynthLogic. + /// For array elements, walks up to the root (non-array-element) parent. + static String _sensitivityName(SynthLogic sl) { + var current = sl; + while (current is SynthLogicArrayElement) { + current = current.parentArray.replacement ?? current.parentArray; + } + return _scName(current.name); + } + + /// Generates an SC_METHOD for inline gates (like SV `assign` stmts). + _MethodResult? _buildInlineGates() { + final inlineGates = _synthModuleDefinition.subModuleInstantiations + .where((s) => + s.needsInstantiation && + (s.module is InlineSystemVerilog || + _isInlinableSystemVerilogGate(s.module))) + .cast() + .toList(); + + if (inlineGates.isEmpty) { + return null; + } + + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + var methodIdx = 0; + + for (final ssmi in inlineGates) { + final m = ssmi.module; + final sensitivities = {}; + final bodyLines = []; + + // Collect inputs — constants become literals, signals get .read() + final inputExprs = {}; + for (final entry in ssmi.inputMapping.entries) { + final sl = entry.value; + if (!sl.isConstant) { + sensitivities.add(_sensitivityName(sl)); + } + inputExprs[entry.key] = _synthLogicReadExpr(sl); + } + + if (m is InlineSystemVerilog) { + final resultSynthLogic = ssmi.inlineResultLogic; + if (resultSynthLogic == null) { + continue; + } + final expr = _gateExpression(m, inputExprs); + final dst = _scName(resultSynthLogic.name); + bodyLines.add(' $dst = $expr;'); + } else if (m is Add) { + // Add has two outputs: sum and carry. + // Emit inline expressions for each used output. + final vals = inputExprs.values.toList(); + final sumPortName = m.sum.name; + for (final entry in ssmi.outputMapping.entries) { + final portName = entry.key; + final dst = _scName(entry.value.name); + if (portName == sumPortName) { + bodyLines.add(' $dst = ${vals[0]} + ${vals[1]};'); + } else { + // carry: high bit of (width+1)-bit addition + final w = m.width; + final w1 = w + 1; + final utype = systemCType(w1); + final carryExpr = 'static_cast' + '($utype($utype(${vals[0]})' + ' + $utype(${vals[1]}))[$w])'; + bodyLines.add(' $dst = $carryExpr;'); + } + } + } + + if (bodyLines.isEmpty) { + continue; + } + + final methodName = 'assign_$methodIdx'; + methodIdx++; + setupBuf.writeln(' SC_METHOD($methodName);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + bodyBuf + ..writeln(' void $methodName() {') + ..writeln(bodyLines.join('\n')) + ..writeln(' }') + ..writeln(); + ssmi.clearInstantiation(); + } + + if (bodyBuf.isEmpty) { + return null; + } + + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + /// Maps an InlineSystemVerilog gate to a C++ expression. + /// + /// Handles all gate types that have SV-specific syntax which needs + /// translation to valid SystemC/C++. + String _gateExpression(InlineSystemVerilog m, Map inputs) { + // ── Single-output bitwise gates (C++ operators identical to SV) ── + if (m is NotGate) { + // For bool (width-1), use logical not; for wider, bitwise not + if ((m as Module).outputs.values.first.width == 1) { + return '!${inputs.values.first}'; + } + return '~${inputs.values.first}'; + } + + // ── Binary operator gates (C++ operators identical to SV) ── + const binaryOps = { + And2Gate: '&', + Or2Gate: '|', + Xor2Gate: '^', + Subtract: '-', + Multiply: '*', + }; + final binOp = binaryOps[m.runtimeType]; + if (binOp != null) { + final vals = inputs.values.toList(); + return '${vals[0]} $binOp ${vals[1]}'; + } + if (m is Divide || m is Modulo) { + final vals = inputs.values.toList(); + final op = m is Divide ? '/' : '%'; + // Guard against zero divisor (sc_uint defaults to 0 at time-0) + return '(${vals[1]} != 0 ? ${vals[0]} $op ${vals[1]} : 0)'; + } + if (m is Power) { + final vals = inputs.values.toList(); + final w = (m as Module).inputs.values.first.width; + return '${systemCType(w)}' + '(static_cast' + '(pow(static_cast(${vals[0]}),' + ' static_cast(${vals[1]}))))'; + } + + // ── Comparison (operators identical) ── + const cmpOps = { + Equals: '==', + NotEquals: '!=', + LessThan: '<', + GreaterThan: '>', + LessThanOrEqual: '<=', + GreaterThanOrEqual: '>=', + }; + final cmpOp = cmpOps[m.runtimeType]; + if (cmpOp != null) { + final vals = inputs.values.toList(); + return '${vals[0]} $cmpOp ${vals[1]}'; + } + + // ── Shifts ── + // Cast shift amount to int to avoid ambiguous overloads. + // Width 1 maps to bool in SystemC (no .to_int()), so use (int) cast. + // Clamp: if shift amount >= operand width, result is 0 (or sign-fill + // for arshift), avoiding .to_int() overflow on huge shift amounts. + if (m is LShift || m is RShift || m is ARShift) { + final vals = inputs.values.toList(); + final w = (m as Module).inputs.values.first.width; + final outType = systemCType(w); + final shiftAmtWidth = (m as Module).inputs.values.toList()[1].width; + final shiftExpr = + shiftAmtWidth == 1 ? '(int)(${vals[1]})' : '(${vals[1]}).to_int()'; + if (m is ARShift) { + final signedType = w <= 64 ? 'sc_int<$w>' : 'sc_bigint<$w>'; + final shiftOp = '$outType(($signedType(${vals[0]})) >> $shiftExpr)'; + if (shiftAmtWidth > 31) { + // Sign-fill: shift by width-1 to replicate MSB when shift >= width + final overflow = '$outType(($signedType(${vals[0]})) >> ${w - 1})'; + return '(${vals[1]} >= $w) ? $overflow : $shiftOp'; + } + return shiftOp; + } + final op = m is LShift ? '<<' : '>>'; + final shiftOp = '$outType(${vals[0]} $op $shiftExpr)'; + if (shiftAmtWidth > 31) { + return '(${vals[1]} >= $w) ? $outType(0) : $shiftOp'; + } + return shiftOp; + } + + // ── Unary reductions ── + if (m is AndUnary || m is OrUnary || m is XorUnary) { + final inputWidth = (m as Module).inputs.values.first.width; + // 1-bit: reduce is identity (and bool has no .xor_reduce() in SystemC) + if (inputWidth == 1) { + return 'static_cast(${inputs.values.first})'; + } + if (m is AndUnary) { + return '${inputs.values.first}.and_reduce()'; + } else if (m is OrUnary) { + return '${inputs.values.first}.or_reduce()'; + } else { + return '${inputs.values.first}.xor_reduce()'; + } + } + + // ── Bus subset (slice / index) ── + if (m is BusSubset) { + final a = inputs.values.first; + final inputWidth = (m as Module).inputs.values.first.width; + // If input is already 1-bit (bool), extracting bit 0 is identity + if (inputWidth == 1 && m.startIndex == 0 && m.endIndex == 0) { + return a; + } + if (m.startIndex == m.endIndex) { + return 'static_cast($a[${m.startIndex}])'; + } + if (m.startIndex > m.endIndex) { + // Reverse order — build bit-by-bit concat + // bits[0]=a[endIndex], ..., bits[N]=a[startIndex] + // SystemC concat is MSB-first: output MSB = input[endIndex] + // Use sc_uint<1> (not bool) so SystemC concat operator is invoked + final bits = List.generate(m.startIndex - m.endIndex + 1, + (i) => 'sc_uint<1>($a[${m.endIndex + i}])'); + return '(${bits.join(', ')})'; + } + final w = m.endIndex - m.startIndex + 1; + final rangeType = w <= 64 ? 'sc_uint' : 'sc_biguint'; + return '$rangeType<$w>($a.range(${m.endIndex}, ${m.startIndex}))'; + } + + // ── Dynamic bit index ── + if (m is IndexGate) { + final vals = inputs.values.toList(); + return 'static_cast(${vals[0]}[${vals[1]}])'; + } + + // ── Mux (ternary) ── + if (m is Mux) { + final vals = inputs.values.toList(); + final w = m.out.width; + final utype = systemCType(w); + // Cast both branches to avoid C++ ternary type mismatch + // (e.g., when one branch is bool and the other is sc_uint<1>) + return '${vals[0]}' + ' ? $utype(${vals[2]})' + ' : $utype(${vals[1]})'; + } + + // ── Replication ── + if (m is ReplicationOp) { + final a = inputs.values.first; + final inputWidth = (m as Module).inputs.values.first.width; + final outputWidth = m.replicated.width; + final numReps = outputWidth ~/ inputWidth; + if (inputWidth == 1) { + // Single-bit replicate: all-1s or all-0s + final utype = systemCType(outputWidth); + return '$utype(' + '$a ' + '? $utype(-1) ' + ': $utype(0))'; + } + // Multi-bit replicate: concat N copies + final copies = List.filled(numReps, a); + return '(${copies.join(', ')})'; + } + + // ── Swizzle (concatenation) ── + if (m is Swizzle) { + // SystemC concatenation: (sig1, sig2, sig3) + // bool operands must be cast to sc_uint<1> to use SystemC concat + // (otherwise C++ comma operator is invoked instead) + final modInputs = (m as Module).inputs.values.toList(); + final exprList = []; + var i = 0; + for (final expr in inputs.values) { + final w = modInputs[i].width; + if (w == 0) { + i++; + continue; // skip zero-width padding + } + // Wrap 1-bit (bool) operands in sc_uint<1>() for concat + if (w == 1) { + exprList.add('sc_uint<1>($expr)'); + } else { + exprList.add(expr); + } + i++; + } + if (exprList.length == 1) { + return exprList.first; + } + // Swizzle stores inputs LSB-first (in0=LSB), but SystemC concat + // is MSB-first: (msb, ..., lsb). So reverse. + return '(${exprList.reversed.join(', ')})'; + } + + // Fallback: use SV inline (may not be valid C++ — flag for review) + return '/* TODO: ${m.runtimeType} */ ${m.inlineVerilog(inputs)}'; + } + + // ──────────────────────────────────────────────────────────────────── + // Clock / trigger edge resolution + // ──────────────────────────────────────────────────────────────────── + + /// Resolves a trigger [SynthLogic] to the effective clock port and edge. + /// + /// If the trigger signal is a module input port, it can be used directly + /// with `SC_CTHREAD`. If it is an internal signal derived from a [NotGate], + /// the method traces through the inversion chain to find the original port + /// and flips the edge accordingly (`negedge(~clk) = posedge(clk)`). + ({String clockName, bool isPort, bool isPosedge}) _resolveClockAndEdge( + SynthLogic triggerSL, bool isPosedge) { + final sl = triggerSL.replacement ?? triggerSL; + + if (sl.isPort(_synthModuleDefinition.module)) { + return (clockName: sl.name, isPort: true, isPosedge: isPosedge); + } + + // Try to trace through a NotGate inversion + for (final logic in sl.logics) { + final src = logic.srcConnection; + if (src != null && src.parentModule is NotGate) { + final notInput = src.parentModule!.inputs.values.first; + final notInputSrc = notInput.srcConnection; + if (notInputSrc != null) { + final srcSL = _synthModuleDefinition.logicToSynthMap[notInputSrc]; + if (srcSL != null) { + // Inversion flips the edge + return _resolveClockAndEdge(srcSL, !isPosedge); + } + } + } + } + + // Fallback — use the signal as-is (SC_THREAD will be needed) + return (clockName: sl.name, isPort: false, isPosedge: isPosedge); + } + + // ──────────────────────────────────────────────────────────────────── + // Combinational / Sequential processes + // ──────────────────────────────────────────────────────────────────── + + _MethodResult? _buildProcesses() { + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + var idx = 0; + + // Collect clocked processes for consolidation by (clock, reset) pair. + // Sequentials and FlipFlops sharing the same clock/reset are merged + // into a single SC_CTHREAD, eliminating repeated async_reset_signal_is. + final clockedGroups = {}; + + for (final ssmi + in _synthModuleDefinition.subModuleInstantiations.toList()) { + ssmi as SystemCSynthSubModuleInstantiation; + final m = ssmi.module; + + if (m is Combinational) { + final name = 'comb_$idx'; + idx++; + + final sensitivities = ssmi.inputMapping.values + .where((sl) => !sl.declarationCleared && !sl.isConstant) + .map(_sensitivityName) + .toSet(); + + setupBuf.writeln(' SC_METHOD($name);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + // Build maps keyed by port name (what verilogContents expects) + final inputsMap = ssmi.inputMapping + .map((k, sl) => MapEntry(k, _synthLogicReadExpr(sl))); + final outputsMap = + ssmi.outputMapping.map((k, sl) => MapEntry(k, _scName(sl.name))); + + bodyBuf.writeln(' void $name() {'); + for (final c in m.conditionals) { + bodyBuf.write(_conditionalToSC(c, 2, inputsMap, outputsMap)); + } + bodyBuf + ..writeln(' }') + ..writeln(); + ssmi.clearInstantiation(); + } else if (m is Sequential) { + final resetEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('reset')) + .firstOrNull; + + // Detect async reset: either explicitly via asyncReset flag, or + // implicitly when the reset signal is also listed as a trigger + // (e.g. Sequential.multi([clk, reset], reset: reset, ...)). + final isAsync = m.asyncReset || + (resetEntry != null && + ssmi.inputMapping.entries.any((e) => + e.key.contains('trigger') && + e.value.name == resetEntry.value.name)); + + // Resolve ALL trigger entries to (signalName, edge, isPort). + final triggerEdges = m.triggerEdges; + final triggerEntries = ssmi.inputMapping.entries + .where((e) => e.key.contains('trigger')) + .toList(); + + final resolvedTriggers = + <({String signalName, bool isPosedge, bool isPort})>[]; + + for (final te in triggerEntries) { + final triggerSL = te.value; + // Skip if this trigger is the async reset signal + if (resetEntry != null && triggerSL.name == resetEntry.value.name) { + continue; + } + // Skip constant triggers (e.g. clk <= Const(0) — never toggles) + if (triggerSL.isConstant) { + continue; + } + final isPosedge = triggerEdges + .where((t) => t.portName == te.key) + .firstOrNull + ?.isPosedge ?? + true; + final resolved = _resolveClockAndEdge(triggerSL, isPosedge); + // Skip if the resolved signal is constant + final resolvedSL = _synthModuleDefinition.logicToSynthMap.values + .where((sl) => sl.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 condBuf = StringBuffer(); + for (final c in m.conditionals) { + condBuf.write(_conditionalToSC(c, 3, inputsMap, outputsMap)); + } + group.whileBodyLines.add(condBuf.toString()); + ssmi.clearInstantiation(); + } else if (m is FlipFlop) { + // Resolve port signals via the input/output mapping + final clkSl = ssmi.inputMapping.entries + .firstWhere((e) => e.key.contains('clk')) + .value; + final dSl = ssmi.inputMapping.entries + .firstWhere((e) => e.key.contains('d')) + .value; + final resetEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('reset') && !e.key.contains('Value')) + .firstOrNull; + final enEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('en')) + .firstOrNull; + final resetValueEntry = ssmi.inputMapping.entries + .where( + (e) => e.key.contains('resetValue') || e.key.contains('Value')) + .firstOrNull; + final qSl = ssmi.outputMapping.values.first; + + final groupKey = + '${clkSl.name}:true|${resetEntry?.value.name ?? '_none_'}'; + final group = clockedGroups.putIfAbsent( + groupKey, + () => _ClockedGroupData( + resetName: resetEntry?.value.name, + isAsyncReset: m.asyncReset, + )); + // FlipFlop always posedge + if (!group.triggers + .any((t) => t.signalName == clkSl.name && t.isPosedge)) { + group.triggers.add(( + signalName: clkSl.name, + isPosedge: true, + isPort: clkSl.isPort(_synthModuleDefinition.module), + )); + } + if (m.asyncReset) { + group.isAsyncReset = true; + } + + // Reset value + String resetValExpr; + if (resetValueEntry != null) { + resetValExpr = _synthLogicReadExpr(resetValueEntry.value); + } else if (m.constantResetValue != null) { + resetValExpr = m.constantResetValue!.toBigInt().toString(); + } else { + resetValExpr = '0'; + } + group.resetLines.add(' ${_scName(qSl.name)} = $resetValExpr;'); + + // Build the data assignment (with optional enable gate) + final assignExpr = + ' ${_scName(qSl.name)} = ${_synthLogicReadExpr(dSl)};\n'; + final bodyLine = enEntry != null + ? ' if (${_synthLogicReadExpr(enEntry.value)}) {\n' + ' $assignExpr' + ' }\n' + : assignExpr; + + // Wrap in sync reset check if needed + if (resetEntry != null && !m.asyncReset) { + group.whileBodyLines + .add(' if (${_scName(resetEntry.value.name)}.read()) {\n' + ' ${_scName(qSl.name)} = $resetValExpr;\n' + ' } else {\n' + ' $bodyLine' + ' }\n'); + } else { + group.whileBodyLines.add(bodyLine); + } + ssmi.clearInstantiation(); + } + } + + // Emit one SC_CTHREAD or SC_THREAD per (clock, reset) group + for (final group in clockedGroups.values) { + final name = 'clocked_$idx'; + idx++; + + final triggers = group.triggers; + + if (triggers.isEmpty) { + // All triggers were constant — skip this group + continue; + } + + // Determine if we can use SC_CTHREAD: + // - exactly one trigger signal + // - that signal is a port (sc_in) + // - only one edge direction + final distinctSignals = triggers.map((t) => t.signalName).toSet(); + final useCthread = distinctSignals.length == 1 && + triggers.first.isPort && + triggers.length == 1; + + if (useCthread) { + final t = triggers.first; + final clockRef = _scName(t.signalName); + final edge = t.isPosedge ? '.pos()' : '.neg()'; + setupBuf.writeln(' SC_CTHREAD($name, $clockRef$edge);'); + if (group.resetName != null && group.isAsyncReset) { + setupBuf.writeln(' async_reset_signal_is(' + '${_scName(group.resetName!)}, true);'); + } + + bodyBuf.writeln(' void $name() {'); + group.resetLines.forEach(bodyBuf.writeln); + bodyBuf + ..writeln(' wait();') + ..writeln(' while (true) {'); + group.whileBodyLines.forEach(bodyBuf.write); + bodyBuf + ..writeln(' wait();') + ..writeln(' }') + ..writeln(' }') + ..writeln(); + } else { + // SC_THREAD with explicit wait on events + setupBuf.writeln(' SC_THREAD($name);'); + + // Build wait expression from all trigger events + String waitExpr; + if (distinctSignals.length == 1) { + // Same signal, but both edges + final sig = _scName(triggers.first.signalName); + final edges = triggers.map((t) => t.isPosedge).toSet(); + if (edges.length == 2) { + waitExpr = '$sig.value_changed_event()'; + } else if (edges.first) { + waitExpr = '$sig.posedge_event()'; + } else { + waitExpr = '$sig.negedge_event()'; + } + } else { + // Multiple distinct trigger signals — OR them together + final eventExprs = []; + for (final t in triggers) { + final sig = _scName(t.signalName); + eventExprs + .add('$sig.${t.isPosedge ? 'posedge' : 'negedge'}_event()'); + } + waitExpr = eventExprs.join(' | '); + } + + bodyBuf.writeln(' void $name() {'); + group.resetLines.forEach(bodyBuf.writeln); + bodyBuf + ..writeln(' while (true) {') + ..writeln(' wait($waitExpr);'); + group.whileBodyLines.forEach(bodyBuf.write); + bodyBuf + ..writeln(' }') + ..writeln(' }') + ..writeln(); + } + } + + if (setupBuf.isEmpty && bodyBuf.isEmpty) { + return null; + } + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Regular sub-module instantiations + // ──────────────────────────────────────────────────────────────────── + + /// Returns true if the sub-module is handled inline (not a real child + /// instantiation) — i.e. it is an inline gate, Always, FlipFlop, or clock. + static bool _isHandledInline(SystemCSynthSubModuleInstantiation ssmi) => + !ssmi.needsInstantiation || + ssmi.module is InlineSystemVerilog || + ssmi.module is Always || + ssmi.module is FlipFlop || + ssmi.module is SimpleClockGenerator || + _isInlinableSystemVerilogGate(ssmi.module); + + String _buildSubModuleMembers( + String Function(Module module) getInstanceTypeOfModule) { + final lines = []; + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + ssmi as SystemCSynthSubModuleInstantiation; + if (_isHandledInline(ssmi)) { + continue; + } + final instanceType = getInstanceTypeOfModule(ssmi.module); + lines.add(' $instanceType ${ssmi.name}{"${ssmi.name}"};'); + } + return lines.join('\n'); + } + + /// Dummy signal declarations needed for unconnected submodule output ports. + /// Populated by [_buildSubModuleBindings]. + final List _unconnectedOutputSignals = []; + + /// Signal declarations for constants bound to submodule input ports. + /// Populated by [_buildSubModuleBindings]. + final List _constInputSignals = []; + + /// Initialization statements for constant signals (in constructor body). + /// Populated by [_buildSubModuleBindings]. + final List _constInputInits = []; + + String _buildSubModuleBindings( + String Function(Module module) getInstanceTypeOfModule) { + final lines = []; + var unconnIdx = 0; + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + ssmi as SystemCSynthSubModuleInstantiation; + if (_isHandledInline(ssmi)) { + continue; + } + + // Bind connected ports (inputs, outputs, inouts) + final allPorts = { + ...ssmi.inputMapping, + ...ssmi.outputMapping, + ...ssmi.inOutMapping, + }; + for (final entry in allPorts.entries) { + if (!entry.value.declarationCleared) { + if (entry.value.isConstant) { + // Constants can't be bound directly to sc_in ports; + // create a signal, initialize it, and bind that. + final constName = _scName('_const_${ssmi.name}' + '_${entry.key}_${_constInputSignals.length}'); + final w = entry.value.width; + final c = entry.value.logics.whereType().first; + final constVal = _typedConstExpr(c.value, c.width); + _constInputSignals + .add(' ${systemCSignalType(w)} $constName{"$constName"};'); + _constInputInits.add(' $constName.write($constVal);'); + lines.add(' ${ssmi.name}.${entry.key}($constName);'); + } else { + lines.add(' ' + '${ssmi.name}.${entry.key}(${_scName(entry.value.name)});'); + } + } + } + + // Bind unconnected ports to dummy signals + // (SystemC requires all sc_in/sc_out ports to be bound) + for (final entry in [ + ...ssmi.outputMapping.entries, + ...ssmi.inputMapping.entries, + ]) { + if (entry.value.declarationCleared) { + final dummyName = '_unused_${ssmi.name}_${entry.key}_$unconnIdx'; + final w = entry.value.width; + _unconnectedOutputSignals + .add(' ${systemCSignalType(w)} $dummyName{"$dummyName"};'); + lines.add(' ${ssmi.name}.${entry.key}($dummyName);'); + unconnIdx++; + } + } + } + return lines.join('\n'); + } + + // ──────────────────────────────────────────────────────────────────── + // Wire assignments + // ──────────────────────────────────────────────────────────────────── + + _MethodResult? _buildWireAssignments() { + if (_synthModuleDefinition.assignments.isEmpty) { + return null; + } + + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + var methodIdx = 0; + + // 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 methodName = 'wire_assign_$methodIdx'; + methodIdx++; + setupBuf.writeln(' SC_METHOD($methodName);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + bodyBuf + ..writeln(' void $methodName() {') + ..writeln(' ${_scName(assignment.dst.name)} = ' + '${_synthLogicReadExpr(assignment.src)};') + ..writeln(' }') + ..writeln(); + } + } + + // 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})'); + } + } + final methodName = 'wire_assign_$methodIdx'; + methodIdx++; + setupBuf.writeln(' SC_METHOD($methodName);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + bodyBuf + ..writeln(' void $methodName() {') + ..writeln(' $dstName = ${parts.join(' | ')};') + ..writeln(' }') + ..writeln(); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Conditional → SystemC + // ──────────────────────────────────────────────────────────────────── + + String _conditionalToSC(Conditional conditional, int indent, + Map inputsMap, Map outputsMap) { + final padding = ' ' * indent; + + if (conditional is ConditionalAssign) { + final driverExpr = _resolveDriver(conditional.driver, inputsMap); + final receiver = _resolveReceiver(conditional.receiver, outputsMap); + return '$padding$receiver = $driverExpr;\n'; + } else if (conditional is If) { + return _ifToSC(conditional, indent, inputsMap, outputsMap); + } else if (conditional is Case) { + return _caseToSC(conditional, indent, inputsMap, outputsMap); + } else if (conditional is ConditionalGroup) { + final buf = StringBuffer(); + for (final c in conditional.conditionals) { + buf.write(_conditionalToSC(c, indent, inputsMap, outputsMap)); + } + return buf.toString(); + } + return ''; + } + + String _ifToSC(If ifBlock, int indent, Map inputsMap, + Map outputsMap) { + final padding = ' ' * indent; + final buf = StringBuffer(); + + for (final iff in ifBlock.iffs) { + final header = iff == ifBlock.iffs.first + ? 'if' + : iff is Else + ? ' else' + : ' else if'; + final condition = + iff is! Else ? ' (${_resolveDriver(iff.condition, inputsMap)})' : ''; + buf.write('$padding$header$condition {\n'); + for (final c in iff.then) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + buf.writeln(); + return buf.toString(); + } + + String _caseToSC(Case caseBlock, int indent, Map inputsMap, + Map outputsMap) { + final padding = ' ' * indent; + final buf = StringBuffer(); + final expr = _resolveDriver(caseBlock.expression, inputsMap); + + // Check if all case items have compile-time constant values + final allConst = + caseBlock.items.every((item) => _isConstCaseItem(item.value)); + + // CaseZ requires mask matching — always use if/else + // Non-const case items also require if/else + if (caseBlock is CaseZ || !allConst) { + return _caseToIfElseSC(caseBlock, indent, inputsMap, outputsMap, expr); + } + + buf.writeln('${padding}switch ($expr) {'); + for (final item in caseBlock.items) { + buf.writeln('$padding case ${_constLit(item.value)}:'); + for (final c in item.then) { + buf.write(_conditionalToSC(c, indent + 2, inputsMap, outputsMap)); + } + buf.writeln('$padding break;'); + } + if (caseBlock.defaultItem != null) { + buf.writeln('$padding default:'); + for (final c in caseBlock.defaultItem!) { + buf.write(_conditionalToSC(c, indent + 2, inputsMap, outputsMap)); + } + buf.writeln('$padding break;'); + } + buf.writeln('$padding}'); + return buf.toString(); + } + + /// Checks whether a case item value is a compile-time constant. + bool _isConstCaseItem(dynamic value) { + if (value is Const) { + return true; + } + if (value is LogicValue) { + return true; + } + if (value is Logic) { + if (value.srcConnection is Const) { + return true; + } + final sl = _synthModuleDefinition.logicToSynthMap[value]; + if (sl != null && sl.isConstant) { + return true; + } + return false; + } + return true; // int, string, etc. + } + + /// Converts a Case/CaseZ block to if/else chain (for non-const items + /// or CaseZ with z-masks). + String _caseToIfElseSC( + Case caseBlock, + int indent, + Map inputsMap, + Map outputsMap, + String expr) { + final padding = ' ' * indent; + final buf = StringBuffer(); + + for (var i = 0; i < caseBlock.items.length; i++) { + final item = caseBlock.items[i]; + final condition = _caseItemCondition(item.value, expr, inputsMap, + isCaseZ: caseBlock is CaseZ); + final header = i == 0 ? 'if' : ' else if'; + buf.write('$padding$header ($condition) {\n'); + for (final c in item.then) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + if (caseBlock.defaultItem != null) { + buf.write(' else {\n'); + for (final c in caseBlock.defaultItem!) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + buf.writeln(); + return buf.toString(); + } + + /// Generates the condition expression for a case item comparison. + String _caseItemCondition( + dynamic value, String expr, Map inputsMap, + {bool isCaseZ = false}) { + // Extract LogicValue from Const for CaseZ mask matching + LogicValue? lv; + if (value is Const) { + lv = value.value; + } else if (value is LogicValue) { + lv = value; + } + if (isCaseZ && lv != null && !lv.isValid) { + // CaseZ: create mask comparison (expr & mask) == pattern + // z bits become don't-care (mask out those bits) + final width = lv.width; + // z→0 in mask, 0/1→1 in mask + var maskStr = ''; + var patStr = ''; + for (var i = width - 1; i >= 0; i--) { + final bit = lv[i]; + if (bit == LogicValue.z || bit == LogicValue.x) { + maskStr += '0'; + patStr += '0'; + } else { + maskStr += '1'; + patStr += bit == LogicValue.one ? '1' : '0'; + } + } + final maskVal = BigInt.parse(maskStr, radix: 2); + final patVal = BigInt.parse(patStr, radix: 2); + return '($expr & $maskVal) == $patVal'; + } + if (value is Logic && value is! Const) { + final resolved = _resolveDriver(value, inputsMap); + return '$expr == $resolved'; + } + return '$expr == ${_constLit(value)}'; + } + + /// Resolves a driver Logic to a SystemC read expression using the + /// SynthModuleDefinition's logicToSynthMap to find the canonical name. + String _resolveDriver(Logic driver, Map inputsMap) { + if (driver is Const) { + return _constLit(driver); + } + // Look up via logicToSynthMap — the SynthLogic has the canonical name + final sl = _synthModuleDefinition.logicToSynthMap[driver]; + if (sl != null) { + return _synthLogicReadExpr(sl); + } + // Try to find via source connection chain — handles cases where + // the Logic object isn't directly in the map but its source is + var src = driver.srcConnection; + while (src != null) { + final srcSl = _synthModuleDefinition.logicToSynthMap[src]; + if (srcSl != null) { + return _synthLogicReadExpr(srcSl); + } + src = src.srcConnection; + } + // Fallback: try inputsMap by port name + if (inputsMap.containsKey(driver.name)) { + return inputsMap[driver.name]!; + } + return '${_scName(driver.name)}.read()'; + } + + /// Resolves a receiver Logic to a SystemC signal name using the + /// SynthModuleDefinition's logicToSynthMap to find the canonical name. + String _resolveReceiver(Logic receiver, Map outputsMap) { + // Look up via logicToSynthMap + final sl = _synthModuleDefinition.logicToSynthMap[receiver]; + if (sl != null) { + return _scName(sl.name); + } + // Fallback + if (outputsMap.containsKey(receiver.name)) { + return outputsMap[receiver.name]!; + } + return _scName(receiver.name); + } + + String _constLit(dynamic value) { + if (value is Const) { + if (value.value.isValid) { + return value.value.toBigInt().toString(); + } + return '0'; // x/z → 0 in SystemC + } else if (value is LogicValue) { + if (value.isValid) { + return value.toBigInt().toString(); + } + return '0'; // x/z → 0 in SystemC + } else if (value is Logic) { + // If the Logic is driven by a Const, resolve to integer literal + if (value.srcConnection is Const) { + final cv = (value.srcConnection! as Const).value; + return cv.isValid ? cv.toBigInt().toString() : '0'; + } + // Check logicToSynthMap for a constant SynthLogic + final sl = _synthModuleDefinition.logicToSynthMap[value]; + if (sl != null && sl.isConstant) { + final constLogic = sl.logics.whereType().firstOrNull; + if (constLogic != null) { + return constLogic.value.isValid + ? constLogic.value.toBigInt().toString() + : '0'; + } + } + // Fallback: use signal read expression + return '${value.name}.read()'; + } + return value.toString(); + } + + // ──────────────────────────────────────────────────────────────────── + // Build all sections + // ──────────────────────────────────────────────────────────────────── + + void _buildModuleBody( + String Function(Module module) getInstanceTypeOfModule) { + _subMembers = _buildSubModuleMembers(getInstanceTypeOfModule); + + final inlineGates = _buildInlineGates(); + final processes = _buildProcesses(); + final wireAssigns = _buildWireAssignments(); + final arrayAssembly = _buildArrayAssemblyMethod(); + final subBindings = _buildSubModuleBindings(getInstanceTypeOfModule); + + // Build internal signals, appending dummy signals for unconnected + // submodule outputs (populated by _buildSubModuleBindings above). + final baseSigs = _buildInternalSignals(); + _internalSigs = [ + baseSigs, + ..._unconnectedOutputSignals, + ..._constInputSignals, + ].where((s) => s.isNotEmpty).join('\n'); + + final ctorParts = [ + if (_constInputInits.isNotEmpty) _constInputInits.join('\n'), + if (inlineGates != null) inlineGates.setup, + if (processes != null) processes.setup, + if (wireAssigns != null) wireAssigns.setup, + if (arrayAssembly != null) arrayAssembly.setup, + if (subBindings.isNotEmpty) subBindings, + ]; + _ctorBody = ctorParts.join(); + + final bodyParts = [ + if (inlineGates != null) inlineGates.body, + if (processes != null) processes.body, + if (wireAssigns != null) wireAssigns.body, + if (arrayAssembly != null) arrayAssembly.body, + ]; + _methodBodies = bodyParts.where((s) => s.isNotEmpty).join('\n'); + } + + /// Builds an SC_METHOD that assembles individual array element signals + /// back into their parent signal via concatenation. + _MethodResult? _buildArrayAssemblyMethod() { + if (_arrayElementsByParent.isEmpty) { + return null; + } + + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + var methodIdx = 0; + + for (final entry in _arrayElementsByParent.entries) { + final parentName = _scName(entry.key); + final elements = entry.value; + final methodName = 'array_assemble_$methodIdx'; + methodIdx++; + + setupBuf.writeln(' SC_METHOD($methodName);'); + for (final elem in elements) { + setupBuf.writeln(' sensitive << ${_scName(elem.elemName)};'); + } + + // Build concatenation: (elem[N-1], ..., elem[1], elem[0]) + // SystemC concat is MSB-first, so highest index first + // Wrap 1-bit (bool) elements in sc_uint<1>() for proper concat + final concatParts = elements.reversed.map((e) { + final read = '${_scName(e.elemName)}.read()'; + return e.width == 1 ? 'sc_uint<1>($read)' : read; + }).toList(); + + bodyBuf + ..writeln(' void $methodName() {') + ..writeln(' $parentName = (${concatParts.join(', ')});') + ..writeln(' }') + ..writeln(); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Final assembly + // ──────────────────────────────────────────────────────────────────── + + String _toSystemC() { + final moduleName = getInstanceTypeOfModule(module); + final buf = StringBuffer()..writeln('SC_MODULE($moduleName) {'); + + if (_portsString.isNotEmpty) { + buf.writeln(_portsString); + } + if (_internalSigs.isNotEmpty) { + buf + ..writeln() + ..writeln(_internalSigs); + } + if (_subMembers.isNotEmpty) { + buf + ..writeln() + ..writeln(_subMembers); + } + + buf + ..writeln() + ..writeln(' SC_CTOR($moduleName) {'); + if (_ctorBody.isNotEmpty) { + buf.write(_ctorBody); + } + buf.writeln(' }'); + + if (_methodBodies.isNotEmpty) { + buf + ..writeln() + ..write(_methodBodies) + ..writeln(); + } + + buf.writeln('};'); + 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}); +} + +/// 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/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index d7850df4e..75cf490d5 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -14,6 +14,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; import 'package:rohd/src/utilities/web.dart'; import 'package:test/test.dart'; @@ -104,10 +105,7 @@ class Vector { final outputPort = module.tryInOut(outputName) ?? module.output(outputName); final expected = expectedOutput.value; - final expectedValue = LogicValue.of( - expected, - width: outputPort.width, - ); + final expectedValue = LogicValue.of(expected, width: outputPort.width); final inputStimulus = inputValues.toString(); if (outputPort is LogicArray) { @@ -125,12 +123,8 @@ class Vector { } final checks = checksList.join('\n'); - final tbVerilog = [ - assignments, - '#$_offset', - checks, - '#${_period - _offset}', - ].join('\n'); + final tbVerilog = + [assignments, '#$_offset', checks, '#${_period - _offset}'].join('\n'); return tbVerilog; } } @@ -202,12 +196,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; @@ -224,23 +216,20 @@ abstract class SimCompare { RegExp(r'sorry: constant selects in always_\* processes' ' are not currently supported'), RegExp('warning: always_comb process has no sensitivities'), - RegExp('finish called at'), + RegExp('finish called at') ]; /// Executes [vectors] against the Icarus Verilog simulator and checks /// that it passes. - 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, - }) { + 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}) { final result = iverilogVector(module, vectors, moduleName: moduleName, dontDeleteTmpFiles: dontDeleteTmpFiles, @@ -255,17 +244,14 @@ abstract class SimCompare { } /// Executes [vectors] against the Icarus Verilog simulator. - 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, - }) { + 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}) { if (kIsWeb) { // if running in web mode, then we can't run icarus verilog return true; @@ -307,7 +293,7 @@ abstract class SimCompare { final topModule = moduleName ?? module.definitionName; final allSignals = { for (final v in vectors) ...v.inputValues.keys, - for (final v in vectors) ...v.expectedOutputValues.keys, + for (final v in vectors) ...v.expectedOutputValues.keys }; late final tbWireUniquifier = Uniquifier(); @@ -335,7 +321,7 @@ abstract class SimCompare { final sigDecl = signalDeclaration(logicName, adjust: toTbWireName, signalTypeOverride: 'wire'); return '$sigDecl; assign $wireName = $logicName;'; - }), + }) ].join('\n'); final moduleConnections = @@ -370,7 +356,7 @@ abstract class SimCompare { stimulus, r'$finish;', // so the test doesn't run forever if there's a clock gen 'end', - 'endmodule', + 'endmodule' ].join('\n'); Directory(dir).createSync(recursive: true); @@ -397,11 +383,7 @@ abstract class SimCompare { } return output.toString().contains(RegExp( - [ - 'error', - 'unable', - if (!allowWarnings) 'warning', - ].join('|'), + ['error', 'unable', if (!allowWarnings) 'warning'].join('|'), caseSensitive: false)); } @@ -424,16 +406,777 @@ abstract class SimCompare { if (!dontDeleteTmpFiles) { try { - File(tmpOutput).deleteSync(); - File(tmpTestFile).deleteSync(); + final outFile = File(tmpOutput); + if (outFile.existsSync()) { + outFile.deleteSync(); + } + final testFile = File(tmpTestFile); + if (testFile.existsSync()) { + testFile.deleteSync(); + } if (dumpWaves) { - File(tmpVcdFile).deleteSync(); + final vcdFile = File(tmpVcdFile); + if (vcdFile.existsSync()) { + vcdFile.deleteSync(); + } } } on Exception catch (e) { print("Couldn't delete: $e"); - return false; } } return true; } + + // ══════════════════════════════════════════════════════════════════════ + // SystemC simulation (Accellera SystemC) + // ══════════════════════════════════════════════════════════════════════ + + /// The default SystemC installation path (Accellera). + static const _systemCDefaultHome = '/opt/systemc/include'; + static const _systemCDefaultLib = '/opt/systemc/lib'; + + /// Cache of compiled SystemC executables keyed by generated code hash. + static final _compilationCache = {}; + + /// Prefix for SystemC artifacts owned by this test process. + static final String _systemCTempPrefix = + '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(_systemCTempPrefix) || 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 executable. + /// + /// Returns a [SystemCExecutable] that can be used to run multiple vector + /// sets without recompilation. Use in `setUpAll` for test groups. + /// Results are cached — calling this with the same module definition + /// returns the previously compiled binary. + static SystemCExecutable? buildSystemCExecutable(Module module, + {String? moduleName, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib}) { + if (kIsWeb) { + return null; + } + + final scHome = systemcHome ?? ''; + final scLib = systemcLib ?? ''; + final (resolvedHome, resolvedLib) = _resolveSystemCPaths(scHome, scLib); + + if (resolvedHome == null || resolvedLib == null) { + print('SystemC installation not found'); + return null; + } + + final topModule = moduleName ?? module.definitionName; + final generatedSystemC = module.generateSystemC(); + + // Check compilation cache + final cacheKey = generatedSystemC.hashCode; + if (_compilationCache.containsKey(cacheKey)) { + 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; + } + + // Generate stdin-driven testbench + final tb = StringBuffer() + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('using namespace std;') + ..writeln() + ..writeln(generatedSystemC) + ..writeln() + ..writeln('int sc_main(int argc, char* argv[]) {'); + + // Clock + for (final clkName in clockSignals) { + tb.writeln( + ' sc_clock $clkName("$clkName", ${Vector._period}, SC_NS);'); + } + + // Signals for all non-clock input ports + for (final entry in inputPorts.entries) { + if (clockSignals.contains(entry.key)) { + continue; + } + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};'); + } + + // Signals for all output ports + for (final entry in outputPorts.entries) { + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};'); + } + + tb + ..writeln() + // DUT instantiation and port binding + ..writeln(' $topModule dut("dut");'); + for (final name in inputPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + for (final clkName in clockSignals) { + if (!inputPorts.containsKey(clkName)) { + tb.writeln(' dut.$clkName($clkName);'); + } + } + for (final name in outputPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + + tb + ..writeln() + ..writeln(' int _tb_errors = 0;') + ..writeln() + ..writeln(' // Initial offset') + ..writeln(' sc_start(sc_time(1, SC_NS));') + ..writeln() + ..writeln(' // Read number of vectors') + ..writeln(' int _tb_nvec;') + ..writeln(' cin >> _tb_nvec;') + ..writeln() + ..writeln(' for (int _tb_v = 0; _tb_v < _tb_nvec; _tb_v++) {'); + + // Read and drive each non-clock input + final drivableInputs = + inputPorts.keys.where((k) => !clockSignals.contains(k)).toList(); + for (final name in drivableInputs) { + final w = inputPorts[name]!; + if (w > 64) { + // BigInt — read as hex string + tb + ..writeln(' { string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _v(_h.c_str());') + ..writeln(' $name.write(_v); }'); + } else { + tb + ..writeln(' { uint64_t _v; cin >> _v;') + ..writeln(' $name.write(_v); }'); + } + } + + // Advance to check point + tb + ..writeln() + ..writeln(' sc_start(sc_time(${Vector._offset}, SC_NS));') + ..writeln() + ..writeln(' // Read number of outputs to check') + ..writeln(' int _tb_nchk;') + ..writeln(' cin >> _tb_nchk;') + ..writeln() + ..writeln(' for (int _tb_c = 0; _tb_c < _tb_nchk; _tb_c++) {') + ..writeln(' string _tb_pn;') + ..writeln(' cin >> _tb_pn;'); + + // Generate if-else chain for each output port + var first = true; + for (final entry in outputPorts.entries) { + final name = entry.key; + final w = entry.value; + final ifKey = first ? 'if' : '} else if'; + first = false; + tb.writeln(' $ifKey (_tb_pn == "$name") {'); + if (w > 64) { + tb + ..writeln(' string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _tb_exp(_h.c_str());') + ..writeln(' if ($name.read() != _tb_exp) {'); + } else { + tb + ..writeln(' uint64_t _tb_exp; cin >> _tb_exp;') + ..writeln(' if ($name.read() != _tb_exp) {'); + } + tb + ..writeln(' cout << "ERROR vector " << _tb_v' + ' << ": expected $name=" << _tb_exp' + ' << ", got " << $name.read() << endl;') + ..writeln(' _tb_errors++;') + ..writeln(' }'); + } + if (outputPorts.isNotEmpty) { + tb + ..writeln(' } else {') + ..writeln(' string _d; cin >> _d; // skip unknown') + ..writeln(' }'); + } + + tb + ..writeln(' }') + ..writeln() + ..writeln(' sc_start(sc_time(' + '${Vector._period - Vector._offset}, SC_NS));') + ..writeln(' }') + ..writeln() + ..writeln(' if (_tb_errors == 0) {') + ..writeln(' cout << "PASS" << endl;') + ..writeln(' } else {') + ..writeln(' cout << "FAIL: " << _tb_errors << " errors" << endl;') + ..writeln(' }') + ..writeln(' return _tb_errors > 0 ? 1 : 0;') + ..writeln('}'); + + final testbenchCode = tb.toString(); + + // Write and compile + const dir = 'tmp_test'; + Directory(dir).createSync(recursive: true); + final compileDir = Directory(dir) + .createTempSync('${_systemCTempPrefix}_${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 = SystemCExecutable._( + binaryPath: tmpOutput, + cppFile: tmpCppFile, + scLib: resolvedLib, + clockSignals: clockSignals, + inputPorts: inputPorts, + outputPorts: outputPorts); + _compilationCache[cacheKey] = exe; + return exe; + } + + /// Runs [vectors] against a pre-compiled [SystemCExecutable]. + /// + /// Returns `true` if all vectors pass. + static bool runSystemCVectors(SystemCExecutable exe, List vectors) { + 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]} '); + } + sb.writeln(); + + // Write expected outputs: count then name/value pairs + // Skip x/z outputs + final checks = {}; + for (final entry in vector.expectedOutputValues.entries) { + final name = entry.key; + final w = exe.outputPorts[name]!; + final expectedLV = LogicValue.of(entry.value, width: w); + if (expectedLV.toString().contains('x') || + expectedLV.toString().contains('z')) { + continue; + } + if (w > 64) { + var hex = expectedLV.toBigInt().toUnsigned(w).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + checks[name] = '0x$hex'; + } else { + checks[name] = '${_systemcIntValue(entry.value, w)}'; + } + } + sb.write('${checks.length} '); + for (final entry in checks.entries) { + sb.write('${entry.key} ${entry.value} '); + } + sb.writeln(); + } + + // Write vectors to 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(SystemCExecutable exe, List vectors) { + expect(runSystemCVectors(exe, vectors), true); + } + + /// Converts a value to an integer for stdin. + static int _systemcIntValue(dynamic value, int width) { + if (value is int) { + return value; + } + if (value is LogicValue) { + if (!value.isValid) { + return 0; + } + return value.toBigInt().toUnsigned(width).toInt(); + } + if (value is BigInt) { + return value.toUnsigned(width).toInt(); + } + if (value is String) { + final lv = LogicValue.of(value, width: width); + if (!lv.isValid) { + return 0; + } + return lv.toBigInt().toUnsigned(width).toInt(); + } + return 0; + } + + /// Executes [vectors] against a SystemC simulator compiled with g++ and + /// checks that it passes (single-shot, compiles each time). + static void checkSystemCVector(Module module, List vectors, + {String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false}) { + if (buildOnly) { + // Just verify SystemC code generation succeeds + module.generateSystemC(); + return; + } + final exe = buildSystemCExecutable(module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib); + if (exe == null) { + // 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 = buildSystemCExecutable(module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib); + if (exe == null) { + return false; + } + if (buildOnly) { + return true; + } + return runSystemCVectors(exe, vectors); + } + + // ══════════════════════════════════════════════════════════════════════ + // Trace-based SystemC co-simulation + // ══════════════════════════════════════════════════════════════════════ + + /// Runs the ROHD simulation using [stimulus], records input/output values + /// at every posedge of [clk], then replays the captured vectors through + /// the SystemC-synthesized version of [module] and compares results. + /// + /// [stimulus] is an async function that sets up and drives the simulation + /// (inject signals, register actions, etc.) but does NOT call + /// [Simulator.run] — that is done internally. + /// + /// [inputNames] and [outputNames] specify which ports to record. If null, + /// all module inputs (excluding clock) and all module outputs are used. + /// + /// Example usage with an existing test: + /// ```dart + /// await SimCompare.systemcSimCompare( + /// counter, + /// clk, + /// stimulus: () async { + /// reset.inject(1); + /// en.inject(0); + /// Simulator.registerAction(25, () { reset.put(0); en.put(1); }); + /// Simulator.setMaxSimTime(100); + /// }, + /// ); + /// ``` + static Future systemcSimCompare(Module module, Logic clk, + {required Future Function() stimulus, + List? inputNames, + List? outputNames, + String? clockName, + String? resetName, + bool dontDeleteTmpFiles = false, + String? systemcHome, + String? systemcLib}) async { + // Determine which signals to record + final clkName = clockName ?? + module.inputs.keys.firstWhere((n) => n == 'clk' || n.contains('clock'), + orElse: () => 'clk'); + + final inputs = + inputNames ?? module.inputs.keys.where((n) => n != clkName).toList(); + final outputs = outputNames ?? module.outputs.keys.toList(); + + // Record snapshots at each posedge. + // Use previousValue for outputs — this gives us the output state from + // BEFORE the clock edge, which matches what the SystemC testbench sees + // when it checks at offset (before the posedge). + // Use current value for inputs — these are the values being presented + // to the DUT when the clock edge fires. + final recordings = []; + + clk.posedge.listen((_) { + // Sample inputs (current value — what's being driven now) + final inputValues = {}; + for (final name in inputs) { + final sig = module.input(name); + final val = sig.value; + inputValues[name] = val.isValid ? val.toBigInt().toInt() : 0; + } + + // Sample outputs using previousValue — the settled output + // from before this tick started, which is what a testbench + // checking before the clock edge would observe. + final outputValues = {}; + for (final name in outputs) { + final sig = module.output(name); + final prev = sig.previousValue; + if (prev != null && prev.isValid) { + outputValues[name] = prev.toBigInt().toInt(); + } + // Skip null/x/z — no check for this output + } + + recordings.add(Vector(inputValues, outputValues)); + }); + + // Run the user's stimulus setup + await stimulus(); + + // Run the ROHD simulation + await Simulator.run(); + + if (recordings.length < 2) { + print('Warning: only ${recordings.length} clock edges recorded,' + ' need at least 2 for comparison'); + return true; + } + + // No shifting needed — previousValue already gives us the output + // state from before the posedge, which matches systemcVector's + // check-before-edge timing. Just pass recordings directly as vectors. + + // Run through SystemC + return systemcVector(module, recordings, + clockName: clkName, + resetName: resetName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + systemcHome: systemcHome, + systemcLib: systemcLib); + } +} + +/// Holds the compiled state of a SystemC executable for reuse across tests. +class SystemCExecutable { + /// Path to the compiled binary. + final String binaryPath; + + /// Path to the generated C++ source. + final String cppFile; + + /// Path to the SystemC library (for LD_LIBRARY_PATH). + final String scLib; + + /// Clock signal names. + final Set clockSignals; + + /// Input port names and widths (excluding promoted clocks). + final Map inputPorts; + + /// Output port names and widths. + final Map outputPorts; + + SystemCExecutable._( + {required this.binaryPath, + required this.cppFile, + required this.scLib, + required this.clockSignals, + required this.inputPorts, + required this.outputPorts}); + + /// Deletes the compiled binary and source. + void cleanup() { + void tryDelete(String path) { + final f = File(path); + if (f.existsSync()) { + f.deleteSync(); + } + } + + try { + final compileDir = File(cppFile).parent; + if (compileDir.existsSync() && + compileDir.uri.pathSegments.last + .startsWith(SimCompare._systemCTempPrefix)) { + compileDir.deleteSync(recursive: true); + return; + } + tryDelete(cppFile); + tryDelete(binaryPath); + } on Exception catch (_) {} + } } diff --git a/lib/src/utilities/systemc_cosim_ffi.dart b/lib/src/utilities/systemc_cosim_ffi.dart new file mode 100644 index 000000000..812aef1f6 --- /dev/null +++ b/lib/src/utilities/systemc_cosim_ffi.dart @@ -0,0 +1,961 @@ +// 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 + // ══════════════════════════════════════════════════════════════════════ + + /// Generates the C++ wrapper with extern "C" API around the ROHD-generated + /// SystemC module code. + String _generateWrapper(String generatedSystemC, String topModule) { + final sb = StringBuffer() + ..writeln('// Auto-generated SystemC FFI Cosim Wrapper') + ..writeln('// Module: $topModule') + ..writeln() + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('using namespace std;') + ..writeln() + ..writeln('// ═══ ROHD-Generated SystemC Module(s) ═══') + ..writeln() + ..writeln(generatedSystemC) + ..writeln() + ..writeln('// ═══ FFI Cosim Context ═══') + ..writeln() + ..writeln('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};'); + } + + sb + ..writeln(' $topModule* dut;') + ..writeln('};') + ..writeln() + ..writeln('extern "C" {') + ..writeln() + ..writeln('// Required by SystemC linker — we never call it directly') + ..writeln('int sc_main(int, char*[]) { return 0; }') + ..writeln() + ..writeln('// Track whether the kernel has been initialized') + ..writeln('static CosimContext* _active_ctx = nullptr;') + ..writeln() + // ──── sc_cosim_create ──── + ..writeln('void* sc_cosim_create(const char* name) {') + ..writeln(' // If a context already exists (same process, new test),') + ..writeln(' // just return the existing one after resetting signals.') + ..writeln(' if (_active_ctx != nullptr) {') + ..writeln(' // 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));'); + } + + sb + ..writeln(' return static_cast(_active_ctx);') + ..writeln(' }') + ..writeln() + ..writeln(' // Guard: cannot create sc_signal after kernel starts') + ..writeln(' if (sc_get_status() != SC_ELABORATION' + ' && sc_get_status() != SC_BEFORE_END_OF_ELABORATION) {') + ..writeln(' return nullptr; // E113 prevention') + ..writeln(' }') + ..writeln() + ..writeln(' auto* ctx = new CosimContext();') + + // Instantiate DUT + ..writeln(' 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);'); + } + + sb + ..writeln() + ..writeln(' // Store context — do NOT call sc_start here.') + ..writeln(' // Deferring sc_start to the first advance allows') + ..writeln(' // multiple module types to be elaborated before') + ..writeln(' // the kernel starts (avoids E113).') + ..writeln(' _active_ctx = ctx;') + ..writeln(' return static_cast(ctx);') + ..writeln('}') + ..writeln() + // ──── sc_cosim_set_input ──── + ..writeln('void sc_cosim_set_input(void* handle, const char* name,' + ' uint64_t value) {') + ..writeln(' auto* ctx = static_cast(handle);'); + + _generateInputDispatch(sb, narrow: true); + + sb + ..writeln('}') + ..writeln() + // ──── sc_cosim_set_input_wide ──── + ..writeln('void sc_cosim_set_input_wide(void* handle, const char* name,' + ' const char* hex_value) {') + ..writeln(' auto* ctx = static_cast(handle);'); + + _generateInputDispatch(sb, narrow: false); + + sb + ..writeln('}') + ..writeln() + // ──── sc_cosim_get_output ──── + ..writeln( + 'uint64_t sc_cosim_get_output(void* handle, const char* name) {') + ..writeln(' auto* ctx = static_cast(handle);'); + + _generateOutputDispatch(sb, narrow: true); + + sb + ..writeln(' return 0;') + ..writeln('}') + ..writeln() + // ──── sc_cosim_get_output_wide ──── + ..writeln('const char* sc_cosim_get_output_wide(void* handle,' + ' const char* name) {') + ..writeln(' auto* ctx = static_cast(handle);') + ..writeln(' static char _buf[512];'); + + _generateOutputDispatch(sb, narrow: false); + + sb + ..writeln(" _buf[0] = '0'; _buf[1] = 0;") + ..writeln(' return _buf;') + ..writeln('}') + ..writeln() + // ──── sc_cosim_advance ──── + ..writeln('void sc_cosim_advance(void* handle, uint64_t time_ps) {') + ..writeln(' // End elaboration on first advance (allows multiple') + ..writeln(' // module types to be instantiated before starting).') + ..writeln(' if (sc_get_status() == SC_ELABORATION) {') + ..writeln(' sc_start(SC_ZERO_TIME);') + ..writeln(' }') + ..writeln(' if (time_ps == 0) {') + ..writeln(' // Zero-time advance: process delta cycles only.') + ..writeln(' // Use SC_ZERO_TIME explicitly (some implementations') + ..writeln(' // treat sc_time(0,SC_PS) differently).') + ..writeln(' sc_start(SC_ZERO_TIME);') + ..writeln(' } else {') + ..writeln( + ' sc_start(sc_time(static_cast(time_ps), SC_PS));') + ..writeln(' }') + ..writeln('}') + ..writeln() + // ──── sc_cosim_destroy ──── + ..writeln('void sc_cosim_destroy(void* handle) {') + ..writeln(' // Do NOT delete or sc_stop — the SystemC kernel is a') + ..writeln(' // process-wide singleton. The context is reused if') + ..writeln(' // sc_cosim_create is called again (same module).') + ..writeln(' // This avoids E113 "insert primitive channel failed".') + ..writeln('}') + ..writeln() + ..writeln('} // 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 { + sb + ..writeln(' sc_biguint<$width> v(hex_value);') + ..writeln(' ctx->$name.write(v);'); + } + } + 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 { + sb + ..writeln(' sc_biguint<$width> v = ctx->$name.read();') + ..writeln(' string s = v.to_string(SC_HEX_US);') + ..writeln(' strncpy(_buf, s.c_str(), sizeof(_buf)-1);') + ..writeln(' _buf[sizeof(_buf)-1] = 0;') + ..writeln(' return _buf;'); + } + } + 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/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_simcompare_test.dart b/test/systemc_simcompare_test.dart new file mode 100644 index 000000000..2ec5c08d2 --- /dev/null +++ b/test/systemc_simcompare_test.dart @@ -0,0 +1,243 @@ +// 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('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/systemc_vector_test.dart b/test/systemc_vector_test.dart new file mode 100644 index 000000000..1a681097c --- /dev/null +++ b/test/systemc_vector_test.dart @@ -0,0 +1,1279 @@ +// Copyright (C) 2024-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_vector_test.dart +// Parallel SystemC simulation tests for all modules tested with iverilog. +// +// 2026 May 7 +// Author: Desmond A. Kirkpatrick + +import 'dart:math'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +// ===== Modules from flop_test.dart ===== + +class FlopTestModule extends Module { + FlopTestModule(Logic a, {Logic? en, Logic? reset, dynamic resetValue}) + : super(name: 'floptestmodule') { + a = addInput('a', a, width: a.width); + if (en != null) { + en = addInput('en', en); + } + if (reset != null) { + reset = addInput('reset', reset); + } + if (resetValue != null && resetValue is Logic) { + resetValue = addInput('resetValue', resetValue, width: a.width); + } + final y = addOutput('y', width: a.width); + final clk = SimpleClockGenerator(10).clk; + y <= flop(clk, a, en: en, reset: reset, resetValue: resetValue); + } +} + +// ===== Modules from counter_test.dart ===== + +class Counter extends Module { + final int width; + Logic get val => output('val'); + Counter(Logic en, Logic reset, {this.width = 8}) : super(name: 'counter') { + en = addInput('en', en); + reset = addInput('reset', reset); + final val = addOutput('val', width: width); + final nextVal = Logic(name: 'nextVal', width: width); + nextVal <= val + 1; + Sequential.multi([ + SimpleClockGenerator(10).clk, + reset + ], [ + If(reset, then: [ + val < 0 + ], orElse: [ + If(en, then: [val < nextVal]) + ]) + ]); + } +} + +// ===== Modules from comparison_test.dart ===== + +class ComparisonTestModule extends Module { + final int c; + ComparisonTestModule(Logic a, Logic b, {this.c = 5}) + : super(name: 'gatetestmodule') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + + final aEqB = addOutput('a_eq_b'); + final aNeqB = addOutput('a_neq_b'); + final aLtB = addOutput('a_lt_b'); + final aLteB = addOutput('a_lte_b'); + final aGtB = addOutput('a_gt_b'); + final aGteB = addOutput('a_gte_b'); + final aGtOperatorB = addOutput('a_gt_operator_b'); + final aGteOperatorB = addOutput('a_gte_operator_b'); + + final aEqC = addOutput('a_eq_c'); + final aNeqC = addOutput('a_neq_c'); + final aLtC = addOutput('a_lt_c'); + final aLteC = addOutput('a_lte_c'); + final aGtC = addOutput('a_gt_c'); + final aGteC = addOutput('a_gte_c'); + final aGtOperatorC = addOutput('a_gt_operator_c'); + final aGteOperatorC = addOutput('a_gte_operator_c'); + + aEqB <= a.eq(b); + aNeqB <= a.neq(b); + aLtB <= a.lt(b); + aLteB <= a.lte(b); + aGtB <= a.gt(b); + aGteB <= a.gte(b); + aGtOperatorB <= (a > b); + aGteOperatorB <= (a >= b); + + aEqC <= a.eq(c); + aNeqC <= a.neq(c); + aLtC <= a.lt(c); + aLteC <= a.lte(c); + aGtC <= a.gt(c); + aGteC <= a.gte(c); + aGtOperatorC <= (a > c); + aGteOperatorC <= (a >= c); + } +} + +// ===== Modules from arithmetic_shift_right_test.dart ===== + +class SraUnsignedTestModule extends Module { + Logic get result => output('result'); + SraUnsignedTestModule(Logic toShift, Logic shiftAmount, Logic maskBit) { + toShift = addInput('toShift', toShift, width: toShift.width); + shiftAmount = + addInput('shiftAmount', shiftAmount, width: shiftAmount.width); + maskBit = addInput('maskBit', maskBit); + addOutput('result', width: toShift.width); + result <= (toShift >> shiftAmount) & maskBit.replicate(toShift.width); + } +} + +// ===== Modules from collapse_test.dart ===== + +class CollapseTestModule extends Module { + CollapseTestModule(Logic a, Logic b) : super(name: 'collapsetestmodule') { + a = addInput('a', a); + b = addInput('b', b); + final c = addOutput('c'); + final d = addOutput('d'); + final e = addOutput('e'); + final f = addOutput('f'); + + final x = Logic(name: 'x'); + final y = Logic(name: 'y'); + final z = Logic(name: 'z', naming: Naming.mergeable); + c <= a & b; + d <= a & b; + x <= a; + y <= x; + e <= a & b & c & x & y; + z <= b & y; + f <= a & z; + + Logic(name: 'internal') <= ~z; + } +} + +// ===== Modules from extend_test.dart ===== + +class ExtendModule extends Module { + ExtendModule(Logic a, int newWidth, ExtendType extendType) { + a = addInput('a', a, width: a.width); + final b = addOutput('b', width: newWidth); + if (extendType == ExtendType.zero) { + b <= a.zeroExtend(newWidth); + } else { + b <= a.signExtend(newWidth); + } + } +} + +enum ExtendType { zero, sign } + +class WithSetModule extends Module { + WithSetModule(Logic a, int startIndex, Logic b) { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + final c = addOutput('c', width: a.width); + c <= a.withSet(startIndex, b); + } +} + +// ===== Modules from bus_test.dart ===== + +class BusTestModule extends Module { + BusTestModule(Logic a, Logic b) : super(name: 'bustestmodule') { + if (a.width != b.width) { + throw Exception('a and b must be same width.'); + } + if (a.width <= 3) { + throw Exception('a must be more than width 3.'); + } + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + + final aBar = addOutput('a_bar', width: a.width); + final aAndB = addOutput('a_and_b', width: a.width); + final aBJoined = addOutput('a_b_joined', width: a.width + b.width); + final aPlusB = addOutput('a_plus_b', width: a.width); + final a1 = addOutput('a1'); + final expressionBitSelect = addOutput('expression_bit_select', width: 4); + + final aReversed = addOutput('a_reversed', width: a.width); + final aShrunk1 = addOutput('a_shrunk1', width: 3); + final aShrunk2 = addOutput('a_shrunk2', width: 2); + final aShrunk3 = addOutput('a_shrunk3'); + final aNegativeShrunk1 = addOutput('a_neg_shrunk1', width: 3); + final aNegativeShrunk2 = addOutput('a_neg_shrunk2', width: 2); + final aNegativeShrunk3 = addOutput('a_neg_shrunk3'); + final aRSliced1 = addOutput('a_rsliced1', width: 5); + final aRSliced2 = addOutput('a_rsliced2', width: 2); + final aRSliced3 = addOutput('a_rsliced3'); + final aRNegativeSliced1 = addOutput('a_r_neg_sliced1', width: 5); + final aRNegativeSliced2 = addOutput('a_r_neg_sliced2', width: 2); + final aRNegativeSliced3 = addOutput('a_r_neg_sliced3'); + final aRange1 = addOutput('a_range1', width: 3); + final aRange2 = addOutput('a_range2', width: 2); + final aRange3 = addOutput('a_range3'); + final aRange4 = addOutput('a_range4', width: 3); + final aNegativeRange1 = addOutput('a_neg_range1', width: 3); + final aNegativeRange2 = addOutput('a_neg_range2', width: 2); + final aNegativeRange3 = addOutput('a_neg_range3'); + final aNegativeRange4 = addOutput('a_neg_range4', width: 3); + final aOperatorIndexing1 = addOutput('a_operator_indexing1'); + final aOperatorIndexing2 = addOutput('a_operator_indexing2'); + final aOperatorIndexing3 = addOutput('a_operator_indexing3'); + final aOperatorNegIndexing1 = addOutput('a_operator_neg_indexing1'); + final aOperatorNegIndexing2 = addOutput('a_operator_neg_indexing2'); + final aOperatorNegIndexing3 = addOutput('a_operator_neg_indexing3'); + + aBar <= ~a; + aAndB <= a & b; + aBJoined <= [b, a].swizzle(); + a1 <= a[1]; + aPlusB <= a + b; + + aShrunk1 <= a.slice(2, 0); + aShrunk2 <= a.slice(1, 0); + aShrunk3 <= a.slice(0, 0); + aNegativeShrunk1 <= a.slice(-6, 0); + aNegativeShrunk2 <= a.slice(-7, 0); + aNegativeShrunk3 <= a.slice(-8, 0); + + aRSliced1 <= a.slice(3, 7); + aRSliced2 <= a.slice(6, 7); + aRSliced3 <= a.slice(7, 7); + aRNegativeSliced1 <= a.slice(-5, -1); + aRNegativeSliced2 <= a.slice(-2, -1); + aRNegativeSliced3 <= a.slice(-1, -1); + + aRange1 <= a.getRange(5, 8); + aRange2 <= a.getRange(6, 8); + aRange3 <= a.getRange(7, 8); + aRange4 <= a.getRange(5); + aNegativeRange1 <= a.getRange(-3, 8); + aNegativeRange2 <= a.getRange(-2, 8); + aNegativeRange3 <= a.getRange(-1, 8); + aNegativeRange4 <= a.getRange(-3); + + aOperatorIndexing1 <= a.elements[0]; + aOperatorIndexing2 <= a[a.width - 1]; + aOperatorIndexing3 <= a[4]; + aOperatorNegIndexing1 <= a[-a.width]; + aOperatorNegIndexing2 <= a[-1]; + aOperatorNegIndexing3 <= a[-2]; + + aReversed <= a.reversed; + + expressionBitSelect <= + [aBJoined, aShrunk1, aRange1, aRSliced1, aPlusB].swizzle().slice(3, 0); + } +} + +class ConstBusModule extends Module { + ConstBusModule(int c, {required bool subset}) { + final outWidth = subset ? 8 : 16; + addOutput('const_subset', width: outWidth) <= + Const(c, width: 16).getRange(0, outWidth); + } +} + +class SingleBitBusSubsetMod extends Module { + SingleBitBusSubsetMod(Logic oneBit) { + oneBit = addInput('oneBit', oneBit); + addOutput('result') <= BusSubset(oneBit, 0, 0).subset; + } +} + +class SelectTestModule extends Module { + SelectTestModule(Logic a1, Logic a2, Logic a3, Logic b, {Logic? defaultValue}) + : super(name: 'selecttestmodule') { + a1 = addInput('a1', a1, width: a1.width); + a2 = addInput('a2', a2, width: a2.width); + a3 = addInput('a3', a3, width: a3.width); + b = addInput('b', b, width: b.width); + + if (defaultValue != null) { + defaultValue = + addInput('defaultValue', defaultValue, width: defaultValue.width); + _selectWithDefault(a1, a2, a3, b, defaultValue); + } else { + _selectWithout(a1, a2, a3, b); + } + } + + void _selectWithout(Logic a1, Logic a2, Logic a3, Logic b) { + final selectIndexValue = addOutput('selectIndexValue', width: a1.width); + final selectFromValue = addOutput('selectFromValue', width: a1.width); + final logicList = [a1, a2, a3]; + selectIndexValue <= logicList.selectIndex(b); + selectFromValue <= b.selectFrom(logicList); + } + + void _selectWithDefault( + Logic a1, Logic a2, Logic a3, Logic b, Logic defaultValue) { + final selectFromValue = addOutput('selectFromValue', width: a1.width); + final selectIndexValue = addOutput('selectIndexValue', width: a1.width); + final logicList = [a1, a2, a3]; + selectFromValue <= b.selectFrom(logicList, defaultValue: defaultValue); + selectIndexValue <= logicList.selectIndex(b, defaultValue: defaultValue); + } +} + +// ===== Modules from conditionals_test.dart ===== + +class LoopyCombModuleSsa extends Module { + Logic get a => input('a'); + Logic get x => output('x'); + LoopyCombModuleSsa(Logic a) : super(name: 'loopycombmodule') { + a = addInput('a', a); + final x = addOutput('x'); + Combinational.ssa((s) => [ + s(x) < a, + s(x) < ~s(x), + ]); + } +} + +class CaseModule extends Module { + CaseModule(Logic a, Logic b) : super(name: 'casemodule') { + a = addInput('a', a); + b = addInput('b', b); + final c = addOutput('c'); + final d = addOutput('d'); + final e = addOutput('e'); + + Combinational([ + Case( + [b, a].swizzle(), + [ + CaseItem(Const(LogicValue.ofString('01')), [c < 1, d < 0]), + CaseItem(Const(LogicValue.ofString('10')), [c < 1, d < 0]), + ], + defaultItem: [c < 0, d < 1], + conditionalType: ConditionalType.unique), + CaseZ( + [b, a].rswizzle(), + [ + CaseItem(Const(LogicValue.ofString('1z')), [e < 1]) + ], + defaultItem: [e < 0], + conditionalType: ConditionalType.priority) + ]); + } +} + +class IfBlockModule extends Module { + IfBlockModule(Logic a, Logic b) : super(name: 'ifblockmodule') { + a = addInput('a', a); + b = addInput('b', b); + final c = addOutput('c'); + final d = addOutput('d'); + + Combinational([ + If.block([ + Iff(a & ~b, [c < 1, d < 0]), + ElseIf(b & ~a, [c < 1, d < 0]), + Else([c < 0, d < 1]) + ]) + ]); + } +} + +class SingleIfBlockModule extends Module { + SingleIfBlockModule(Logic a) : super(name: 'singleifblockmodule') { + a = addInput('a', a); + final c = addOutput('c'); + Combinational([ + If.block([Iff.s(a, c < 1)]) + ]); + } +} + +class ElseIfBlockModule extends Module { + ElseIfBlockModule(Logic a, Logic b) : super(name: 'ifblockmodule') { + a = addInput('a', a); + b = addInput('b', b); + final c = addOutput('c'); + final d = addOutput('d'); + + Combinational([ + If.block([ + ElseIf(a & ~b, [c < 1, d < 0]), + ElseIf(b & ~a, [c < 1, d < 0]), + Else([c < 0, d < 1]) + ]) + ]); + } +} + +class SingleElseIfBlockModule extends Module { + SingleElseIfBlockModule(Logic a) : super(name: 'singleifblockmodule') { + a = addInput('a', a); + final c = addOutput('c'); + final d = addOutput('d'); + Combinational([ + If.block([ + ElseIf.s(a, c < 1), + Else([c < 0, d < 1]) + ]) + ]); + } +} + +class CombModule extends Module { + CombModule(Logic a, Logic b, Logic d) : super(name: 'combmodule') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + final z = addOutput('z'); + final x = addOutput('x'); + d = addInput('d', d, width: d.width); + final q = addOutput('q', width: d.width); + + Combinational([ + If(a, then: [ + y < a, + z < b, + x < a & b, + q < d, + ], orElse: [ + If(b, then: [ + y < b, + z < a, + q < 13, + ], orElse: [ + y < 0, + z < 1, + ]) + ]) + ]); + } +} + +class SequentialModule extends Module { + SequentialModule(Logic a, Logic b, Logic d) : super(name: 'ffmodule') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + final z = addOutput('z'); + final x = addOutput('x'); + d = addInput('d', d, width: d.width); + final q = addOutput('q', width: d.width); + + Sequential(SimpleClockGenerator(10).clk, [ + If(a, then: [ + q < d, + y < a, + z < b, + x < ~x, + ], orElse: [ + x < a, + If(b, then: [ + y < b, + z < a + ], orElse: [ + y < 0, + z < 1, + ]) + ]) + ]); + } +} + +class SingleIfModule extends Module { + SingleIfModule(Logic a) : super(name: 'combmodule') { + a = addInput('a', a); + final q = addOutput('q'); + Combinational([If.s(a, q < 1)]); + } +} + +class SingleIfOrElseModule extends Module { + SingleIfOrElseModule(Logic a, Logic b) : super(name: 'combmodule') { + a = addInput('a', a); + b = addInput('b', b); + final q = addOutput('q'); + final x = addOutput('x'); + Combinational([If.s(a, q < 1, x < 1)]); + } +} + +class SingleElseModule extends Module { + SingleElseModule(Logic a, Logic b) : super(name: 'combmodule') { + a = addInput('a', a); + b = addInput('b', b); + final q = addOutput('q'); + final x = addOutput('x'); + Combinational([ + If.block([Iff.s(a, q < 1), Else.s(x < 1)]) + ]); + } +} + +class SignalRedrivenSequentialModule extends Module { + SignalRedrivenSequentialModule(Logic a, Logic b, Logic d, + {required bool allowRedrive}) + : super(name: 'ffmodule') { + a = addInput('a', a); + b = addInput('b', b); + final q = addOutput('q', width: d.width); + d = addInput('d', d, width: d.width); + final k = addOutput('k', width: 8); + Sequential( + SimpleClockGenerator(10).clk, + [ + If(a, then: [k < k, q < k, q < d]) + ], + allowMultipleAssignments: allowRedrive, + ); + } +} + +// ===== Modules from assignment_test.dart ===== + +class ConstAssignModule extends Module { + ConstAssignModule() { + final out = addOutput('out'); + final val = Logic(name: 'val'); + val <= Const(1); + Combinational([out < val]); + } + + Logic get out => output('out'); +} + +// ========================================================================= +// Tests +// ========================================================================= + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + tearDownAll(SimCompare.cleanupSystemCCache); + + // ===== Flop tests (from flop_test.dart) ===== + group('flop', () { + test('flop bit', () async { + final ftm = FlopTestModule(Logic()); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'a': 0}, {}), + Vector({'a': 1}, {'y': 0}), + Vector({'a': 1}, {'y': 1}), + Vector({'a': 0}, {'y': 1}), + Vector({'a': 0}, {'y': 0}), + ]); + }); + + test('flop bit with enable', () async { + final ftm = FlopTestModule(Logic(), en: Logic()); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'a': 0, 'en': 1}, {}), + Vector({'a': 1, 'en': 1}, {'y': 0}), + Vector({'a': 1, 'en': 1}, {'y': 1}), + Vector({'a': 0, 'en': 1}, {'y': 1}), + Vector({'a': 0, 'en': 1}, {'y': 0}), + Vector({'a': 1, 'en': 1}, {'y': 0}), + Vector({'a': 1, 'en': 0}, {'y': 1}), + Vector({'a': 0, 'en': 0}, {'y': 1}), + Vector({'a': 0, 'en': 1}, {'y': 1}), + Vector({'a': 1, 'en': 1}, {'y': 0}), + Vector({'a': 0, 'en': 0}, {'y': 1}), + Vector({'a': 1, 'en': 0}, {'y': 1}), + ]); + }); + + test('flop bus', () async { + final ftm = FlopTestModule(Logic(width: 8)); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'a': 0}, {}), + Vector({'a': 0xff}, {'y': 0}), + Vector({'a': 0xaa}, {'y': 0xff}), + Vector({'a': 0x55}, {'y': 0xaa}), + Vector({'a': 0x1}, {'y': 0x55}), + ]); + }); + + test('flop bus with enable', () async { + final ftm = FlopTestModule(Logic(width: 8), en: Logic()); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'a': 0, 'en': 1}, {}), + Vector({'a': 0xff, 'en': 1}, {'y': 0}), + Vector({'a': 0xaa, 'en': 1}, {'y': 0xff}), + Vector({'a': 0x55, 'en': 1}, {'y': 0xaa}), + Vector({'a': 0x1, 'en': 1}, {'y': 0x55}), + Vector({'a': 0, 'en': 1}, {'y': 0x1}), + Vector({'a': 0xff, 'en': 1}, {'y': 0}), + Vector({'a': 0xaa, 'en': 1}, {'y': 0xff}), + Vector({'a': 0x55, 'en': 0}, {'y': 0xaa}), + Vector({'a': 0x1, 'en': 0}, {'y': 0xaa}), + Vector({'a': 0x55, 'en': 1}, {'y': 0xaa}), + Vector({'a': 0x1, 'en': 1}, {'y': 0x55}), + Vector({'a': 0x55, 'en': 0}, {'y': 0x1}), + Vector({'a': 0x1, 'en': 1}, {'y': 0x1}), + ]); + }); + + test('flop bus reset, no reset value', () async { + final ftm = FlopTestModule(Logic(width: 8), reset: Logic()); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'reset': 1}, {}), + Vector({'reset': 0, 'a': 0xa5}, {'y': 0}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + + test('flop bus reset, const reset value', () async { + final ftm = + FlopTestModule(Logic(width: 8), reset: Logic(), resetValue: 3); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'reset': 1}, {}), + Vector({'reset': 0, 'a': 0xa5}, {'y': 3}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + + test('flop bus reset, logic reset value', () async { + final ftm = FlopTestModule(Logic(width: 8), + reset: Logic(), resetValue: Logic(width: 8)); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'reset': 1, 'resetValue': 5}, {}), + Vector({'reset': 0, 'a': 0xa5}, {'y': 5}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + + test('flop bus no reset, const reset value', () async { + final ftm = FlopTestModule(Logic(width: 8), resetValue: 9); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({}, {}), + Vector({'a': 0xa5}, {}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + + test('flop bus, enable, reset, const reset value', () async { + final ftm = FlopTestModule(Logic(width: 8), + en: Logic(), reset: Logic(), resetValue: 12); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'reset': 1, 'en': 0}, {}), + Vector({'reset': 0, 'a': 0xa5}, {'y': 12}), + Vector({}, {'y': 12}), + Vector({'en': 1}, {'y': 12}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + }); + + // ===== Counter tests (from counter_test.dart) ===== + group('counter', () { + test('counter', () async { + final counter = Counter(Logic(), Logic()); + await counter.build(); + SimCompare.checkSystemCVector(counter, [ + Vector({'en': 0, 'reset': 0}, {}), + Vector({'en': 0, 'reset': 1}, {'val': 0}), + Vector({'en': 1, 'reset': 1}, {'val': 0}), + Vector({'en': 1, 'reset': 0}, {'val': 0}), + Vector({'en': 1, 'reset': 0}, {'val': 1}), + Vector({'en': 1, 'reset': 0}, {'val': 2}), + Vector({'en': 1, 'reset': 0}, {'val': 3}), + Vector({'en': 0, 'reset': 0}, {'val': 4}), + Vector({'en': 0, 'reset': 0}, {'val': 4}), + Vector({'en': 1, 'reset': 0}, {'val': 4}), + Vector({'en': 0, 'reset': 0}, {'val': 5}), + ]); + }); + }); + + // ===== Comparison tests (from comparison_test.dart) ===== + group('comparison', () { + test('compares', () async { + final gtm = ComparisonTestModule(Logic(width: 8), Logic(width: 8)); + await gtm.build(); + SimCompare.checkSystemCVector(gtm, [ + Vector({ + 'a': 0, + 'b': 0 + }, { + 'a_eq_b': 1, + 'a_neq_b': 0, + 'a_lt_b': 0, + 'a_lte_b': 1, + 'a_gt_b': 0, + 'a_gte_b': 1, + 'a_gt_operator_b': 0, + 'a_gte_operator_b': 1, + 'a_eq_c': 0, + 'a_neq_c': 1, + 'a_lt_c': 1, + 'a_lte_c': 1, + 'a_gt_c': 0, + 'a_gte_c': 0, + 'a_gt_operator_c': 0, + 'a_gte_operator_c': 0, + }), + Vector({ + 'a': 5, + 'b': 6 + }, { + 'a_eq_b': 0, + 'a_neq_b': 1, + 'a_lt_b': 1, + 'a_lte_b': 1, + 'a_gt_b': 0, + 'a_gte_b': 0, + 'a_gt_operator_b': 0, + 'a_gte_operator_b': 0, + 'a_eq_c': 1, + 'a_neq_c': 0, + 'a_lt_c': 0, + 'a_lte_c': 1, + 'a_gt_c': 0, + 'a_gte_c': 1, + 'a_gt_operator_c': 0, + 'a_gte_operator_c': 1, + }), + Vector({ + 'a': 9, + 'b': 7 + }, { + 'a_eq_b': 0, + 'a_neq_b': 1, + 'a_lt_b': 0, + 'a_lte_b': 0, + 'a_gt_b': 1, + 'a_gte_b': 1, + 'a_gt_operator_b': 1, + 'a_gte_operator_b': 1, + 'a_eq_c': 0, + 'a_neq_c': 1, + 'a_lt_c': 0, + 'a_lte_c': 0, + 'a_gt_c': 1, + 'a_gte_c': 1, + 'a_gt_operator_c': 1, + 'a_gte_operator_c': 1, + }), + ]); + }); + }); + + // ===== Arithmetic shift right tests ===== + group('arithmetic shift right', () { + test('shift right and mask', () async { + final mod = + SraUnsignedTestModule(Logic(width: 32), Logic(width: 32), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'toShift': 0xe0000000, 'shiftAmount': 4, 'maskBit': 1}, + {'result': 0xfe000000}), + Vector({'toShift': 0x10000000, 'shiftAmount': 4, 'maskBit': 1}, + {'result': 0x01000000}), + Vector({'toShift': 0xe0000000, 'shiftAmount': 4, 'maskBit': 0}, + {'result': 0}), + ]); + }); + }); + + // ===== Collapse tests ===== + group('collapse', () { + test('collapse functional', () async { + final mod = CollapseTestModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1, 'b': 1}, {'c': 1, 'd': 1, 'e': 1, 'f': 1}), + Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 0, 'e': 0, 'f': 0}), + ]); + }); + }); + + // ===== Extend tests ===== + group('extend', () { + Future extendVectors( + List vectors, int newWidth, ExtendType extendType, + {int originalWidth = 8}) async { + final mod = + ExtendModule(Logic(width: originalWidth), newWidth, extendType); + await mod.build(); + SimCompare.checkSystemCVector(mod, vectors); + } + + test('zero extend same width', () async { + await extendVectors([ + Vector({'a': 0}, {'b': 0}), + Vector({'a': 0xff}, {'b': 0xff}), + Vector({'a': 0x5a}, {'b': 0x5a}), + ], 8, ExtendType.zero); + }); + + test('sign extend same width', () async { + await extendVectors([ + Vector({'a': 0}, {'b': 0}), + Vector({'a': 0xff}, {'b': 0xff}), + Vector({'a': 0x5a}, {'b': 0x5a}), + ], 8, ExtendType.sign); + }); + + test('zero extend pads 0s', () async { + await extendVectors([ + Vector({'a': 0xff}, {'b': 0xff}), + Vector({'a': 0x5a}, {'b': 0x5a}), + ], 12, ExtendType.zero); + }); + + test('sign extend positive pads 0s', () async { + await extendVectors([ + Vector({'a': 0x5a}, {'b': 0x5a}), + ], 12, ExtendType.sign); + }); + + test('sign extend negative pads 1s', () async { + await extendVectors([ + Vector({'a': 0xff}, {'b': 0xfff}), + ], 12, ExtendType.sign); + }); + + test('sign extend single bit(0) pads 0s', () async { + await extendVectors([ + Vector({'a': LogicValue.zero}, {'b': 0x000}), + ], 12, ExtendType.sign, originalWidth: 1); + }); + + test('sign extend single bit(1) pads 1s', () async { + await extendVectors([ + Vector({'a': LogicValue.one}, {'b': 0xfff}), + ], 12, ExtendType.sign, originalWidth: 1); + }); + }); + + group('withSet', () { + Future withSetVectors( + List vectors, int startIndex, int updateWidth) async { + final mod = + WithSetModule(Logic(width: 8), startIndex, Logic(width: updateWidth)); + await mod.build(); + SimCompare.checkSystemCVector(mod, vectors); + } + + test('setting same width', () async { + await withSetVectors([ + Vector({'a': 0x23, 'b': 0xff}, {'c': 0xff}), + Vector({'a': 0x45, 'b': 0x5a}, {'c': 0x5a}), + ], 0, 8); + }); + + test('setting at front', () async { + await withSetVectors([ + Vector({'a': 0x23, 'b': 0xf}, {'c': 0x2f}), + Vector({'a': 0x4a, 'b': 0x5}, {'c': 0x45}), + ], 0, 4); + }); + + test('setting at end', () async { + await withSetVectors([ + Vector({'a': 0x23, 'b': 0xf}, {'c': 0xf3}), + Vector({'a': 0x4a, 'b': 0x5}, {'c': 0x5a}), + ], 4, 4); + }); + + test('setting in the middle', () async { + await withSetVectors([ + Vector({'a': 0xff, 'b': 0x0}, {'c': bin('11000011')}), + Vector( + {'a': bin('01111110'), 'b': bin('0110')}, {'c': bin('01011010')}), + ], 2, 4); + }); + }); + + // ===== Bus tests ===== + group('bus', () { + test('single-bit bus subset', () async { + final mod = SingleBitBusSubsetMod(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'oneBit': 0}, {'result': 0}), + Vector({'oneBit': 1}, {'result': 1}), + ]); + }); + + test('const subset', () async { + final mod = ConstBusModule(0xabcd, subset: true); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({}, {'const_subset': 0xcd}), + ]); + }); + + test('const assignment', () async { + final mod = ConstBusModule(0xabcd, subset: false); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({}, {'const_subset': 0xabcd}), + ]); + }); + + // All tests below share the same BusTestModule — compile once + group('BusTestModule', () { + SystemCExecutable? exe; + + setUpAll(() async { + final gtm = BusTestModule(Logic(width: 8), Logic(width: 8)); + await gtm.build(); + exe = SimCompare.buildSystemCExecutable(gtm); + }); + + tearDownAll(() { + exe?.cleanup(); + }); + + test('NotGate bus', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0xff}, {'a_bar': 0}), + Vector({'a': 0}, {'a_bar': 0xff}), + Vector({'a': 0x55}, {'a_bar': 0xaa}), + Vector({'a': 1}, {'a_bar': 0xfe}), + ]); + }); + + test('And2Gate bus', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0, 'b': 0}, {'a_and_b': 0}), + Vector({'a': 0, 'b': 1}, {'a_and_b': 0}), + Vector({'a': 1, 'b': 0}, {'a_and_b': 0}), + Vector({'a': 1, 'b': 1}, {'a_and_b': 1}), + Vector({'a': 0xff, 'b': 0xaa}, {'a_and_b': 0xaa}), + ]); + }); + + test('Operator indexing', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': bin('11111110')}, {'a_operator_indexing1': 0}), + Vector({'a': bin('10000000')}, {'a_operator_indexing2': 1}), + Vector({'a': bin('11101111')}, {'a_operator_indexing3': 0}), + Vector({'a': bin('11111110')}, {'a_operator_neg_indexing1': 0}), + Vector({'a': bin('10000000')}, {'a_operator_neg_indexing2': 1}), + Vector({'a': bin('10111111')}, {'a_operator_neg_indexing3': 0}), + ]); + }); + + test('Bus shrink', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a_shrunk1': 0}), + Vector({'a': 0xfa}, {'a_shrunk1': bin('010')}), + Vector({'a': 0xab}, {'a_shrunk1': 3}), + Vector({'a': 0}, {'a_shrunk2': 0}), + Vector({'a': 0xec}, {'a_shrunk2': bin('00')}), + Vector({'a': 0xfa}, {'a_shrunk2': 2}), + Vector({'a': 0}, {'a_shrunk3': 0}), + Vector({'a': 0xff}, {'a_shrunk3': bin('1')}), + Vector({'a': 0xba}, {'a_shrunk3': 0}), + Vector({'a': 0}, {'a_neg_shrunk1': 0}), + Vector({'a': 0xfa}, {'a_neg_shrunk1': bin('010')}), + Vector({'a': 0xab}, {'a_neg_shrunk1': 3}), + Vector({'a': 0}, {'a_neg_shrunk2': 0}), + Vector({'a': 0xec}, {'a_neg_shrunk2': bin('00')}), + Vector({'a': 0xfa}, {'a_neg_shrunk2': 2}), + Vector({'a': 0}, {'a_neg_shrunk3': 0}), + Vector({'a': 0xff}, {'a_neg_shrunk3': bin('1')}), + Vector({'a': 0xba}, {'a_neg_shrunk3': 0}), + ]); + }); + + test('Bus reverse slice', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a_rsliced1': 0}), + Vector({'a': 0xac}, {'a_rsliced1': bin('10101')}), + Vector({'a': 0xf5}, {'a_rsliced1': 0xf}), + Vector({'a': 0}, {'a_rsliced2': 0}), + Vector({'a': 0xab}, {'a_rsliced2': bin('01')}), + Vector({'a': 0xac}, {'a_rsliced2': 1}), + Vector({'a': 0}, {'a_rsliced3': 0}), + Vector({'a': 0xaf}, {'a_rsliced3': bin('1')}), + Vector({'a': 0xaf}, {'a_rsliced3': 1}), + Vector({'a': 0}, {'a_r_neg_sliced1': 0}), + Vector({'a': 0xac}, {'a_r_neg_sliced1': bin('10101')}), + Vector({'a': 0xf5}, {'a_r_neg_sliced1': 0xf}), + Vector({'a': 0}, {'a_r_neg_sliced2': 0}), + Vector({'a': 0xab}, {'a_r_neg_sliced2': bin('01')}), + Vector({'a': 0xac}, {'a_r_neg_sliced2': 1}), + Vector({'a': 0}, {'a_r_neg_sliced3': 0}), + Vector({'a': 0xaf}, {'a_r_neg_sliced3': bin('1')}), + Vector({'a': 0xaf}, {'a_r_neg_sliced3': 1}), + ]); + }); + + test('Bus reversed', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a_reversed': 0}), + Vector({'a': 0xff}, {'a_reversed': 0xff}), + Vector({'a': 0xf5}, {'a_reversed': 0xaf}), + ]); + }); + + test('Bus range', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a_range1': 0}), + Vector({'a': 0xaf}, {'a_range1': 5}), + Vector({'a': bin('11000101')}, {'a_range1': bin('110')}), + Vector({'a': 0}, {'a_range2': 0}), + Vector({'a': 0xaf}, {'a_range2': 2}), + Vector({'a': bin('10111111')}, {'a_range2': bin('10')}), + Vector({'a': 0}, {'a_range3': 0}), + Vector({'a': 0x80}, {'a_range3': 1}), + Vector({'a': bin('10000000')}, {'a_range3': bin('1')}), + Vector({'a': 0}, {'a_range4': 0}), + Vector({'a': 0xaf}, {'a_range4': 5}), + Vector({'a': bin('11000101')}, {'a_range4': bin('110')}), + Vector({'a': 0}, {'a_neg_range1': 0}), + Vector({'a': 0xaf}, {'a_neg_range1': 5}), + Vector({'a': bin('11000101')}, {'a_neg_range1': bin('110')}), + Vector({'a': 0}, {'a_neg_range2': 0}), + Vector({'a': 0xaf}, {'a_neg_range2': 2}), + Vector({'a': bin('10111111')}, {'a_neg_range2': bin('10')}), + Vector({'a': 0}, {'a_neg_range3': 0}), + Vector({'a': 0x80}, {'a_neg_range3': 1}), + Vector({'a': bin('10000000')}, {'a_neg_range3': bin('1')}), + Vector({'a': 0}, {'a_neg_range4': 0}), + Vector({'a': 0xaf}, {'a_neg_range4': 5}), + Vector({'a': bin('11000101')}, {'a_neg_range4': bin('110')}), + ]); + }); + + test('Bus swizzle', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0, 'b': 0}, {'a_b_joined': 0}), + Vector({'a': 0xff, 'b': 0xff}, {'a_b_joined': 0xffff}), + Vector({'a': 0xff, 'b': 0}, {'a_b_joined': 0xff}), + Vector({'a': 0, 'b': 0xff}, {'a_b_joined': 0xff00}), + Vector({'a': 0xaa, 'b': 0x55}, {'a_b_joined': 0x55aa}), + ]); + }); + + test('Bus bit', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a1': 0}), + Vector({'a': 0xff}, {'a1': 1}), + Vector({'a': 0xf5}, {'a1': 0}), + ]); + }); + + test('add busses', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0, 'b': 0}, {'a_plus_b': 0}), + Vector({'a': 0, 'b': 1}, {'a_plus_b': 1}), + Vector({'a': 1, 'b': 0}, {'a_plus_b': 1}), + Vector({'a': 1, 'b': 1}, {'a_plus_b': 2}), + Vector({'a': 6, 'b': 7}, {'a_plus_b': 13}), + ]); + }); + + test('expression bit select', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 1, 'b': 1}, {'expression_bit_select': 2}), + ]); + }); + }); // end BusTestModule group + + test('selectFrom and selectIndex', () async { + final gtm = SelectTestModule(Logic(width: 8), Logic(width: 8), + Logic(width: 8), Logic(width: (log(8) / log(2)).ceil())); + await gtm.build(); + SimCompare.checkSystemCVector(gtm, [ + Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 1}, + {'selectIndexValue': 2, 'selectFromValue': 2}), + Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 0}, + {'selectIndexValue': 1, 'selectFromValue': 1}), + Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 2}, + {'selectIndexValue': 3, 'selectFromValue': 3}), + ]); + }); + + test('selectFrom with default Value', () async { + final gtm = SelectTestModule(Logic(width: 8), Logic(width: 8), + Logic(width: 8), Logic(width: (log(8) / log(2)).ceil()), + defaultValue: Logic(width: 8)); + await gtm.build(); + SimCompare.checkSystemCVector(gtm, [ + Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 4, 'defaultValue': 5}, + {'selectFromValue': 5, 'selectIndexValue': 5}), + ]); + }); + }); + + // ===== Conditionals tests ===== + group('conditionals', () { + test('conditional comb', () async { + final mod = CombModule(Logic(), Logic(), Logic(width: 10)); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0, 'b': 0, 'd': 5}, + {'y': 0, 'z': 1, 'x': LogicValue.x, 'q': LogicValue.x}), + Vector({'a': 0, 'b': 1, 'd': 6}, + {'y': 1, 'z': 0, 'x': LogicValue.x, 'q': 13}), + Vector({'a': 1, 'b': 0, 'd': 7}, {'y': 1, 'z': 0, 'x': 0, 'q': 7}), + Vector({'a': 1, 'b': 1, 'd': 8}, {'y': 1, 'z': 1, 'x': 1, 'q': 8}), + ]); + }); + + test('iffblock comb', () async { + final mod = IfBlockModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 1}), + Vector({'a': 0, 'b': 1}, {'c': 1, 'd': 0}), + Vector({'a': 1, 'b': 0}, {'c': 1, 'd': 0}), + Vector({'a': 1, 'b': 1}, {'c': 0, 'd': 1}), + ]); + }); + + test('single iffblock comb', () async { + final mod = SingleIfBlockModule(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'c': 1}), + ]); + }); + + test('elseifblock comb', () async { + final mod = ElseIfBlockModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 1}), + Vector({'a': 0, 'b': 1}, {'c': 1, 'd': 0}), + Vector({'a': 1, 'b': 0}, {'c': 1, 'd': 0}), + Vector({'a': 1, 'b': 1}, {'c': 0, 'd': 1}), + ]); + }); + + test('single elseifblock comb', () async { + final mod = SingleElseIfBlockModule(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'c': 1}), + Vector({'a': 0}, {'c': 0, 'd': 1}), + ]); + }); + + test('case comb', () async { + final mod = CaseModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 1, 'e': 0}), + Vector({'a': 0, 'b': 1}, {'c': 1, 'd': 0, 'e': 0}), + Vector({'a': 1, 'b': 0}, {'c': 1, 'd': 0, 'e': 1}), + Vector({'a': 1, 'b': 1}, {'c': 0, 'd': 1, 'e': 1}), + ]); + }); + + test('conditional ff', () async { + final mod = SequentialModule(Logic(), Logic(), Logic(width: 8)); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1, 'd': 1}, {}), + Vector({'a': 0, 'b': 0, 'd': 2}, {'q': 1}), + Vector({'a': 0, 'b': 1, 'd': 3}, {'y': 0, 'z': 1, 'x': 0, 'q': 1}), + Vector({'a': 1, 'b': 0, 'd': 4}, {'y': 1, 'z': 0, 'x': 0, 'q': 1}), + Vector({'a': 1, 'b': 1, 'd': 5}, {'y': 1, 'z': 0, 'x': 1, 'q': 4}), + Vector({}, {'y': 1, 'z': 1, 'x': 0, 'q': 5}), + ]); + }); + + test('loopy comb ssa', () async { + final mod = LoopyCombModuleSsa(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0}, {'x': 1}), + Vector({'a': 1}, {'x': 0}), + ]); + }); + + test('single if', () async { + final mod = SingleIfModule(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'q': 1}), + ]); + }); + + test('single if or else', () async { + final mod = SingleIfOrElseModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'q': 1}), + Vector({'a': 0}, {'x': 1}), + ]); + }); + + test('single else', () async { + final mod = SingleElseModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'q': 1}), + Vector({'a': 0}, {'x': 1}), + ]); + }); + + test('redrive allowed', () async { + final mod = SignalRedrivenSequentialModule( + Logic(), Logic(), Logic(width: 8), + allowRedrive: true); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1, 'd': 1}, {}), + Vector({'a': 1, 'b': 0, 'd': 2}, {'q': 1}), + Vector({'a': 1, 'b': 0, 'd': 3}, {'q': 2}), + ]); + }); + }); + + // ===== Assignment tests ===== + group('assignment', () { + test('const comb assignment', () async { + final mod = ConstAssignModule(); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({}, {'out': 1}), + ]); + }); + }); +} diff --git a/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 5933cee30..ed61f8ca5 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 From 2826cd731a1c275cf0208862e95c142bccd4d2bb Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 7 Jul 2026 12:41:11 -0700 Subject: [PATCH 02/14] Propagate devtools waveform updates --- .../rohd_devtools/ui/signal_details_card.dart | 5 +- .../lib/rohd_devtools/ui/signal_table.dart | 30 +---- rohd_devtools_extension/pubspec.yaml | 2 +- .../tree_structure_page_test.dart | 118 ++++++++++-------- 4 files changed, 72 insertions(+), 83 deletions(-) diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart index 0d3fdeb3a..12a29927f 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart @@ -16,10 +16,7 @@ import 'package:rohd_devtools_extension/rohd_devtools/ui/signal_table.dart'; class SignalDetailsCard extends StatefulWidget { final TreeModel? module; - const SignalDetailsCard({ - Key? key, - this.module, - }) : super(key: key); + const SignalDetailsCard({Key? key, this.module}) : super(key: key); @override SignalDetailsCardState createState() => SignalDetailsCardState(); diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart index 8e97328d8..1b66861da 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart @@ -90,29 +90,12 @@ class _SignalTableState extends State { TableRow _generateSignalRow(SignalModel signal) { return TableRow( children: [ + SizedBox(height: 32, child: Center(child: Text(signal.name))), + SizedBox(height: 32, child: Center(child: Text(signal.direction))), + SizedBox(height: 32, child: Center(child: Text(signal.value))), SizedBox( height: 32, - child: Center( - child: Text(signal.name), - ), - ), - SizedBox( - height: 32, - child: Center( - child: Text(signal.direction), - ), - ), - SizedBox( - height: 32, - child: Center( - child: Text(signal.value), - ), - ), - SizedBox( - height: 32, - child: Center( - child: Text(signal.width.toString()), - ), + child: Center(child: Text(signal.width.toString())), ), ], ); @@ -124,10 +107,7 @@ class _SignalTableState extends State { child: Center( child: Text( text, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), ), ), ); diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index 0aa366e78..8b5bb226f 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: flutter_lints: ^3.0.1 build_runner: ^2.4.7 mocktail: ^1.0.2 - bloc_lint: ^0.1.0 + bloc_lint: ^0.3.7 flutter: uses-material-design: true diff --git a/rohd_devtools_extension/test/modules/tree_structure/tree_structure_page_test.dart b/rohd_devtools_extension/test/modules/tree_structure/tree_structure_page_test.dart index 8ccd28d4c..15287d70e 100644 --- a/rohd_devtools_extension/test/modules/tree_structure/tree_structure_page_test.dart +++ b/rohd_devtools_extension/test/modules/tree_structure/tree_structure_page_test.dart @@ -29,71 +29,83 @@ void main() { }); testWidgets( - 'displays ModuleTreeCard when state is RohdServiceLoaded with treeModel', - (tester) async { - final treeModel = MockTreeModel(); + 'displays ModuleTreeCard when state is RohdServiceLoaded with treeModel', + (tester) async { + final treeModel = MockTreeModel(); - when(() => rohdServiceCubit.state) - .thenReturn(RohdServiceLoaded(treeModel)); - when(() => rohdServiceCubit.stream) - .thenAnswer((_) => Stream.value(RohdServiceLoaded(treeModel))); - when(() => treeSearchTermCubit.state).thenReturn(null); - when(() => treeSearchTermCubit.stream) - .thenAnswer((_) => Stream.value(null)); + when( + () => rohdServiceCubit.state, + ).thenReturn(RohdServiceLoaded(treeModel)); + when( + () => rohdServiceCubit.stream, + ).thenAnswer((_) => Stream.value(RohdServiceLoaded(treeModel))); + when(() => treeSearchTermCubit.state).thenReturn(null); + when( + () => treeSearchTermCubit.stream, + ).thenAnswer((_) => Stream.value(null)); - await tester.pumpWidget( - MultiBlocProvider( - providers: [ - BlocProvider.value(value: rohdServiceCubit), - BlocProvider.value(value: treeSearchTermCubit), - ], - child: MaterialApp( - home: Scaffold( - body: TreeStructurePage(screenSize: const Size(2000, 1000)), + await tester.pumpWidget( + MultiBlocProvider( + providers: [ + BlocProvider.value(value: rohdServiceCubit), + BlocProvider.value( + value: treeSearchTermCubit, + ), + ], + child: MaterialApp( + home: Scaffold( + body: TreeStructurePage(screenSize: const Size(2000, 1000)), + ), ), ), - ), - ); + ); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - expect(find.byType(ModuleTreeCard), findsOneWidget); - }); + expect(find.byType(ModuleTreeCard), findsOneWidget); + }, + ); testWidgets( - 'displays SignalDetailsCard when state is RohdServiceLoaded with selected module', - (tester) async { - final treeModel = MockTreeModel(); - final signalModelList = [ - MockSignalModel(), - MockSignalModel() - ]; - when(() => rohdServiceCubit.state) - .thenReturn(RohdServiceLoaded(treeModel)); - when(() => rohdServiceCubit.stream) - .thenAnswer((_) => Stream.value(RohdServiceLoaded(treeModel))); - when(() => treeModel.inputs).thenReturn(signalModelList); - when(() => treeModel.outputs).thenReturn(signalModelList); - when(() => treeSearchTermCubit.stream) - .thenAnswer((_) => Stream.value(null)); + 'displays SignalDetailsCard when state is RohdServiceLoaded with selected module', + (tester) async { + final treeModel = MockTreeModel(); + final signalModelList = [ + MockSignalModel(), + MockSignalModel(), + ]; + when( + () => rohdServiceCubit.state, + ).thenReturn(RohdServiceLoaded(treeModel)); + when( + () => rohdServiceCubit.stream, + ).thenAnswer((_) => Stream.value(RohdServiceLoaded(treeModel))); + when(() => treeModel.inputs).thenReturn(signalModelList); + when(() => treeModel.outputs).thenReturn(signalModelList); + when( + () => treeSearchTermCubit.stream, + ).thenAnswer((_) => Stream.value(null)); - await tester.pumpWidget( - MultiBlocProvider( - providers: [ - BlocProvider.value(value: rohdServiceCubit), - BlocProvider.value(value: treeSearchTermCubit), - ], - child: MaterialApp( - home: Scaffold( - body: TreeStructurePage(screenSize: const Size(800, 600)), + await tester.pumpWidget( + MultiBlocProvider( + providers: [ + BlocProvider.value(value: rohdServiceCubit), + BlocProvider.value( + value: treeSearchTermCubit, + ), + ], + child: MaterialApp( + home: Scaffold( + body: TreeStructurePage(screenSize: const Size(800, 600)), + ), ), ), - ), - ); + ); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - expect(find.byType(SignalDetailsCard), findsOneWidget); - }); + expect(find.byType(SignalDetailsCard), findsOneWidget); + }, + ); }); } From 170447a2fcf97844b08522593863c63a9912f383 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 7 Jul 2026 16:04:43 -0700 Subject: [PATCH 03/14] Use ROHD icons for devtools web app --- .../web/icons/Icon-192.png | Bin 5292 -> 4911 bytes .../web/icons/Icon-512.png | Bin 8252 -> 50043 bytes .../web/icons/Icon-maskable-192.png | Bin 5594 -> 4911 bytes .../web/icons/Icon-maskable-512.png | Bin 20998 -> 50043 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/rohd_devtools_extension/web/icons/Icon-192.png b/rohd_devtools_extension/web/icons/Icon-192.png index b749bfef07473333cf1dd31e9eed89862a5d52aa..acd165142d5e74105a26e389c46231011d4df16a 100644 GIT binary patch literal 4911 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 diff --git a/rohd_devtools_extension/web/icons/Icon-512.png b/rohd_devtools_extension/web/icons/Icon-512.png index 88cfd48dff1169879ba46840804b412fe02fefd6..bb667c609122ee453334839b1af4b3eea08e982d 100644 GIT binary patch literal 50043 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 diff --git a/rohd_devtools_extension/web/icons/Icon-maskable-192.png b/rohd_devtools_extension/web/icons/Icon-maskable-192.png index eb9b4d76e525556d5d89141648c724331630325d..acd165142d5e74105a26e389c46231011d4df16a 100644 GIT binary patch literal 4911 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! diff --git a/rohd_devtools_extension/web/icons/Icon-maskable-512.png b/rohd_devtools_extension/web/icons/Icon-maskable-512.png index d69c56691fbdb0b7efa65097c7cc1edac12a6d3e..bb667c609122ee453334839b1af4b3eea08e982d 100644 GIT binary patch literal 50043 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 MPgg&ebxsLQ025IeI{*Lx From 3ea2a02a55860e74420e68ee97601bada5d5cd96 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 10 Jul 2026 10:38:45 -0700 Subject: [PATCH 04/14] Simplified assignment output. --- .../systemc/systemc_synthesis_result.dart | 118 ++++++++++++------ test/systemc_simcompare_test.dart | 16 +++ 2 files changed, 95 insertions(+), 39 deletions(-) diff --git a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart index 7097b63b0..7c626f5a0 100644 --- a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart +++ b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart @@ -494,14 +494,13 @@ class SystemCSynthesisResult extends SynthesisResult { return null; } - final setupBuf = StringBuffer(); - final bodyBuf = StringBuffer(); - var methodIdx = 0; + 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() final inputExprs = {}; @@ -520,6 +519,7 @@ class SystemCSynthesisResult extends SynthesisResult { } 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. @@ -529,6 +529,7 @@ class SystemCSynthesisResult extends SynthesisResult { 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 { @@ -548,29 +549,19 @@ class SystemCSynthesisResult extends SynthesisResult { continue; } - final methodName = 'assign_$methodIdx'; - methodIdx++; - setupBuf.writeln(' SC_METHOD($methodName);'); - for (final sig in sensitivities) { - setupBuf.writeln(' sensitive << $sig;'); - } - - bodyBuf - ..writeln(' void $methodName() {') - ..writeln(bodyLines.join('\n')) - ..writeln(' }') - ..writeln(); + assignments.add(_ScMethodAssignment( + bodyLines: bodyLines, + sensitivities: sensitivities, + destinations: destinations, + )); ssmi.clearInstantiation(); } - if (bodyBuf.isEmpty) { + if (assignments.isEmpty) { return null; } - return _MethodResult( - setup: setupBuf.toString(), - body: bodyBuf.toString(), - ); + return _emitGroupedAssignments('assign', assignments); } /// Maps an InlineSystemVerilog gate to a C++ expression. @@ -1232,9 +1223,7 @@ class SystemCSynthesisResult extends SynthesisResult { return null; } - final setupBuf = StringBuffer(); - final bodyBuf = StringBuffer(); - var methodIdx = 0; + final assignments = <_ScMethodAssignment>[]; // Group partial assignments by destination for concatenated writes final partialsByDst = >{}; @@ -1249,18 +1238,13 @@ class SystemCSynthesisResult extends SynthesisResult { if (!assignment.src.isConstant) { sensitivities.add(_sensitivityName(assignment.src)); } - final methodName = 'wire_assign_$methodIdx'; - methodIdx++; - setupBuf.writeln(' SC_METHOD($methodName);'); - for (final sig in sensitivities) { - setupBuf.writeln(' sensitive << $sig;'); - } - bodyBuf - ..writeln(' void $methodName() {') - ..writeln(' ${_scName(assignment.dst.name)} = ' - '${_synthLogicReadExpr(assignment.src)};') - ..writeln(' }') - ..writeln(); + final bodyLine = ' ${_scName(assignment.dst.name)} = ' + '${_synthLogicReadExpr(assignment.src)};'; + assignments.add(_ScMethodAssignment( + bodyLines: [bodyLine], + sensitivities: sensitivities, + destinations: {_scName(assignment.dst.name)}, + )); } } @@ -1286,15 +1270,43 @@ class SystemCSynthesisResult extends SynthesisResult { parts.add('($utype($srcExpr) << ${p.dstLowerIndex})'); } } - final methodName = 'wire_assign_$methodIdx'; - methodIdx++; + 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 sensitivities) { + for (final sig in group.sensitivities) { setupBuf.writeln(' sensitive << $sig;'); } + bodyBuf ..writeln(' void $methodName() {') - ..writeln(' $dstName = ${parts.join(' | ')};') + ..writeln(group.bodyLines.join('\n')) ..writeln(' }') ..writeln(); } @@ -1696,6 +1708,34 @@ class _MethodResult { 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; diff --git a/test/systemc_simcompare_test.dart b/test/systemc_simcompare_test.dart index 2ec5c08d2..9374b479a 100644 --- a/test/systemc_simcompare_test.dart +++ b/test/systemc_simcompare_test.dart @@ -173,6 +173,22 @@ void main() { 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'), From e42cacb7bd1972731eb52740e33380ab4a886e89 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 10 Jul 2026 17:17:32 -0700 Subject: [PATCH 05/14] 24.04 test in CI --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5c23bb9c0..12e172c74 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu { - "image": "mcr.microsoft.com/devcontainers/base:ubuntu-22.04", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", "updateContentCommand": "tool/gh_codespaces/run_setup.sh", From 4be1ad5e4ea9499100c7d68e638115f699140806 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 19 Jul 2026 11:48:57 -0700 Subject: [PATCH 06/14] cleanups from review, inout support, merged vector tests --- .github/workflows/general.yml | 4 - .../systemc/systemc_synthesis_result.dart | 18 +- lib/src/utilities/simcompare.dart | 80 +- lib/src/utilities/systemc_cosim_ffi.dart | 270 ++-- test/arithmetic_shift_right_test.dart | 1 + test/assignment_test.dart | 2 + test/bus_test.dart | 32 + test/collapse_test.dart | 1 + test/comparison_test.dart | 1 + test/conditionals_test.dart | 12 + test/counter_test.dart | 1 + test/extend_test.dart | 2 + test/flop_test.dart | 9 + test/net_test.dart | 1 + test/systemc_vector_test.dart | 1279 ----------------- 15 files changed, 291 insertions(+), 1422 deletions(-) delete mode 100644 test/systemc_vector_test.dart diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 2cb429c95..b74924d3e 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -70,9 +70,6 @@ jobs: - name: Run project tests run: tool/gh_actions/run_tests.sh - - name: Run SystemC tests - run: dart test test/systemc_vector_test.dart - - name: Clean SystemC temporary files run: tool/gh_actions/cleanup_systemc_tmp.sh @@ -85,7 +82,6 @@ jobs: with: runCmd: | tool/gh_actions/run_tests.sh - dart test test/systemc_vector_test.dart tool/gh_actions/cleanup_systemc_tmp.sh tool/gh_actions/check_tmp_test.sh diff --git a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart index 7c626f5a0..5bcafc12a 100644 --- a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart +++ b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart @@ -211,6 +211,10 @@ class SystemCSynthesisResult extends SynthesisResult { /// 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)}>'; @@ -234,6 +238,10 @@ class SystemCSynthesisResult extends SynthesisResult { final n = _scName(sig.name); lines.add(' ${systemCOutType(sig.width)} $n{"$n"};'); } + for (final sig in _synthModuleDefinition.inOuts) { + final n = _scName(sig.name); + lines.add(' ${systemCInOutType(sig.width)} $n{"$n"};'); + } return lines.join('\n'); } @@ -502,9 +510,15 @@ class SystemCSynthesisResult extends SynthesisResult { final bodyLines = []; final destinations = {}; - // Collect inputs — constants become literals, signals get .read() + // 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 = {}; - for (final entry in ssmi.inputMapping.entries) { + final inputMappings = {...ssmi.inputMapping, ...ssmi.inOutMapping}; + if (m is InlineSystemVerilog) { + inputMappings.remove(m.resultSignalName); + } + for (final entry in inputMappings.entries) { final sl = entry.value; if (!sl.isConstant) { sensitivities.add(_sensitivityName(sl)); diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index a357bf1d0..8f96953f4 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -636,6 +636,10 @@ abstract class SimCompare { 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() @@ -674,6 +678,13 @@ abstract class SimCompare { ' ${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 @@ -689,6 +700,9 @@ abstract class SimCompare { for (final name in outputPorts.keys) { tb.writeln(' dut.$name($name);'); } + for (final name in inOutPorts.keys) { + tb.writeln(' dut.$name($name);'); + } tb ..writeln() @@ -720,6 +734,21 @@ abstract class SimCompare { ..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 @@ -734,9 +763,10 @@ abstract class SimCompare { ..writeln(' string _tb_pn;') ..writeln(' cin >> _tb_pn;'); - // Generate if-else chain for each output port + // Generate if-else chain for each output and inout port var first = true; - for (final entry in outputPorts.entries) { + final checkablePorts = {...outputPorts, ...inOutPorts}; + for (final entry in checkablePorts.entries) { final name = entry.key; final w = entry.value; final ifKey = first ? 'if' : '} else if'; @@ -759,7 +789,7 @@ abstract class SimCompare { ..writeln(' _tb_errors++;') ..writeln(' }'); } - if (outputPorts.isNotEmpty) { + if (checkablePorts.isNotEmpty) { tb ..writeln(' } else {') ..writeln(' string _d; cin >> _d; // skip unknown') @@ -823,7 +853,8 @@ abstract class SimCompare { scLib: resolvedLib, clockSignals: clockSignals, inputPorts: inputPorts, - outputPorts: outputPorts); + outputPorts: outputPorts, + inOutPorts: inOutPorts); _compilationCache[cacheKey] = exe; return exe; } @@ -871,6 +902,22 @@ abstract class SimCompare { 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 @@ -878,18 +925,15 @@ abstract class SimCompare { final checks = {}; for (final entry in vector.expectedOutputValues.entries) { final name = entry.key; - final w = exe.outputPorts[name]!; + 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) { - var hex = expectedLV.toBigInt().toUnsigned(w).toRadixString(16); - if (hex.length.isOdd) { - hex = '0$hex'; - } - checks[name] = '0x$hex'; + checks[name] = _systemcHexValue(entry.value, w); } else { checks[name] = '${_systemcIntValue(entry.value, w)}'; } @@ -964,6 +1008,16 @@ abstract class SimCompare { 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, @@ -1151,13 +1205,17 @@ class SystemCExecutable { /// Output port names and widths. final Map outputPorts; + /// Inout port names and widths. + final Map inOutPorts; + SystemCExecutable._( {required this.binaryPath, required this.cppFile, required this.scLib, required this.clockSignals, required this.inputPorts, - required this.outputPorts}); + required this.outputPorts, + required this.inOutPorts}); /// Deletes the compiled binary and source. void cleanup() { diff --git a/lib/src/utilities/systemc_cosim_ffi.dart b/lib/src/utilities/systemc_cosim_ffi.dart index 812aef1f6..9e55e5567 100644 --- a/lib/src/utilities/systemc_cosim_ffi.dart +++ b/lib/src/utilities/systemc_cosim_ffi.dart @@ -629,26 +629,45 @@ class SystemCFfiCosim { // 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() - ..writeln('// Auto-generated SystemC FFI Cosim Wrapper') - ..writeln('// Module: $topModule') - ..writeln() - ..writeln('#include ') - ..writeln('#include ') - ..writeln('#include ') - ..writeln('#include ') - ..writeln('using namespace std;') - ..writeln() - ..writeln('// ═══ ROHD-Generated SystemC Module(s) ═══') - ..writeln() - ..writeln(generatedSystemC) - ..writeln() - ..writeln('// ═══ FFI Cosim Context ═══') - ..writeln() - ..writeln('struct CosimContext {'); + 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) { @@ -661,44 +680,42 @@ class SystemCFfiCosim { sb.writeln(' sc_signal<$type> ${entry.key};'); } - sb - ..writeln(' $topModule* dut;') - ..writeln('};') - ..writeln() - ..writeln('extern "C" {') - ..writeln() - ..writeln('// Required by SystemC linker — we never call it directly') - ..writeln('int sc_main(int, char*[]) { return 0; }') - ..writeln() - ..writeln('// Track whether the kernel has been initialized') - ..writeln('static CosimContext* _active_ctx = nullptr;') - ..writeln() - // ──── sc_cosim_create ──── - ..writeln('void* sc_cosim_create(const char* name) {') - ..writeln(' // If a context already exists (same process, new test),') - ..writeln(' // just return the existing one after resetting signals.') - ..writeln(' if (_active_ctx != nullptr) {') - ..writeln(' // Reset all input signals to 0'); + _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));'); } - sb - ..writeln(' return static_cast(_active_ctx);') - ..writeln(' }') - ..writeln() - ..writeln(' // Guard: cannot create sc_signal after kernel starts') - ..writeln(' if (sc_get_status() != SC_ELABORATION' - ' && sc_get_status() != SC_BEFORE_END_OF_ELABORATION) {') - ..writeln(' return nullptr; // E113 prevention') - ..writeln(' }') - ..writeln() - ..writeln(' auto* ctx = new CosimContext();') + _writeCode(sb, ''' + return static_cast(_active_ctx); + } - // Instantiate DUT - ..writeln(' ctx->dut = new $topModule("dut");'); + // 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) { @@ -709,87 +726,80 @@ class SystemCFfiCosim { sb.writeln(' ctx->dut->$name(ctx->$name);'); } - sb - ..writeln() - ..writeln(' // Store context — do NOT call sc_start here.') - ..writeln(' // Deferring sc_start to the first advance allows') - ..writeln(' // multiple module types to be elaborated before') - ..writeln(' // the kernel starts (avoids E113).') - ..writeln(' _active_ctx = ctx;') - ..writeln(' return static_cast(ctx);') - ..writeln('}') - ..writeln() - // ──── sc_cosim_set_input ──── - ..writeln('void sc_cosim_set_input(void* handle, const char* name,' - ' uint64_t value) {') - ..writeln(' auto* ctx = static_cast(handle);'); + _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); - sb - ..writeln('}') - ..writeln() - // ──── sc_cosim_set_input_wide ──── - ..writeln('void sc_cosim_set_input_wide(void* handle, const char* name,' - ' const char* hex_value) {') - ..writeln(' auto* ctx = static_cast(handle);'); + _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); - sb - ..writeln('}') - ..writeln() - // ──── sc_cosim_get_output ──── - ..writeln( - 'uint64_t sc_cosim_get_output(void* handle, const char* name) {') - ..writeln(' auto* ctx = static_cast(handle);'); + _writeCode(sb, ''' + } + + uint64_t sc_cosim_get_output(void* handle, const char* name) { + auto* ctx = static_cast(handle); + '''); _generateOutputDispatch(sb, narrow: true); - sb - ..writeln(' return 0;') - ..writeln('}') - ..writeln() - // ──── sc_cosim_get_output_wide ──── - ..writeln('const char* sc_cosim_get_output_wide(void* handle,' - ' const char* name) {') - ..writeln(' auto* ctx = static_cast(handle);') - ..writeln(' static char _buf[512];'); + _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); - sb - ..writeln(" _buf[0] = '0'; _buf[1] = 0;") - ..writeln(' return _buf;') - ..writeln('}') - ..writeln() - // ──── sc_cosim_advance ──── - ..writeln('void sc_cosim_advance(void* handle, uint64_t time_ps) {') - ..writeln(' // End elaboration on first advance (allows multiple') - ..writeln(' // module types to be instantiated before starting).') - ..writeln(' if (sc_get_status() == SC_ELABORATION) {') - ..writeln(' sc_start(SC_ZERO_TIME);') - ..writeln(' }') - ..writeln(' if (time_ps == 0) {') - ..writeln(' // Zero-time advance: process delta cycles only.') - ..writeln(' // Use SC_ZERO_TIME explicitly (some implementations') - ..writeln(' // treat sc_time(0,SC_PS) differently).') - ..writeln(' sc_start(SC_ZERO_TIME);') - ..writeln(' } else {') - ..writeln( - ' sc_start(sc_time(static_cast(time_ps), SC_PS));') - ..writeln(' }') - ..writeln('}') - ..writeln() - // ──── sc_cosim_destroy ──── - ..writeln('void sc_cosim_destroy(void* handle) {') - ..writeln(' // Do NOT delete or sc_stop — the SystemC kernel is a') - ..writeln(' // process-wide singleton. The context is reused if') - ..writeln(' // sc_cosim_create is called again (same module).') - ..writeln(' // This avoids E113 "insert primitive channel failed".') - ..writeln('}') - ..writeln() - ..writeln('} // extern "C"'); + _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(); } @@ -816,9 +826,13 @@ class SystemCFfiCosim { final type = SystemCSynthesisResult.systemCType(width); sb.writeln(' ctx->$name.write(static_cast<$type>(value));'); } else { - sb - ..writeln(' sc_biguint<$width> v(hex_value);') - ..writeln(' ctx->$name.write(v);'); + _writeCode( + sb, + ''' + sc_biguint<$width> v(hex_value); + ctx->$name.write(v); + ''', + prefix: ' '); } } if (!first) { @@ -847,12 +861,16 @@ class SystemCFfiCosim { if (narrow) { sb.writeln(' return static_cast(ctx->$name.read());'); } else { - sb - ..writeln(' sc_biguint<$width> v = ctx->$name.read();') - ..writeln(' string s = v.to_string(SC_HEX_US);') - ..writeln(' strncpy(_buf, s.c_str(), sizeof(_buf)-1);') - ..writeln(' _buf[sizeof(_buf)-1] = 0;') - ..writeln(' return _buf;'); + _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) { 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/bus_test.dart b/test/bus_test.dart index 08ccb4c9b..d622400d1 100644 --- a/test/bus_test.dart +++ b/test/bus_test.dart @@ -379,6 +379,23 @@ void main() { }); group('simcompare', () { + SystemCExecutable? busSystemCExe; + + setUpAll(() async { + final gtm = BusTestModule(Logic(width: 8), Logic(width: 8)); + await gtm.build(); + busSystemCExe = SimCompare.buildSystemCExecutable(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 0ef7e00c5..3a4b49427 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -52,6 +52,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('collapse pretty', () async { diff --git a/test/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/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/systemc_vector_test.dart b/test/systemc_vector_test.dart deleted file mode 100644 index 1a681097c..000000000 --- a/test/systemc_vector_test.dart +++ /dev/null @@ -1,1279 +0,0 @@ -// Copyright (C) 2024-2026 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// -// systemc_vector_test.dart -// Parallel SystemC simulation tests for all modules tested with iverilog. -// -// 2026 May 7 -// Author: Desmond A. Kirkpatrick - -import 'dart:math'; -import 'package:rohd/rohd.dart'; -import 'package:rohd/src/utilities/simcompare.dart'; -import 'package:test/test.dart'; - -// ===== Modules from flop_test.dart ===== - -class FlopTestModule extends Module { - FlopTestModule(Logic a, {Logic? en, Logic? reset, dynamic resetValue}) - : super(name: 'floptestmodule') { - a = addInput('a', a, width: a.width); - if (en != null) { - en = addInput('en', en); - } - if (reset != null) { - reset = addInput('reset', reset); - } - if (resetValue != null && resetValue is Logic) { - resetValue = addInput('resetValue', resetValue, width: a.width); - } - final y = addOutput('y', width: a.width); - final clk = SimpleClockGenerator(10).clk; - y <= flop(clk, a, en: en, reset: reset, resetValue: resetValue); - } -} - -// ===== Modules from counter_test.dart ===== - -class Counter extends Module { - final int width; - Logic get val => output('val'); - Counter(Logic en, Logic reset, {this.width = 8}) : super(name: 'counter') { - en = addInput('en', en); - reset = addInput('reset', reset); - final val = addOutput('val', width: width); - final nextVal = Logic(name: 'nextVal', width: width); - nextVal <= val + 1; - Sequential.multi([ - SimpleClockGenerator(10).clk, - reset - ], [ - If(reset, then: [ - val < 0 - ], orElse: [ - If(en, then: [val < nextVal]) - ]) - ]); - } -} - -// ===== Modules from comparison_test.dart ===== - -class ComparisonTestModule extends Module { - final int c; - ComparisonTestModule(Logic a, Logic b, {this.c = 5}) - : super(name: 'gatetestmodule') { - a = addInput('a', a, width: a.width); - b = addInput('b', b, width: b.width); - - final aEqB = addOutput('a_eq_b'); - final aNeqB = addOutput('a_neq_b'); - final aLtB = addOutput('a_lt_b'); - final aLteB = addOutput('a_lte_b'); - final aGtB = addOutput('a_gt_b'); - final aGteB = addOutput('a_gte_b'); - final aGtOperatorB = addOutput('a_gt_operator_b'); - final aGteOperatorB = addOutput('a_gte_operator_b'); - - final aEqC = addOutput('a_eq_c'); - final aNeqC = addOutput('a_neq_c'); - final aLtC = addOutput('a_lt_c'); - final aLteC = addOutput('a_lte_c'); - final aGtC = addOutput('a_gt_c'); - final aGteC = addOutput('a_gte_c'); - final aGtOperatorC = addOutput('a_gt_operator_c'); - final aGteOperatorC = addOutput('a_gte_operator_c'); - - aEqB <= a.eq(b); - aNeqB <= a.neq(b); - aLtB <= a.lt(b); - aLteB <= a.lte(b); - aGtB <= a.gt(b); - aGteB <= a.gte(b); - aGtOperatorB <= (a > b); - aGteOperatorB <= (a >= b); - - aEqC <= a.eq(c); - aNeqC <= a.neq(c); - aLtC <= a.lt(c); - aLteC <= a.lte(c); - aGtC <= a.gt(c); - aGteC <= a.gte(c); - aGtOperatorC <= (a > c); - aGteOperatorC <= (a >= c); - } -} - -// ===== Modules from arithmetic_shift_right_test.dart ===== - -class SraUnsignedTestModule extends Module { - Logic get result => output('result'); - SraUnsignedTestModule(Logic toShift, Logic shiftAmount, Logic maskBit) { - toShift = addInput('toShift', toShift, width: toShift.width); - shiftAmount = - addInput('shiftAmount', shiftAmount, width: shiftAmount.width); - maskBit = addInput('maskBit', maskBit); - addOutput('result', width: toShift.width); - result <= (toShift >> shiftAmount) & maskBit.replicate(toShift.width); - } -} - -// ===== Modules from collapse_test.dart ===== - -class CollapseTestModule extends Module { - CollapseTestModule(Logic a, Logic b) : super(name: 'collapsetestmodule') { - a = addInput('a', a); - b = addInput('b', b); - final c = addOutput('c'); - final d = addOutput('d'); - final e = addOutput('e'); - final f = addOutput('f'); - - final x = Logic(name: 'x'); - final y = Logic(name: 'y'); - final z = Logic(name: 'z', naming: Naming.mergeable); - c <= a & b; - d <= a & b; - x <= a; - y <= x; - e <= a & b & c & x & y; - z <= b & y; - f <= a & z; - - Logic(name: 'internal') <= ~z; - } -} - -// ===== Modules from extend_test.dart ===== - -class ExtendModule extends Module { - ExtendModule(Logic a, int newWidth, ExtendType extendType) { - a = addInput('a', a, width: a.width); - final b = addOutput('b', width: newWidth); - if (extendType == ExtendType.zero) { - b <= a.zeroExtend(newWidth); - } else { - b <= a.signExtend(newWidth); - } - } -} - -enum ExtendType { zero, sign } - -class WithSetModule extends Module { - WithSetModule(Logic a, int startIndex, Logic b) { - a = addInput('a', a, width: a.width); - b = addInput('b', b, width: b.width); - final c = addOutput('c', width: a.width); - c <= a.withSet(startIndex, b); - } -} - -// ===== Modules from bus_test.dart ===== - -class BusTestModule extends Module { - BusTestModule(Logic a, Logic b) : super(name: 'bustestmodule') { - if (a.width != b.width) { - throw Exception('a and b must be same width.'); - } - if (a.width <= 3) { - throw Exception('a must be more than width 3.'); - } - a = addInput('a', a, width: a.width); - b = addInput('b', b, width: b.width); - - final aBar = addOutput('a_bar', width: a.width); - final aAndB = addOutput('a_and_b', width: a.width); - final aBJoined = addOutput('a_b_joined', width: a.width + b.width); - final aPlusB = addOutput('a_plus_b', width: a.width); - final a1 = addOutput('a1'); - final expressionBitSelect = addOutput('expression_bit_select', width: 4); - - final aReversed = addOutput('a_reversed', width: a.width); - final aShrunk1 = addOutput('a_shrunk1', width: 3); - final aShrunk2 = addOutput('a_shrunk2', width: 2); - final aShrunk3 = addOutput('a_shrunk3'); - final aNegativeShrunk1 = addOutput('a_neg_shrunk1', width: 3); - final aNegativeShrunk2 = addOutput('a_neg_shrunk2', width: 2); - final aNegativeShrunk3 = addOutput('a_neg_shrunk3'); - final aRSliced1 = addOutput('a_rsliced1', width: 5); - final aRSliced2 = addOutput('a_rsliced2', width: 2); - final aRSliced3 = addOutput('a_rsliced3'); - final aRNegativeSliced1 = addOutput('a_r_neg_sliced1', width: 5); - final aRNegativeSliced2 = addOutput('a_r_neg_sliced2', width: 2); - final aRNegativeSliced3 = addOutput('a_r_neg_sliced3'); - final aRange1 = addOutput('a_range1', width: 3); - final aRange2 = addOutput('a_range2', width: 2); - final aRange3 = addOutput('a_range3'); - final aRange4 = addOutput('a_range4', width: 3); - final aNegativeRange1 = addOutput('a_neg_range1', width: 3); - final aNegativeRange2 = addOutput('a_neg_range2', width: 2); - final aNegativeRange3 = addOutput('a_neg_range3'); - final aNegativeRange4 = addOutput('a_neg_range4', width: 3); - final aOperatorIndexing1 = addOutput('a_operator_indexing1'); - final aOperatorIndexing2 = addOutput('a_operator_indexing2'); - final aOperatorIndexing3 = addOutput('a_operator_indexing3'); - final aOperatorNegIndexing1 = addOutput('a_operator_neg_indexing1'); - final aOperatorNegIndexing2 = addOutput('a_operator_neg_indexing2'); - final aOperatorNegIndexing3 = addOutput('a_operator_neg_indexing3'); - - aBar <= ~a; - aAndB <= a & b; - aBJoined <= [b, a].swizzle(); - a1 <= a[1]; - aPlusB <= a + b; - - aShrunk1 <= a.slice(2, 0); - aShrunk2 <= a.slice(1, 0); - aShrunk3 <= a.slice(0, 0); - aNegativeShrunk1 <= a.slice(-6, 0); - aNegativeShrunk2 <= a.slice(-7, 0); - aNegativeShrunk3 <= a.slice(-8, 0); - - aRSliced1 <= a.slice(3, 7); - aRSliced2 <= a.slice(6, 7); - aRSliced3 <= a.slice(7, 7); - aRNegativeSliced1 <= a.slice(-5, -1); - aRNegativeSliced2 <= a.slice(-2, -1); - aRNegativeSliced3 <= a.slice(-1, -1); - - aRange1 <= a.getRange(5, 8); - aRange2 <= a.getRange(6, 8); - aRange3 <= a.getRange(7, 8); - aRange4 <= a.getRange(5); - aNegativeRange1 <= a.getRange(-3, 8); - aNegativeRange2 <= a.getRange(-2, 8); - aNegativeRange3 <= a.getRange(-1, 8); - aNegativeRange4 <= a.getRange(-3); - - aOperatorIndexing1 <= a.elements[0]; - aOperatorIndexing2 <= a[a.width - 1]; - aOperatorIndexing3 <= a[4]; - aOperatorNegIndexing1 <= a[-a.width]; - aOperatorNegIndexing2 <= a[-1]; - aOperatorNegIndexing3 <= a[-2]; - - aReversed <= a.reversed; - - expressionBitSelect <= - [aBJoined, aShrunk1, aRange1, aRSliced1, aPlusB].swizzle().slice(3, 0); - } -} - -class ConstBusModule extends Module { - ConstBusModule(int c, {required bool subset}) { - final outWidth = subset ? 8 : 16; - addOutput('const_subset', width: outWidth) <= - Const(c, width: 16).getRange(0, outWidth); - } -} - -class SingleBitBusSubsetMod extends Module { - SingleBitBusSubsetMod(Logic oneBit) { - oneBit = addInput('oneBit', oneBit); - addOutput('result') <= BusSubset(oneBit, 0, 0).subset; - } -} - -class SelectTestModule extends Module { - SelectTestModule(Logic a1, Logic a2, Logic a3, Logic b, {Logic? defaultValue}) - : super(name: 'selecttestmodule') { - a1 = addInput('a1', a1, width: a1.width); - a2 = addInput('a2', a2, width: a2.width); - a3 = addInput('a3', a3, width: a3.width); - b = addInput('b', b, width: b.width); - - if (defaultValue != null) { - defaultValue = - addInput('defaultValue', defaultValue, width: defaultValue.width); - _selectWithDefault(a1, a2, a3, b, defaultValue); - } else { - _selectWithout(a1, a2, a3, b); - } - } - - void _selectWithout(Logic a1, Logic a2, Logic a3, Logic b) { - final selectIndexValue = addOutput('selectIndexValue', width: a1.width); - final selectFromValue = addOutput('selectFromValue', width: a1.width); - final logicList = [a1, a2, a3]; - selectIndexValue <= logicList.selectIndex(b); - selectFromValue <= b.selectFrom(logicList); - } - - void _selectWithDefault( - Logic a1, Logic a2, Logic a3, Logic b, Logic defaultValue) { - final selectFromValue = addOutput('selectFromValue', width: a1.width); - final selectIndexValue = addOutput('selectIndexValue', width: a1.width); - final logicList = [a1, a2, a3]; - selectFromValue <= b.selectFrom(logicList, defaultValue: defaultValue); - selectIndexValue <= logicList.selectIndex(b, defaultValue: defaultValue); - } -} - -// ===== Modules from conditionals_test.dart ===== - -class LoopyCombModuleSsa extends Module { - Logic get a => input('a'); - Logic get x => output('x'); - LoopyCombModuleSsa(Logic a) : super(name: 'loopycombmodule') { - a = addInput('a', a); - final x = addOutput('x'); - Combinational.ssa((s) => [ - s(x) < a, - s(x) < ~s(x), - ]); - } -} - -class CaseModule extends Module { - CaseModule(Logic a, Logic b) : super(name: 'casemodule') { - a = addInput('a', a); - b = addInput('b', b); - final c = addOutput('c'); - final d = addOutput('d'); - final e = addOutput('e'); - - Combinational([ - Case( - [b, a].swizzle(), - [ - CaseItem(Const(LogicValue.ofString('01')), [c < 1, d < 0]), - CaseItem(Const(LogicValue.ofString('10')), [c < 1, d < 0]), - ], - defaultItem: [c < 0, d < 1], - conditionalType: ConditionalType.unique), - CaseZ( - [b, a].rswizzle(), - [ - CaseItem(Const(LogicValue.ofString('1z')), [e < 1]) - ], - defaultItem: [e < 0], - conditionalType: ConditionalType.priority) - ]); - } -} - -class IfBlockModule extends Module { - IfBlockModule(Logic a, Logic b) : super(name: 'ifblockmodule') { - a = addInput('a', a); - b = addInput('b', b); - final c = addOutput('c'); - final d = addOutput('d'); - - Combinational([ - If.block([ - Iff(a & ~b, [c < 1, d < 0]), - ElseIf(b & ~a, [c < 1, d < 0]), - Else([c < 0, d < 1]) - ]) - ]); - } -} - -class SingleIfBlockModule extends Module { - SingleIfBlockModule(Logic a) : super(name: 'singleifblockmodule') { - a = addInput('a', a); - final c = addOutput('c'); - Combinational([ - If.block([Iff.s(a, c < 1)]) - ]); - } -} - -class ElseIfBlockModule extends Module { - ElseIfBlockModule(Logic a, Logic b) : super(name: 'ifblockmodule') { - a = addInput('a', a); - b = addInput('b', b); - final c = addOutput('c'); - final d = addOutput('d'); - - Combinational([ - If.block([ - ElseIf(a & ~b, [c < 1, d < 0]), - ElseIf(b & ~a, [c < 1, d < 0]), - Else([c < 0, d < 1]) - ]) - ]); - } -} - -class SingleElseIfBlockModule extends Module { - SingleElseIfBlockModule(Logic a) : super(name: 'singleifblockmodule') { - a = addInput('a', a); - final c = addOutput('c'); - final d = addOutput('d'); - Combinational([ - If.block([ - ElseIf.s(a, c < 1), - Else([c < 0, d < 1]) - ]) - ]); - } -} - -class CombModule extends Module { - CombModule(Logic a, Logic b, Logic d) : super(name: 'combmodule') { - a = addInput('a', a); - b = addInput('b', b); - final y = addOutput('y'); - final z = addOutput('z'); - final x = addOutput('x'); - d = addInput('d', d, width: d.width); - final q = addOutput('q', width: d.width); - - Combinational([ - If(a, then: [ - y < a, - z < b, - x < a & b, - q < d, - ], orElse: [ - If(b, then: [ - y < b, - z < a, - q < 13, - ], orElse: [ - y < 0, - z < 1, - ]) - ]) - ]); - } -} - -class SequentialModule extends Module { - SequentialModule(Logic a, Logic b, Logic d) : super(name: 'ffmodule') { - a = addInput('a', a); - b = addInput('b', b); - final y = addOutput('y'); - final z = addOutput('z'); - final x = addOutput('x'); - d = addInput('d', d, width: d.width); - final q = addOutput('q', width: d.width); - - Sequential(SimpleClockGenerator(10).clk, [ - If(a, then: [ - q < d, - y < a, - z < b, - x < ~x, - ], orElse: [ - x < a, - If(b, then: [ - y < b, - z < a - ], orElse: [ - y < 0, - z < 1, - ]) - ]) - ]); - } -} - -class SingleIfModule extends Module { - SingleIfModule(Logic a) : super(name: 'combmodule') { - a = addInput('a', a); - final q = addOutput('q'); - Combinational([If.s(a, q < 1)]); - } -} - -class SingleIfOrElseModule extends Module { - SingleIfOrElseModule(Logic a, Logic b) : super(name: 'combmodule') { - a = addInput('a', a); - b = addInput('b', b); - final q = addOutput('q'); - final x = addOutput('x'); - Combinational([If.s(a, q < 1, x < 1)]); - } -} - -class SingleElseModule extends Module { - SingleElseModule(Logic a, Logic b) : super(name: 'combmodule') { - a = addInput('a', a); - b = addInput('b', b); - final q = addOutput('q'); - final x = addOutput('x'); - Combinational([ - If.block([Iff.s(a, q < 1), Else.s(x < 1)]) - ]); - } -} - -class SignalRedrivenSequentialModule extends Module { - SignalRedrivenSequentialModule(Logic a, Logic b, Logic d, - {required bool allowRedrive}) - : super(name: 'ffmodule') { - a = addInput('a', a); - b = addInput('b', b); - final q = addOutput('q', width: d.width); - d = addInput('d', d, width: d.width); - final k = addOutput('k', width: 8); - Sequential( - SimpleClockGenerator(10).clk, - [ - If(a, then: [k < k, q < k, q < d]) - ], - allowMultipleAssignments: allowRedrive, - ); - } -} - -// ===== Modules from assignment_test.dart ===== - -class ConstAssignModule extends Module { - ConstAssignModule() { - final out = addOutput('out'); - final val = Logic(name: 'val'); - val <= Const(1); - Combinational([out < val]); - } - - Logic get out => output('out'); -} - -// ========================================================================= -// Tests -// ========================================================================= - -void main() { - tearDown(() async { - await Simulator.reset(); - }); - - tearDownAll(SimCompare.cleanupSystemCCache); - - // ===== Flop tests (from flop_test.dart) ===== - group('flop', () { - test('flop bit', () async { - final ftm = FlopTestModule(Logic()); - await ftm.build(); - SimCompare.checkSystemCVector(ftm, [ - Vector({'a': 0}, {}), - Vector({'a': 1}, {'y': 0}), - Vector({'a': 1}, {'y': 1}), - Vector({'a': 0}, {'y': 1}), - Vector({'a': 0}, {'y': 0}), - ]); - }); - - test('flop bit with enable', () async { - final ftm = FlopTestModule(Logic(), en: Logic()); - await ftm.build(); - SimCompare.checkSystemCVector(ftm, [ - Vector({'a': 0, 'en': 1}, {}), - Vector({'a': 1, 'en': 1}, {'y': 0}), - Vector({'a': 1, 'en': 1}, {'y': 1}), - Vector({'a': 0, 'en': 1}, {'y': 1}), - Vector({'a': 0, 'en': 1}, {'y': 0}), - Vector({'a': 1, 'en': 1}, {'y': 0}), - Vector({'a': 1, 'en': 0}, {'y': 1}), - Vector({'a': 0, 'en': 0}, {'y': 1}), - Vector({'a': 0, 'en': 1}, {'y': 1}), - Vector({'a': 1, 'en': 1}, {'y': 0}), - Vector({'a': 0, 'en': 0}, {'y': 1}), - Vector({'a': 1, 'en': 0}, {'y': 1}), - ]); - }); - - test('flop bus', () async { - final ftm = FlopTestModule(Logic(width: 8)); - await ftm.build(); - SimCompare.checkSystemCVector(ftm, [ - Vector({'a': 0}, {}), - Vector({'a': 0xff}, {'y': 0}), - Vector({'a': 0xaa}, {'y': 0xff}), - Vector({'a': 0x55}, {'y': 0xaa}), - Vector({'a': 0x1}, {'y': 0x55}), - ]); - }); - - test('flop bus with enable', () async { - final ftm = FlopTestModule(Logic(width: 8), en: Logic()); - await ftm.build(); - SimCompare.checkSystemCVector(ftm, [ - Vector({'a': 0, 'en': 1}, {}), - Vector({'a': 0xff, 'en': 1}, {'y': 0}), - Vector({'a': 0xaa, 'en': 1}, {'y': 0xff}), - Vector({'a': 0x55, 'en': 1}, {'y': 0xaa}), - Vector({'a': 0x1, 'en': 1}, {'y': 0x55}), - Vector({'a': 0, 'en': 1}, {'y': 0x1}), - Vector({'a': 0xff, 'en': 1}, {'y': 0}), - Vector({'a': 0xaa, 'en': 1}, {'y': 0xff}), - Vector({'a': 0x55, 'en': 0}, {'y': 0xaa}), - Vector({'a': 0x1, 'en': 0}, {'y': 0xaa}), - Vector({'a': 0x55, 'en': 1}, {'y': 0xaa}), - Vector({'a': 0x1, 'en': 1}, {'y': 0x55}), - Vector({'a': 0x55, 'en': 0}, {'y': 0x1}), - Vector({'a': 0x1, 'en': 1}, {'y': 0x1}), - ]); - }); - - test('flop bus reset, no reset value', () async { - final ftm = FlopTestModule(Logic(width: 8), reset: Logic()); - await ftm.build(); - SimCompare.checkSystemCVector(ftm, [ - Vector({'reset': 1}, {}), - Vector({'reset': 0, 'a': 0xa5}, {'y': 0}), - Vector({'a': 0xff}, {'y': 0xa5}), - Vector({}, {'y': 0xff}), - ]); - }); - - test('flop bus reset, const reset value', () async { - final ftm = - FlopTestModule(Logic(width: 8), reset: Logic(), resetValue: 3); - await ftm.build(); - SimCompare.checkSystemCVector(ftm, [ - Vector({'reset': 1}, {}), - Vector({'reset': 0, 'a': 0xa5}, {'y': 3}), - Vector({'a': 0xff}, {'y': 0xa5}), - Vector({}, {'y': 0xff}), - ]); - }); - - test('flop bus reset, logic reset value', () async { - final ftm = FlopTestModule(Logic(width: 8), - reset: Logic(), resetValue: Logic(width: 8)); - await ftm.build(); - SimCompare.checkSystemCVector(ftm, [ - Vector({'reset': 1, 'resetValue': 5}, {}), - Vector({'reset': 0, 'a': 0xa5}, {'y': 5}), - Vector({'a': 0xff}, {'y': 0xa5}), - Vector({}, {'y': 0xff}), - ]); - }); - - test('flop bus no reset, const reset value', () async { - final ftm = FlopTestModule(Logic(width: 8), resetValue: 9); - await ftm.build(); - SimCompare.checkSystemCVector(ftm, [ - Vector({}, {}), - Vector({'a': 0xa5}, {}), - Vector({'a': 0xff}, {'y': 0xa5}), - Vector({}, {'y': 0xff}), - ]); - }); - - test('flop bus, enable, reset, const reset value', () async { - final ftm = FlopTestModule(Logic(width: 8), - en: Logic(), reset: Logic(), resetValue: 12); - await ftm.build(); - SimCompare.checkSystemCVector(ftm, [ - Vector({'reset': 1, 'en': 0}, {}), - Vector({'reset': 0, 'a': 0xa5}, {'y': 12}), - Vector({}, {'y': 12}), - Vector({'en': 1}, {'y': 12}), - Vector({'a': 0xff}, {'y': 0xa5}), - Vector({}, {'y': 0xff}), - ]); - }); - }); - - // ===== Counter tests (from counter_test.dart) ===== - group('counter', () { - test('counter', () async { - final counter = Counter(Logic(), Logic()); - await counter.build(); - SimCompare.checkSystemCVector(counter, [ - Vector({'en': 0, 'reset': 0}, {}), - Vector({'en': 0, 'reset': 1}, {'val': 0}), - Vector({'en': 1, 'reset': 1}, {'val': 0}), - Vector({'en': 1, 'reset': 0}, {'val': 0}), - Vector({'en': 1, 'reset': 0}, {'val': 1}), - Vector({'en': 1, 'reset': 0}, {'val': 2}), - Vector({'en': 1, 'reset': 0}, {'val': 3}), - Vector({'en': 0, 'reset': 0}, {'val': 4}), - Vector({'en': 0, 'reset': 0}, {'val': 4}), - Vector({'en': 1, 'reset': 0}, {'val': 4}), - Vector({'en': 0, 'reset': 0}, {'val': 5}), - ]); - }); - }); - - // ===== Comparison tests (from comparison_test.dart) ===== - group('comparison', () { - test('compares', () async { - final gtm = ComparisonTestModule(Logic(width: 8), Logic(width: 8)); - await gtm.build(); - SimCompare.checkSystemCVector(gtm, [ - Vector({ - 'a': 0, - 'b': 0 - }, { - 'a_eq_b': 1, - 'a_neq_b': 0, - 'a_lt_b': 0, - 'a_lte_b': 1, - 'a_gt_b': 0, - 'a_gte_b': 1, - 'a_gt_operator_b': 0, - 'a_gte_operator_b': 1, - 'a_eq_c': 0, - 'a_neq_c': 1, - 'a_lt_c': 1, - 'a_lte_c': 1, - 'a_gt_c': 0, - 'a_gte_c': 0, - 'a_gt_operator_c': 0, - 'a_gte_operator_c': 0, - }), - Vector({ - 'a': 5, - 'b': 6 - }, { - 'a_eq_b': 0, - 'a_neq_b': 1, - 'a_lt_b': 1, - 'a_lte_b': 1, - 'a_gt_b': 0, - 'a_gte_b': 0, - 'a_gt_operator_b': 0, - 'a_gte_operator_b': 0, - 'a_eq_c': 1, - 'a_neq_c': 0, - 'a_lt_c': 0, - 'a_lte_c': 1, - 'a_gt_c': 0, - 'a_gte_c': 1, - 'a_gt_operator_c': 0, - 'a_gte_operator_c': 1, - }), - Vector({ - 'a': 9, - 'b': 7 - }, { - 'a_eq_b': 0, - 'a_neq_b': 1, - 'a_lt_b': 0, - 'a_lte_b': 0, - 'a_gt_b': 1, - 'a_gte_b': 1, - 'a_gt_operator_b': 1, - 'a_gte_operator_b': 1, - 'a_eq_c': 0, - 'a_neq_c': 1, - 'a_lt_c': 0, - 'a_lte_c': 0, - 'a_gt_c': 1, - 'a_gte_c': 1, - 'a_gt_operator_c': 1, - 'a_gte_operator_c': 1, - }), - ]); - }); - }); - - // ===== Arithmetic shift right tests ===== - group('arithmetic shift right', () { - test('shift right and mask', () async { - final mod = - SraUnsignedTestModule(Logic(width: 32), Logic(width: 32), Logic()); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'toShift': 0xe0000000, 'shiftAmount': 4, 'maskBit': 1}, - {'result': 0xfe000000}), - Vector({'toShift': 0x10000000, 'shiftAmount': 4, 'maskBit': 1}, - {'result': 0x01000000}), - Vector({'toShift': 0xe0000000, 'shiftAmount': 4, 'maskBit': 0}, - {'result': 0}), - ]); - }); - }); - - // ===== Collapse tests ===== - group('collapse', () { - test('collapse functional', () async { - final mod = CollapseTestModule(Logic(), Logic()); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'a': 1, 'b': 1}, {'c': 1, 'd': 1, 'e': 1, 'f': 1}), - Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 0, 'e': 0, 'f': 0}), - ]); - }); - }); - - // ===== Extend tests ===== - group('extend', () { - Future extendVectors( - List vectors, int newWidth, ExtendType extendType, - {int originalWidth = 8}) async { - final mod = - ExtendModule(Logic(width: originalWidth), newWidth, extendType); - await mod.build(); - SimCompare.checkSystemCVector(mod, vectors); - } - - test('zero extend same width', () async { - await extendVectors([ - Vector({'a': 0}, {'b': 0}), - Vector({'a': 0xff}, {'b': 0xff}), - Vector({'a': 0x5a}, {'b': 0x5a}), - ], 8, ExtendType.zero); - }); - - test('sign extend same width', () async { - await extendVectors([ - Vector({'a': 0}, {'b': 0}), - Vector({'a': 0xff}, {'b': 0xff}), - Vector({'a': 0x5a}, {'b': 0x5a}), - ], 8, ExtendType.sign); - }); - - test('zero extend pads 0s', () async { - await extendVectors([ - Vector({'a': 0xff}, {'b': 0xff}), - Vector({'a': 0x5a}, {'b': 0x5a}), - ], 12, ExtendType.zero); - }); - - test('sign extend positive pads 0s', () async { - await extendVectors([ - Vector({'a': 0x5a}, {'b': 0x5a}), - ], 12, ExtendType.sign); - }); - - test('sign extend negative pads 1s', () async { - await extendVectors([ - Vector({'a': 0xff}, {'b': 0xfff}), - ], 12, ExtendType.sign); - }); - - test('sign extend single bit(0) pads 0s', () async { - await extendVectors([ - Vector({'a': LogicValue.zero}, {'b': 0x000}), - ], 12, ExtendType.sign, originalWidth: 1); - }); - - test('sign extend single bit(1) pads 1s', () async { - await extendVectors([ - Vector({'a': LogicValue.one}, {'b': 0xfff}), - ], 12, ExtendType.sign, originalWidth: 1); - }); - }); - - group('withSet', () { - Future withSetVectors( - List vectors, int startIndex, int updateWidth) async { - final mod = - WithSetModule(Logic(width: 8), startIndex, Logic(width: updateWidth)); - await mod.build(); - SimCompare.checkSystemCVector(mod, vectors); - } - - test('setting same width', () async { - await withSetVectors([ - Vector({'a': 0x23, 'b': 0xff}, {'c': 0xff}), - Vector({'a': 0x45, 'b': 0x5a}, {'c': 0x5a}), - ], 0, 8); - }); - - test('setting at front', () async { - await withSetVectors([ - Vector({'a': 0x23, 'b': 0xf}, {'c': 0x2f}), - Vector({'a': 0x4a, 'b': 0x5}, {'c': 0x45}), - ], 0, 4); - }); - - test('setting at end', () async { - await withSetVectors([ - Vector({'a': 0x23, 'b': 0xf}, {'c': 0xf3}), - Vector({'a': 0x4a, 'b': 0x5}, {'c': 0x5a}), - ], 4, 4); - }); - - test('setting in the middle', () async { - await withSetVectors([ - Vector({'a': 0xff, 'b': 0x0}, {'c': bin('11000011')}), - Vector( - {'a': bin('01111110'), 'b': bin('0110')}, {'c': bin('01011010')}), - ], 2, 4); - }); - }); - - // ===== Bus tests ===== - group('bus', () { - test('single-bit bus subset', () async { - final mod = SingleBitBusSubsetMod(Logic()); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'oneBit': 0}, {'result': 0}), - Vector({'oneBit': 1}, {'result': 1}), - ]); - }); - - test('const subset', () async { - final mod = ConstBusModule(0xabcd, subset: true); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({}, {'const_subset': 0xcd}), - ]); - }); - - test('const assignment', () async { - final mod = ConstBusModule(0xabcd, subset: false); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({}, {'const_subset': 0xabcd}), - ]); - }); - - // All tests below share the same BusTestModule — compile once - group('BusTestModule', () { - SystemCExecutable? exe; - - setUpAll(() async { - final gtm = BusTestModule(Logic(width: 8), Logic(width: 8)); - await gtm.build(); - exe = SimCompare.buildSystemCExecutable(gtm); - }); - - tearDownAll(() { - exe?.cleanup(); - }); - - test('NotGate bus', () { - if (exe == null) { - return; - } - SimCompare.checkSystemCVectors(exe!, [ - Vector({'a': 0xff}, {'a_bar': 0}), - Vector({'a': 0}, {'a_bar': 0xff}), - Vector({'a': 0x55}, {'a_bar': 0xaa}), - Vector({'a': 1}, {'a_bar': 0xfe}), - ]); - }); - - test('And2Gate bus', () { - if (exe == null) { - return; - } - SimCompare.checkSystemCVectors(exe!, [ - Vector({'a': 0, 'b': 0}, {'a_and_b': 0}), - Vector({'a': 0, 'b': 1}, {'a_and_b': 0}), - Vector({'a': 1, 'b': 0}, {'a_and_b': 0}), - Vector({'a': 1, 'b': 1}, {'a_and_b': 1}), - Vector({'a': 0xff, 'b': 0xaa}, {'a_and_b': 0xaa}), - ]); - }); - - test('Operator indexing', () { - if (exe == null) { - return; - } - SimCompare.checkSystemCVectors(exe!, [ - Vector({'a': bin('11111110')}, {'a_operator_indexing1': 0}), - Vector({'a': bin('10000000')}, {'a_operator_indexing2': 1}), - Vector({'a': bin('11101111')}, {'a_operator_indexing3': 0}), - Vector({'a': bin('11111110')}, {'a_operator_neg_indexing1': 0}), - Vector({'a': bin('10000000')}, {'a_operator_neg_indexing2': 1}), - Vector({'a': bin('10111111')}, {'a_operator_neg_indexing3': 0}), - ]); - }); - - test('Bus shrink', () { - if (exe == null) { - return; - } - SimCompare.checkSystemCVectors(exe!, [ - Vector({'a': 0}, {'a_shrunk1': 0}), - Vector({'a': 0xfa}, {'a_shrunk1': bin('010')}), - Vector({'a': 0xab}, {'a_shrunk1': 3}), - Vector({'a': 0}, {'a_shrunk2': 0}), - Vector({'a': 0xec}, {'a_shrunk2': bin('00')}), - Vector({'a': 0xfa}, {'a_shrunk2': 2}), - Vector({'a': 0}, {'a_shrunk3': 0}), - Vector({'a': 0xff}, {'a_shrunk3': bin('1')}), - Vector({'a': 0xba}, {'a_shrunk3': 0}), - Vector({'a': 0}, {'a_neg_shrunk1': 0}), - Vector({'a': 0xfa}, {'a_neg_shrunk1': bin('010')}), - Vector({'a': 0xab}, {'a_neg_shrunk1': 3}), - Vector({'a': 0}, {'a_neg_shrunk2': 0}), - Vector({'a': 0xec}, {'a_neg_shrunk2': bin('00')}), - Vector({'a': 0xfa}, {'a_neg_shrunk2': 2}), - Vector({'a': 0}, {'a_neg_shrunk3': 0}), - Vector({'a': 0xff}, {'a_neg_shrunk3': bin('1')}), - Vector({'a': 0xba}, {'a_neg_shrunk3': 0}), - ]); - }); - - test('Bus reverse slice', () { - if (exe == null) { - return; - } - SimCompare.checkSystemCVectors(exe!, [ - Vector({'a': 0}, {'a_rsliced1': 0}), - Vector({'a': 0xac}, {'a_rsliced1': bin('10101')}), - Vector({'a': 0xf5}, {'a_rsliced1': 0xf}), - Vector({'a': 0}, {'a_rsliced2': 0}), - Vector({'a': 0xab}, {'a_rsliced2': bin('01')}), - Vector({'a': 0xac}, {'a_rsliced2': 1}), - Vector({'a': 0}, {'a_rsliced3': 0}), - Vector({'a': 0xaf}, {'a_rsliced3': bin('1')}), - Vector({'a': 0xaf}, {'a_rsliced3': 1}), - Vector({'a': 0}, {'a_r_neg_sliced1': 0}), - Vector({'a': 0xac}, {'a_r_neg_sliced1': bin('10101')}), - Vector({'a': 0xf5}, {'a_r_neg_sliced1': 0xf}), - Vector({'a': 0}, {'a_r_neg_sliced2': 0}), - Vector({'a': 0xab}, {'a_r_neg_sliced2': bin('01')}), - Vector({'a': 0xac}, {'a_r_neg_sliced2': 1}), - Vector({'a': 0}, {'a_r_neg_sliced3': 0}), - Vector({'a': 0xaf}, {'a_r_neg_sliced3': bin('1')}), - Vector({'a': 0xaf}, {'a_r_neg_sliced3': 1}), - ]); - }); - - test('Bus reversed', () { - if (exe == null) { - return; - } - SimCompare.checkSystemCVectors(exe!, [ - Vector({'a': 0}, {'a_reversed': 0}), - Vector({'a': 0xff}, {'a_reversed': 0xff}), - Vector({'a': 0xf5}, {'a_reversed': 0xaf}), - ]); - }); - - test('Bus range', () { - if (exe == null) { - return; - } - SimCompare.checkSystemCVectors(exe!, [ - Vector({'a': 0}, {'a_range1': 0}), - Vector({'a': 0xaf}, {'a_range1': 5}), - Vector({'a': bin('11000101')}, {'a_range1': bin('110')}), - Vector({'a': 0}, {'a_range2': 0}), - Vector({'a': 0xaf}, {'a_range2': 2}), - Vector({'a': bin('10111111')}, {'a_range2': bin('10')}), - Vector({'a': 0}, {'a_range3': 0}), - Vector({'a': 0x80}, {'a_range3': 1}), - Vector({'a': bin('10000000')}, {'a_range3': bin('1')}), - Vector({'a': 0}, {'a_range4': 0}), - Vector({'a': 0xaf}, {'a_range4': 5}), - Vector({'a': bin('11000101')}, {'a_range4': bin('110')}), - Vector({'a': 0}, {'a_neg_range1': 0}), - Vector({'a': 0xaf}, {'a_neg_range1': 5}), - Vector({'a': bin('11000101')}, {'a_neg_range1': bin('110')}), - Vector({'a': 0}, {'a_neg_range2': 0}), - Vector({'a': 0xaf}, {'a_neg_range2': 2}), - Vector({'a': bin('10111111')}, {'a_neg_range2': bin('10')}), - Vector({'a': 0}, {'a_neg_range3': 0}), - Vector({'a': 0x80}, {'a_neg_range3': 1}), - Vector({'a': bin('10000000')}, {'a_neg_range3': bin('1')}), - Vector({'a': 0}, {'a_neg_range4': 0}), - Vector({'a': 0xaf}, {'a_neg_range4': 5}), - Vector({'a': bin('11000101')}, {'a_neg_range4': bin('110')}), - ]); - }); - - test('Bus swizzle', () { - if (exe == null) { - return; - } - SimCompare.checkSystemCVectors(exe!, [ - Vector({'a': 0, 'b': 0}, {'a_b_joined': 0}), - Vector({'a': 0xff, 'b': 0xff}, {'a_b_joined': 0xffff}), - Vector({'a': 0xff, 'b': 0}, {'a_b_joined': 0xff}), - Vector({'a': 0, 'b': 0xff}, {'a_b_joined': 0xff00}), - Vector({'a': 0xaa, 'b': 0x55}, {'a_b_joined': 0x55aa}), - ]); - }); - - test('Bus bit', () { - if (exe == null) { - return; - } - SimCompare.checkSystemCVectors(exe!, [ - Vector({'a': 0}, {'a1': 0}), - Vector({'a': 0xff}, {'a1': 1}), - Vector({'a': 0xf5}, {'a1': 0}), - ]); - }); - - test('add busses', () { - if (exe == null) { - return; - } - SimCompare.checkSystemCVectors(exe!, [ - Vector({'a': 0, 'b': 0}, {'a_plus_b': 0}), - Vector({'a': 0, 'b': 1}, {'a_plus_b': 1}), - Vector({'a': 1, 'b': 0}, {'a_plus_b': 1}), - Vector({'a': 1, 'b': 1}, {'a_plus_b': 2}), - Vector({'a': 6, 'b': 7}, {'a_plus_b': 13}), - ]); - }); - - test('expression bit select', () { - if (exe == null) { - return; - } - SimCompare.checkSystemCVectors(exe!, [ - Vector({'a': 1, 'b': 1}, {'expression_bit_select': 2}), - ]); - }); - }); // end BusTestModule group - - test('selectFrom and selectIndex', () async { - final gtm = SelectTestModule(Logic(width: 8), Logic(width: 8), - Logic(width: 8), Logic(width: (log(8) / log(2)).ceil())); - await gtm.build(); - SimCompare.checkSystemCVector(gtm, [ - Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 1}, - {'selectIndexValue': 2, 'selectFromValue': 2}), - Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 0}, - {'selectIndexValue': 1, 'selectFromValue': 1}), - Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 2}, - {'selectIndexValue': 3, 'selectFromValue': 3}), - ]); - }); - - test('selectFrom with default Value', () async { - final gtm = SelectTestModule(Logic(width: 8), Logic(width: 8), - Logic(width: 8), Logic(width: (log(8) / log(2)).ceil()), - defaultValue: Logic(width: 8)); - await gtm.build(); - SimCompare.checkSystemCVector(gtm, [ - Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 4, 'defaultValue': 5}, - {'selectFromValue': 5, 'selectIndexValue': 5}), - ]); - }); - }); - - // ===== Conditionals tests ===== - group('conditionals', () { - test('conditional comb', () async { - final mod = CombModule(Logic(), Logic(), Logic(width: 10)); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'a': 0, 'b': 0, 'd': 5}, - {'y': 0, 'z': 1, 'x': LogicValue.x, 'q': LogicValue.x}), - Vector({'a': 0, 'b': 1, 'd': 6}, - {'y': 1, 'z': 0, 'x': LogicValue.x, 'q': 13}), - Vector({'a': 1, 'b': 0, 'd': 7}, {'y': 1, 'z': 0, 'x': 0, 'q': 7}), - Vector({'a': 1, 'b': 1, 'd': 8}, {'y': 1, 'z': 1, 'x': 1, 'q': 8}), - ]); - }); - - test('iffblock comb', () async { - final mod = IfBlockModule(Logic(), Logic()); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 1}), - Vector({'a': 0, 'b': 1}, {'c': 1, 'd': 0}), - Vector({'a': 1, 'b': 0}, {'c': 1, 'd': 0}), - Vector({'a': 1, 'b': 1}, {'c': 0, 'd': 1}), - ]); - }); - - test('single iffblock comb', () async { - final mod = SingleIfBlockModule(Logic()); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'a': 1}, {'c': 1}), - ]); - }); - - test('elseifblock comb', () async { - final mod = ElseIfBlockModule(Logic(), Logic()); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 1}), - Vector({'a': 0, 'b': 1}, {'c': 1, 'd': 0}), - Vector({'a': 1, 'b': 0}, {'c': 1, 'd': 0}), - Vector({'a': 1, 'b': 1}, {'c': 0, 'd': 1}), - ]); - }); - - test('single elseifblock comb', () async { - final mod = SingleElseIfBlockModule(Logic()); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'a': 1}, {'c': 1}), - Vector({'a': 0}, {'c': 0, 'd': 1}), - ]); - }); - - test('case comb', () async { - final mod = CaseModule(Logic(), Logic()); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 1, 'e': 0}), - Vector({'a': 0, 'b': 1}, {'c': 1, 'd': 0, 'e': 0}), - Vector({'a': 1, 'b': 0}, {'c': 1, 'd': 0, 'e': 1}), - Vector({'a': 1, 'b': 1}, {'c': 0, 'd': 1, 'e': 1}), - ]); - }); - - test('conditional ff', () async { - final mod = SequentialModule(Logic(), Logic(), Logic(width: 8)); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'a': 1, 'd': 1}, {}), - Vector({'a': 0, 'b': 0, 'd': 2}, {'q': 1}), - Vector({'a': 0, 'b': 1, 'd': 3}, {'y': 0, 'z': 1, 'x': 0, 'q': 1}), - Vector({'a': 1, 'b': 0, 'd': 4}, {'y': 1, 'z': 0, 'x': 0, 'q': 1}), - Vector({'a': 1, 'b': 1, 'd': 5}, {'y': 1, 'z': 0, 'x': 1, 'q': 4}), - Vector({}, {'y': 1, 'z': 1, 'x': 0, 'q': 5}), - ]); - }); - - test('loopy comb ssa', () async { - final mod = LoopyCombModuleSsa(Logic()); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'a': 0}, {'x': 1}), - Vector({'a': 1}, {'x': 0}), - ]); - }); - - test('single if', () async { - final mod = SingleIfModule(Logic()); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'a': 1}, {'q': 1}), - ]); - }); - - test('single if or else', () async { - final mod = SingleIfOrElseModule(Logic(), Logic()); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'a': 1}, {'q': 1}), - Vector({'a': 0}, {'x': 1}), - ]); - }); - - test('single else', () async { - final mod = SingleElseModule(Logic(), Logic()); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'a': 1}, {'q': 1}), - Vector({'a': 0}, {'x': 1}), - ]); - }); - - test('redrive allowed', () async { - final mod = SignalRedrivenSequentialModule( - Logic(), Logic(), Logic(width: 8), - allowRedrive: true); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({'a': 1, 'd': 1}, {}), - Vector({'a': 1, 'b': 0, 'd': 2}, {'q': 1}), - Vector({'a': 1, 'b': 0, 'd': 3}, {'q': 2}), - ]); - }); - }); - - // ===== Assignment tests ===== - group('assignment', () { - test('const comb assignment', () async { - final mod = ConstAssignModule(); - await mod.build(); - SimCompare.checkSystemCVector(mod, [ - Vector({}, {'out': 1}), - ]); - }); - }); -} From f3a8e01f033fb15421d64ebe46786490b5b2f847 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 20 Jul 2026 09:45:30 -0700 Subject: [PATCH 07/14] Add semantic leaf emission for SystemC --- lib/src/synthesizers/systemc/systemc.dart | 4 +- .../systemc/systemc_leaf_emitter.dart | 238 ++++++++++++++++++ .../synthesizers/systemc/systemc_mixins.dart | 20 ++ ...ystemc_synth_sub_module_instantiation.dart | 59 ++--- .../utilities/inline_leaf_emitter.dart | 35 +++ .../utilities/leaf_cell_spec.dart | 106 ++++++++ .../utilities/leaf_cell_spec_inference.dart | 188 ++++++++++++++ .../utilities/leaf_expression_plan.dart | 67 +++++ lib/src/synthesizers/utilities/utilities.dart | 4 + test/leaf_cell_spec_inference_test.dart | 203 +++++++++++++++ test/leaf_expression_plan_test.dart | 181 +++++++++++++ test/leaf_test_module_factories.dart | 69 +++++ test/systemc_leaf_emitter_test.dart | 68 +++++ 13 files changed, 1212 insertions(+), 30 deletions(-) create mode 100644 lib/src/synthesizers/systemc/systemc_leaf_emitter.dart create mode 100644 lib/src/synthesizers/systemc/systemc_mixins.dart create mode 100644 lib/src/synthesizers/utilities/inline_leaf_emitter.dart create mode 100644 lib/src/synthesizers/utilities/leaf_cell_spec.dart create mode 100644 lib/src/synthesizers/utilities/leaf_cell_spec_inference.dart create mode 100644 lib/src/synthesizers/utilities/leaf_expression_plan.dart create mode 100644 test/leaf_cell_spec_inference_test.dart create mode 100644 test/leaf_expression_plan_test.dart create mode 100644 test/leaf_test_module_factories.dart create mode 100644 test/systemc_leaf_emitter_test.dart diff --git a/lib/src/synthesizers/systemc/systemc.dart b/lib/src/synthesizers/systemc/systemc.dart index 7bf0f1211..fc7a188e4 100644 --- a/lib/src/synthesizers/systemc/systemc.dart +++ b/lib/src/synthesizers/systemc/systemc.dart @@ -1,7 +1,7 @@ // Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// systemc_synthesizer.dart +// systemc.dart // Definition for SystemC Synthesizer // // 2026 May @@ -10,6 +10,8 @@ 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, 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..b8a3cb7ed --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_leaf_emitter.dart @@ -0,0 +1,238 @@ +// 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/systemc/systemc_mixins.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(InlineSystemVerilog 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_sub_module_instantiation.dart b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart index 7e692ff8a..34f9a02ea 100644 --- a/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart @@ -8,28 +8,48 @@ // 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); - /// 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]); + /// If [module] is [InlineSystemVerilog], this is the [SynthLogic] mapped + /// from its [InlineSystemVerilog.resultSignalName]. + SynthLogic? get inlineResultLogic { + final m = module; + if (m is! InlineSystemVerilog) { + return null; + } + return outputMapping[m.resultSignalName] ?? + inOutMapping[m.resultSignalName]; + } /// Mapping from [SynthLogic]s which are outputs of inlineable modules to /// those inlineable modules. Map? synthLogicToInlineableSynthSubmoduleMap; - /// Provides a mapping from ports of this module to a string that can be fed - /// into that port, which may include inline expressions. + /// Resolves module ports, recursively inlining mapped leaf expressions. Map _modulePortsMapWithInline( Map plainPorts) => plainPorts.map((name, synthLogic) => MapEntry( @@ -57,27 +77,8 @@ class SystemCSynthSubModuleInstantiation extends SynthSubModuleInstantiation { String _inlineSystemCExpression(Map inputs) { final m = module; - if (m is NotGate) { - final inVal = inputs.values.first; - return '~$inVal'; - } else if (m is And2Gate) { - return '${inputs.values.first} & ${inputs.values.last}'; - } else if (m is Or2Gate) { - return '${inputs.values.first} | ${inputs.values.last}'; - } else if (m is Xor2Gate) { - return '${inputs.values.first} ^ ${inputs.values.last}'; - } else if (m is Mux) { - // Mux has inputs: control, d0, d1 → output: y - // In SystemC: control ? d1 : d0 - final entries = inputs.entries.toList(); - final control = entries[0].value; - final d0 = entries[1].value; - final d1 = entries[2].value; - return '$control ? $d1 : $d0'; - } else if (m is InlineSystemVerilog) { - // Fallback: use the verilog inline expression as a reasonable - // approximation (many operators are identical between SV and C++) - return m.inlineVerilog(inputs); + if (m is InlineSystemVerilog) { + return leafEmitter.expressionFor(m, inputs); } throw SynthException('Unsupported inline module type: ${m.runtimeType}'); 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..4249bda48 --- /dev/null +++ b/lib/src/synthesizers/utilities/inline_leaf_emitter.dart @@ -0,0 +1,35 @@ +// 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(InlineSystemVerilog module, Map inputs) { + throw UnimplementedError( + 'InlineLeafEmitter.expressionFor must be implemented by subclasses.', + ); + } +} + +/// Minimal passthrough implementation for backends without dedicated leaf +/// rendering logic. +class PassthroughInlineLeafEmitter implements InlineLeafEmitter { + /// Creates a passthrough inline leaf emitter. + const PassthroughInlineLeafEmitter(); + + @override + String expressionFor( + InlineSystemVerilog module, Map inputs) => + module.inlineVerilog(inputs); +} 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..4dc0dd393 --- /dev/null +++ b/lib/src/synthesizers/utilities/leaf_cell_spec_inference.dart @@ -0,0 +1,188 @@ +// 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(InlineSystemVerilog 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) { + return LeafCellSpec( + operation: LeafOperationKind.swizzle, + metadata: { + 'inputCount': module.inputs.length, + 'inputWidths': + module.inputs.values.map((input) => input.width).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; +} 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..e30507775 --- /dev/null +++ b/lib/src/synthesizers/utilities/leaf_expression_plan.dart @@ -0,0 +1,67 @@ +// 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( + InlineSystemVerilog module, + Map inputs, + ) { + final spec = leafCellSpecForInlineModule(module); + return LeafExpressionPlan( + sourceModule: 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; + } + + /// Invokes the legacy SystemVerilog inline hook for this source module. + /// + /// This exists only for the staged SystemVerilog migration. Other backends + /// must emit [operation] or use an explicit backend extension. + String legacySystemVerilogExpression() => + (sourceModule as InlineSystemVerilog).inlineVerilog(inputsByPort); +} diff --git a/lib/src/synthesizers/utilities/utilities.dart b/lib/src/synthesizers/utilities/utilities.dart index c3cccdf32..5ba2a7901 100644 --- a/lib/src/synthesizers/utilities/utilities.dart +++ b/lib/src/synthesizers/utilities/utilities.dart @@ -1,6 +1,10 @@ // Copyright (C) 2024-2025 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'inline_leaf_emitter.dart'; +export 'leaf_cell_spec.dart'; +export 'leaf_cell_spec_inference.dart'; +export 'leaf_expression_plan.dart'; export 'synth_assignment.dart'; export 'synth_logic.dart'; export 'synth_module_definition.dart'; diff --git a/test/leaf_cell_spec_inference_test.dart b/test/leaf_cell_spec_inference_test.dart new file mode 100644 index 000000000..c0e138196 --- /dev/null +++ b/test/leaf_cell_spec_inference_test.dart @@ -0,0 +1,203 @@ +// 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(), + ); + }); + + 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 = <({ + InlineSystemVerilog 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(), + }, + ), + ]; + + 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..6fe5ca9b6 --- /dev/null +++ b/test/leaf_expression_plan_test.dart @@ -0,0 +1,181 @@ +// 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( + InlineSystemVerilog 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 = <({ + InlineSystemVerilog 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], + }, + ), + ]; + + 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(InlineSystemVerilog 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..36e9a7ee0 --- /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/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()), + ); + }); + }); +} From 7ff6b868dec165602dbcabf6bfc8b1efb6899aca Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 20 Jul 2026 09:53:33 -0700 Subject: [PATCH 08/14] Add header to synthesis utility exports --- lib/src/synthesizers/utilities/utilities.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/synthesizers/utilities/utilities.dart b/lib/src/synthesizers/utilities/utilities.dart index 5ba2a7901..5bda4c4ba 100644 --- a/lib/src/synthesizers/utilities/utilities.dart +++ b/lib/src/synthesizers/utilities/utilities.dart @@ -1,5 +1,11 @@ // Copyright (C) 2024-2025 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 'inline_leaf_emitter.dart'; export 'leaf_cell_spec.dart'; From c1fc0db99717e3ca2b5e5aafb14c1d0fe93648a7 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 20 Jul 2026 09:47:13 -0700 Subject: [PATCH 09/14] Complete shared backend emission architecture --- lib/src/module.dart | 1 - lib/src/modules/conditionals/always.dart | 33 +- lib/src/modules/conditionals/case.dart | 49 +- lib/src/modules/conditionals/conditional.dart | 8 + .../conditionals/conditional_assign.dart | 13 +- .../conditionals/conditional_group.dart | 14 +- lib/src/modules/conditionals/if.dart | 32 +- lib/src/modules/conditionals/sequential.dart | 7 + lib/src/synthesizers/synthesizers.dart | 2 + .../systemc/systemc_conditional_emitter.dart | 150 +++++ .../systemc/systemc_leaf_emitter.dart | 1 - ...ystemc_synth_sub_module_instantiation.dart | 24 +- .../systemc/systemc_synthesis_result.dart | 392 ++---------- .../systemverilog_conditional_emitter.dart | 112 ++++ .../systemverilog_leaf_emitter.dart | 194 ++++++ .../systemverilog/systemverilog_mixins.dart | 44 +- .../systemverilog_process_emitter.dart | 66 ++ ...systemverilog_synth_module_definition.dart | 14 +- ...erilog_synth_sub_module_instantiation.dart | 44 +- .../systemverilog_synthesis_result.dart | 46 +- .../systemverilog_synthesizer.dart | 34 +- ...stemverilog_synthesizer_configuration.dart | 8 + .../utilities/backend_artifact.dart | 109 ++++ .../utilities/conditional_emission_plan.dart | 156 +++++ .../utilities/conditional_emitter.dart | 153 +++++ .../utilities/module_emission_plan.dart | 104 ++++ .../utilities/process_emission_plan.dart | 113 ++++ .../synth_sub_module_instantiation.dart | 31 + lib/src/synthesizers/utilities/utilities.dart | 5 + test/backend_artifact_test.dart | 72 +++ test/leaf_backend_conformance_test.dart | 333 ++++++++++ test/module_emission_plan_test.dart | 45 ++ test/process_emission_plan_test.dart | 55 ++ test/synth_test_helpers.dart | 12 + test/systemverilog_leaf_plan_option_test.dart | 584 ++++++++++++++++++ 35 files changed, 2552 insertions(+), 508 deletions(-) create mode 100644 lib/src/synthesizers/systemc/systemc_conditional_emitter.dart create mode 100644 lib/src/synthesizers/systemverilog/systemverilog_conditional_emitter.dart create mode 100644 lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart create mode 100644 lib/src/synthesizers/systemverilog/systemverilog_process_emitter.dart create mode 100644 lib/src/synthesizers/utilities/backend_artifact.dart create mode 100644 lib/src/synthesizers/utilities/conditional_emission_plan.dart create mode 100644 lib/src/synthesizers/utilities/conditional_emitter.dart create mode 100644 lib/src/synthesizers/utilities/module_emission_plan.dart create mode 100644 lib/src/synthesizers/utilities/process_emission_plan.dart create mode 100644 test/backend_artifact_test.dart create mode 100644 test/leaf_backend_conformance_test.dart create mode 100644 test/module_emission_plan_test.dart create mode 100644 test/process_emission_plan_test.dart create mode 100644 test/synth_test_helpers.dart create mode 100644 test/systemverilog_leaf_plan_option_test.dart diff --git a/lib/src/module.dart b/lib/src/module.dart index f61e97b2a..f98d91fab 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -14,7 +14,6 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; import 'package:rohd/src/diagnostics/inspector_service.dart'; -import 'package:rohd/src/synthesizers/systemc/systemc.dart'; import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; 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/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/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 8871202fd..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'; @@ -143,6 +144,12 @@ class Sequential extends Always { .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/synthesizers/synthesizers.dart b/lib/src/synthesizers/synthesizers.dart index b8c8523ec..47bedd8a5 100644 --- a/lib/src/synthesizers/synthesizers.dart +++ b/lib/src/synthesizers/synthesizers.dart @@ -5,4 +5,6 @@ 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'; 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 index b8a3cb7ed..f4706a240 100644 --- a/lib/src/synthesizers/systemc/systemc_leaf_emitter.dart +++ b/lib/src/synthesizers/systemc/systemc_leaf_emitter.dart @@ -8,7 +8,6 @@ // Author: Desmond A. Kirkpatrick import 'package:rohd/rohd.dart'; -import 'package:rohd/src/synthesizers/systemc/systemc_mixins.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; /// Emits backend-specific SystemC/C++ expressions for semantic leaf gates. diff --git a/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart index 34f9a02ea..fa580252e 100644 --- a/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart @@ -33,38 +33,20 @@ class SystemCSynthSubModuleInstantiation extends SynthSubModuleInstantiation { /// [module]. SystemCSynthSubModuleInstantiation(super.module); - /// If [module] is [InlineSystemVerilog], this is the [SynthLogic] mapped - /// from its [InlineSystemVerilog.resultSignalName]. - SynthLogic? get inlineResultLogic { - final m = module; - if (m is! InlineSystemVerilog) { - return null; - } - return outputMapping[m.resultSignalName] ?? - inOutMapping[m.resultSignalName]; - } - /// Mapping from [SynthLogic]s which are outputs of inlineable modules to /// those inlineable modules. Map? synthLogicToInlineableSynthSubmoduleMap; - /// Resolves module ports, recursively inlining mapped leaf expressions. - Map _modulePortsMapWithInline( - Map plainPorts) => - plainPorts.map((name, synthLogic) => MapEntry( - name, - synthLogicToInlineableSynthSubmoduleMap?[synthLogic] - ?.inlineSystemC() ?? - (synthLogic.declarationCleared ? '' : synthLogic.name))); - /// Provides the inline SystemC expression for this module. /// /// Should only be called if [module] is [InlineSystemVerilog]. String inlineSystemC() { - final portNameToValueMapping = _modulePortsMapWithInline( + final portNameToValueMapping = modulePortsMapWithInline( {...inputMapping, ...inOutMapping} ..remove((module as InlineSystemVerilog).resultSignalName), + synthLogicToInlineableSynthSubmoduleMap, + (submodule) => submodule.inlineSystemC(), ); final inlineRepresentation = diff --git a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart index 5bcafc12a..355afab04 100644 --- a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart +++ b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart @@ -10,12 +10,18 @@ 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; @@ -25,6 +31,9 @@ class SystemCSynthesisResult 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; @@ -38,6 +47,7 @@ class SystemCSynthesisResult extends SynthesisResult { /// 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); @@ -97,14 +107,14 @@ class SystemCSynthesisResult extends SynthesisResult { _scLineMap.clear(); final targets = { - for (final sig in _synthModuleDefinition.inputs) sig.name, - for (final sig in _synthModuleDefinition.outputs) sig.name, - for (final sig in _synthModuleDefinition.inOuts) sig.name, - for (final sig in _synthModuleDefinition.internalSignals - .where((e) => e.needsDeclaration)) + 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 _synthModuleDefinition.subModuleInstantiations - .where((s) => s.needsInstantiation)) + for (final smi + in _emissionPlan.instances.where((s) => s.needsInstantiation)) smi.name, }; @@ -173,7 +183,7 @@ class SystemCSynthesisResult extends SynthesisResult { /// and internal clocks that should be promoted to ports. void _findClockResetSignals() { final promotedClocks = {}; - for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + for (final ssmi in _emissionPlan.instances) { final m = ssmi.module; // Detect SimpleClockGenerator and promote its output to a port if (m is SimpleClockGenerator) { @@ -225,7 +235,7 @@ class SystemCSynthesisResult extends SynthesisResult { String _systemCPorts() { final lines = []; - for (final sig in _synthModuleDefinition.inputs) { + for (final sig in _emissionPlan.inputs) { final n = _scName(sig.name); lines.add(' ${systemCInType(sig.width)} $n{"$n"};'); } @@ -234,11 +244,11 @@ class SystemCSynthesisResult extends SynthesisResult { final n = _scName(clkName); lines.add(' ${systemCInType(1)} $n{"$n"};'); } - for (final sig in _synthModuleDefinition.outputs) { + for (final sig in _emissionPlan.outputs) { final n = _scName(sig.name); lines.add(' ${systemCOutType(sig.width)} $n{"$n"};'); } - for (final sig in _synthModuleDefinition.inOuts) { + for (final sig in _emissionPlan.inOuts) { final n = _scName(sig.name); lines.add(' ${systemCInOutType(sig.width)} $n{"$n"};'); } @@ -251,7 +261,7 @@ class SystemCSynthesisResult extends SynthesisResult { String _buildInternalSignals() { final declarations = []; - for (final sig in _synthModuleDefinition.internalSignals + for (final sig in _emissionPlan.internalSignals .where((e) => e.needsDeclaration) .where((e) => !_promotedClockSignals.contains(e.name)) .sorted((a, b) => a.name.compareTo(b.name))) { @@ -288,7 +298,7 @@ class SystemCSynthesisResult extends SynthesisResult { } } - for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + for (final ssmi in _emissionPlan.instances) { final m = ssmi.module; // All submodule output mappings @@ -311,7 +321,7 @@ class SystemCSynthesisResult extends SynthesisResult { } // Wire assignments targeting array elements - for (final assignment in _synthModuleDefinition.assignments) { + for (final assignment in _emissionPlan.assignments) { addIfArrayElement(assignment.dst); } @@ -582,200 +592,8 @@ class SystemCSynthesisResult extends SynthesisResult { /// /// Handles all gate types that have SV-specific syntax which needs /// translation to valid SystemC/C++. - String _gateExpression(InlineSystemVerilog m, Map inputs) { - // ── Single-output bitwise gates (C++ operators identical to SV) ── - if (m is NotGate) { - // For bool (width-1), use logical not; for wider, bitwise not - if ((m as Module).outputs.values.first.width == 1) { - return '!${inputs.values.first}'; - } - return '~${inputs.values.first}'; - } - - // ── Binary operator gates (C++ operators identical to SV) ── - const binaryOps = { - And2Gate: '&', - Or2Gate: '|', - Xor2Gate: '^', - Subtract: '-', - Multiply: '*', - }; - final binOp = binaryOps[m.runtimeType]; - if (binOp != null) { - final vals = inputs.values.toList(); - return '${vals[0]} $binOp ${vals[1]}'; - } - if (m is Divide || m is Modulo) { - final vals = inputs.values.toList(); - final op = m is Divide ? '/' : '%'; - // Guard against zero divisor (sc_uint defaults to 0 at time-0) - return '(${vals[1]} != 0 ? ${vals[0]} $op ${vals[1]} : 0)'; - } - if (m is Power) { - final vals = inputs.values.toList(); - final w = (m as Module).inputs.values.first.width; - return '${systemCType(w)}' - '(static_cast' - '(pow(static_cast(${vals[0]}),' - ' static_cast(${vals[1]}))))'; - } - - // ── Comparison (operators identical) ── - const cmpOps = { - Equals: '==', - NotEquals: '!=', - LessThan: '<', - GreaterThan: '>', - LessThanOrEqual: '<=', - GreaterThanOrEqual: '>=', - }; - final cmpOp = cmpOps[m.runtimeType]; - if (cmpOp != null) { - final vals = inputs.values.toList(); - return '${vals[0]} $cmpOp ${vals[1]}'; - } - - // ── Shifts ── - // Cast shift amount to int to avoid ambiguous overloads. - // Width 1 maps to bool in SystemC (no .to_int()), so use (int) cast. - // Clamp: if shift amount >= operand width, result is 0 (or sign-fill - // for arshift), avoiding .to_int() overflow on huge shift amounts. - if (m is LShift || m is RShift || m is ARShift) { - final vals = inputs.values.toList(); - final w = (m as Module).inputs.values.first.width; - final outType = systemCType(w); - final shiftAmtWidth = (m as Module).inputs.values.toList()[1].width; - final shiftExpr = - shiftAmtWidth == 1 ? '(int)(${vals[1]})' : '(${vals[1]}).to_int()'; - if (m is ARShift) { - final signedType = w <= 64 ? 'sc_int<$w>' : 'sc_bigint<$w>'; - final shiftOp = '$outType(($signedType(${vals[0]})) >> $shiftExpr)'; - if (shiftAmtWidth > 31) { - // Sign-fill: shift by width-1 to replicate MSB when shift >= width - final overflow = '$outType(($signedType(${vals[0]})) >> ${w - 1})'; - return '(${vals[1]} >= $w) ? $overflow : $shiftOp'; - } - return shiftOp; - } - final op = m is LShift ? '<<' : '>>'; - final shiftOp = '$outType(${vals[0]} $op $shiftExpr)'; - if (shiftAmtWidth > 31) { - return '(${vals[1]} >= $w) ? $outType(0) : $shiftOp'; - } - return shiftOp; - } - - // ── Unary reductions ── - if (m is AndUnary || m is OrUnary || m is XorUnary) { - final inputWidth = (m as Module).inputs.values.first.width; - // 1-bit: reduce is identity (and bool has no .xor_reduce() in SystemC) - if (inputWidth == 1) { - return 'static_cast(${inputs.values.first})'; - } - if (m is AndUnary) { - return '${inputs.values.first}.and_reduce()'; - } else if (m is OrUnary) { - return '${inputs.values.first}.or_reduce()'; - } else { - return '${inputs.values.first}.xor_reduce()'; - } - } - - // ── Bus subset (slice / index) ── - if (m is BusSubset) { - final a = inputs.values.first; - final inputWidth = (m as Module).inputs.values.first.width; - // If input is already 1-bit (bool), extracting bit 0 is identity - if (inputWidth == 1 && m.startIndex == 0 && m.endIndex == 0) { - return a; - } - if (m.startIndex == m.endIndex) { - return 'static_cast($a[${m.startIndex}])'; - } - if (m.startIndex > m.endIndex) { - // Reverse order — build bit-by-bit concat - // bits[0]=a[endIndex], ..., bits[N]=a[startIndex] - // SystemC concat is MSB-first: output MSB = input[endIndex] - // Use sc_uint<1> (not bool) so SystemC concat operator is invoked - final bits = List.generate(m.startIndex - m.endIndex + 1, - (i) => 'sc_uint<1>($a[${m.endIndex + i}])'); - return '(${bits.join(', ')})'; - } - final w = m.endIndex - m.startIndex + 1; - final rangeType = w <= 64 ? 'sc_uint' : 'sc_biguint'; - return '$rangeType<$w>($a.range(${m.endIndex}, ${m.startIndex}))'; - } - - // ── Dynamic bit index ── - if (m is IndexGate) { - final vals = inputs.values.toList(); - return 'static_cast(${vals[0]}[${vals[1]}])'; - } - - // ── Mux (ternary) ── - if (m is Mux) { - final vals = inputs.values.toList(); - final w = m.out.width; - final utype = systemCType(w); - // Cast both branches to avoid C++ ternary type mismatch - // (e.g., when one branch is bool and the other is sc_uint<1>) - return '${vals[0]}' - ' ? $utype(${vals[2]})' - ' : $utype(${vals[1]})'; - } - - // ── Replication ── - if (m is ReplicationOp) { - final a = inputs.values.first; - final inputWidth = (m as Module).inputs.values.first.width; - final outputWidth = m.replicated.width; - final numReps = outputWidth ~/ inputWidth; - if (inputWidth == 1) { - // Single-bit replicate: all-1s or all-0s - final utype = systemCType(outputWidth); - return '$utype(' - '$a ' - '? $utype(-1) ' - ': $utype(0))'; - } - // Multi-bit replicate: concat N copies - final copies = List.filled(numReps, a); - return '(${copies.join(', ')})'; - } - - // ── Swizzle (concatenation) ── - if (m is Swizzle) { - // SystemC concatenation: (sig1, sig2, sig3) - // bool operands must be cast to sc_uint<1> to use SystemC concat - // (otherwise C++ comma operator is invoked instead) - final modInputs = (m as Module).inputs.values.toList(); - final exprList = []; - var i = 0; - for (final expr in inputs.values) { - final w = modInputs[i].width; - if (w == 0) { - i++; - continue; // skip zero-width padding - } - // Wrap 1-bit (bool) operands in sc_uint<1>() for concat - if (w == 1) { - exprList.add('sc_uint<1>($expr)'); - } else { - exprList.add(expr); - } - i++; - } - if (exprList.length == 1) { - return exprList.first; - } - // Swizzle stores inputs LSB-first (in0=LSB), but SystemC concat - // is MSB-first: (msb, ..., lsb). So reverse. - return '(${exprList.reversed.join(', ')})'; - } - - // Fallback: use SV inline (may not be valid C++ — flag for review) - return '/* TODO: ${m.runtimeType} */ ${m.inlineVerilog(inputs)}'; - } + String _gateExpression(InlineSystemVerilog m, Map inputs) => + _leafEmitter.expressionFor(m, inputs); // ──────────────────────────────────────────────────────────────────── // Clock / trigger edge resolution @@ -848,17 +666,20 @@ class SystemCSynthesisResult extends SynthesisResult { setupBuf.writeln(' sensitive << $sig;'); } - // Build maps keyed by port name (what verilogContents expects) + 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() {'); - for (final c in m.conditionals) { - bodyBuf.write(_conditionalToSC(c, 2, inputsMap, outputsMap)); - } bodyBuf + ..writeln(' void $name() {') + ..write(_emitSystemCProcessBody( + processPlan, + 2, + inputsMap, + outputsMap, + )) ..writeln(' }') ..writeln(); ssmi.clearInstantiation(); @@ -958,11 +779,13 @@ class SystemCSynthesisResult extends SynthesisResult { for (final outName in outputsMap.values) { group.resetLines.add(' $outName = 0;'); } - final condBuf = StringBuffer(); - for (final c in m.conditionals) { - condBuf.write(_conditionalToSC(c, 3, inputsMap, outputsMap)); - } - group.whileBodyLines.add(condBuf.toString()); + 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 @@ -1335,84 +1158,28 @@ class SystemCSynthesisResult extends SynthesisResult { // Conditional → SystemC // ──────────────────────────────────────────────────────────────────── - String _conditionalToSC(Conditional conditional, int indent, - Map inputsMap, Map outputsMap) { - final padding = ' ' * indent; - - if (conditional is ConditionalAssign) { - final driverExpr = _resolveDriver(conditional.driver, inputsMap); - final receiver = _resolveReceiver(conditional.receiver, outputsMap); - return '$padding$receiver = $driverExpr;\n'; - } else if (conditional is If) { - return _ifToSC(conditional, indent, inputsMap, outputsMap); - } else if (conditional is Case) { - return _caseToSC(conditional, indent, inputsMap, outputsMap); - } else if (conditional is ConditionalGroup) { - final buf = StringBuffer(); - for (final c in conditional.conditionals) { - buf.write(_conditionalToSC(c, indent, inputsMap, outputsMap)); - } - return buf.toString(); - } - return ''; - } - - String _ifToSC(If ifBlock, int indent, Map inputsMap, - Map outputsMap) { - final padding = ' ' * indent; - final buf = StringBuffer(); - - for (final iff in ifBlock.iffs) { - final header = iff == ifBlock.iffs.first - ? 'if' - : iff is Else - ? ' else' - : ' else if'; - final condition = - iff is! Else ? ' (${_resolveDriver(iff.condition, inputsMap)})' : ''; - buf.write('$padding$header$condition {\n'); - for (final c in iff.then) { - buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); - } - buf.write('$padding}'); - } - buf.writeln(); - return buf.toString(); - } - - String _caseToSC(Case caseBlock, int indent, Map inputsMap, - Map outputsMap) { - final padding = ' ' * indent; - final buf = StringBuffer(); - final expr = _resolveDriver(caseBlock.expression, inputsMap); - - // Check if all case items have compile-time constant values - final allConst = - caseBlock.items.every((item) => _isConstCaseItem(item.value)); - - // CaseZ requires mask matching — always use if/else - // Non-const case items also require if/else - if (caseBlock is CaseZ || !allConst) { - return _caseToIfElseSC(caseBlock, indent, inputsMap, outputsMap, expr); - } - - buf.writeln('${padding}switch ($expr) {'); - for (final item in caseBlock.items) { - buf.writeln('$padding case ${_constLit(item.value)}:'); - for (final c in item.then) { - buf.write(_conditionalToSC(c, indent + 2, inputsMap, outputsMap)); - } - buf.writeln('$padding break;'); - } - if (caseBlock.defaultItem != null) { - buf.writeln('$padding default:'); - for (final c in caseBlock.defaultItem!) { - buf.write(_conditionalToSC(c, indent + 2, inputsMap, outputsMap)); - } - buf.writeln('$padding break;'); - } - buf.writeln('$padding}'); - return buf.toString(); + 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. @@ -1436,39 +1203,6 @@ class SystemCSynthesisResult extends SynthesisResult { return true; // int, string, etc. } - /// Converts a Case/CaseZ block to if/else chain (for non-const items - /// or CaseZ with z-masks). - String _caseToIfElseSC( - Case caseBlock, - int indent, - Map inputsMap, - Map outputsMap, - String expr) { - final padding = ' ' * indent; - final buf = StringBuffer(); - - for (var i = 0; i < caseBlock.items.length; i++) { - final item = caseBlock.items[i]; - final condition = _caseItemCondition(item.value, expr, inputsMap, - isCaseZ: caseBlock is CaseZ); - final header = i == 0 ? 'if' : ' else if'; - buf.write('$padding$header ($condition) {\n'); - for (final c in item.then) { - buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); - } - buf.write('$padding}'); - } - if (caseBlock.defaultItem != null) { - buf.write(' else {\n'); - for (final c in caseBlock.defaultItem!) { - buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); - } - buf.write('$padding}'); - } - buf.writeln(); - return buf.toString(); - } - /// Generates the condition expression for a case item comparison. String _caseItemCondition( dynamic value, String expr, Map inputsMap, 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..6f3b39930 --- /dev/null +++ b/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart @@ -0,0 +1,194 @@ +// 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 planned inline SystemVerilog expressions for semantic leaf gates. +class SystemVerilogLeafEmitter implements InlineLeafEmitter { + /// Creates a leaf emitter. + const SystemVerilogLeafEmitter(); + + @override + String expressionFor(InlineSystemVerilog module, Map inputs) { + final plan = LeafExpressionPlan.fromInlineModule(module, inputs); + + final op = plan.operation; + final vals = plan.inputValues; + + if (op == LeafOperationKind.not && vals.length == 1) { + final outputWidth = plan.meta('outputWidth') ?? + plan.sourceModule.outputs.values.first.width; + return outputWidth == 1 ? '!${vals[0]}' : '~${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: '>>', + LeafOperationKind.arithmeticShiftRight: '>>>', + }; + final binaryOp = binaryOps[op]; + if (binaryOp != null && vals.length >= 2) { + return '${vals[0]} $binaryOp ${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) { + return plan.legacySystemVerilogExpression(); + } + + final a = vals[0]; + if (inputWidth == 1) { + return a; + } + 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'; + } + + 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 && vals.isNotEmpty) { + final inputWidths = plan.meta>('inputWidths'); + final inputCount = plan.meta('inputCount'); + if (inputWidths == null || inputCount == null) { + return plan.legacySystemVerilogExpression(); + } + + if (vals.length != inputCount && vals.length != inputCount + 1) { + return plan.legacySystemVerilogExpression(); + } + + final filtered = <({String expression, int width})>[]; + for (var i = 0; i < inputWidths.length && i < vals.length; i++) { + final width = inputWidths[i]; + if (width > 0) { + filtered.add((expression: vals[i], width: width)); + } + } + + if (filtered.isEmpty) { + return plan.legacySystemVerilogExpression(); + } + if (filtered.length == 1) { + return filtered.single.expression; + } + + final outWidth = filtered.fold(0, (sum, entry) => sum + entry.width); + final widthDescriptions = <({int upper, int? lower})>[]; + var upperIndex = outWidth - 1; + for (final entry in filtered) { + 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 < filtered.length; i++) { + final entry = filtered[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}'; + } + + // Fallback keeps behavior stable while migration is incremental. + return plan.legacySystemVerilogExpression(); + } +} diff --git a/lib/src/synthesizers/systemverilog/systemverilog_mixins.dart b/lib/src/synthesizers/systemverilog/systemverilog_mixins.dart index 20de51535..4f66887af 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_mixins.dart +++ b/lib/src/synthesizers/systemverilog/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. /// @@ -124,6 +162,10 @@ 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 { + @override + BackendArtifact? artifactFor(BackendArtifactContext context) => + _systemVerilogArtifactFor(this, context); + /// Generates custom SystemVerilog to be injected in place of the output /// port's corresponding signal name. /// 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..484185606 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. @@ -205,7 +211,11 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { @override SynthSubModuleInstantiation createSubModuleInstantiation(Module m) => - SystemVerilogSynthSubModuleInstantiation(m); + SystemVerilogSynthSubModuleInstantiation( + m, + useLeafExpressionPlanForInlineRendering: + configuration.useLeafExpressionPlanForInlineRendering, + ); /// Creates a new [_NetConnect] module to synthesize assignment between two /// [LogicNet]s. 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..c4ed50a93 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart @@ -9,45 +9,38 @@ 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(); + + /// Whether inline expressions should be rendered using [LeafExpressionPlan]. + final bool useLeafExpressionPlanForInlineRendering; /// Creates a new [SystemVerilogSynthSubModuleInstantiation] for the given /// [module]. - SystemVerilogSynthSubModuleInstantiation(super.module); + SystemVerilogSynthSubModuleInstantiation( + super.module, { + this.useLeafExpressionPlanForInlineRendering = false, + }); /// Mapping from [SynthLogic]s which are outputs of inlineable SV to those /// inlineable modules. Map? synthLogicToInlineableSynthSubmoduleMap; - /// Provides a mapping from ports of this module to a string that can be fed - /// into that port, which may include inline 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]. String inlineVerilog() { - final portNameToValueMapping = _modulePortsMapWithInline( + final portNameToValueMapping = modulePortsMapWithInline( {...inputMapping, ...inOutMapping} ..remove((module as InlineSystemVerilog).resultSignalName), + synthLogicToInlineableSynthSubmoduleMap, + (submodule) => submodule.inlineVerilog(), ); assert( @@ -57,8 +50,12 @@ class SystemVerilogSynthSubModuleInstantiation '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 = useLeafExpressionPlanForInlineRendering + ? _leafEmitter.expressionFor( + module as InlineSystemVerilog, + portNameToValueMapping, + ) + : (module as InlineSystemVerilog).inlineVerilog(portNameToValueMapping); return '($inlineSvRepresentation)'; } @@ -72,10 +69,11 @@ class SystemVerilogSynthSubModuleInstantiation module: module, instanceType: instanceType, instanceName: name, - ports: _modulePortsMapWithInline({ + ports: modulePortsMapWithInline({ ...inputMapping, ...outputMapping, ...inOutMapping, - })); + }, synthLogicToInlineableSynthSubmoduleMap, + (submodule) => submodule.inlineVerilog())); } } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart index b86fc4d34..0c0e0ed59 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); @@ -148,7 +163,7 @@ class SystemVerilogSynthesisResult extends SynthesisResult { /// Representation of all internal net declarations in generated SV. String _verilogInternalSignals() { final declarations = []; - for (final sig in _synthModuleDefinition.internalSignals + for (final sig in _emissionPlan.internalSignals .where((e) => e.needsDeclaration) .sorted((a, b) => a.name.compareTo(b.name))) { declarations.add('${sig.definitionType()} ${sig.definitionName()};'); @@ -164,7 +179,7 @@ class SystemVerilogSynthesisResult extends SynthesisResult { ? '[$upperIndex]' : '[$upperIndex:$lowerIndex]'; - for (final assignment in _synthModuleDefinition.assignments) { + for (final assignment in _emissionPlan.assignments) { assert( !(assignment.src.isNet && assignment.dst.isNet), 'Net connections should have been implemented as' @@ -198,8 +213,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); diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index 5aea3cf58..8f4b3e714 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart @@ -57,18 +57,28 @@ 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 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/systemverilog/systemverilog_synthesizer_configuration.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart index cea6493eb..e4733f309 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart @@ -24,9 +24,17 @@ class SystemVerilogSynthesizerConfiguration { /// Whether port data types, such as `logic`, are explicit. final SystemVerilogPortType portDataType; + /// Whether inline leaf expressions are rendered via the leaf-expression + /// planner path. + /// + /// This is an opt-in migration flag for the metadata-driven inline + /// rendering path. The default keeps existing inline rendering behavior. + final bool useLeafExpressionPlanForInlineRendering; + /// Creates a new configuration for SystemVerilog synthesis. const SystemVerilogSynthesizerConfiguration({ this.portObjectType = SystemVerilogPortType.explicit, this.portDataType = SystemVerilogPortType.explicit, + this.useLeafExpressionPlanForInlineRendering = false, }); } 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/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_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 1eccf9da9..c05358d6a 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -114,6 +114,37 @@ class SynthSubModuleInstantiation { bool get needsInstantiation => _needsInstantiation; bool _needsInstantiation = true; + /// If [module] is [InlineSystemVerilog], this is the [SynthLogic] mapped + /// from its [InlineSystemVerilog.resultSignalName]. + SynthLogic? get inlineResultLogic { + final m = module; + if (m is! InlineSystemVerilog) { + 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( + Map plainPorts, + Map? synthLogicToInlineableSynthSubmoduleMap, + String Function(T subModuleInstantiation) inlineExpressionFor, + ) => + plainPorts.map((name, synthLogic) { + final inlineSubModule = + synthLogicToInlineableSynthSubmoduleMap?[synthLogic]; + if (inlineSubModule != null) { + return MapEntry(name, inlineExpressionFor(inlineSubModule)); + } + + // Cleared declarations map to empty port connections. + return MapEntry( + name, synthLogic.declarationCleared ? '' : synthLogic.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 5bda4c4ba..7f706e55c 100644 --- a/lib/src/synthesizers/utilities/utilities.dart +++ b/lib/src/synthesizers/utilities/utilities.dart @@ -7,10 +7,15 @@ // 2026 July // Author: Desmond A. Kirkpatrick +export 'backend_artifact.dart'; +export 'conditional_emission_plan.dart'; +export 'conditional_emitter.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/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/leaf_backend_conformance_test.dart b/test/leaf_backend_conformance_test.dart new file mode 100644 index 000000000..bde52a855 --- /dev/null +++ b/test/leaf_backend_conformance_test.dart @@ -0,0 +1,333 @@ +// 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/utilities/leaf_cell_spec.dart'; +import 'package:test/test.dart'; + +import 'synth_test_helpers.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; synthesis path uses + // inlineVerilog when this module is inlined. + 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( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + 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('SystemC rejects unknown SystemVerilog-only inline module', () async { + final emitter = SystemCLeafEmitter( + typeForWidth: (width) => + width <= 64 ? 'sc_uint<$width>' : 'sc_biguint<$width>', + ); + + final unknown = _InlineUnknownNand( + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + ); + expect( + () => emitter.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(); + + final baseline = mod.generateSynth(); + final planned = mod.generateSynth( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + expect(baseline, contains('~(a & b)')); + expect(planned, contains('~(a & b)')); + expect( + normalizeSynthHeader(planned), + equals(normalizeSynthHeader(baseline)), + ); + }); + + test('unknown inline module is invariant across planner option states', + () async { + final mod = _BackendFallbackModule( + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + ); + await mod.build(); + + final defaultSynth = mod.generateSynth(); + final explicitFalseConfig = SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: [false].single, + ); + final explicitFalse = mod.generateSynth( + configuration: explicitFalseConfig, + ); + final optIn = mod.generateSynth( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + expect( + normalizeSynthHeader(defaultSynth), + equals(normalizeSynthHeader(explicitFalse)), + ); + expect( + normalizeSynthHeader(optIn), + equals(normalizeSynthHeader(defaultSynth)), + ); + + expect(defaultSynth, contains('~(a & b)')); + expect(explicitFalse, contains('~(a & b)')); + expect(optIn, contains('~(a & b)')); + }); + + test('SystemC rejects unknown inline module matrix', () { + final emitter = SystemCLeafEmitter( + typeForWidth: (width) => + width <= 64 ? 'sc_uint<$width>' : 'sc_biguint<$width>', + ); + + final scenarios = <({ + InlineSystemVerilog 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/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/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/systemverilog_leaf_plan_option_test.dart b/test/systemverilog_leaf_plan_option_test.dart new file mode 100644 index 000000000..c4d0e53ac --- /dev/null +++ b/test/systemverilog_leaf_plan_option_test.dart @@ -0,0 +1,584 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemverilog_leaf_plan_option_test.dart +// Tests for opt-in SystemVerilog leaf expression plan rendering. +// +// 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 opt-in', () 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( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + 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('opt-in 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( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + 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('opt-in 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( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + 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('opt-in 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( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + 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('opt-in 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( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + expect(baseline, contains('assign y_swizzle_zero = a;')); + expect(planned, contains('assign y_swizzle_zero = a;')); + expect( + normalizeSynthHeader(planned), + equals(normalizeSynthHeader(baseline)), + ); + }); + + test('opt-in 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( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + 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('opt-in 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( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + 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('opt-in 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( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + 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('opt-in 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( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + 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('opt-in 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( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + 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('opt-in 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( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + 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('default option matches explicit false and opt-in parity on 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 defaultSynth = mod.generateSynth(); + final explicitFalseConfiguration = SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: [false].single, + ); + final explicitFalse = + mod.generateSynth(configuration: explicitFalseConfiguration); + final optIn = mod.generateSynth( + configuration: const SystemVerilogSynthesizerConfiguration( + useLeafExpressionPlanForInlineRendering: true, + ), + ); + + expect( + normalizeSynthHeader(defaultSynth), + equals(normalizeSynthHeader(explicitFalse)), + ); + expect( + normalizeSynthHeader(optIn), + equals(normalizeSynthHeader(defaultSynth)), + ); + + expect(defaultSynth, contains('assign y_and = a & b;')); + expect(defaultSynth, contains('assign y_mux = control ? a : b;')); + expect(defaultSynth, contains('assign y_pow = {a ** b};')); + expect(defaultSynth, contains('assign y_idx = a[idx];')); + }); +} From 7af89ed4b37c950d91ca540fa3b04d92b94ebfd9 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 20 Jul 2026 09:53:56 -0700 Subject: [PATCH 10/14] Add header to synthesizer exports --- lib/src/synthesizers/synthesizers.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/synthesizers/synthesizers.dart b/lib/src/synthesizers/synthesizers.dart index 47bedd8a5..ab22bfb26 100644 --- a/lib/src/synthesizers/synthesizers.dart +++ b/lib/src/synthesizers/synthesizers.dart @@ -1,5 +1,11 @@ // Copyright (C) 2021-2025 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'; From abff4130cce02dae346254af81a90ce622c75bcf Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 20 Jul 2026 16:41:04 -0700 Subject: [PATCH 11/14] make leaf processing symmetric among languages --- lib/src/modules/bus.dart | 214 +-- lib/src/modules/gates.dart | 203 +-- lib/src/synthesizers/synthesizers.dart | 3 +- lib/src/synthesizers/systemc/systemc.dart | 3 +- .../systemc/systemc_leaf_emitter.dart | 2 +- ...ystemc_synth_sub_module_instantiation.dart | 6 +- .../systemc/systemc_synthesis_result.dart | 15 +- .../systemverilog_leaf_emitter.dart | 156 ++- .../systemverilog/systemverilog_mixins.dart | 7 +- ...systemverilog_synth_module_definition.dart | 34 +- ...erilog_synth_sub_module_instantiation.dart | 44 +- .../systemverilog_synthesizer.dart | 26 +- ...stemverilog_synthesizer_configuration.dart | 8 - .../synthesizers/utilities/inline_leaf.dart | 36 + .../utilities/inline_leaf_emitter.dart | 14 +- .../utilities/leaf_cell_spec_inference.dart | 29 +- .../utilities/leaf_expression_plan.dart | 11 +- .../synthesizers/utilities/synth_logic.dart | 3 + .../utilities/synth_module_definition.dart | 31 +- .../synth_sub_module_instantiation.dart | 6 +- lib/src/synthesizers/utilities/utilities.dart | 3 +- lib/src/utilities/simcompare.dart | 1163 ++--------------- lib/src/utilities/systemc_simcompare.dart | 841 ++++++++++++ .../utilities/systemverilog_simcompare.dart | 380 ++++++ test/bus_test.dart | 6 +- test/leaf_backend_conformance_test.dart | 72 +- test/leaf_cell_spec_inference_test.dart | 14 +- test/leaf_expression_plan_test.dart | 8 +- test/leaf_test_module_factories.dart | 4 +- test/systemverilog_leaf_plan_option_test.dart | 122 +- 30 files changed, 1783 insertions(+), 1681 deletions(-) create mode 100644 lib/src/synthesizers/utilities/inline_leaf.dart create mode 100644 lib/src/utilities/systemc_simcompare.dart create mode 100644 lib/src/utilities/systemverilog_simcompare.dart 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/gates.dart b/lib/src/modules/gates.dart index 2780cde9d..703442fe0 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 @@ -821,7 +727,7 @@ Logic mux(Logic control, Logic d1, Logic d0) => Mux(control, d1, d0).out; /// /// 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; @@ -901,22 +807,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; @@ -973,20 +869,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]. @@ -997,7 +879,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; @@ -1071,13 +953,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 ab22bfb26..7eff9bdf9 100644 --- a/lib/src/synthesizers/synthesizers.dart +++ b/lib/src/synthesizers/synthesizers.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synthesizers.dart @@ -14,3 +14,4 @@ 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 index fc7a188e4..07bf3adb6 100644 --- a/lib/src/synthesizers/systemc/systemc.dart +++ b/lib/src/synthesizers/systemc/systemc.dart @@ -20,7 +20,8 @@ class SystemCSynthesizer extends Synthesizer { @override bool generatesDefinition(Module module) => // ignore: deprecated_member_use_from_same_package - !((module is CustomSystemVerilog) || + !((module is InlineLeaf) || + (module is CustomSystemVerilog) || (module is SystemVerilog && module.generatedDefinitionType == DefinitionGenerationType.none)); diff --git a/lib/src/synthesizers/systemc/systemc_leaf_emitter.dart b/lib/src/synthesizers/systemc/systemc_leaf_emitter.dart index f4706a240..2b33b1e39 100644 --- a/lib/src/synthesizers/systemc/systemc_leaf_emitter.dart +++ b/lib/src/synthesizers/systemc/systemc_leaf_emitter.dart @@ -22,7 +22,7 @@ class SystemCLeafEmitter implements InlineLeafEmitter { /// /// [inputs] maps module input port names to SystemC read expressions. @override - String expressionFor(InlineSystemVerilog m, Map inputs) { + String expressionFor(InlineLeaf m, Map inputs) { final plan = LeafExpressionPlan.fromInlineModule(m, inputs); final op = plan.operation; diff --git a/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart index fa580252e..9a0287234 100644 --- a/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart @@ -40,11 +40,11 @@ class SystemCSynthSubModuleInstantiation extends SynthSubModuleInstantiation { /// Provides the inline SystemC expression for this module. /// - /// Should only be called if [module] is [InlineSystemVerilog]. + /// Should only be called if [module] is [InlineLeaf]. String inlineSystemC() { final portNameToValueMapping = modulePortsMapWithInline( {...inputMapping, ...inOutMapping} - ..remove((module as InlineSystemVerilog).resultSignalName), + ..remove((module as InlineLeaf).resultSignalName), synthLogicToInlineableSynthSubmoduleMap, (submodule) => submodule.inlineSystemC(), ); @@ -59,7 +59,7 @@ class SystemCSynthSubModuleInstantiation extends SynthSubModuleInstantiation { String _inlineSystemCExpression(Map inputs) { final m = module; - if (m is InlineSystemVerilog) { + if (m is InlineLeaf) { return leafEmitter.expressionFor(m, inputs); } diff --git a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart index 355afab04..fd89fbb07 100644 --- a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart +++ b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart @@ -407,7 +407,7 @@ class SystemCSynthesisResult extends SynthesisResult { /// definition and should be inlined (like Add). static bool _isInlinableSystemVerilogGate(Module m) => m is SystemVerilog && - m is! InlineSystemVerilog && + m is! InlineLeaf && m is! Always && m is! FlipFlop && m.generatedDefinitionType == DefinitionGenerationType.none; @@ -503,8 +503,7 @@ class SystemCSynthesisResult extends SynthesisResult { final inlineGates = _synthModuleDefinition.subModuleInstantiations .where((s) => s.needsInstantiation && - (s.module is InlineSystemVerilog || - _isInlinableSystemVerilogGate(s.module))) + (s.module is InlineLeaf || _isInlinableSystemVerilogGate(s.module))) .cast() .toList(); @@ -525,7 +524,7 @@ class SystemCSynthesisResult extends SynthesisResult { // inouts, so include non-result inout mappings as inputs. final inputExprs = {}; final inputMappings = {...ssmi.inputMapping, ...ssmi.inOutMapping}; - if (m is InlineSystemVerilog) { + if (m is InlineLeaf) { inputMappings.remove(m.resultSignalName); } for (final entry in inputMappings.entries) { @@ -536,9 +535,9 @@ class SystemCSynthesisResult extends SynthesisResult { inputExprs[entry.key] = _synthLogicReadExpr(sl); } - if (m is InlineSystemVerilog) { + if (m is InlineLeaf) { final resultSynthLogic = ssmi.inlineResultLogic; - if (resultSynthLogic == null) { + if (resultSynthLogic == null || !resultSynthLogic.hasName) { continue; } final expr = _gateExpression(m, inputExprs); @@ -592,7 +591,7 @@ class SystemCSynthesisResult extends SynthesisResult { /// /// Handles all gate types that have SV-specific syntax which needs /// translation to valid SystemC/C++. - String _gateExpression(InlineSystemVerilog m, Map inputs) => + String _gateExpression(InlineLeaf m, Map inputs) => _leafEmitter.expressionFor(m, inputs); // ──────────────────────────────────────────────────────────────────── @@ -963,7 +962,7 @@ class SystemCSynthesisResult extends SynthesisResult { /// instantiation) — i.e. it is an inline gate, Always, FlipFlop, or clock. static bool _isHandledInline(SystemCSynthSubModuleInstantiation ssmi) => !ssmi.needsInstantiation || - ssmi.module is InlineSystemVerilog || + ssmi.module is InlineLeaf || ssmi.module is Always || ssmi.module is FlipFlop || ssmi.module is SimpleClockGenerator || diff --git a/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart b/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart index 6f3b39930..0d630f993 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart @@ -10,13 +10,93 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; -/// Emits planned inline SystemVerilog expressions for semantic leaf gates. +/// 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 firstSelect = first.width == 1 && first.canCollapse + ? _singleBitSelect(first.expression) + : (target: null, index: null); + + if (firstSelect.target == null || firstSelect.index == null) { + collapsed.add((expression: first.expression, width: first.width)); + index++; + continue; + } + + var lastIndex = index; + var expectedBit = firstSelect.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 != firstSelect.target || + nextSelect.index != expectedBit) { + break; + } + lastIndex++; + expectedBit--; + } + + if (lastIndex == index) { + collapsed.add(( + expression: '${firstSelect.target}[${firstSelect.index}]', + width: 1, + )); + } else { + final lowerSelect = _singleBitSelect(operands[lastIndex].expression); + collapsed.add(( + expression: + '${firstSelect.target}[${firstSelect.index}:${lowerSelect.index}]', + width: lastIndex - index + 1, + )); + } + + index = lastIndex + 1; + } + + return collapsed; + } + @override - String expressionFor(InlineSystemVerilog module, Map inputs) { + String expressionFor(InlineLeaf module, Map inputs) { final plan = LeafExpressionPlan.fromInlineModule(module, inputs); final op = plan.operation; @@ -44,13 +124,16 @@ class SystemVerilogLeafEmitter implements InlineLeafEmitter { LeafOperationKind.greaterThanOrEqual: '>=', LeafOperationKind.shiftLeft: '<<', LeafOperationKind.shiftRight: '>>', - LeafOperationKind.arithmeticShiftRight: '>>>', }; 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]}'; } @@ -77,7 +160,10 @@ class SystemVerilogLeafEmitter implements InlineLeafEmitter { final startIndex = plan.meta('startIndex'); final endIndex = plan.meta('endIndex'); if (startIndex == null || endIndex == null) { - return plan.legacySystemVerilogExpression(); + throw SynthException( + 'SystemVerilog bus subset leaf requires startIndex and endIndex ' + 'metadata.', + ); } final a = vals[0]; @@ -87,7 +173,7 @@ class SystemVerilogLeafEmitter implements InlineLeafEmitter { if (startIndex > endIndex) { final swizzleContents = List.generate( startIndex - endIndex + 1, - (i) => '$a[${endIndex + i}]', + (i) => _bitSelect(a, endIndex + i), ).join(','); return '{$swizzleContents}'; } @@ -116,33 +202,62 @@ class SystemVerilogLeafEmitter implements InlineLeafEmitter { if (op == LeafOperationKind.swizzle && vals.isNotEmpty) { 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) { - return plan.legacySystemVerilogExpression(); + throw SynthException( + 'SystemVerilog swizzle leaf requires inputWidths and inputCount ' + 'metadata.', + ); } if (vals.length != inputCount && vals.length != inputCount + 1) { - return plan.legacySystemVerilogExpression(); + throw SynthException( + 'SystemVerilog swizzle leaf expected $inputCount inputs, but saw ' + '${vals.length}.', + ); } - final filtered = <({String expression, int width})>[]; - for (var i = 0; i < inputWidths.length && i < vals.length; i++) { + 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) { - filtered.add((expression: vals[i], width: width)); + final isArrayMember = + i < inputIsArrayMember.length && inputIsArrayMember[i]; + final hasUnpackedArraySource = + i < inputHasUnpackedArraySource.length && + inputHasUnpackedArraySource[i]; + filtered.add(( + canCollapse: !isArrayMember && !hasUnpackedArraySource, + expression: inputExpressions[i], + width: width, + )); } } if (filtered.isEmpty) { - return plan.legacySystemVerilogExpression(); + throw SynthException( + 'SystemVerilog swizzle leaf requires at least one non-zero-width ' + 'input.', + ); } - if (filtered.length == 1) { - return filtered.single.expression; + + final operands = _collapseBitSelects(filtered); + if (operands.length == 1) { + return operands.single.expression; } - final outWidth = filtered.fold(0, (sum, entry) => sum + entry.width); + final outWidth = operands.fold(0, (sum, entry) => sum + entry.width); final widthDescriptions = <({int upper, int? lower})>[]; var upperIndex = outWidth - 1; - for (final entry in filtered) { + for (final entry in operands) { if (entry.width > 1) { final lowerIndex = upperIndex - entry.width + 1; widthDescriptions.add((upper: upperIndex, lower: lowerIndex)); @@ -169,8 +284,8 @@ class SystemVerilogLeafEmitter implements InlineLeafEmitter { final inputLines = []; var lineUpper = outWidth - 1; - for (var i = 0; i < filtered.length; i++) { - final entry = filtered[i]; + for (var i = 0; i < operands.length; i++) { + final entry = operands[i]; final desc = widthDescriptions[i]; final alignedDesc = desc.lower != null @@ -188,7 +303,10 @@ class SystemVerilogLeafEmitter implements InlineLeafEmitter { return '{\n${inputLines.join('\n')}\n}'; } - // Fallback keeps behavior stable while migration is incremental. - return plan.legacySystemVerilogExpression(); + 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 4f66887af..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 @@ -161,7 +161,7 @@ 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); @@ -184,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,' @@ -212,6 +213,7 @@ mixin InlineSystemVerilog on Module implements SystemVerilog { return 'assign $result = $inline; // $instanceName'; } + @override @override @protected final List expressionlessInputs = const []; @@ -228,6 +230,7 @@ mixin InlineSystemVerilog on Module implements SystemVerilog { @internal @override + @override bool get isWiresOnly => false; } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 484185606..5ab6a7add 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -63,7 +63,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(( @@ -75,7 +75,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); } @@ -109,7 +109,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 @@ -211,11 +215,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { @override SynthSubModuleInstantiation createSubModuleInstantiation(Module m) => - SystemVerilogSynthSubModuleInstantiation( - m, - useLeafExpressionPlanForInlineRendering: - configuration.useLeafExpressionPlanForInlineRendering, - ); + SystemVerilogSynthSubModuleInstantiation(m); /// Creates a new [_NetConnect] module to synthesize assignment between two /// [LogicNet]s. @@ -404,6 +404,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; @@ -884,6 +888,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; @@ -1116,7 +1124,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; @@ -1396,13 +1404,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, @@ -1416,8 +1424,8 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { subModuleInstantiation.clearInstantiation(); - final resultName = (subModuleInstantiation.module as InlineSystemVerilog) - .resultSignalName; + final resultName = + (subModuleInstantiation.module as InlineLeaf).resultSignalName; final subModResult = subModuleInstantiation.inOutMapping[resultName]!; 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 c4ed50a93..5a28854b1 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 @@ -17,15 +17,9 @@ class SystemVerilogSynthSubModuleInstantiation extends SynthSubModuleInstantiation { static const _leafEmitter = SystemVerilogLeafEmitter(); - /// Whether inline expressions should be rendered using [LeafExpressionPlan]. - final bool useLeafExpressionPlanForInlineRendering; - /// Creates a new [SystemVerilogSynthSubModuleInstantiation] for the given /// [module]. - SystemVerilogSynthSubModuleInstantiation( - super.module, { - this.useLeafExpressionPlanForInlineRendering = false, - }); + SystemVerilogSynthSubModuleInstantiation(super.module); /// Mapping from [SynthLogic]s which are outputs of inlineable SV to those /// inlineable modules. @@ -34,11 +28,11 @@ class SystemVerilogSynthSubModuleInstantiation /// 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( {...inputMapping, ...inOutMapping} - ..remove((module as InlineSystemVerilog).resultSignalName), + ..remove((module as InlineLeaf).resultSignalName), synthLogicToInlineableSynthSubmoduleMap, (submodule) => submodule.inlineVerilog(), ); @@ -50,12 +44,10 @@ class SystemVerilogSynthSubModuleInstantiation 'Inline modules should not ever receive empty port values,' ' only module instantiations can get something like `.port_name()`.'); - final inlineSvRepresentation = useLeafExpressionPlanForInlineRendering - ? _leafEmitter.expressionFor( - module as InlineSystemVerilog, - portNameToValueMapping, - ) - : (module as InlineSystemVerilog).inlineVerilog(portNameToValueMapping); + final inlineSvRepresentation = _leafEmitter.expressionFor( + module as InlineLeaf, + portNameToValueMapping, + ); return '($inlineSvRepresentation)'; } @@ -65,15 +57,23 @@ class SystemVerilogSynthSubModuleInstantiation if (!needsInstantiation) { return null; } + final ports = modulePortsMapWithInline({ + ...inputMapping, + ...outputMapping, + ...inOutMapping, + }, synthLogicToInlineableSynthSubmoduleMap, + (submodule) => submodule.inlineVerilog()); + + if (module is InlineLeaf) { + final resultName = (module as InlineLeaf).resultSignalName; + if ((ports[resultName] ?? '').isEmpty) { + return null; + } + } return SystemVerilogSynthesizer.instantiationVerilogFor( module: module, instanceType: instanceType, instanceName: name, - ports: modulePortsMapWithInline({ - ...inputMapping, - ...outputMapping, - ...inOutMapping, - }, synthLogicToInlineableSynthSubmoduleMap, - (submodule) => submodule.inlineVerilog())); + ports: ports); } } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index 8f4b3e714..f9bfb8e8f 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 @@ -26,7 +27,8 @@ class SystemVerilogSynthesizer extends Synthesizer { @override bool generatesDefinition(Module module) => // ignore: deprecated_member_use_from_same_package - !((module is CustomSystemVerilog) || + !((module is InlineLeaf) || + (module is CustomSystemVerilog) || (module is SystemVerilog && module.generatedDefinitionType == DefinitionGenerationType.none)); @@ -57,6 +59,28 @@ class SystemVerilogSynthesizer extends Synthesizer { Map? parameters, bool forceStandardInstantiation = false}) { if (!forceStandardInstantiation) { + 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( diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart index e4733f309..cea6493eb 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart @@ -24,17 +24,9 @@ class SystemVerilogSynthesizerConfiguration { /// Whether port data types, such as `logic`, are explicit. final SystemVerilogPortType portDataType; - /// Whether inline leaf expressions are rendered via the leaf-expression - /// planner path. - /// - /// This is an opt-in migration flag for the metadata-driven inline - /// rendering path. The default keeps existing inline rendering behavior. - final bool useLeafExpressionPlanForInlineRendering; - /// Creates a new configuration for SystemVerilog synthesis. const SystemVerilogSynthesizerConfiguration({ this.portObjectType = SystemVerilogPortType.explicit, this.portDataType = SystemVerilogPortType.explicit, - this.useLeafExpressionPlanForInlineRendering = false, }); } 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 index 4249bda48..90f0cab3d 100644 --- a/lib/src/synthesizers/utilities/inline_leaf_emitter.dart +++ b/lib/src/synthesizers/utilities/inline_leaf_emitter.dart @@ -15,21 +15,9 @@ class InlineLeafEmitter { const InlineLeafEmitter(); /// Emits a backend-specific expression for [module] given [inputs]. - String expressionFor(InlineSystemVerilog module, Map inputs) { + String expressionFor(InlineLeaf module, Map inputs) { throw UnimplementedError( 'InlineLeafEmitter.expressionFor must be implemented by subclasses.', ); } } - -/// Minimal passthrough implementation for backends without dedicated leaf -/// rendering logic. -class PassthroughInlineLeafEmitter implements InlineLeafEmitter { - /// Creates a passthrough inline leaf emitter. - const PassthroughInlineLeafEmitter(); - - @override - String expressionFor( - InlineSystemVerilog module, Map inputs) => - module.inlineVerilog(inputs); -} diff --git a/lib/src/synthesizers/utilities/leaf_cell_spec_inference.dart b/lib/src/synthesizers/utilities/leaf_cell_spec_inference.dart index 4dc0dd393..3fb87e578 100644 --- a/lib/src/synthesizers/utilities/leaf_cell_spec_inference.dart +++ b/lib/src/synthesizers/utilities/leaf_cell_spec_inference.dart @@ -15,7 +15,7 @@ import 'package:rohd/src/synthesizers/utilities/leaf_cell_spec.dart'; /// This bridges current type-based inline modules into backend-neutral leaf /// metadata without requiring those modules to implement [LeafCellProvider] /// immediately. -LeafCellSpec? leafCellSpecForInlineModule(InlineSystemVerilog module) { +LeafCellSpec? leafCellSpecForInlineModule(InlineLeaf module) { if (module is LeafCellProvider) { return (module as LeafCellProvider).leafCellSpec; } @@ -162,12 +162,19 @@ LeafCellSpec? leafCellSpecForInlineModule(InlineSystemVerilog module) { } if (module is Swizzle) { + final inputPorts = { + ...module.inputs, + ...module.inOuts, + }..remove(module.resultSignalName); return LeafCellSpec( operation: LeafOperationKind.swizzle, metadata: { - 'inputCount': module.inputs.length, - 'inputWidths': - module.inputs.values.map((input) => input.width).toList(), + '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(), }, ); } @@ -186,3 +193,17 @@ LeafCellSpec? leafCellSpecForInlineModule(InlineSystemVerilog module) { 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 index e30507775..cdb1c4e72 100644 --- a/lib/src/synthesizers/utilities/leaf_expression_plan.dart +++ b/lib/src/synthesizers/utilities/leaf_expression_plan.dart @@ -39,12 +39,12 @@ class LeafExpressionPlan { /// Builds a plan for [module] and [inputs]. factory LeafExpressionPlan.fromInlineModule( - InlineSystemVerilog module, + InlineLeaf module, Map inputs, ) { final spec = leafCellSpecForInlineModule(module); return LeafExpressionPlan( - sourceModule: module, + sourceModule: module as Module, operation: spec?.operation, metadata: spec?.metadata ?? const {}, inputValues: inputs.values.toList(), @@ -57,11 +57,4 @@ class LeafExpressionPlan { final value = metadata[key]; return value is T ? value : null; } - - /// Invokes the legacy SystemVerilog inline hook for this source module. - /// - /// This exists only for the staged SystemVerilog migration. Other backends - /// must emit [operation] or use an explicit backend extension. - String legacySystemVerilogExpression() => - (sourceModule as InlineSystemVerilog).inlineVerilog(inputsByPort); } 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 4fa0e8d69..76e23e214 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 @@ -666,8 +671,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 = {}; @@ -792,7 +796,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) => @@ -821,7 +832,7 @@ class SynthModuleDefinition { SynthLogic? _inlineResultLogic(SynthSubModuleInstantiation instantiation) { final subModule = instantiation.module; - if (subModule is! InlineSystemVerilog) { + if (subModule is! InlineLeaf) { return null; } @@ -1250,7 +1261,7 @@ class SynthModuleDefinition { for (final submoduleInstantiation in subModuleInstantiations) { if (!submoduleInstantiation.needsInstantiation || - submoduleInstantiation.module is InlineSystemVerilog) { + submoduleInstantiation.module is InlineLeaf) { continue; } @@ -1297,7 +1308,7 @@ class SynthModuleDefinition { for (final submoduleInstantiation in subModuleInstantiations) { if (!submoduleInstantiation.needsInstantiation || - submoduleInstantiation.module is InlineSystemVerilog) { + submoduleInstantiation.module is InlineLeaf) { continue; } @@ -2728,7 +2739,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 c05358d6a..2751ddb7c 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -114,11 +114,11 @@ class SynthSubModuleInstantiation { bool get needsInstantiation => _needsInstantiation; bool _needsInstantiation = true; - /// If [module] is [InlineSystemVerilog], this is the [SynthLogic] mapped - /// from its [InlineSystemVerilog.resultSignalName]. + /// If [module] is [InlineLeaf], this is the [SynthLogic] mapped from its + /// [InlineLeaf.resultSignalName]. SynthLogic? get inlineResultLogic { final m = module; - if (m is! InlineSystemVerilog) { + if (m is! InlineLeaf) { return null; } return outputMapping[m.resultSignalName] ?? diff --git a/lib/src/synthesizers/utilities/utilities.dart b/lib/src/synthesizers/utilities/utilities.dart index 7f706e55c..fffb315a9 100644 --- a/lib/src/synthesizers/utilities/utilities.dart +++ b/lib/src/synthesizers/utilities/utilities.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // utilities.dart @@ -10,6 +10,7 @@ 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'; diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index f0bc56c9c..8d52eebde 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -2,13 +2,12 @@ // 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'; @@ -19,6 +18,9 @@ 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. @@ -44,89 +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 @@ -210,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( @@ -234,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( @@ -262,840 +175,126 @@ 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'); - - 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; - } - - // ══════════════════════════════════════════════════════════════════════ - // SystemC simulation (Accellera SystemC) - // ══════════════════════════════════════════════════════════════════════ - - /// The default SystemC installation path (Accellera). - static const _systemCDefaultHome = '/opt/systemc/include'; - static const _systemCDefaultLib = '/opt/systemc/lib'; - - /// Cache of compiled SystemC executables keyed by generated code hash. - static final _compilationCache = {}; - - /// Prefix for SystemC artifacts owned by this test process. - static final String _systemCTempPrefix = - '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'; - } + }) => + _SystemVerilogSimCompare.iverilogVector( + module, + vectors, + moduleName: moduleName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + dumpWaves: dumpWaves, + iverilogExtraArgs: iverilogExtraArgs, + allowWarnings: allowWarnings, + maskKnownWarnings: maskKnownWarnings, + buildOnly: buildOnly, + synthesizerConfiguration: synthesizerConfiguration, + ); /// 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(_systemCTempPrefix) || name == 'Makefile_sc') { - entity.deleteSync(recursive: true); - continue; - } + static void cleanupSystemCCache({bool keepPch = true}) => + _SystemCSimCompare.cleanupSystemCCache(keepPch: keepPch); - // 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 executable. + /// Compiles a SystemC module into a reusable stdin-driven vector-testbench + /// executable. /// - /// Returns a [SystemCExecutable] that can be used to run multiple vector - /// sets without recompilation. Use in `setUpAll` for test groups. + /// 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 SystemCExecutable? buildSystemCExecutable(Module module, - {String? moduleName, - String? clockName, - String? resetName, - String? systemcHome, - String? systemcLib}) { - if (kIsWeb) { - return null; - } - - final scHome = systemcHome ?? ''; - final scLib = systemcLib ?? ''; - final (resolvedHome, resolvedLib) = _resolveSystemCPaths(scHome, scLib); - - if (resolvedHome == null || resolvedLib == null) { - print('SystemC installation not found'); - return null; - } - - final topModule = moduleName ?? module.definitionName; - final generatedSystemC = module.generateSystemC(); - - // Check compilation cache - final cacheKey = generatedSystemC.hashCode; - if (_compilationCache.containsKey(cacheKey)) { - 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() - ..writeln('#include ') - ..writeln('#include ') - ..writeln('#include ') - ..writeln('#include ') - ..writeln('#include ') - ..writeln('#include ') - ..writeln('using namespace std;') - ..writeln() - ..writeln(generatedSystemC) - ..writeln() - ..writeln('int sc_main(int argc, char* argv[]) {'); - - // Clock - for (final clkName in clockSignals) { - tb.writeln( - ' sc_clock $clkName("$clkName", ${Vector._period}, SC_NS);'); - } - - // Signals for all non-clock input ports - for (final entry in inputPorts.entries) { - if (clockSignals.contains(entry.key)) { - continue; - } - tb.writeln( - ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' - ' ${entry.key};'); - } - - // Signals for all output ports - for (final entry in outputPorts.entries) { - tb.writeln( - ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' - ' ${entry.key};'); - } - - // 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 - ..writeln() - ..writeln(' int _tb_errors = 0;') - ..writeln() - ..writeln(' // Initial offset') - ..writeln(' sc_start(sc_time(1, SC_NS));') - ..writeln() - ..writeln(' // Read number of vectors') - ..writeln(' int _tb_nvec;') - ..writeln(' cin >> _tb_nvec;') - ..writeln() - ..writeln(' for (int _tb_v = 0; _tb_v < _tb_nvec; _tb_v++) {'); - - // Read and drive each non-clock input - final drivableInputs = - inputPorts.keys.where((k) => !clockSignals.contains(k)).toList(); - for (final name in drivableInputs) { - final w = inputPorts[name]!; - if (w > 64) { - // BigInt — read as hex string - tb - ..writeln(' { string _h; cin >> _h;') - ..writeln(' sc_biguint<$w> _v(_h.c_str());') - ..writeln(' $name.write(_v); }'); - } else { - tb - ..writeln(' { uint64_t _v; cin >> _v;') - ..writeln(' $name.write(_v); }'); - } - } - 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 - ..writeln() - ..writeln(' sc_start(sc_time(${Vector._offset}, SC_NS));') - ..writeln() - ..writeln(' // Read number of outputs to check') - ..writeln(' int _tb_nchk;') - ..writeln(' cin >> _tb_nchk;') - ..writeln() - ..writeln(' for (int _tb_c = 0; _tb_c < _tb_nchk; _tb_c++) {') - ..writeln(' string _tb_pn;') - ..writeln(' cin >> _tb_pn;'); - - // Generate if-else chain for each output 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 - ..writeln(' }') - ..writeln() - ..writeln(' sc_start(sc_time(' - '${Vector._period - Vector._offset}, SC_NS));') - ..writeln(' }') - ..writeln() - ..writeln(' if (_tb_errors == 0) {') - ..writeln(' cout << "PASS" << endl;') - ..writeln(' } else {') - ..writeln(' cout << "FAIL: " << _tb_errors << " errors" << endl;') - ..writeln(' }') - ..writeln(' return _tb_errors > 0 ? 1 : 0;') - ..writeln('}'); - - final testbenchCode = tb.toString(); - - // Write and compile - const dir = 'tmp_test'; - Directory(dir).createSync(recursive: true); - final compileDir = Directory(dir) - .createTempSync('${_systemCTempPrefix}_${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; - } + 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, + ); - final exe = SystemCExecutable._( - binaryPath: tmpOutput, - cppFile: tmpCppFile, - scLib: resolvedLib, - clockSignals: clockSignals, - inputPorts: inputPorts, - outputPorts: outputPorts, - inOutPorts: inOutPorts); - _compilationCache[cacheKey] = exe; - return exe; - } + /// 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, + ); - /// Runs [vectors] against a pre-compiled [SystemCExecutable]. + /// Runs [vectors] against a pre-compiled [SystemCVectorExecutable]. /// /// Returns `true` if all vectors pass. - static bool runSystemCVectors(SystemCExecutable 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'); - } + 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(SystemCExecutable exe, List vectors) { - expect(runSystemCVectors(exe, vectors), true); - } - - /// Converts a value to an integer for stdin. - static int _systemcIntValue(dynamic value, int width) { - if (value is int) { - return value; - } - if (value is LogicValue) { - if (!value.isValid) { - return 0; - } - return value.toBigInt().toUnsigned(width).toInt(); - } - if (value is BigInt) { - return value.toUnsigned(width).toInt(); - } - if (value is String) { - final lv = LogicValue.of(value, width: width); - if (!lv.isValid) { - return 0; - } - return lv.toBigInt().toUnsigned(width).toInt(); - } - return 0; - } - - /// 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'; - } + 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}) { - if (buildOnly) { - // Just verify SystemC code generation succeeds - module.generateSystemC(); - return; - } - final exe = buildSystemCExecutable(module, - moduleName: moduleName, - clockName: clockName, - resetName: resetName, - systemcHome: systemcHome, - systemcLib: systemcLib); - if (exe == null) { - // 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); - } + {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}) { - if (kIsWeb) { - return true; - } - final exe = buildSystemCExecutable(module, + 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); - if (exe == null) { - return false; - } - if (buildOnly) { - return true; - } - return runSystemCVectors(exe, vectors); - } - - // ══════════════════════════════════════════════════════════════════════ - // Trace-based SystemC co-simulation - // ══════════════════════════════════════════════════════════════════════ + systemcLib: systemcLib, + buildOnly: buildOnly, + ); /// Runs the ROHD simulation using [stimulus], records input/output values /// at every posedge of [clk], then replays the captured vectors through @@ -1121,134 +320,28 @@ abstract class SimCompare { /// }, /// ); /// ``` - 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, + 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); - } -} - -/// Holds the compiled state of a SystemC executable for reuse across tests. -class SystemCExecutable { - /// Path to the compiled binary. - final String binaryPath; - - /// Path to the generated C++ source. - final String cppFile; - - /// Path to the SystemC library (for LD_LIBRARY_PATH). - final String scLib; - - /// Clock signal names. - final Set clockSignals; - - /// Input port names and widths (excluding promoted clocks). - final Map inputPorts; - - /// Output port names and widths. - final Map outputPorts; - - /// Inout port names and widths. - final Map inOutPorts; - - SystemCExecutable._( - {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(SimCompare._systemCTempPrefix)) { - compileDir.deleteSync(recursive: true); - return; - } - tryDelete(cppFile); - tryDelete(binaryPath); - } on Exception catch (_) {} - } + systemcLib: systemcLib, + ); } 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/bus_test.dart b/test/bus_test.dart index d622400d1..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,12 +379,12 @@ void main() { }); group('simcompare', () { - SystemCExecutable? busSystemCExe; + SystemCVectorExecutable? busSystemCExe; setUpAll(() async { final gtm = BusTestModule(Logic(width: 8), Logic(width: 8)); await gtm.build(); - busSystemCExe = SimCompare.buildSystemCExecutable(gtm); + busSystemCExe = SimCompare.buildSystemCVectorExecutable(gtm); }); tearDownAll(SimCompare.cleanupSystemCCache); diff --git a/test/leaf_backend_conformance_test.dart b/test/leaf_backend_conformance_test.dart index bde52a855..dce98c0f4 100644 --- a/test/leaf_backend_conformance_test.dart +++ b/test/leaf_backend_conformance_test.dart @@ -9,11 +9,10 @@ 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'; -import 'synth_test_helpers.dart'; - class _BackendConformanceModule extends Module { _BackendConformanceModule(Logic a, Logic b, Logic sel, Logic idx) { a = addInput('a', a, width: 4); @@ -41,8 +40,8 @@ class _InlineUnknownNand extends Module with InlineSystemVerilog { b = addInput('b', b, width: b.width); out = addOutput('out', width: a.width); - // Functional behavior is arbitrary for this test; synthesis path uses - // inlineVerilog when this module is inlined. + // Functional behavior is arbitrary for this test; semantic leaf emission + // rejects this module because it has no backend-neutral metadata. out <= a & b; } @@ -182,11 +181,7 @@ void main() { ); await mod.build(); - final planned = mod.generateSynth( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); + final planned = mod.generateSynth(); expect(planned, contains('assign y_and = a & b;')); expect(planned, contains('assign y_mux = sel ? a : b;')); @@ -194,18 +189,29 @@ void main() { expect(planned, contains('assign y_idx = a[idx];')); }); - test('SystemC rejects unknown SystemVerilog-only inline module', () async { - final emitter = SystemCLeafEmitter( + 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( - () => emitter.expressionFor(unknown, {'a': 'a_expr', 'b': 'b_expr'}), + () => systemCEmitter.expressionFor( + unknown, + {'a': 'a_expr', 'b': 'b_expr'}, + ), + throwsA(isA()), + ); + expect( + () => systemVerilogEmitter.expressionFor( + unknown, + {'a': 'a_expr', 'b': 'b_expr'}, + ), throwsA(isA()), ); @@ -215,22 +221,13 @@ void main() { ); await mod.build(); - final baseline = mod.generateSynth(); - final planned = mod.generateSynth( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); - - expect(baseline, contains('~(a & b)')); - expect(planned, contains('~(a & b)')); expect( - normalizeSynthHeader(planned), - equals(normalizeSynthHeader(baseline)), + mod.generateSynth, + throwsA(isA()), ); }); - test('unknown inline module is invariant across planner option states', + test('unknown inline module is rejected by SystemVerilog synthesis', () async { final mod = _BackendFallbackModule( Logic(name: 'a', width: 4), @@ -238,31 +235,10 @@ void main() { ); await mod.build(); - final defaultSynth = mod.generateSynth(); - final explicitFalseConfig = SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: [false].single, - ); - final explicitFalse = mod.generateSynth( - configuration: explicitFalseConfig, - ); - final optIn = mod.generateSynth( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); - - expect( - normalizeSynthHeader(defaultSynth), - equals(normalizeSynthHeader(explicitFalse)), - ); expect( - normalizeSynthHeader(optIn), - equals(normalizeSynthHeader(defaultSynth)), + mod.generateSynth, + throwsA(isA()), ); - - expect(defaultSynth, contains('~(a & b)')); - expect(explicitFalse, contains('~(a & b)')); - expect(optIn, contains('~(a & b)')); }); test('SystemC rejects unknown inline module matrix', () { @@ -272,7 +248,7 @@ void main() { ); final scenarios = <({ - InlineSystemVerilog module, + InlineLeaf module, Map inputs, })>[ ( diff --git a/test/leaf_cell_spec_inference_test.dart b/test/leaf_cell_spec_inference_test.dart index c0e138196..0a84e32de 100644 --- a/test/leaf_cell_spec_inference_test.dart +++ b/test/leaf_cell_spec_inference_test.dart @@ -87,6 +87,14 @@ void main() { 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', () { @@ -97,7 +105,7 @@ void main() { ]); final scenarios = <({ - InlineSystemVerilog module, + InlineLeaf module, LeafOperationKind operation, Map metadata, })>[ @@ -170,6 +178,10 @@ void main() { 'inputCount': 3, 'inputWidths': swizzle.inputs.values.map((input) => input.width).toList(), + 'inputIsArrayMember': swizzle.inputs.values + .map((input) => input.isArrayMember) + .toList(), + 'inputHasUnpackedArraySource': [false, false, false], }, ), ]; diff --git a/test/leaf_expression_plan_test.dart b/test/leaf_expression_plan_test.dart index 6fe5ca9b6..e7c51442f 100644 --- a/test/leaf_expression_plan_test.dart +++ b/test/leaf_expression_plan_test.dart @@ -43,7 +43,7 @@ void main() { test('planner contract matrix across representative leaf operations', () { Map orderedInputs( - InlineSystemVerilog module, + InlineLeaf module, List values, ) { final keys = module.inputs.keys.toList(); @@ -69,7 +69,7 @@ void main() { ]); final scenarios = <({ - InlineSystemVerilog module, + InlineLeaf module, LeafOperationKind op, List inputs, Map metadata, @@ -116,6 +116,8 @@ void main() { metadata: { 'inputCount': 3, 'inputWidths': [3, 1, 2], + 'inputIsArrayMember': [false, false, false], + 'inputHasUnpackedArraySource': [false, false, false], }, ), ]; @@ -137,7 +139,7 @@ void main() { }); test('plan mirrors inferred leaf spec across module matrix', () { - Map taggedInputs(InlineSystemVerilog module) { + Map taggedInputs(InlineLeaf module) { final mapping = {}; for (final port in module.inputs.keys) { mapping[port] = '${port}_expr'; diff --git a/test/leaf_test_module_factories.dart b/test/leaf_test_module_factories.dart index 36e9a7ee0..f94f9a951 100644 --- a/test/leaf_test_module_factories.dart +++ b/test/leaf_test_module_factories.dart @@ -10,7 +10,7 @@ import 'package:rohd/rohd.dart'; /// Representative inline leaf modules for focused contract tests. -List representativeInlineLeafModules() => [ +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)), @@ -30,7 +30,7 @@ List representativeInlineLeafModules() => [ ]; /// All known built-in inline leaf modules expected to have inference coverage. -List allKnownInlineLeafModules() => [ +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)), diff --git a/test/systemverilog_leaf_plan_option_test.dart b/test/systemverilog_leaf_plan_option_test.dart index c4d0e53ac..4c86addd8 100644 --- a/test/systemverilog_leaf_plan_option_test.dart +++ b/test/systemverilog_leaf_plan_option_test.dart @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause // // systemverilog_leaf_plan_option_test.dart -// Tests for opt-in SystemVerilog leaf expression plan rendering. +// Tests for SystemVerilog leaf expression plan rendering configuration. // // 2026 July // Author: Desmond A. Kirkpatrick @@ -174,7 +174,7 @@ class _InlineMixedOptionGateModule extends Module { } void main() { - test('leaf-expression-plan inline rendering is opt-in', () async { + test('leaf-expression-plan inline rendering is the default', () async { final mod = _InlineOpsModule( Logic(name: 'a', width: 4), Logic(name: 'b', width: 4), @@ -183,11 +183,7 @@ void main() { await mod.build(); final baseline = mod.generateSynth(); - final planned = mod.generateSynth( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); + final planned = mod.generateSynth(); expect(baseline, contains('assign y_and = a & b;')); expect(baseline, contains('assign y_not = ~a;')); @@ -198,17 +194,13 @@ void main() { expect(planned, contains('assign y_mux = control ? a : b;')); }); - test('opt-in path preserves inline output for range/replication/swizzle', + 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( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); + final planned = mod.generateSynth(); expect( baseline, @@ -241,7 +233,7 @@ void main() { ); }); - test('opt-in path preserves inline output for power/index', () async { + test('planned path preserves inline output for power/index', () async { final mod = _InlinePowerIndexModule( Logic(name: 'a', width: 4), Logic(name: 'b', width: 4), @@ -250,11 +242,7 @@ void main() { await mod.build(); final baseline = mod.generateSynth(); - final planned = mod.generateSynth( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); + final planned = mod.generateSynth(); expect(baseline, contains(RegExp(r'assign y_pow = \{a \*\* b\};'))); expect(baseline, contains(RegExp(r'assign y_idx = a\[idx\];'))); @@ -267,7 +255,7 @@ void main() { ); }); - test('opt-in path preserves inline output for single-bit edge cases', + test('planned path preserves inline output for single-bit edge cases', () async { final mod = _InlineSingleBitEdgesModule( Logic(name: 'a', width: 4), @@ -277,11 +265,7 @@ void main() { await mod.build(); final baseline = mod.generateSynth(); - final planned = mod.generateSynth( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); + final planned = mod.generateSynth(); expect(baseline, contains('assign y_subset_single = a[2];')); expect(baseline, contains('assign y_idx_single = scalar;')); @@ -294,16 +278,12 @@ void main() { ); }); - test('opt-in path preserves swizzle output with zero-width input', () async { + 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( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); + final planned = mod.generateSynth(); expect(baseline, contains('assign y_swizzle_zero = a;')); expect(planned, contains('assign y_swizzle_zero = a;')); @@ -313,18 +293,14 @@ void main() { ); }); - test('opt-in path preserves swizzle contiguous-select collapsing', () async { + 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( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); + final planned = mod.generateSynth(); expect(baseline, contains('assign y_swizzle_collapse = a[7:5];')); expect(planned, contains('assign y_swizzle_collapse = a[7:5];')); @@ -334,18 +310,14 @@ void main() { ); }); - test('opt-in path preserves swizzle partial contiguous-collapse', () async { + 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( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); + final planned = mod.generateSynth(); expect(baseline, contains('a[7:6]')); expect(baseline, contains('a[4]')); @@ -357,7 +329,7 @@ void main() { ); }); - test('opt-in path preserves non-collapsible ascending swizzle order', + test('planned path preserves non-collapsible ascending swizzle order', () async { final mod = _InlineSwizzleAscendingSelectsModule( Logic(name: 'a', width: 8), @@ -365,11 +337,7 @@ void main() { await mod.build(); final baseline = mod.generateSynth(); - final planned = mod.generateSynth( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); + final planned = mod.generateSynth(); expect(baseline, contains('a[5]')); expect(baseline, contains('a[6]')); @@ -386,7 +354,7 @@ void main() { ); }); - test('opt-in path preserves per-source swizzle collapsing', () async { + test('planned path preserves per-source swizzle collapsing', () async { final mod = _InlineSwizzleMultiSourceCollapseModule( Logic(name: 'a', width: 8), Logic(name: 'b', width: 8), @@ -394,11 +362,7 @@ void main() { await mod.build(); final baseline = mod.generateSynth(); - final planned = mod.generateSynth( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); + final planned = mod.generateSynth(); expect(baseline, contains('a[7:6]')); expect(baseline, contains('b[3:2]')); @@ -410,18 +374,14 @@ void main() { ); }); - test('opt-in path preserves unpacked-array swizzle non-collapse', () async { + 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( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); + final planned = mod.generateSynth(); expect(baseline, contains('arr[3]')); expect(baseline, contains('arr[2]')); @@ -440,7 +400,7 @@ void main() { ); }); - test('opt-in path preserves swizzle parity matrix', () async { + test('planned path preserves swizzle parity matrix', () async { final scenarios = <({ String name, Module Function() build, @@ -503,11 +463,7 @@ void main() { await mod.build(); final baseline = mod.generateSynth(); - final planned = mod.generateSynth( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); + final planned = mod.generateSynth(); for (final expected in scenario.contains) { expect( @@ -545,8 +501,7 @@ void main() { } }); - test('default option matches explicit false and opt-in parity on mixed ops', - () async { + test('semantic leaf emission handles mixed ops', () async { final mod = _InlineMixedOptionGateModule( Logic(name: 'a', width: 8), Logic(name: 'b', width: 8), @@ -555,30 +510,11 @@ void main() { ); await mod.build(); - final defaultSynth = mod.generateSynth(); - final explicitFalseConfiguration = SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: [false].single, - ); - final explicitFalse = - mod.generateSynth(configuration: explicitFalseConfiguration); - final optIn = mod.generateSynth( - configuration: const SystemVerilogSynthesizerConfiguration( - useLeafExpressionPlanForInlineRendering: true, - ), - ); - - expect( - normalizeSynthHeader(defaultSynth), - equals(normalizeSynthHeader(explicitFalse)), - ); - expect( - normalizeSynthHeader(optIn), - equals(normalizeSynthHeader(defaultSynth)), - ); + final synth = mod.generateSynth(); - expect(defaultSynth, contains('assign y_and = a & b;')); - expect(defaultSynth, contains('assign y_mux = control ? a : b;')); - expect(defaultSynth, contains('assign y_pow = {a ** b};')); - expect(defaultSynth, contains('assign y_idx = a[idx];')); + 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];')); }); } From f9cd485bed64ed5e1afbbd254bad6800475c0d56 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 20 Jul 2026 17:35:12 -0700 Subject: [PATCH 12/14] bring back deprecated member --- lib/src/synthesizers/systemc/systemc.dart | 15 +++++++++------ .../systemverilog/systemverilog_leaf_emitter.dart | 12 ++++++------ .../systemverilog/systemverilog_synthesizer.dart | 15 +++++++++------ 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/lib/src/synthesizers/systemc/systemc.dart b/lib/src/synthesizers/systemc/systemc.dart index 07bf3adb6..7522a194d 100644 --- a/lib/src/synthesizers/systemc/systemc.dart +++ b/lib/src/synthesizers/systemc/systemc.dart @@ -18,12 +18,15 @@ export 'systemc_mixins.dart'; /// using the same naming strategy as the SystemVerilog synthesizer. class SystemCSynthesizer extends Synthesizer { @override - bool generatesDefinition(Module module) => - // ignore: deprecated_member_use_from_same_package - !((module is InlineLeaf) || - (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; + } @override SynthesisResult synthesize(Module module, diff --git a/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart b/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart index 0d630f993..432ad0bb5 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart @@ -50,24 +50,24 @@ class SystemVerilogLeafEmitter implements InlineLeafEmitter { var index = 0; while (index < operands.length) { final first = operands[index]; - final firstSelect = first.width == 1 && first.canCollapse + final firstSel = first.width == 1 && first.canCollapse ? _singleBitSelect(first.expression) : (target: null, index: null); - if (firstSelect.target == null || firstSelect.index == null) { + if (firstSel.target == null || firstSel.index == null) { collapsed.add((expression: first.expression, width: first.width)); index++; continue; } var lastIndex = index; - var expectedBit = firstSelect.index! - 1; + 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 != firstSelect.target || + if (nextSelect.target != firstSel.target || nextSelect.index != expectedBit) { break; } @@ -77,14 +77,14 @@ class SystemVerilogLeafEmitter implements InlineLeafEmitter { if (lastIndex == index) { collapsed.add(( - expression: '${firstSelect.target}[${firstSelect.index}]', + expression: '${firstSel.target}[${firstSel.index}]', width: 1, )); } else { final lowerSelect = _singleBitSelect(operands[lastIndex].expression); collapsed.add(( expression: - '${firstSelect.target}[${firstSelect.index}:${lowerSelect.index}]', + '${firstSel.target}[${firstSel.index}:${lowerSelect.index}]', width: lastIndex - index + 1, )); } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index f9bfb8e8f..203b95158 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart @@ -25,12 +25,15 @@ class SystemVerilogSynthesizer extends Synthesizer { }); @override - bool generatesDefinition(Module module) => - // ignore: deprecated_member_use_from_same_package - !((module is InlineLeaf) || - (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]. /// From dc7bc3d32e61f4a2ec32dd1346b1bda99d126d69 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 20 Jul 2026 21:31:38 -0700 Subject: [PATCH 13/14] fixed port issues along with a load of other swizzle problems --- .../modules/conditionals/combinational.dart | 5 +- .../systemverilog_leaf_emitter.dart | 14 +- ...systemverilog_synth_module_definition.dart | 230 +++++++++++++++++- ...erilog_synth_sub_module_instantiation.dart | 66 ++++- .../systemverilog_synthesis_result.dart | 57 ++++- .../utilities/synth_module_definition.dart | 39 ++- .../synth_sub_module_instantiation.dart | 27 +- 7 files changed, 397 insertions(+), 41 deletions(-) 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/synthesizers/systemverilog/systemverilog_leaf_emitter.dart b/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart index 432ad0bb5..1d9f08b35 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_leaf_emitter.dart @@ -103,9 +103,7 @@ class SystemVerilogLeafEmitter implements InlineLeafEmitter { final vals = plan.inputValues; if (op == LeafOperationKind.not && vals.length == 1) { - final outputWidth = plan.meta('outputWidth') ?? - plan.sourceModule.outputs.values.first.width; - return outputWidth == 1 ? '!${vals[0]}' : '~${vals[0]}'; + return '~${vals[0]}'; } const binaryOps = { @@ -199,7 +197,7 @@ class SystemVerilogLeafEmitter implements InlineLeafEmitter { return '${vals[0]}[${vals[1]}]'; } - if (op == LeafOperationKind.swizzle && vals.isNotEmpty) { + if (op == LeafOperationKind.swizzle) { final inputWidths = plan.meta>('inputWidths'); final inputCount = plan.meta('inputCount'); final inputIsArrayMember = @@ -214,6 +212,10 @@ class SystemVerilogLeafEmitter implements InlineLeafEmitter { ); } + if (inputCount == 0 && vals.isEmpty) { + return ''; + } + if (vals.length != inputCount && vals.length != inputCount + 1) { throw SynthException( 'SystemVerilog swizzle leaf expected $inputCount inputs, but saw ' @@ -236,7 +238,9 @@ class SystemVerilogLeafEmitter implements InlineLeafEmitter { inputHasUnpackedArraySource[i]; filtered.add(( canCollapse: !isArrayMember && !hasUnpackedArraySource, - expression: inputExpressions[i], + expression: inputExpressions[i].isEmpty + ? "$width'b${'z' * width}" + : inputExpressions[i], width: width, )); } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 5ab6a7add..7fa8bf473 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -40,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. @@ -179,7 +229,9 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { continue; } - _addSwizzleConnect(bus, sources); + if (!_addSwizzleConnect(bus, sources)) { + continue; + } removedAssignments ..addAll(drivers) ..addAll(constantDrivers); @@ -304,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) { @@ -379,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; @@ -453,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 @@ -484,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; } @@ -545,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. @@ -570,6 +690,10 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { changed = true; } assignments.removeWhere(removedAssignments.contains); + for (final signal in removedSignals) { + signal.clearDeclaration(); + internalSignals.remove(signal); + } } } @@ -960,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(); @@ -1267,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 @@ -1310,6 +1439,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { supportingModules.add(swizzle); _inlineableSubmoduleMap[agg] = swizzleInst; + return true; } /// Collapses chainable, inlineable modules after naming. @@ -1422,13 +1552,48 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { subModuleInstantiation as SystemVerilogSynthSubModuleInstantiation; - subModuleInstantiation.clearInstantiation(); - 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( @@ -1445,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` @@ -1523,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 5a28854b1..7cb5a49ec 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart @@ -40,6 +40,7 @@ class SystemVerilogSynthSubModuleInstantiation 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()`.'); @@ -49,7 +50,7 @@ class SystemVerilogSynthSubModuleInstantiation portNameToValueMapping, ); - return '($inlineSvRepresentation)'; + return inlineSvRepresentation.isEmpty ? '' : '($inlineSvRepresentation)'; } /// Provides the full SV instantiation for this module. @@ -64,11 +65,29 @@ class SystemVerilogSynthSubModuleInstantiation }, 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; - if ((ports[resultName] ?? '').isEmpty) { + final resultLogic = inlineResultLogic; + if (resultLogic == null || !resultLogic.hasName) { return null; } + ports[resultName] = resultLogic.name; } return SystemVerilogSynthesizer.instantiationVerilogFor( module: module, @@ -76,4 +95,47 @@ class SystemVerilogSynthSubModuleInstantiation instanceName: name, 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 0c0e0ed59..219f6c9b7 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart @@ -161,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 _emissionPlan.internalSignals - .where((e) => e.needsDeclaration) + .where((e) => e.needsDeclaration && !excludedSignals.contains(e)) .sorted((a, b) => a.name.compareTo(b.name))) { declarations.add('${sig.definitionType()} ${sig.definitionName()};'); } @@ -172,7 +172,7 @@ 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 @@ -180,6 +180,12 @@ class SystemVerilogSynthesisResult extends SynthesisResult { : '[$upperIndex:$lowerIndex]'; 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' @@ -231,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/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 76e23e214..0b2aca83e 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -927,7 +927,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 @@ -1044,7 +1047,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, @@ -1174,6 +1182,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; @@ -1223,6 +1248,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) { @@ -1235,6 +1265,11 @@ class SynthModuleDefinition { if (sourceIsMappedOutput) { assignmentConnectedSignals.add(sourceBase); } + if (destinationFeedsInlineOperation) { + assignmentConnectedSignals + ..add(destinationBase) + ..add(sourceBase); + } } return assignmentConnectedSignals; diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 2751ddb7c..89d12349a 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -127,23 +127,24 @@ class SynthSubModuleInstantiation { /// Creates a port-name to expression map, optionally inlining source /// submodule expressions for mapped [SynthLogic] values. - Map - modulePortsMapWithInline( + Map modulePortsMapWithInline< + T extends SynthSubModuleInstantiation>( Map plainPorts, Map? synthLogicToInlineableSynthSubmoduleMap, String Function(T subModuleInstantiation) inlineExpressionFor, ) => - plainPorts.map((name, synthLogic) { - final inlineSubModule = - synthLogicToInlineableSynthSubmoduleMap?[synthLogic]; - if (inlineSubModule != null) { - return MapEntry(name, inlineExpressionFor(inlineSubModule)); - } - - // Cleared declarations map to empty port connections. - return MapEntry( - name, synthLogic.declarationCleared ? '' : synthLogic.name); - }); + 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]). From 0bc97d0f48afb488b9572ae1ba6575331facf53e Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 20 Jul 2026 23:22:59 -0700 Subject: [PATCH 14/14] odd merge problem in git --- lib/src/synthesizers/utilities/synth_module_definition.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 30dfb0dc4..67b0d4a13 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -851,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