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/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart b/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart index 5765f08bf..18efb5a2d 100644 --- a/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart +++ b/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart @@ -7,7 +7,6 @@ import '../../chapter_3/answers/helper.dart'; import '../../chapter_5/answers/full_subtractor.dart'; class FullSubtractorComb extends FullSubtractor { - @override FullSubtractorComb(super.a, super.b, super.borrowIn) { // Declare input and output final a = input('a'); 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/exceptions/logic/put_exception.dart b/lib/src/exceptions/logic/put_exception.dart index 36d8f8015..96bd51602 100644 --- a/lib/src/exceptions/logic/put_exception.dart +++ b/lib/src/exceptions/logic/put_exception.dart @@ -9,10 +9,11 @@ import 'package:rohd/rohd.dart'; -/// An exception that thrown when a [Logic] signal fails to `put`. +/// An exception that thrown when a [Logic] signal fails to [Logic.put]. class PutException extends RohdException { - /// Creates an exception for when a `put` fails on a `Logic` with [context] as - /// to where the + /// Creates an exception for when a [Logic.put] fails on a [Logic] with + /// [context] as to where the failure occurred and [message] describing the + /// failure. PutException(String context, String message) : super('Failed to put value on signal ($context): $message'); } diff --git a/lib/src/module.dart b/lib/src/module.dart index 92fc410e0..dc0f67e9f 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // module.dart @@ -11,7 +11,6 @@ import 'dart:async'; import 'dart:collection'; import 'package:meta/meta.dart'; - import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; import 'package:rohd/src/diagnostics/inspector_service.dart'; @@ -1131,6 +1130,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/synth_builder.dart b/lib/src/synthesizers/synth_builder.dart index 54e312ab3..f9d0a0d08 100644 --- a/lib/src/synthesizers/synth_builder.dart +++ b/lib/src/synthesizers/synth_builder.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synth_builder.dart diff --git a/lib/src/synthesizers/synthesizer.dart b/lib/src/synthesizers/synthesizer.dart index b70c9338e..687bbab03 100644 --- a/lib/src/synthesizers/synthesizer.dart +++ b/lib/src/synthesizers/synthesizer.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synthesizer.dart @@ -6,7 +6,6 @@ // // 2021 August 26 // Author: Max Korbel -// import 'package:rohd/rohd.dart'; diff --git a/lib/src/synthesizers/synthesizers.dart b/lib/src/synthesizers/synthesizers.dart index b8c8523ec..5246eb98a 100644 --- a/lib/src/synthesizers/synthesizers.dart +++ b/lib/src/synthesizers/synthesizers.dart @@ -5,4 +5,5 @@ export 'synth_builder.dart'; export 'synth_file_contents.dart'; export 'synthesis_result.dart'; export 'synthesizer.dart'; +export 'systemc/systemc.dart'; export 'systemverilog/systemverilog.dart'; diff --git a/lib/src/synthesizers/systemc/systemc.dart b/lib/src/synthesizers/systemc/systemc.dart new file mode 100644 index 000000000..7bf0f1211 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc.dart @@ -0,0 +1,29 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synthesizer.dart +// Definition for SystemC Synthesizer +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; + +/// A [Synthesizer] which generates equivalent SystemC as the given [Module]. +/// +/// Attempts to maintain signal naming and structure as much as possible, +/// using the same naming strategy as the SystemVerilog synthesizer. +class SystemCSynthesizer extends Synthesizer { + @override + bool generatesDefinition(Module module) => + // ignore: deprecated_member_use_from_same_package + !((module is CustomSystemVerilog) || + (module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none)); + + @override + SynthesisResult synthesize(Module module, + String Function(Module module) getInstanceTypeOfModule) => + SystemCSynthesisResult(module, getInstanceTypeOfModule); +} diff --git a/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart b/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart new file mode 100644 index 000000000..e670279d4 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart @@ -0,0 +1,31 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synth_module_definition.dart +// Definition for SystemCSynthModuleDefinition +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// A special [SynthModuleDefinition] for SystemC modules. +class SystemCSynthModuleDefinition extends SynthModuleDefinition { + /// Creates a new [SystemCSynthModuleDefinition] for the given [module]. + SystemCSynthModuleDefinition(super.module); + + @override + void process() { + // For now, do not collapse inline modules. Each InlineSystemVerilog gate + // remains as a sub-module instantiation and gets emitted as an assign-style + // expression in the generated SystemC (similar to SV `assign x = a & b`). + // + // Future: implement chain-collapsing for compound expressions. + } + + @override + SynthSubModuleInstantiation createSubModuleInstantiation(Module m) => + SystemCSynthSubModuleInstantiation(m); +} diff --git a/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart new file mode 100644 index 000000000..7e692ff8a --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart @@ -0,0 +1,113 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synth_sub_module_instantiation.dart +// Definition for SystemCSynthSubModuleInstantiation +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// Represents a submodule instantiation for SystemC. +class SystemCSynthSubModuleInstantiation extends SynthSubModuleInstantiation { + /// Creates a new [SystemCSynthSubModuleInstantiation] for the given + /// [module]. + SystemCSynthSubModuleInstantiation(super.module); + + /// If [module] is [InlineSystemVerilog], this will be the [SynthLogic] that + /// is the `result` of that module. Otherwise, `null`. + SynthLogic? get inlineResultLogic => module is! InlineSystemVerilog + ? null + : (outputMapping[(module as InlineSystemVerilog).resultSignalName] ?? + inOutMapping[(module as InlineSystemVerilog).resultSignalName]); + + /// Mapping from [SynthLogic]s which are outputs of inlineable modules to + /// those inlineable modules. + Map? + synthLogicToInlineableSynthSubmoduleMap; + + /// Provides a mapping from ports of this module to a string that can be fed + /// into that port, which may include inline expressions. + Map _modulePortsMapWithInline( + Map plainPorts) => + plainPorts.map((name, synthLogic) => MapEntry( + name, + synthLogicToInlineableSynthSubmoduleMap?[synthLogic] + ?.inlineSystemC() ?? + (synthLogic.declarationCleared ? '' : synthLogic.name))); + + /// Provides the inline SystemC expression for this module. + /// + /// Should only be called if [module] is [InlineSystemVerilog]. + String inlineSystemC() { + final portNameToValueMapping = _modulePortsMapWithInline( + {...inputMapping, ...inOutMapping} + ..remove((module as InlineSystemVerilog).resultSignalName), + ); + + final inlineRepresentation = + _inlineSystemCExpression(portNameToValueMapping); + + return '($inlineRepresentation)'; + } + + /// Generates the inline SystemC expression for the gate module. + String _inlineSystemCExpression(Map inputs) { + final m = module; + + if (m is NotGate) { + final inVal = inputs.values.first; + return '~$inVal'; + } else if (m is And2Gate) { + return '${inputs.values.first} & ${inputs.values.last}'; + } else if (m is Or2Gate) { + return '${inputs.values.first} | ${inputs.values.last}'; + } else if (m is Xor2Gate) { + return '${inputs.values.first} ^ ${inputs.values.last}'; + } else if (m is Mux) { + // Mux has inputs: control, d0, d1 → output: y + // In SystemC: control ? d1 : d0 + final entries = inputs.entries.toList(); + final control = entries[0].value; + final d0 = entries[1].value; + final d1 = entries[2].value; + return '$control ? $d1 : $d0'; + } else if (m is InlineSystemVerilog) { + // Fallback: use the verilog inline expression as a reasonable + // approximation (many operators are identical between SV and C++) + return m.inlineVerilog(inputs); + } + + throw SynthException('Unsupported inline module type: ${m.runtimeType}'); + } + + /// Provides the full SystemC instantiation for this module as a member + /// declaration and port binding in the constructor. + /// + /// Returns null if this module does not need instantiation. + String? memberDeclaration(String instanceType) { + if (!needsInstantiation) { + return null; + } + return '$instanceType $name{"$name"};'; + } + + /// Generates port binding statements for the constructor body. + String? portBindings() { + if (!needsInstantiation) { + return null; + } + final bindings = []; + final allPorts = {...inputMapping, ...outputMapping, ...inOutMapping}; + for (final entry in allPorts.entries) { + final portName = entry.key; + final synthLogic = entry.value; + if (!synthLogic.declarationCleared) { + bindings.add('$name.$portName(${synthLogic.name});'); + } + } + return bindings.join('\n'); + } +} diff --git a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart new file mode 100644 index 000000000..fa73b7f30 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart @@ -0,0 +1,1678 @@ +// 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 sensitivities = {}; + final bodyLines = []; + + for (final ssmi in inlineGates) { + final m = ssmi.module; + + // Collect inputs — constants become literals, signals get .read() + final inputExprs = {}; + for (final entry in ssmi.inputMapping.entries) { + final sl = entry.value; + if (!sl.isConstant) { + sensitivities.add(_sensitivityName(sl)); + } + inputExprs[entry.key] = _synthLogicReadExpr(sl); + } + + if (m is InlineSystemVerilog) { + final resultSynthLogic = ssmi.inlineResultLogic; + if (resultSynthLogic == null) { + continue; + } + final expr = _gateExpression(m, inputExprs); + final dst = _scName(resultSynthLogic.name); + bodyLines.add(' $dst = $expr;'); + } else if (m is Add) { + // Add has two outputs: sum and carry. + // Emit inline expressions for each used output. + final vals = inputExprs.values.toList(); + final sumPortName = m.sum.name; + for (final entry in ssmi.outputMapping.entries) { + final portName = entry.key; + final dst = _scName(entry.value.name); + if (portName == sumPortName) { + bodyLines.add(' $dst = ${vals[0]} + ${vals[1]};'); + } else { + // carry: high bit of (width+1)-bit addition + final w = m.width; + final w1 = w + 1; + final utype = systemCType(w1); + final carryExpr = 'static_cast' + '($utype($utype(${vals[0]})' + ' + $utype(${vals[1]}))[$w])'; + bodyLines.add(' $dst = $carryExpr;'); + } + } + } + ssmi.clearInstantiation(); + } + + if (bodyLines.isEmpty) { + return null; + } + + final setupBuf = StringBuffer()..writeln(' SC_METHOD(assign_method);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: ' void assign_method() {\n' + '${bodyLines.join('\n')}\n' + ' }', + ); + } + + /// Maps an InlineSystemVerilog gate to a C++ expression. + /// + /// Handles all gate types that have SV-specific syntax which needs + /// translation to valid SystemC/C++. + String _gateExpression(InlineSystemVerilog m, Map inputs) { + // ── Single-output bitwise gates (C++ operators identical to SV) ── + if (m is NotGate) { + // For bool (width-1), use logical not; for wider, bitwise not + if ((m as Module).outputs.values.first.width == 1) { + return '!${inputs.values.first}'; + } + return '~${inputs.values.first}'; + } + + // ── Binary operator gates (C++ operators identical to SV) ── + const binaryOps = { + And2Gate: '&', + Or2Gate: '|', + Xor2Gate: '^', + Subtract: '-', + Multiply: '*', + }; + final binOp = binaryOps[m.runtimeType]; + if (binOp != null) { + final vals = inputs.values.toList(); + return '${vals[0]} $binOp ${vals[1]}'; + } + if (m is Divide || m is Modulo) { + final vals = inputs.values.toList(); + final op = m is Divide ? '/' : '%'; + // Guard against zero divisor (sc_uint defaults to 0 at time-0) + return '(${vals[1]} != 0 ? ${vals[0]} $op ${vals[1]} : 0)'; + } + if (m is Power) { + final vals = inputs.values.toList(); + final w = (m as Module).inputs.values.first.width; + return '${systemCType(w)}' + '(static_cast' + '(pow(static_cast(${vals[0]}),' + ' static_cast(${vals[1]}))))'; + } + + // ── Comparison (operators identical) ── + const cmpOps = { + Equals: '==', + NotEquals: '!=', + LessThan: '<', + GreaterThan: '>', + LessThanOrEqual: '<=', + GreaterThanOrEqual: '>=', + }; + final cmpOp = cmpOps[m.runtimeType]; + if (cmpOp != null) { + final vals = inputs.values.toList(); + return '${vals[0]} $cmpOp ${vals[1]}'; + } + + // ── Shifts ── + // Cast shift amount to int to avoid ambiguous overloads. + // Width 1 maps to bool in SystemC (no .to_int()), so use (int) cast. + // Clamp: if shift amount >= operand width, result is 0 (or sign-fill + // for arshift), avoiding .to_int() overflow on huge shift amounts. + if (m is LShift || m is RShift || m is ARShift) { + final vals = inputs.values.toList(); + final w = (m as Module).inputs.values.first.width; + final outType = systemCType(w); + final shiftAmtWidth = (m as Module).inputs.values.toList()[1].width; + final shiftExpr = + shiftAmtWidth == 1 ? '(int)(${vals[1]})' : '(${vals[1]}).to_int()'; + if (m is ARShift) { + final signedType = w <= 64 ? 'sc_int<$w>' : 'sc_bigint<$w>'; + final shiftOp = '$outType(($signedType(${vals[0]})) >> $shiftExpr)'; + if (shiftAmtWidth > 31) { + // Sign-fill: shift by width-1 to replicate MSB when shift >= width + final overflow = '$outType(($signedType(${vals[0]})) >> ${w - 1})'; + return '(${vals[1]} >= $w) ? $overflow : $shiftOp'; + } + return shiftOp; + } + final op = m is LShift ? '<<' : '>>'; + final shiftOp = '$outType(${vals[0]} $op $shiftExpr)'; + if (shiftAmtWidth > 31) { + return '(${vals[1]} >= $w) ? $outType(0) : $shiftOp'; + } + return shiftOp; + } + + // ── Unary reductions ── + if (m is AndUnary || m is OrUnary || m is XorUnary) { + final inputWidth = (m as Module).inputs.values.first.width; + // 1-bit: reduce is identity (and bool has no .xor_reduce() in SystemC) + if (inputWidth == 1) { + return 'static_cast(${inputs.values.first})'; + } + if (m is AndUnary) { + return '${inputs.values.first}.and_reduce()'; + } else if (m is OrUnary) { + return '${inputs.values.first}.or_reduce()'; + } else { + return '${inputs.values.first}.xor_reduce()'; + } + } + + // ── Bus subset (slice / index) ── + if (m is BusSubset) { + final a = inputs.values.first; + final inputWidth = (m as Module).inputs.values.first.width; + // If input is already 1-bit (bool), extracting bit 0 is identity + if (inputWidth == 1 && m.startIndex == 0 && m.endIndex == 0) { + return a; + } + if (m.startIndex == m.endIndex) { + return 'static_cast($a[${m.startIndex}])'; + } + if (m.startIndex > m.endIndex) { + // Reverse order — build bit-by-bit concat + // bits[0]=a[endIndex], ..., bits[N]=a[startIndex] + // SystemC concat is MSB-first: output MSB = input[endIndex] + // Use sc_uint<1> (not bool) so SystemC concat operator is invoked + final bits = List.generate(m.startIndex - m.endIndex + 1, + (i) => 'sc_uint<1>($a[${m.endIndex + i}])'); + return '(${bits.join(', ')})'; + } + final w = m.endIndex - m.startIndex + 1; + final rangeType = w <= 64 ? 'sc_uint' : 'sc_biguint'; + return '$rangeType<$w>($a.range(${m.endIndex}, ${m.startIndex}))'; + } + + // ── Dynamic bit index ── + if (m is IndexGate) { + final vals = inputs.values.toList(); + return 'static_cast(${vals[0]}[${vals[1]}])'; + } + + // ── Mux (ternary) ── + if (m is Mux) { + final vals = inputs.values.toList(); + final w = m.out.width; + final utype = systemCType(w); + // Cast both branches to avoid C++ ternary type mismatch + // (e.g., when one branch is bool and the other is sc_uint<1>) + return '${vals[0]}' + ' ? $utype(${vals[2]})' + ' : $utype(${vals[1]})'; + } + + // ── Replication ── + if (m is ReplicationOp) { + final a = inputs.values.first; + final inputWidth = (m as Module).inputs.values.first.width; + final outputWidth = m.replicated.width; + final numReps = outputWidth ~/ inputWidth; + if (inputWidth == 1) { + // Single-bit replicate: all-1s or all-0s + final utype = systemCType(outputWidth); + return '$utype(' + '$a ' + '? $utype(-1) ' + ': $utype(0))'; + } + // Multi-bit replicate: concat N copies + final copies = List.filled(numReps, a); + return '(${copies.join(', ')})'; + } + + // ── Swizzle (concatenation) ── + if (m is Swizzle) { + // SystemC concatenation: (sig1, sig2, sig3) + // bool operands must be cast to sc_uint<1> to use SystemC concat + // (otherwise C++ comma operator is invoked instead) + final modInputs = (m as Module).inputs.values.toList(); + final exprList = []; + var i = 0; + for (final expr in inputs.values) { + final w = modInputs[i].width; + if (w == 0) { + i++; + continue; // skip zero-width padding + } + // Wrap 1-bit (bool) operands in sc_uint<1>() for concat + if (w == 1) { + exprList.add('sc_uint<1>($expr)'); + } else { + exprList.add(expr); + } + i++; + } + if (exprList.length == 1) { + return exprList.first; + } + // Swizzle stores inputs LSB-first (in0=LSB), but SystemC concat + // is MSB-first: (msb, ..., lsb). So reverse. + return '(${exprList.reversed.join(', ')})'; + } + + // Fallback: use SV inline (may not be valid C++ — flag for review) + return '/* TODO: ${m.runtimeType} */ ${m.inlineVerilog(inputs)}'; + } + + // ──────────────────────────────────────────────────────────────────── + // Clock / trigger edge resolution + // ──────────────────────────────────────────────────────────────────── + + /// Resolves a trigger [SynthLogic] to the effective clock port and edge. + /// + /// If the trigger signal is a module input port, it can be used directly + /// with `SC_CTHREAD`. If it is an internal signal derived from a [NotGate], + /// the method traces through the inversion chain to find the original port + /// and flips the edge accordingly (`negedge(~clk) = posedge(clk)`). + ({String clockName, bool isPort, bool isPosedge}) _resolveClockAndEdge( + SynthLogic triggerSL, bool isPosedge) { + final sl = triggerSL.replacement ?? triggerSL; + + if (sl.isPort(_synthModuleDefinition.module)) { + return (clockName: sl.name, isPort: true, isPosedge: isPosedge); + } + + // Try to trace through a NotGate inversion + for (final logic in sl.logics) { + final src = logic.srcConnection; + if (src != null && src.parentModule is NotGate) { + final notInput = src.parentModule!.inputs.values.first; + final notInputSrc = notInput.srcConnection; + if (notInputSrc != null) { + final srcSL = _synthModuleDefinition.logicToSynthMap[notInputSrc]; + if (srcSL != null) { + // Inversion flips the edge + return _resolveClockAndEdge(srcSL, !isPosedge); + } + } + } + } + + // Fallback — use the signal as-is (SC_THREAD will be needed) + return (clockName: sl.name, isPort: false, isPosedge: isPosedge); + } + + // ──────────────────────────────────────────────────────────────────── + // Combinational / Sequential processes + // ──────────────────────────────────────────────────────────────────── + + _MethodResult? _buildProcesses() { + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + var idx = 0; + + // Collect clocked processes for consolidation by (clock, reset) pair. + // Sequentials and FlipFlops sharing the same clock/reset are merged + // into a single SC_CTHREAD, eliminating repeated async_reset_signal_is. + final clockedGroups = {}; + + for (final ssmi + in _synthModuleDefinition.subModuleInstantiations.toList()) { + ssmi as SystemCSynthSubModuleInstantiation; + final m = ssmi.module; + + if (m is Combinational) { + final name = 'comb_$idx'; + idx++; + + final sensitivities = ssmi.inputMapping.values + .where((sl) => !sl.declarationCleared && !sl.isConstant) + .map(_sensitivityName) + .toSet(); + + setupBuf.writeln(' SC_METHOD($name);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + // Build maps keyed by port name (what verilogContents expects) + final inputsMap = ssmi.inputMapping + .map((k, sl) => MapEntry(k, _synthLogicReadExpr(sl))); + final outputsMap = + ssmi.outputMapping.map((k, sl) => MapEntry(k, _scName(sl.name))); + + bodyBuf.writeln(' void $name() {'); + for (final c in m.conditionals) { + bodyBuf.write(_conditionalToSC(c, 2, inputsMap, outputsMap)); + } + bodyBuf + ..writeln(' }') + ..writeln(); + ssmi.clearInstantiation(); + } else if (m is Sequential) { + final resetEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('reset')) + .firstOrNull; + + // Detect async reset: either explicitly via asyncReset flag, or + // implicitly when the reset signal is also listed as a trigger + // (e.g. Sequential.multi([clk, reset], reset: reset, ...)). + final isAsync = m.asyncReset || + (resetEntry != null && + ssmi.inputMapping.entries.any((e) => + e.key.contains('trigger') && + e.value.name == resetEntry.value.name)); + + // Resolve ALL trigger entries to (signalName, edge, isPort). + final triggerEdges = m.triggerEdges; + final triggerEntries = ssmi.inputMapping.entries + .where((e) => e.key.contains('trigger')) + .toList(); + + final resolvedTriggers = + <({String signalName, bool isPosedge, bool isPort})>[]; + + for (final te in triggerEntries) { + final triggerSL = te.value; + // Skip if this trigger is the async reset signal + if (resetEntry != null && triggerSL.name == resetEntry.value.name) { + continue; + } + // Skip constant triggers (e.g. clk <= Const(0) — never toggles) + if (triggerSL.isConstant) { + continue; + } + final isPosedge = triggerEdges + .where((t) => t.portName == te.key) + .firstOrNull + ?.isPosedge ?? + true; + final resolved = _resolveClockAndEdge(triggerSL, isPosedge); + // Skip if the resolved signal is constant + final resolvedSL = _synthModuleDefinition.logicToSynthMap.values + .where((sl) => sl.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 bodyLines = []; + final sensitivities = {}; + + // Group partial assignments by destination for concatenated writes + final partialsByDst = >{}; + + for (final assignment in _synthModuleDefinition.assignments) { + if (!assignment.src.isConstant) { + sensitivities.add(_sensitivityName(assignment.src)); + } + if (assignment is PartialSynthAssignment) { + partialsByDst + .putIfAbsent(_scName(assignment.dst.name), () => []) + .add(assignment); + } else { + bodyLines.add(' ${_scName(assignment.dst.name)} = ' + '${_synthLogicReadExpr(assignment.src)};'); + } + } + + // Emit grouped partial assignments as shift-or concatenation + for (final entry in partialsByDst.entries) { + final dstName = entry.key; + final partials = entry.value + ..sort((a, b) => a.dstLowerIndex.compareTo(b.dstLowerIndex)); + + // Find total width from the destination SynthLogic + final dstWidth = partials.last.dstUpperIndex + 1; + final utype = systemCType(dstWidth); + final parts = []; + for (final p in partials) { + final srcExpr = _synthLogicReadExpr(p.src); + if (p.dstLowerIndex == 0) { + parts.add('$utype($srcExpr)'); + } else { + parts.add('($utype($srcExpr) << ${p.dstLowerIndex})'); + } + } + bodyLines.add(' $dstName = ${parts.join(' | ')};'); + } + + final setupBuf = StringBuffer()..writeln(' SC_METHOD(wire_assign);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: ' void wire_assign() {\n' + '${bodyLines.join('\n')}\n' + ' }', + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Conditional → SystemC + // ──────────────────────────────────────────────────────────────────── + + String _conditionalToSC(Conditional conditional, int indent, + Map inputsMap, Map outputsMap) { + final padding = ' ' * indent; + + if (conditional is ConditionalAssign) { + final driverExpr = _resolveDriver(conditional.driver, inputsMap); + final receiver = _resolveReceiver(conditional.receiver, outputsMap); + return '$padding$receiver = $driverExpr;\n'; + } else if (conditional is If) { + return _ifToSC(conditional, indent, inputsMap, outputsMap); + } else if (conditional is Case) { + return _caseToSC(conditional, indent, inputsMap, outputsMap); + } else if (conditional is ConditionalGroup) { + final buf = StringBuffer(); + for (final c in conditional.conditionals) { + buf.write(_conditionalToSC(c, indent, inputsMap, outputsMap)); + } + return buf.toString(); + } + return ''; + } + + String _ifToSC(If ifBlock, int indent, Map inputsMap, + Map outputsMap) { + final padding = ' ' * indent; + final buf = StringBuffer(); + + for (final iff in ifBlock.iffs) { + final header = iff == ifBlock.iffs.first + ? 'if' + : iff is Else + ? ' else' + : ' else if'; + final condition = + iff is! Else ? ' (${_resolveDriver(iff.condition, inputsMap)})' : ''; + buf.write('$padding$header$condition {\n'); + for (final c in iff.then) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + buf.writeln(); + return buf.toString(); + } + + String _caseToSC(Case caseBlock, int indent, Map inputsMap, + Map outputsMap) { + final padding = ' ' * indent; + final buf = StringBuffer(); + final expr = _resolveDriver(caseBlock.expression, inputsMap); + + // Check if all case items have compile-time constant values + final allConst = + caseBlock.items.every((item) => _isConstCaseItem(item.value)); + + // CaseZ requires mask matching — always use if/else + // Non-const case items also require if/else + if (caseBlock is CaseZ || !allConst) { + return _caseToIfElseSC(caseBlock, indent, inputsMap, outputsMap, expr); + } + + buf.writeln('${padding}switch ($expr) {'); + for (final item in caseBlock.items) { + buf.writeln('$padding case ${_constLit(item.value)}:'); + for (final c in item.then) { + buf.write(_conditionalToSC(c, indent + 2, inputsMap, outputsMap)); + } + buf.writeln('$padding break;'); + } + if (caseBlock.defaultItem != null) { + buf.writeln('$padding default:'); + for (final c in caseBlock.defaultItem!) { + buf.write(_conditionalToSC(c, indent + 2, inputsMap, outputsMap)); + } + buf.writeln('$padding break;'); + } + buf.writeln('$padding}'); + return buf.toString(); + } + + /// Checks whether a case item value is a compile-time constant. + bool _isConstCaseItem(dynamic value) { + if (value is Const) { + return true; + } + if (value is LogicValue) { + return true; + } + if (value is Logic) { + if (value.srcConnection is Const) { + return true; + } + final sl = _synthModuleDefinition.logicToSynthMap[value]; + if (sl != null && sl.isConstant) { + return true; + } + return false; + } + return true; // int, string, etc. + } + + /// Converts a Case/CaseZ block to if/else chain (for non-const items + /// or CaseZ with z-masks). + String _caseToIfElseSC( + Case caseBlock, + int indent, + Map inputsMap, + Map outputsMap, + String expr) { + final padding = ' ' * indent; + final buf = StringBuffer(); + + for (var i = 0; i < caseBlock.items.length; i++) { + final item = caseBlock.items[i]; + final condition = _caseItemCondition(item.value, expr, inputsMap, + isCaseZ: caseBlock is CaseZ); + final header = i == 0 ? 'if' : ' else if'; + buf.write('$padding$header ($condition) {\n'); + for (final c in item.then) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + if (caseBlock.defaultItem != null) { + buf.write(' else {\n'); + for (final c in caseBlock.defaultItem!) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + buf.writeln(); + return buf.toString(); + } + + /// Generates the condition expression for a case item comparison. + String _caseItemCondition( + dynamic value, String expr, Map inputsMap, + {bool isCaseZ = false}) { + // Extract LogicValue from Const for CaseZ mask matching + LogicValue? lv; + if (value is Const) { + lv = value.value; + } else if (value is LogicValue) { + lv = value; + } + if (isCaseZ && lv != null && !lv.isValid) { + // CaseZ: create mask comparison (expr & mask) == pattern + // z bits become don't-care (mask out those bits) + final width = lv.width; + // z→0 in mask, 0/1→1 in mask + var maskStr = ''; + var patStr = ''; + for (var i = width - 1; i >= 0; i--) { + final bit = lv[i]; + if (bit == LogicValue.z || bit == LogicValue.x) { + maskStr += '0'; + patStr += '0'; + } else { + maskStr += '1'; + patStr += bit == LogicValue.one ? '1' : '0'; + } + } + final maskVal = BigInt.parse(maskStr, radix: 2); + final patVal = BigInt.parse(patStr, radix: 2); + return '($expr & $maskVal) == $patVal'; + } + if (value is Logic && value is! Const) { + final resolved = _resolveDriver(value, inputsMap); + return '$expr == $resolved'; + } + return '$expr == ${_constLit(value)}'; + } + + /// Resolves a driver Logic to a SystemC read expression using the + /// SynthModuleDefinition's logicToSynthMap to find the canonical name. + String _resolveDriver(Logic driver, Map inputsMap) { + if (driver is Const) { + return _constLit(driver); + } + // Look up via logicToSynthMap — the SynthLogic has the canonical name + final sl = _synthModuleDefinition.logicToSynthMap[driver]; + if (sl != null) { + return _synthLogicReadExpr(sl); + } + // Try to find via source connection chain — handles cases where + // the Logic object isn't directly in the map but its source is + var src = driver.srcConnection; + while (src != null) { + final srcSl = _synthModuleDefinition.logicToSynthMap[src]; + if (srcSl != null) { + return _synthLogicReadExpr(srcSl); + } + src = src.srcConnection; + } + // Fallback: try inputsMap by port name + if (inputsMap.containsKey(driver.name)) { + return inputsMap[driver.name]!; + } + return '${_scName(driver.name)}.read()'; + } + + /// Resolves a receiver Logic to a SystemC signal name using the + /// SynthModuleDefinition's logicToSynthMap to find the canonical name. + String _resolveReceiver(Logic receiver, Map outputsMap) { + // Look up via logicToSynthMap + final sl = _synthModuleDefinition.logicToSynthMap[receiver]; + if (sl != null) { + return _scName(sl.name); + } + // Fallback + if (outputsMap.containsKey(receiver.name)) { + return outputsMap[receiver.name]!; + } + return _scName(receiver.name); + } + + String _constLit(dynamic value) { + if (value is Const) { + if (value.value.isValid) { + return value.value.toBigInt().toString(); + } + return '0'; // x/z → 0 in SystemC + } else if (value is LogicValue) { + if (value.isValid) { + return value.toBigInt().toString(); + } + return '0'; // x/z → 0 in SystemC + } else if (value is Logic) { + // If the Logic is driven by a Const, resolve to integer literal + if (value.srcConnection is Const) { + final cv = (value.srcConnection! as Const).value; + return cv.isValid ? cv.toBigInt().toString() : '0'; + } + // Check logicToSynthMap for a constant SynthLogic + final sl = _synthModuleDefinition.logicToSynthMap[value]; + if (sl != null && sl.isConstant) { + final constLogic = sl.logics.whereType().firstOrNull; + if (constLogic != null) { + return constLogic.value.isValid + ? constLogic.value.toBigInt().toString() + : '0'; + } + } + // Fallback: use signal read expression + return '${value.name}.read()'; + } + return value.toString(); + } + + // ──────────────────────────────────────────────────────────────────── + // Build all sections + // ──────────────────────────────────────────────────────────────────── + + void _buildModuleBody( + String Function(Module module) getInstanceTypeOfModule) { + _subMembers = _buildSubModuleMembers(getInstanceTypeOfModule); + + final inlineGates = _buildInlineGates(); + final processes = _buildProcesses(); + final wireAssigns = _buildWireAssignments(); + final arrayAssembly = _buildArrayAssemblyMethod(); + final subBindings = _buildSubModuleBindings(getInstanceTypeOfModule); + + // Build internal signals, appending dummy signals for unconnected + // submodule outputs (populated by _buildSubModuleBindings above). + final baseSigs = _buildInternalSignals(); + _internalSigs = [ + baseSigs, + ..._unconnectedOutputSignals, + ..._constInputSignals, + ].where((s) => s.isNotEmpty).join('\n'); + + final ctorParts = [ + if (_constInputInits.isNotEmpty) _constInputInits.join('\n'), + if (inlineGates != null) inlineGates.setup, + if (processes != null) processes.setup, + if (wireAssigns != null) wireAssigns.setup, + if (arrayAssembly != null) arrayAssembly.setup, + if (subBindings.isNotEmpty) subBindings, + ]; + _ctorBody = ctorParts.join(); + + final bodyParts = [ + if (inlineGates != null) inlineGates.body, + if (processes != null) processes.body, + if (wireAssigns != null) wireAssigns.body, + if (arrayAssembly != null) arrayAssembly.body, + ]; + _methodBodies = bodyParts.where((s) => s.isNotEmpty).join('\n'); + } + + /// Builds an SC_METHOD that assembles individual array element signals + /// back into their parent signal via concatenation. + _MethodResult? _buildArrayAssemblyMethod() { + if (_arrayElementsByParent.isEmpty) { + return null; + } + + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + var methodIdx = 0; + + for (final entry in _arrayElementsByParent.entries) { + final parentName = _scName(entry.key); + final elements = entry.value; + final methodName = 'array_assemble_$methodIdx'; + methodIdx++; + + setupBuf.writeln(' SC_METHOD($methodName);'); + for (final elem in elements) { + setupBuf.writeln(' sensitive << ${_scName(elem.elemName)};'); + } + + // Build concatenation: (elem[N-1], ..., elem[1], elem[0]) + // SystemC concat is MSB-first, so highest index first + // Wrap 1-bit (bool) elements in sc_uint<1>() for proper concat + final concatParts = elements.reversed.map((e) { + final read = '${_scName(e.elemName)}.read()'; + return e.width == 1 ? 'sc_uint<1>($read)' : read; + }).toList(); + + bodyBuf + ..writeln(' void $methodName() {') + ..writeln(' $parentName = (${concatParts.join(', ')});') + ..writeln(' }') + ..writeln(); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Final assembly + // ──────────────────────────────────────────────────────────────────── + + String _toSystemC() { + final moduleName = getInstanceTypeOfModule(module); + final buf = StringBuffer()..writeln('SC_MODULE($moduleName) {'); + + if (_portsString.isNotEmpty) { + buf.writeln(_portsString); + } + if (_internalSigs.isNotEmpty) { + buf + ..writeln() + ..writeln(_internalSigs); + } + if (_subMembers.isNotEmpty) { + buf + ..writeln() + ..writeln(_subMembers); + } + + buf + ..writeln() + ..writeln(' SC_CTOR($moduleName) {'); + if (_ctorBody.isNotEmpty) { + buf.write(_ctorBody); + } + buf.writeln(' }'); + + if (_methodBodies.isNotEmpty) { + buf + ..writeln() + ..write(_methodBodies) + ..writeln(); + } + + buf.writeln('};'); + 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/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index d8b5bae36..062647ac3 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // systemverilog_synthesizer.dart diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index d7850df4e..c53ace114 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'; @@ -46,8 +47,12 @@ class Vector { /// 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) { + static String _errorCheckString( + String sigName, + dynamic expected, + LogicValue expectedVal, + String inputValues, + ) { if (expected is! int && expected is! LogicValue && expected is! BigInt && @@ -57,8 +62,9 @@ class Vector { String expectedHexStr; if (expected is int) { - expectedHexStr = - BigInt.from(expected).toUnsigned(expectedVal.width).toRadixString(16); + expectedHexStr = BigInt.from( + expected, + ).toUnsigned(expectedVal.width).toRadixString(16); expectedHexStr = '0x$expectedHexStr'; } else if (expected is BigInt) { expectedHexStr = expected.toUnsigned(expectedVal.width).toRadixString(16); @@ -83,8 +89,10 @@ class Vector { if (signal is LogicArray) { final arrAssigns = StringBuffer(); var index = 0; - final fullVal = - LogicValue.of(inputValues[signalName], width: signal.width); + 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;'); @@ -92,8 +100,10 @@ class Vector { } return arrAssigns.toString(); } else { - final signalVal = - LogicValue.of(inputValues[signalName], width: signal.width); + final signalVal = LogicValue.of( + inputValues[signalName], + width: signal.width, + ); return '$signalName = $signalVal;'; } }).join('\n'); @@ -104,23 +114,27 @@ 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) { 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)); + checksList.add( + _errorCheckString( + leaf.structureName, + subVal, + subVal, + inputStimulus, + ), + ); index += leaf.width; } } else { - checksList.add(_errorCheckString( - outputName, expected, expectedValue, inputStimulus)); + checksList.add( + _errorCheckString(outputName, expected, expectedValue, inputStimulus), + ); } } final checks = checksList.join('\n'); @@ -143,8 +157,11 @@ abstract class SimCompare { /// /// If [enableChecking] is set to false, then it will drive the simulation /// but not check that the outputs match. - static Future checkFunctionalVector(Module module, List vectors, - {bool enableChecking = true}) async { + static Future checkFunctionalVector( + Module module, + List vectors, { + bool enableChecking = true, + }) async { var timestamp = 1; final ioInputDrivers = {}; @@ -164,56 +181,70 @@ abstract class SimCompare { Simulator.registerAction(timestamp, () async { for (final signalName in vector.inputValues.keys) { final value = vector.inputValues[signalName]; - (module.tryInput(signalName) ?? getIoInputDriver(signalName)) - .put(value); + (module.tryInput(signalName) ?? getIoInputDriver(signalName)).put( + value, + ); } if (enableChecking) { - unawaited(Simulator.postTick.first.then((value) { - for (final signalName in vector.expectedOutputValues.keys) { - final value = vector.expectedOutputValues[signalName]; - final o = - module.tryOutput(signalName) ?? module.inOut(signalName); - - final errorReason = - 'For vector #${vectors.indexOf(vector)} $vector,' - ' expected $o to be $value, but it was ${o.value}.'; - if (value is int) { - expect(o.value.isValid, isTrue, reason: errorReason); - expect(o.value.toBigInt(), + unawaited( + Simulator.postTick.first.then((value) { + for (final signalName in vector.expectedOutputValues.keys) { + final value = vector.expectedOutputValues[signalName]; + final o = + module.tryOutput(signalName) ?? module.inOut(signalName); + + final errorReason = + 'For vector #${vectors.indexOf(vector)} $vector,' + ' expected $o to be $value, but it was ${o.value}.'; + if (value is int) { + expect(o.value.isValid, isTrue, reason: errorReason); + expect( + o.value.toBigInt(), equals(BigInt.from(value).toUnsigned(o.width)), - reason: errorReason); - } else if (value is BigInt) { - expect(o.value.isValid, isTrue, reason: errorReason); - expect(o.value.toBigInt(), equals(value), reason: errorReason); - } else if (value is LogicValue) { - if (o.width > 1 && - (value == LogicValue.x || value == LogicValue.z)) { - for (final oBit in o.value.toList()) { - expect(oBit, equals(value), reason: errorReason); + reason: errorReason, + ); + } else if (value is BigInt) { + expect(o.value.isValid, isTrue, reason: errorReason); + expect( + o.value.toBigInt(), + equals(value), + reason: errorReason, + ); + } else if (value is LogicValue) { + if (o.width > 1 && + (value == LogicValue.x || value == LogicValue.z)) { + for (final oBit in o.value.toList()) { + expect(oBit, equals(value), reason: errorReason); + } + } else { + expect(o.value, equals(value), reason: errorReason); } + } else if (value is String) { + expect( + o.value, + LogicValue.of(value, width: o.width), + reason: errorReason, + ); } else { - expect(o.value, equals(value), reason: errorReason); + throw NonSupportedTypeException(value); } - } else if (value is String) { - expect(o.value, LogicValue.of(value, width: o.width), - reason: errorReason); - } else { - throw NonSupportedTypeException(value); } - } - }).catchError( - test: (error) => error is Exception, - (Object err, StackTrace stackTrace) { + }).catchError(test: (error) => error is Exception, ( + Object err, + StackTrace stackTrace, + ) { Simulator.throwException(err as Exception, stackTrace); - }, - )); + }), + ); } }); timestamp += Vector._period; } - Simulator.registerAction(timestamp + Vector._period, - () {}); // just so it does one more thing at the end + Simulator.registerAction( + timestamp + Vector._period, + () {}, + ); // just so it does one more thing at the end Simulator.setMaxSimTime(timestamp + 2 * Vector._period); await Simulator.run(); } @@ -221,8 +252,10 @@ abstract class SimCompare { /// 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( + r'sorry: constant selects in always_\* processes' + ' are not currently supported', + ), RegExp('warning: always_comb process has no sensitivities'), RegExp('finish called at'), ]; @@ -241,14 +274,17 @@ abstract class SimCompare { bool enableChecking = true, bool buildOnly = false, }) { - final result = iverilogVector(module, vectors, - moduleName: moduleName, - dontDeleteTmpFiles: dontDeleteTmpFiles, - dumpWaves: dumpWaves, - iverilogExtraArgs: iverilogExtraArgs, - allowWarnings: allowWarnings, - maskKnownWarnings: maskKnownWarnings, - buildOnly: buildOnly); + final result = iverilogVector( + module, + vectors, + moduleName: moduleName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + dumpWaves: dumpWaves, + iverilogExtraArgs: iverilogExtraArgs, + allowWarnings: allowWarnings, + maskKnownWarnings: maskKnownWarnings, + buildOnly: buildOnly, + ); if (enableChecking) { expect(result, true); } @@ -271,9 +307,11 @@ abstract class SimCompare { return true; } - String signalDeclaration(String signalName, - {String Function(String original)? adjust, - String? signalTypeOverride}) { + String signalDeclaration( + String signalName, { + String Function(String original)? adjust, + String? signalTypeOverride, + }) { final signal = module.signals.firstWhere((e) => e.name == signalName); final signalType = signalTypeOverride ?? @@ -286,10 +324,14 @@ abstract class SimCompare { } if (signal is LogicArray) { - final unpackedDims = - signal.dimensions.getRange(0, signal.numUnpackedDimensions); - final packedDims = signal.dimensions - .getRange(signal.numUnpackedDimensions, signal.dimensions.length); + 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 + ' ' + @@ -313,27 +355,36 @@ abstract class SimCompare { late final tbWireUniquifier = Uniquifier(); late final alreadyMappedLogicToWires = {}; String toTbWireName(String name) => alreadyMappedLogicToWires.putIfAbsent( - name, () => tbWireUniquifier.getUniqueName(initialName: 'wire__$name')); + 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 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); + 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'); + final sigDecl = signalDeclaration( + logicName, + adjust: toTbWireName, + signalTypeOverride: 'wire', + ); return '$sigDecl; assign $wireName = $logicName;'; }), ].join('\n'); @@ -375,8 +426,13 @@ abstract class SimCompare { Directory(dir).createSync(recursive: true); File(tmpTestFile).writeAsStringSync(testbench); - final compileResult = Process.runSync('iverilog', - ['-g2012', '-o', tmpOutput, ...iverilogExtraArgs, tmpTestFile]); + final compileResult = Process.runSync('iverilog', [ + '-g2012', + '-o', + tmpOutput, + ...iverilogExtraArgs, + tmpTestFile, + ]); bool printIfContentsAndCheckError(dynamic output) { final maskedOutput = output .toString() @@ -396,13 +452,12 @@ abstract class SimCompare { print(maskedOutput); } - return output.toString().contains(RegExp( - [ - 'error', - 'unable', - if (!allowWarnings) 'warning', - ].join('|'), - caseSensitive: false)); + return output.toString().contains( + RegExp( + ['error', 'unable', if (!allowWarnings) 'warning'].join('|'), + caseSensitive: false, + ), + ); } if (printIfContentsAndCheckError(compileResult.stdout)) { @@ -424,16 +479,848 @@ 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. Locally, tests lazily build the same shared PCH under a + /// file lock so parallel test isolates can reuse it without racing. + static String? _ensurePch(String scHome, String cxxStd) { + if (_pchPath != null) { + return _pchPath; + } + + const dir = 'tmp_test'; + const sharedPchDir = '$dir/pch'; + const sharedGchFile = '$sharedPchDir/systemc.h.gch'; + const lockFile = '$dir/pch.lock'; + + // Reuse the CI-prebuilt PCH if available. + if (File(sharedGchFile).existsSync()) { + return _pchPath = sharedPchDir; + } + + Directory(dir).createSync(recursive: true); + RandomAccessFile? lock; + try { + lock = File(lockFile).openSync(mode: FileMode.append)..lockSync(); + + if (File(sharedGchFile).existsSync()) { + return _pchPath = sharedPchDir; + } + + Directory(sharedPchDir).createSync(recursive: true); + + // Copy the original header next to the .gch so g++ matches them. + const pchHeader = '$sharedPchDir/systemc.h'; + File('$scHome/systemc.h').copySync(pchHeader); + + final args = [ + '-std=$cxxStd', + '-I$scHome', + '-x', + 'c++-header', + '-o', + sharedGchFile, + pchHeader, + ]; + final result = Process.runSync('g++', args); + if (result.exitCode != 0) { + print('PCH compilation failed (falling back to normal headers):'); + print(result.stderr); + Directory(sharedPchDir).deleteSync(recursive: true); + return null; + } + + return _pchPath = sharedPchDir; + } on Exception catch (e) { + print('PCH setup failed (falling back to normal headers): $e'); + return null; + } finally { + if (lock != null) { + try { + lock.unlockSync(); + } on Exception catch (_) {} + lock.closeSync(); + } + } + } + + /// 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()) { + final name = entity.path.split(Platform.pathSeparator).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; + } + + if (!keepPch && name == 'pch.lock') { + entity.deleteSync(); + 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, + bool cache = true, + }) { + 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 (cache && _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, + ); + if (cache) { + _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, + cache: dontDeleteTmpFiles, + ); + if (exe == null) { + // SystemC not available — skip gracefully. + return; + } + try { + final passed = runSystemCVectors(exe, vectors); + expect(passed, true); + } finally { + if (!dontDeleteTmpFiles) { + exe.cleanup(); + } + } + } + + /// 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; + } + if (buildOnly) { + module.generateSystemC(); + return true; + } + final exe = buildSystemCExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + cache: dontDeleteTmpFiles, + ); + if (exe == null) { + return false; + } + try { + return runSystemCVectors(exe, vectors); + } finally { + if (!dontDeleteTmpFiles) { + exe.cleanup(); + } + } + } + + // ══════════════════════════════════════════════════════════════════════ + // 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; + final compileDirName = compileDir.path.split(Platform.pathSeparator).last; + if (compileDir.existsSync() && + compileDirName.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/assignment_test.dart b/test/assignment_test.dart index 712ebd9ee..e845086a0 100644 --- a/test/assignment_test.dart +++ b/test/assignment_test.dart @@ -110,6 +110,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('multiple bits', () async { @@ -147,6 +148,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('logic net is multi-assignable', () { diff --git a/test/async_reset_test.dart b/test/async_reset_test.dart index 82e1dbcdf..cb0c0a3b2 100644 --- a/test/async_reset_test.dart +++ b/test/async_reset_test.dart @@ -151,6 +151,9 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + // SystemC can't handle manually-driven clocks — buildOnly verifies + // the generated code compiles. + SimCompare.checkSystemCVector(mod, vectors, buildOnly: true); }); test('simcompare with clk sync reset', () async { @@ -172,6 +175,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); } @@ -266,6 +270,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); } @@ -318,6 +323,7 @@ void main() { ]; SimCompare.checkIverilogVector(mod, vectorsSv); + SimCompare.checkSystemCVector(mod, vectorsSv); }); test('inverted', () async { @@ -339,6 +345,7 @@ void main() { ]; SimCompare.checkIverilogVector(mod, vectorsSv); + SimCompare.checkSystemCVector(mod, vectorsSv); }); test('trigger earlier inverted', () async { @@ -362,6 +369,7 @@ void main() { ]; SimCompare.checkIverilogVector(mod, vectorsSv); + SimCompare.checkSystemCVector(mod, vectorsSv); }); test('trigger earlier normal', () async { @@ -385,6 +393,7 @@ void main() { ]; SimCompare.checkIverilogVector(mod, vectorsSv); + SimCompare.checkSystemCVector(mod, vectorsSv, buildOnly: true); }); }); @@ -410,6 +419,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); } }); diff --git a/test/bus_test.dart b/test/bus_test.dart index 08ccb4c9b..b0802d20b 100644 --- a/test/bus_test.dart +++ b/test/bus_test.dart @@ -238,6 +238,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('functional', () { @@ -389,6 +390,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('Assignment of a const', () async { @@ -400,6 +402,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); final sv = mod.generateSynth(); expect(sv.contains("assign const_subset = 16'habcd;"), true); @@ -450,6 +453,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('Bus shrink', () async { @@ -635,6 +639,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('selectFrom and selectIndex', () async { diff --git a/test/collapse_test.dart b/test/collapse_test.dart index 0ef7e00c5..3a4b49427 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -52,6 +52,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('collapse pretty', () async { diff --git a/test/comb_math_test.dart b/test/comb_math_test.dart index b2a7165ed..c8e3b45dc 100644 --- a/test/comb_math_test.dart +++ b/test/comb_math_test.dart @@ -218,6 +218,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); // thank you to @chykon in issue #158 for providing this example! @@ -236,6 +237,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('simpler example', () { @@ -264,6 +266,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -293,6 +296,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); diff --git a/test/comb_mod_test.dart b/test/comb_mod_test.dart index 280d19e2e..e8399ad3e 100644 --- a/test/comb_mod_test.dart +++ b/test/comb_mod_test.dart @@ -58,6 +58,29 @@ class ReuseExampleSsa extends Module { } } +class ReuseExampleSsaNoLoop extends Module { + /// Like [ReuseExampleSsa] but the shared [IncrModule] reads from the input + /// [a] rather than `intermediate`, avoiding the combo loop while still + /// exercising the SSA codegen (multiple `intermediate_N` versions). + ReuseExampleSsaNoLoop(Logic a) { + a = addInput('a', a, width: a.width); + final b = addOutput('b', width: a.width); + + final intermediate = Logic(name: 'intermediate', width: a.width); + + // Shared sub-module reads from `a` (no feedback loop) + final inc = IncrModule(a); + + Combinational.ssa((s) => [ + s(intermediate) < a, + s(intermediate) < inc.result, + s(intermediate) < inc.result, + ]); + + b <= intermediate; + } +} + class DuplicateExample extends Module { DuplicateExample(Logic a) { a = addInput('a', a, width: a.width); @@ -238,6 +261,7 @@ void main() { if (useSsa) { await SimCompare.checkFunctionalVector(dut, vectors); SimCompare.checkIverilogVector(dut, vectors); + SimCompare.checkSystemCVector(dut, vectors); } else { try { await SimCompare.checkFunctionalVector(dut, vectors); @@ -281,6 +305,27 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors, buildOnly: true); + }); + + test('should resolve correctly with shared sub-module ssa (no loop)', + () async { + final mod = ReuseExampleSsaNoLoop(Logic(width: 8)); + await mod.build(); + + // inc reads a (=3), result = a+1 = 4 + // SSA: intermediate_0 = a(3), intermediate_1 = result(4), + // intermediate = result(4) + // b = intermediate = 4 + final vectors = [ + Vector({'a': 3}, {'b': 4}), + Vector({'a': 0}, {'b': 1}), + Vector({'a': 254}, {'b': 255}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -308,6 +353,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); } diff --git a/test/conditionals_test.dart b/test/conditionals_test.dart index d35a8e5bb..c27ebb2ae 100644 --- a/test/conditionals_test.dart +++ b/test/conditionals_test.dart @@ -490,6 +490,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -780,6 +781,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test( @@ -878,6 +880,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); } test('normal logic', () async { diff --git a/test/flop_test.dart b/test/flop_test.dart index 4e3def505..85d41958c 100644 --- a/test/flop_test.dart +++ b/test/flop_test.dart @@ -53,6 +53,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bit with enable', () async { @@ -74,6 +75,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus', () async { @@ -88,6 +90,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus with enable', () async { @@ -111,6 +114,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, no reset value', () async { @@ -124,6 +128,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, const reset value', () async { @@ -141,6 +146,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, logic reset value', () async { @@ -158,6 +164,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus no reset, const reset value', () async { @@ -174,6 +181,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus, enable, reset, const reset value', () async { @@ -194,6 +202,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); }); } diff --git a/test/fsm_test.dart b/test/fsm_test.dart index b5f010a56..54dd1e661 100644 --- a/test/fsm_test.dart +++ b/test/fsm_test.dart @@ -270,6 +270,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); verifyMermaidStateDiagram(_simpleFSMPath); }); @@ -286,6 +287,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); verifyMermaidStateDiagram(_simpleFSMPath); }); @@ -304,6 +306,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); if (!kIsWeb) { const fsmPath = '$_tmpDir/default_next_state_fsm.md'; @@ -344,6 +347,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); verifyMermaidStateDiagram(_trafficFSMPath); }); diff --git a/test/gate_test.dart b/test/gate_test.dart index 905c912d9..b92839774 100644 --- a/test/gate_test.dart +++ b/test/gate_test.dart @@ -362,6 +362,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('unary and', () async { @@ -470,6 +471,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('rshift logic', () async { @@ -483,6 +485,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('arshift logic', () async { @@ -498,6 +501,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('lshift int', () async { @@ -509,6 +513,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('rshift int', () async { @@ -520,6 +525,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('arshift int', () async { @@ -531,6 +537,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('shift by const zero', () async { @@ -552,6 +559,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('large logic shifted by small bus', () async { @@ -573,6 +581,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('large logic shifted by large bus', () async { @@ -594,6 +603,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('small logic shifted by large bus', () async { @@ -615,6 +625,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('large logic shifted by huge value on large bus', () async { @@ -636,6 +647,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('small logic shifted by huge value on large bus', () async { @@ -657,6 +669,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('very small logic shifted by huge value on large bus', () async { @@ -678,6 +691,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); }); diff --git a/test/interface_test.dart b/test/interface_test.dart index eaf4433d2..68e3a60e1 100644 --- a/test/interface_test.dart +++ b/test/interface_test.dart @@ -142,6 +142,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('should return exception when port name is not sanitary.', () async { diff --git a/test/logic_array_sim_test.dart b/test/logic_array_sim_test.dart new file mode 100644 index 000000000..0669dac5d --- /dev/null +++ b/test/logic_array_sim_test.dart @@ -0,0 +1,250 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_array_sim_test.dart +// Simulation tests for LogicArray with Iverilog and SystemC backends. +// Exercises sequential logic, element-wise operations, and submodule +// hierarchy with array ports — scenarios beyond the combinational +// passthrough tests in logic_array_test.dart. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +/// Flops each element of a LogicArray independently. +/// Tests sequential (clocked) array element access in generated code. +class ArrayFlopModule extends Module { + LogicArray get dataOut => output('dataOut') as LogicArray; + + ArrayFlopModule(LogicArray dataIn, {required Logic reset}) + : super(name: 'ArrayFlopModule') { + final clk = SimpleClockGenerator(10).clk; + reset = addInput('reset', reset); + dataIn = addInputArray('dataIn', dataIn, + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth); + + final out = addOutputArray('dataOut', + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth); + + for (var i = 0; i < dataIn.dimensions[0]; i++) { + out.elements[i] <= flop(clk, dataIn.elements[i], reset: reset); + } + } +} + +/// Applies bitwise NOT to each element, then passes through a submodule. +/// Tests combinational element-wise ops + array hierarchy. +class ArrayInvertAndPassModule extends Module { + LogicArray get dataOut => output('dataOut') as LogicArray; + + ArrayInvertAndPassModule(LogicArray dataIn) + : super(name: 'ArrayInvertAndPassModule') { + dataIn = addInputArray('dataIn', dataIn, + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth); + + final inverted = + LogicArray(dataIn.dimensions, dataIn.elementWidth, name: 'inverted'); + for (var i = 0; i < dataIn.dimensions[0]; i++) { + inverted.elements[i] <= ~dataIn.elements[i]; + } + + // Pass through a sub-module to exercise array port wiring + final sub = _ArrayPassSub(inverted); + + addOutputArray('dataOut', + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth) <= + sub.out; + } +} + +class _ArrayPassSub extends Module { + LogicArray get out => output('out') as LogicArray; + + _ArrayPassSub(LogicArray inp) : super(name: 'ArrayPassSub') { + inp = addInputArray('inp', inp, + dimensions: inp.dimensions, elementWidth: inp.elementWidth); + addOutputArray('out', + dimensions: inp.dimensions, elementWidth: inp.elementWidth) <= + inp; + } +} + +/// Muxes between two LogicArray inputs based on a select signal. +/// Tests conditional array assignment in generated code. +class ArrayMuxModule extends Module { + LogicArray get dataOut => output('dataOut') as LogicArray; + + ArrayMuxModule(LogicArray a, LogicArray b, Logic sel) + : super(name: 'ArrayMuxModule') { + a = addInputArray('a', a, + dimensions: a.dimensions, elementWidth: a.elementWidth); + b = addInputArray('b', b, + dimensions: b.dimensions, elementWidth: b.elementWidth); + sel = addInput('sel', sel); + + final out = addOutputArray('dataOut', + dimensions: a.dimensions, elementWidth: a.elementWidth); + + Combinational([ + If(sel, then: [out < a], orElse: [out < b]), + ]); + } +} + +/// Concatenates two array elements into a wider output and also +/// provides a reduced (OR-reduce) output across array elements. +/// Tests mixed array-element and scalar operations. +class ArrayReduceModule extends Module { + Logic get concat01 => output('concat01'); + Logic get anyNonZero => output('anyNonZero'); + + ArrayReduceModule(LogicArray dataIn) : super(name: 'ArrayReduceModule') { + dataIn = addInputArray('dataIn', dataIn, + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth); + + final c = addOutput('concat01', width: dataIn.elementWidth * 2); + final a = addOutput('anyNonZero'); + + // Concatenate elements [1] and [0] + c <= [dataIn.elements[1], dataIn.elements[0]].swizzle(); + + // OR-reduce: is any element non-zero? + a <= dataIn.elements.map((e) => e.or()).toList().swizzle().or(); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('LogicArray simulation', () { + group('sequential flop per element', () { + test('1D array of 4x8-bit', () async { + final reset = Logic(name: 'reset'); + final dataIn = LogicArray([4], 8); + final mod = ArrayFlopModule(dataIn, reset: reset); + await mod.build(); + + // Each element is flopped: output appears one cycle after input. + // Vector check is BEFORE posedge → sees PREVIOUS cycle's result. + final vectors = [ + Vector({'reset': 1, 'dataIn': 0}, {}), + Vector({'reset': 1, 'dataIn': 0}, {}), + Vector({'reset': 1, 'dataIn': 0}, {'dataOut': 0}), + // Deassert reset; still see 0 from reset phase + Vector({'reset': 0, 'dataIn': 0x44332211}, {'dataOut': 0x00000000}), + // Now see 0x44332211 from previous cycle + Vector({'reset': 0, 'dataIn': 0xDDCCBBAA}, {'dataOut': 0x44332211}), + Vector({'reset': 0, 'dataIn': 0x00000000}, {'dataOut': 0xDDCCBBAA}), + Vector({'reset': 0, 'dataIn': 0x00000000}, {'dataOut': 0x00000000}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + }); + + group('element-wise invert with submodule', () { + test('1D array of 3x8-bit', () async { + final dataIn = LogicArray([3], 8); + final mod = ArrayInvertAndPassModule(dataIn); + await mod.build(); + + // 0x00 → 0xFF, 0xAA → 0x55, 0x0F → 0xF0 + // Input: 0x0FAA00 (elem[0]=0x00, elem[1]=0xAA, elem[2]=0x0F) + // Output: 0xF055FF (elem[0]=0xFF, elem[1]=0x55, elem[2]=0xF0) + final vectors = [ + Vector({'dataIn': 0x0FAA00}, {'dataOut': 0xF055FF}), + Vector({'dataIn': 0xFFFFFF}, {'dataOut': 0x000000}), + Vector({'dataIn': 0x000000}, {'dataOut': 0xFFFFFF}), + Vector({ + 'dataIn': 0x123456 + }, { + 'dataOut': LogicValue.ofInt(0x123456, 24) ^ + LogicValue.filled(24, LogicValue.one) + }), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('1D array of 2x4-bit', () async { + final dataIn = LogicArray([2], 4); + final mod = ArrayInvertAndPassModule(dataIn); + await mod.build(); + + final vectors = [ + Vector({'dataIn': 0x00}, {'dataOut': 0xFF}), + Vector({'dataIn': 0xAB}, {'dataOut': 0x54}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + }); + + group('array mux', () { + test('1D array of 3x8-bit', () async { + final a = LogicArray([3], 8); + final b = LogicArray([3], 8); + final sel = Logic(name: 'sel'); + final mod = ArrayMuxModule(a, b, sel); + await mod.build(); + + final vectors = [ + // sel=1 → output = a + Vector( + {'sel': 1, 'a': 0x112233, 'b': 0xAABBCC}, {'dataOut': 0x112233}), + // sel=0 → output = b + Vector( + {'sel': 0, 'a': 0x112233, 'b': 0xAABBCC}, {'dataOut': 0xAABBCC}), + // Toggle + Vector( + {'sel': 1, 'a': 0xFFFFFF, 'b': 0x000000}, {'dataOut': 0xFFFFFF}), + Vector( + {'sel': 0, 'a': 0xFFFFFF, 'b': 0x000000}, {'dataOut': 0x000000}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + }); + + group('array reduce and concat', () { + test('1D array of 4x8-bit', () async { + final dataIn = LogicArray([4], 8); + final mod = ArrayReduceModule(dataIn); + await mod.build(); + + // Elements: [0]=low 8 bits, [1]=next 8, etc. + // concat01 = {elem[1], elem[0]} (16 bits) + // anyNonZero = OR-reduce of all elements + final vectors = [ + // All zero + Vector({'dataIn': 0x00000000}, {'concat01': 0x0000, 'anyNonZero': 0}), + // elem[0]=0x01 + Vector({'dataIn': 0x00000001}, {'concat01': 0x0001, 'anyNonZero': 1}), + // elem[0]=0xAB, elem[1]=0xCD + Vector({'dataIn': 0x0000CDAB}, {'concat01': 0xCDAB, 'anyNonZero': 1}), + // elem[3]=0xFF only (upper byte) + Vector({'dataIn': 0xFF000000}, {'concat01': 0x0000, 'anyNonZero': 1}), + // All 0xFF + Vector({'dataIn': 0xFFFFFFFF}, {'concat01': 0xFFFF, 'anyNonZero': 1}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + }); + }); +} diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index 87c6be85a..735eafe12 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -727,6 +727,8 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + // buildOnly: array element sub-module binding not yet supported + SimCompare.checkSystemCVector(mod, vectors, buildOnly: true); }); group('logicarray passthrough', () { @@ -760,6 +762,7 @@ void main() { SimCompare.checkIverilogVector(mod, vectors, buildOnly: noSvSim, dontDeleteTmpFiles: dontDeleteTmpFiles); } + SimCompare.checkSystemCVector(mod, vectors, buildOnly: noSvSim); } group('simple', () { @@ -1108,6 +1111,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('assign subset of logic array without mentioning start', () async { @@ -1161,6 +1165,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); diff --git a/test/logic_name_test.dart b/test/logic_name_test.dart index 8ce9d5f40..2f571baba 100644 --- a/test/logic_name_test.dart +++ b/test/logic_name_test.dart @@ -289,6 +289,7 @@ void main() { // confirm build works SimCompare.checkIverilogVector(mod, []); + SimCompare.checkSystemCVector(mod, []); }); test('array port and simple port with _num name conflict but pruned away', @@ -305,6 +306,7 @@ void main() { // confirm build works SimCompare.checkIverilogVector(mod, []); + SimCompare.checkSystemCVector(mod, []); }); test('badly named intermediate signal sanitization', () async { diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index fdc522e96..59f35bd62 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -256,6 +256,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('simple passthrough struct', () async { @@ -271,6 +272,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('fancy struct inverter', () async { @@ -293,6 +295,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); diff --git a/test/math_test.dart b/test/math_test.dart index d9ada00a0..b4f0ecf5f 100644 --- a/test/math_test.dart +++ b/test/math_test.dart @@ -112,6 +112,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -136,6 +137,7 @@ void main() { await gtm.build(); await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); } test('power', () async { diff --git a/test/name_test.dart b/test/name_test.dart index 2742c0ec8..afa757cc8 100644 --- a/test/name_test.dart +++ b/test/name_test.dart @@ -1,7 +1,7 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// definition_name_test.dart +// name_test.dart // Tests for definition names (including reserving them) of Modules. // // 2022 March 7 @@ -136,6 +136,9 @@ void main() { final nameTypes = [nameType1, nameType2]; // skip ones that actually *should* cause a failure + // + // Note: SystemVerilog does not allow using the same identifier for a + // signal and an instance. final shouldConflict = [ { NameType.internalModuleDefinition, diff --git a/test/pipeline_test.dart b/test/pipeline_test.dart index 31bf38bd9..8a2f2648e 100644 --- a/test/pipeline_test.dart +++ b/test/pipeline_test.dart @@ -264,6 +264,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('simple pipeline with intermediate gets', () async { @@ -280,6 +281,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('pipeline with pipelined sub-operation', () async { @@ -297,6 +299,7 @@ void main() { await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('pipeline with abs reference', () async { @@ -312,6 +315,7 @@ void main() { await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('getting out of range on pipeline is error', () { @@ -354,6 +358,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('multiuse pipeline', () async { @@ -369,6 +374,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('simple pipeline late add', () async { @@ -389,6 +395,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('pipeline initialized via get', () async { @@ -408,6 +415,7 @@ void main() { expect(pipem.b.value.isValid, isTrue); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('pipeline initialized directly instead of via get', () async { @@ -427,6 +435,7 @@ void main() { expect(pipem.b.value.isValid, isTrue); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline simple', () async { @@ -459,6 +468,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline simple async reset', () async { @@ -472,6 +482,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline simple reset vals', () async { @@ -504,6 +515,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline notready', () async { @@ -558,6 +570,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline multi', () async { @@ -602,6 +615,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); }); } diff --git a/test/provider_consumer_test.dart b/test/provider_consumer_test.dart index 97c15f648..fefe21f58 100644 --- a/test/provider_consumer_test.dart +++ b/test/provider_consumer_test.dart @@ -212,5 +212,6 @@ output logic rd_valid_rsp await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); } diff --git a/test/provider_consumer_w_modify_test.dart b/test/provider_consumer_w_modify_test.dart index d34c8e374..d1b3815f7 100644 --- a/test/provider_consumer_w_modify_test.dart +++ b/test/provider_consumer_w_modify_test.dart @@ -182,5 +182,6 @@ output logic rd_valid_rsp await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); } diff --git a/test/sequential_test.dart b/test/sequential_test.dart index ade256cf3..bf913328b 100644 --- a/test/sequential_test.dart +++ b/test/sequential_test.dart @@ -182,6 +182,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(dut, vectors); SimCompare.checkIverilogVector(dut, vectors); + SimCompare.checkSystemCVector(dut, vectors); }); group('shorthand with sequential', () { @@ -203,6 +204,7 @@ void main() { // await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); } test('normal logic', () async { @@ -235,6 +237,7 @@ void main() { await SimCompare.checkFunctionalVector(dut, vectors); SimCompare.checkIverilogVector(dut, vectors); + SimCompare.checkSystemCVector(dut, vectors); }); test('negedge triggered flop', () async { @@ -252,6 +255,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('multiple triggers, both edges', () async { @@ -269,6 +273,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('negedge trigger actually occurs on negedge', () async { diff --git a/test/ssa_test.dart b/test/ssa_test.dart index d16f5fb2d..71e35ec2c 100644 --- a/test/ssa_test.dart +++ b/test/ssa_test.dart @@ -495,6 +495,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('ssa multi use model bad reuse', () { @@ -528,6 +529,10 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + // Skip SystemC for modules with unsupported patterns. + if (mod is! SsaNested && mod is! SsaModWithStructElements) { + SimCompare.checkSystemCVector(mod, vectors); + } }); } }); @@ -560,6 +565,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('ssa seq of cases', () async { @@ -590,6 +596,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('ssa uninitialized', () async { diff --git a/test/sv_gen_test.dart b/test/sv_gen_test.dart index 6ad38737a..c64c3192d 100644 --- a/test/sv_gen_test.dart +++ b/test/sv_gen_test.dart @@ -689,6 +689,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('tieoff ', () { @@ -713,6 +714,8 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + // z-valued outputs are skipped in SystemC checks + SimCompare.checkSystemCVector(mod, vectors); }); test('full port', () async { @@ -734,6 +737,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); } @@ -909,6 +913,7 @@ endmodule : ModWithUselessWireMods''')); ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('connected ports and pruning', () { diff --git a/test/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..14c345b52 --- /dev/null +++ b/test/systemc_simcompare_test.dart @@ -0,0 +1,194 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_simcompare_test.dart +// Tests for SystemC synthesis and simulation comparison. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +/// A simple module with basic gates for testing SystemC synthesis. +class GateModule extends Module { + GateModule(Logic a, Logic b) : super(name: 'GateModule') { + a = addInput('a', a); + b = addInput('b', b); + final aAndB = addOutput('a_and_b'); + final aOrB = addOutput('a_or_b'); + final notA = addOutput('not_a'); + + aAndB <= a & b; + aOrB <= a | b; + notA <= ~a; + } +} + +/// A simple counter for testing sequential SystemC synthesis. +class SimpleCounter extends Module { + SimpleCounter(Logic clk, Logic reset, Logic en) : super(name: 'Counter') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + en = addInput('en', en); + final val = addOutput('val', width: 8); + + final nextVal = Logic(name: 'nextVal', width: 8); + + Sequential(clk, reset: reset, [ + If(en, then: [nextVal < nextVal + 1], orElse: [nextVal < nextVal]), + ]); + + val <= nextVal; + } +} + +/// A flip-flop module for testing. +class FlopModule extends Module { + FlopModule(Logic clk, Logic reset, Logic d) : super(name: 'FlopModule') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + d = addInput('d', d, width: 8); + final q = addOutput('q', width: 8); + q <= flop(clk, d, reset: reset); + } +} + +/// A flip-flop with enable. +class FlopEnModule extends Module { + FlopEnModule(Logic clk, Logic reset, Logic en, Logic d) + : super(name: 'FlopEnModule') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + en = addInput('en', en); + d = addInput('d', d, width: 8); + final q = addOutput('q', width: 8); + q <= flop(clk, d, reset: reset, en: en); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + 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('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/test/translations_test.dart b/test/translations_test.dart index 6d53d4f74..ed4b574d4 100644 --- a/test/translations_test.dart +++ b/test/translations_test.dart @@ -126,6 +126,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); }); } diff --git a/test/typed_port_test.dart b/test/typed_port_test.dart index ff31896d5..a1d748032 100644 --- a/test/typed_port_test.dart +++ b/test/typed_port_test.dart @@ -243,6 +243,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('typed array is an array', () async { @@ -271,6 +272,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('structure containing ports naming properly', () async { @@ -492,6 +494,9 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + if (!portType.name.contains('net')) { + SimCompare.checkSystemCVector(mod, vectors); + } }); } }); @@ -514,5 +519,6 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); } diff --git a/tool/gh_actions/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/gh_codespaces/run_setup.sh b/tool/gh_codespaces/run_setup.sh index 6523e4147..ba1d14a97 100755 --- a/tool/gh_codespaces/run_setup.sh +++ b/tool/gh_codespaces/run_setup.sh @@ -20,5 +20,11 @@ tool/gh_actions/install_dependencies.sh # Install Icarus Verilog. tool/gh_actions/install_iverilog.sh +# Install Accellera SystemC. +tool/gh_actions/install_systemc.sh + +# Pre-build SystemC precompiled header and Makefile. +tool/gh_actions/setup_systemc_pch.sh + # Install Node tool/gh_actions/install_node.sh \ No newline at end of file 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