diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index f2ed2cf65..b74924d3e 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -61,9 +61,18 @@ jobs: - name: Install software - Icarus Verilog run: tool/gh_actions/install_iverilog.sh + - name: Install software - Accellera SystemC + run: tool/gh_actions/install_systemc.sh + + - name: Pre-build SystemC PCH and Makefile + run: tool/gh_actions/setup_systemc_pch.sh + - name: Run project tests run: tool/gh_actions/run_tests.sh + - name: Clean SystemC temporary files + run: tool/gh_actions/cleanup_systemc_tmp.sh + - name: Check temporary test files run: tool/gh_actions/check_tmp_test.sh @@ -71,7 +80,10 @@ jobs: - name: Build dev container and run tests in it uses: devcontainers/ci@v0.3 with: - runCmd: tool/gh_actions/run_tests.sh + runCmd: | + tool/gh_actions/run_tests.sh + tool/gh_actions/cleanup_systemc_tmp.sh + tool/gh_actions/check_tmp_test.sh deploy-documentation: name: Deploy Documentation diff --git a/README.md b/README.md index a996ccccb..1863edac9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ [![Chat](https://img.shields.io/discord/1001179329411166267?label=Chat)](https://discord.gg/jubxF84yGw) [![License](https://img.shields.io/badge/License-BSD--3-blue)](https://github.com/intel/rohd/blob/main/LICENSE) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/intel/rohd/blob/main/CODE_OF_CONDUCT.md) -[![Coverage](https://raw.githubusercontent.com/intel/rohd/refs/heads/badges/coverage/main.svg)](https://github.com/intel/rohd/blob/main/.github/workflows/coverage.yml) ROHD (pronounced like "road") is a framework for describing and verifying hardware in the Dart programming language. @@ -45,7 +44,7 @@ You can also open this repository in a GitHub Codespace to run the example in yo - **Simple and fast build**, free of complex build systems and EDA vendor tools - Can use the excellent pub.dev **package manager** and all the packages it has to offer - Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform dumper** to .vcd file format -- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption +- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** and **SystemC** for integration or downstream tool consumption - **Run-time dynamic** module port definitions (numbers, names, widths, etc.) and internal module logic, including recursive module contents - Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components. - Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. @@ -69,5 +68,6 @@ One of ROHD's goals is to help grow an open-source community around reusable har ROHD is under active development. If you're interested in contributing, have feedback or a question, or found a bug, please see [CONTRIBUTING.md](https://github.com/intel/rohd/blob/main/CONTRIBUTING.md). ---------------- + Copyright (C) 2021-2026 Intel Corporation SPDX-License-Identifier: BSD-3-Clause diff --git a/dart_test.yaml b/dart_test.yaml new file mode 100644 index 000000000..30eaa9f01 --- /dev/null +++ b/dart_test.yaml @@ -0,0 +1,15 @@ +# Test configuration for ROHD. +# +# To exclude FFI-dependent tests (e.g. in CI without native code support): +# dart test --preset no-ffi +# +# To run all tests including FFI (requires native shared libraries): +# dart test + +tags: + ffi: + # Tests requiring dart:ffi and native shared libraries. + +presets: + no-ffi: + exclude_tags: ffi diff --git a/doc/architecture.md b/doc/architecture.md index cc1e775ae..aacf29c35 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -24,7 +24,7 @@ The `Simulator` acts as a statically accessible driver of the overall simulation ### Synthesizer -A separate type of object responsible for taking a `Module` and converting it to some output, such as SystemVerilog. +A separate type of object responsible for taking a `Module` and converting it to some output, such as SystemVerilog or SystemC. ## Organization @@ -44,7 +44,7 @@ Contains a collection of `Module` implementations that can be used as primitive ### Synthesizers -Contains logic for synthesizing `Module`s into some output. It is structured to maximize reusability across different output types (including those not yet supported). +Contains logic for synthesizing `Module`s into some output (e.g. SystemVerilog, SystemC). It is structured to maximize reusability across different output types. ### Utilities diff --git a/doc/user_guide/_docs/A21-generation.md b/doc/user_guide/_docs/A21-generation.md index 5c172eeb9..1c6a51bcd 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,15 +28,45 @@ void main() async { The `generateSynth` function will return a `String` with the SystemVerilog `module` definitions for the top-level it is called on, as well as any sub-modules (recursively). You can dump the entire contents to a file and use it anywhere you would any other SystemVerilog. +## SystemC generation + +ROHD can also generate SystemC (C++ with the SystemC library) from the same hardware description. Use the `generateSystemC` helper method: + +```dart +void main() async { + final myModule = MyModule(); + await myModule.build(); + + final generatedSc = myModule.generateSystemC(); + + // write it to a file + File('myHardware.h').writeAsStringSync(generatedSc); +} +``` + +The generated SystemC uses `SC_MODULE`, `SC_METHOD`, and `SC_CTHREAD` constructs. Combinational logic becomes `SC_METHOD` processes, sequential logic (flip-flops and `Sequential` blocks) sharing the same clock and reset are consolidated into a single `SC_CTHREAD`, and sub-modules are instantiated with port bindings. All signal types map to SystemC equivalents (`bool`, `sc_uint`, `sc_biguint`). + +For more control over SystemC generation, use `SynthBuilder` with `SystemCSynthesizer()` directly. + ## Controlling port types -Generated ports have explicit object and data types by default: inputs are `input wire logic`, outputs are `output var logic`, and inouts are `inout wire logic`. Use a `SystemVerilogSynthesizerConfiguration` to omit either category and rely on SystemVerilog's implicit types: +Generated ports default to `input logic`, `output logic`, and `inout wire`, preserving the traditional ROHD declarations. Use a `SystemVerilogSynthesizerConfiguration` to independently control whether object types, such as `wire` and `var`, and data types, such as `logic`, are explicit for each port direction: ```dart final generatedSv = myModule.generateSynth( configuration: const SystemVerilogSynthesizerConfiguration( - portObjectType: SystemVerilogPortType.implicit, - portDataType: SystemVerilogPortType.implicit, + inputPortType: SystemVerilogPortTypeConfiguration( + objectType: SystemVerilogPortType.explicit, + dataType: SystemVerilogPortType.implicit, + ), + outputPortType: SystemVerilogPortTypeConfiguration( + objectType: SystemVerilogPortType.implicit, + dataType: SystemVerilogPortType.implicit, + ), + inOutPortType: SystemVerilogPortTypeConfiguration( + objectType: SystemVerilogPortType.implicit, + dataType: SystemVerilogPortType.explicit, + ), ), ); ``` diff --git a/doc/user_guide/_get-started/01-overview.md b/doc/user_guide/_get-started/01-overview.md index c1a98cdc1..c30c9f87f 100644 --- a/doc/user_guide/_get-started/01-overview.md +++ b/doc/user_guide/_get-started/01-overview.md @@ -19,7 +19,7 @@ Features of ROHD include: - **Simple and fast build**, free of complex build systems and EDA vendor tools - Can use the excellent pub.dev **package manager** and all the packages it has to offer - Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform dumper** to .vcd file format -- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption +- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** and **SystemC** for integration or downstream tool consumption - **Run-time dynamic** module port definitions (numbers, names, widths, etc.) and internal module logic, including recursive module contents - Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components. - Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. diff --git a/lib/src/module.dart b/lib/src/module.dart index a1cb8ec5c..f61e97b2a 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -14,6 +14,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; import 'package:rohd/src/diagnostics/inspector_service.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc.dart'; import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; @@ -1161,6 +1162,27 @@ abstract class Module { SystemVerilogSynthesizer(configuration: configuration), ).getSynthFileContents().join('\n\n////////////////////\n\n'); } + + /// Returns a synthesized SystemC version of this [Module]. + /// + /// Generates SystemC code that is equivalent to the hardware described by + /// this module, using the same naming strategy as [generateSynth]. + String generateSystemC() { + if (!_hasBuilt) { + throw ModuleNotBuiltException(this); + } + + final synthBuilder = SynthBuilder(this, SystemCSynthesizer()); + final moduleContents = + synthBuilder.getSynthFileContents().map((e) => e.contents).join('\n'); + return '// Generated by ROHD - www.github.com/intel/rohd\n' + '// Generation time: ${Timestamper.stamp()}\n' + '// ROHD Version: ${Config.version}\n' + '\n' + '#include \n' + '\n' + '$moduleContents'; + } } extension on LogicStructure { diff --git a/lib/src/modules/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/signals/const.dart b/lib/src/signals/const.dart index 3885b3d4f..3e6989145 100644 --- a/lib/src/signals/const.dart +++ b/lib/src/signals/const.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // const.dart @@ -9,8 +9,60 @@ part of 'signals.dart'; +/// Returns [preferredRadix] if it is supported for generated literals. +/// +/// Throws a [LogicValueConversionException] for unsupported radices. +int? _validatePreferredRadix(int? preferredRadix) { + if (preferredRadix != null && + !const {2, 8, 10, 16}.contains(preferredRadix)) { + throw LogicValueConversionException( + 'Unsupported preferred radix: $preferredRadix'); + } + + return preferredRadix; +} + +/// Creates an identifier-friendly name for a constant [value]. +/// +/// Fully known values use [preferredRadix], or decimal if none is +/// preferred. Values containing `x` or `z` use binary so no state is lost. +String _constName(LogicValue value, int? preferredRadix) { + final radix = value.isValid ? preferredRadix ?? 10 : 2; + final digits = value.toRadixString( + radix: radix, + includeWidth: false, + sepChar: '', + ); + final prefix = switch (radix) { + 2 => '0b', + 8 => '0o', + 10 => '', + 16 => '0x', + _ => throw StateError('Unexpected radix: $radix'), + }; + + return 'const_$prefix$digits'; +} + /// Represents a [Logic] that never changes value. +/// +/// Attempts to assign, [put], or [inject] a new value throw an +/// [UnassignableException], including through another [Logic] driven by the +/// [Const]. class Const extends Logic { + /// The explanation included when an attempt is made to modify a [Const]. + static const _unassignableMessage = + 'A `Const` value cannot be modified, including through another `Logic` ' + 'driven by it.'; + + /// The preferred radix for displaying this constant in generated outputs. + /// + /// Supported radices are binary (2), octal (8), decimal (10), and + /// hexadecimal (16). If omitted, generated outputs select a radix + /// automatically. A generator may fall back to another radix when the + /// preferred radix cannot represent this constant's value. + final int? preferredRadix; + /// Constructs a [Const] with the specified value. /// /// [val] should be processable by [LogicValue.of]. @@ -18,18 +70,163 @@ class Const extends Logic { /// If a [width] is provided, the [Const] will be that width. If not, and /// [val] is a [LogicValue], the [Const] will be the width of [val]. /// Otherwise, the [Const] will be 1 bit wide. - Const(dynamic val, {int? width, bool fill = false}) + /// + /// [preferredRadix] controls how the constant is displayed in generated + /// outputs and its normalized name. Supported values are 2, 8, 10, and 16. + /// If omitted, generated outputs select a radix automatically and the name + /// uses decimal. Values containing `x` or `z` may fall back to binary. + Const( + dynamic val, { + int? width, + bool fill = false, + int? preferredRadix, + }) : this._( + LogicValue.of( + val, + width: width ?? (val is LogicValue ? val.width : 1), + fill: fill, + ), + preferredRadix: _validatePreferredRadix(preferredRadix), + ); + + /// Constructs a [Const] from an already normalized [value]. + Const._(LogicValue value, {required this.preferredRadix}) : super( - name: 'const_$val', - width: width ?? (val is LogicValue ? val.width : 1), + name: _constName(value, preferredRadix), + width: value.width, // we don't care about maintaining this node unless necessary naming: Naming.unnamed, ) { - _wire.put(val, fill: fill, signalName: name); + _wire + ..put(value, signalName: name) + ..makeImmutable(this, reason: _unassignableMessage); - makeUnassignable(reason: '`Const` signals are unassignable.'); + makeUnassignable(reason: _unassignableMessage); } @override - Const clone({String? name}) => Const(value, width: width); + Const clone({String? name}) => + Const(value, width: width, preferredRadix: preferredRadix); + + @override + void put(dynamic val, {bool fill = false}) => + throw UnassignableException(this, reason: _unassignableMessage); + + @override + void inject(dynamic val, {bool fill = false}) => + throw UnassignableException(this, reason: _unassignableMessage); + + /// Verifies that [other] has the same width as this [Const]. + void _checkMatchingWidth(Logic other) { + if (width != other.width) { + throw PortWidthMismatchException.equalWidth(this, other); + } + } + + @override + Logic operator ~() => Const(~value); + + @override + Logic operator &(Logic other) { + _checkMatchingWidth(other); + + if (other is Const) { + return Const(value & other.value); + } else if (value.isValid && value.isZero) { + return this; + } + + return And2Gate(this, other).out; + } + + @override + Logic operator |(Logic other) { + _checkMatchingWidth(other); + + if (other is Const) { + return Const(value | other.value); + } else if (value.isValid && + value == LogicValue.filled(width, LogicValue.one)) { + return this; + } + + return Or2Gate(this, other).out; + } + + @override + Logic operator ^(Logic other) { + _checkMatchingWidth(other); + + if (other is Const) { + return Const(value ^ other.value); + } + + return Xor2Gate(this, other).out; + } + + @override + Logic operator >>(dynamic other) { + if (Logic._isZeroShiftAmount(other)) { + return this; + } else if (other is Logic && other is! Const) { + return ARShift(this, other).out; + } + + return Const(value >> (other is Const ? other.value : other)); + } + + @override + Logic operator <<(dynamic other) { + if (Logic._isZeroShiftAmount(other)) { + return this; + } else if (other is Logic && other is! Const) { + return LShift(this, other).out; + } + + return Const(value << (other is Const ? other.value : other)); + } + + @override + Logic operator >>>(dynamic other) { + if (Logic._isZeroShiftAmount(other)) { + return this; + } else if (other is Logic && other is! Const) { + return RShift(this, other).out; + } + + return Const(value >>> (other is Const ? other.value : other)); + } + + @override + Logic and() => Const(value.and()); + + @override + Logic or() => Const(value.or()); + + @override + Logic xor() => Const(value.xor()); + + @override + Logic eq(dynamic other) { + if (other is Logic) { + _checkMatchingWidth(other); + return other is Const + ? Const(value.eq(other.value)) + : Equals(this, other).out; + } + + return Const(value.eq(LogicValue.of(other, width: width))); + } + + @override + Logic neq(dynamic other) { + if (other is Logic) { + _checkMatchingWidth(other); + return other is Const + ? Const(value.neq(other.value)) + : NotEquals(this, other).out; + } + + return Const(value.neq(LogicValue.of(other, width: width))); + } } diff --git a/lib/src/signals/logic.dart b/lib/src/signals/logic.dart index a412f0943..f964d320a 100644 --- a/lib/src/signals/logic.dart +++ b/lib/src/signals/logic.dart @@ -445,10 +445,22 @@ class Logic { Logic operator ~() => NotGate(this).out; /// Logical bitwise AND. - Logic operator &(Logic other) => And2Gate(this, other).out; + Logic operator &(Logic other) { + if (other is Const) { + return other & this; + } + + return And2Gate(this, other).out; + } /// Logical bitwise OR. - Logic operator |(Logic other) => Or2Gate(this, other).out; + Logic operator |(Logic other) { + if (other is Const) { + return other | this; + } + + return Or2Gate(this, other).out; + } /// Logical bitwise XOR. Logic operator ^(Logic other) => Xor2Gate(this, other).out; @@ -478,6 +490,10 @@ class Logic { /// /// If [isNet] and [other] is constant, then the result will also be a net. Logic operator >>(dynamic other) { + if (_isZeroShiftAmount(other)) { + return this; + } + if (isNet) { // many SV simulators don't support shifting of nets, so default this final shamt = _constShiftAmount(other); @@ -498,6 +514,10 @@ class Logic { /// /// If [isNet] and [other] is constant, then the result will also be a net. Logic operator <<(dynamic other) { + if (_isZeroShiftAmount(other)) { + return this; + } + if (isNet) { // many SV simulators don't support shifting of nets, so default this final shamt = _constShiftAmount(other); @@ -518,6 +538,10 @@ class Logic { /// /// If [isNet] and [other] is constant, then the result will also be a net. Logic operator >>>(dynamic other) { + if (_isZeroShiftAmount(other)) { + return this; + } + if (isNet) { // many SV simulators don't support shifting of nets, so default this final shamt = _constShiftAmount(other); @@ -543,6 +567,17 @@ class Logic { } } + /// Indicates whether [other] represents a known constant shift of zero. + static bool _isZeroShiftAmount(dynamic other) { + if (other is Const) { + return other.value.isValid && other.value.isZero; + } else if (other is Logic) { + return false; + } else { + return LogicValue.ofInferWidth(other).isZero; + } + } + /// Unary AND. Logic and() => AndUnary(this).out; @@ -927,8 +962,16 @@ class Logic { /// The input [multiplier] cannot be negative or 0; an exception will be /// thrown, otherwise. /// + /// If [multiplier] is 1, then this signal is returned directly, since + /// replicating once is a no-op and would otherwise generate a redundant + /// `1{...}` in the output SystemVerilog. + /// /// If [isNet], then the result will also be a net. Logic replicate(int multiplier) { + if (multiplier == 1) { + return this; + } + if (isNet) { // many SV simulators don't support replication of nets return List.generate(multiplier, (i) => this).swizzle(); diff --git a/lib/src/signals/wire.dart b/lib/src/signals/wire.dart index 883e3eaf0..812b09866 100644 --- a/lib/src/signals/wire.dart +++ b/lib/src/signals/wire.dart @@ -26,6 +26,39 @@ class _Wire { /// The current active value of this signal. LogicValue _currentValue; + /// The [Logic] whose immutability makes this wire immutable. + Logic? _immutableOwner; + + /// Additional context explaining why this wire is immutable. + String? _immutableReason; + + /// Makes this wire immutable on behalf of [owner]. + /// + /// If this wire is already immutable, the original owner and reason are + /// preserved. + void makeImmutable(Logic owner, {String? reason}) { + _immutableOwner ??= owner; + _immutableReason ??= reason; + } + + /// Throws an [UnassignableException] if this wire is immutable, identifying + /// [signalName] as the signal through which the update was attempted. + void _assertMutable({required String signalName}) { + final immutableOwner = _immutableOwner; + if (immutableOwner != null) { + final attemptedUpdateReason = + 'Signal "$signalName" cannot be updated because it is driven by ' + 'immutable signal "$immutableOwner".'; + throw UnassignableException( + immutableOwner, + reason: [ + attemptedUpdateReason, + if (_immutableReason != null) _immutableReason, + ].join(' '), + ); + } + } + /// The last value of this signal before the [Simulator] tick. /// /// This is useful for detecting when to trigger an edge. @@ -143,6 +176,11 @@ class _Wire { /// Tells this [_Wire] to adopt all the behavior of [other] so that /// it can replace [other]. Returns the [_Wire] that has adopted everything. _Wire _adopt(_Wire other) { + if (_immutableOwner == null && other._immutableOwner != null) { + _immutableOwner = other._immutableOwner; + _immutableReason = other._immutableReason; + } + _glitchController.emitter.adopt(other._glitchController.emitter); other._migrateChangedTriggers(this); @@ -218,6 +256,7 @@ class _Wire { /// /// This function calls [put()] in [Simulator.injectAction()]. void inject(dynamic val, {required String signalName, bool fill = false}) { + _assertMutable(signalName: signalName); Simulator.injectAction(() => put(val, signalName: signalName, fill: fill)); } @@ -233,6 +272,8 @@ class _Wire { /// This function is used for propagating glitches through connected signals. /// Use this function for custom definitions of [Module] behavior. void put(dynamic val, {required String signalName, bool fill = false}) { + _assertMutable(signalName: signalName); + var newValue = LogicValue.of(val, fill: fill, width: width); if (newValue.width != width) { 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..5bcafc12a --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart @@ -0,0 +1,1764 @@ +// 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 inout port type for a given width. + static String systemCInOutType(int width) => + 'sc_inout<${systemCType(width)}>'; + + /// SystemC signal type for a given width. + static String systemCSignalType(int width) => + 'sc_signal<${systemCType(width)}>'; + + // ──────────────────────────────────────────────────────────────────── + // Port declarations + // ──────────────────────────────────────────────────────────────────── + + String _systemCPorts() { + final lines = []; + for (final sig in _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"};'); + } + for (final sig in _synthModuleDefinition.inOuts) { + final n = _scName(sig.name); + lines.add(' ${systemCInOutType(sig.width)} $n{"$n"};'); + } + return lines.join('\n'); + } + + // ──────────────────────────────────────────────────────────────────── + // Internal signals + // ──────────────────────────────────────────────────────────────────── + + String _buildInternalSignals() { + final declarations = []; + for (final sig in _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 assignments = <_ScMethodAssignment>[]; + + for (final ssmi in inlineGates) { + final m = ssmi.module; + final sensitivities = {}; + final bodyLines = []; + final destinations = {}; + + // Collect inputs — constants become literals, signals get .read(). + // Inline modules connected to LogicNets can have source ports mapped as + // inouts, so include non-result inout mappings as inputs. + final inputExprs = {}; + final inputMappings = {...ssmi.inputMapping, ...ssmi.inOutMapping}; + if (m is InlineSystemVerilog) { + inputMappings.remove(m.resultSignalName); + } + for (final entry in inputMappings.entries) { + final sl = entry.value; + if (!sl.isConstant) { + sensitivities.add(_sensitivityName(sl)); + } + inputExprs[entry.key] = _synthLogicReadExpr(sl); + } + + if (m is InlineSystemVerilog) { + final resultSynthLogic = ssmi.inlineResultLogic; + if (resultSynthLogic == null) { + continue; + } + final expr = _gateExpression(m, inputExprs); + final dst = _scName(resultSynthLogic.name); + destinations.add(dst); + bodyLines.add(' $dst = $expr;'); + } else if (m is Add) { + // Add has two outputs: sum and carry. + // Emit inline expressions for each used output. + final vals = inputExprs.values.toList(); + final sumPortName = m.sum.name; + for (final entry in ssmi.outputMapping.entries) { + final portName = entry.key; + final dst = _scName(entry.value.name); + destinations.add(dst); + if (portName == sumPortName) { + bodyLines.add(' $dst = ${vals[0]} + ${vals[1]};'); + } else { + // carry: high bit of (width+1)-bit addition + final w = m.width; + final w1 = w + 1; + final utype = systemCType(w1); + final carryExpr = 'static_cast' + '($utype($utype(${vals[0]})' + ' + $utype(${vals[1]}))[$w])'; + bodyLines.add(' $dst = $carryExpr;'); + } + } + } + + if (bodyLines.isEmpty) { + continue; + } + + assignments.add(_ScMethodAssignment( + bodyLines: bodyLines, + sensitivities: sensitivities, + destinations: destinations, + )); + ssmi.clearInstantiation(); + } + + if (assignments.isEmpty) { + return null; + } + + return _emitGroupedAssignments('assign', assignments); + } + + /// Maps an InlineSystemVerilog gate to a C++ expression. + /// + /// Handles all gate types that have SV-specific syntax which needs + /// translation to valid SystemC/C++. + String _gateExpression(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 assignments = <_ScMethodAssignment>[]; + + // Group partial assignments by destination for concatenated writes + final partialsByDst = >{}; + + for (final assignment in _synthModuleDefinition.assignments) { + if (assignment is PartialSynthAssignment) { + partialsByDst + .putIfAbsent(_scName(assignment.dst.name), () => []) + .add(assignment); + } else { + final sensitivities = {}; + if (!assignment.src.isConstant) { + sensitivities.add(_sensitivityName(assignment.src)); + } + final bodyLine = ' ${_scName(assignment.dst.name)} = ' + '${_synthLogicReadExpr(assignment.src)};'; + assignments.add(_ScMethodAssignment( + bodyLines: [bodyLine], + sensitivities: sensitivities, + destinations: {_scName(assignment.dst.name)}, + )); + } + } + + // Emit grouped partial assignments as shift-or concatenation + for (final entry in partialsByDst.entries) { + final dstName = entry.key; + final partials = entry.value + ..sort((a, b) => a.dstLowerIndex.compareTo(b.dstLowerIndex)); + + // Find total width from the destination SynthLogic + final dstWidth = partials.last.dstUpperIndex + 1; + final utype = systemCType(dstWidth); + final parts = []; + final sensitivities = {}; + for (final p in partials) { + if (!p.src.isConstant) { + sensitivities.add(_sensitivityName(p.src)); + } + final srcExpr = _synthLogicReadExpr(p.src); + if (p.dstLowerIndex == 0) { + parts.add('$utype($srcExpr)'); + } else { + parts.add('($utype($srcExpr) << ${p.dstLowerIndex})'); + } + } + assignments.add(_ScMethodAssignment( + bodyLines: [' $dstName = ${parts.join(' | ')};'], + sensitivities: sensitivities, + destinations: {dstName}, + )); + } + + return _emitGroupedAssignments('wire_assign', assignments); + } + + _MethodResult _emitGroupedAssignments( + String methodPrefix, List<_ScMethodAssignment> assignments) { + final groups = <_ScMethodAssignmentGroup>[]; + + for (final assignment in assignments) { + final group = groups.firstWhereOrNull((g) => g.canAdd(assignment)); + if (group == null) { + groups.add(_ScMethodAssignmentGroup()..add(assignment)); + } else { + group.add(assignment); + } + } + + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + + for (var i = 0; i < groups.length; i++) { + final group = groups[i]; + final methodName = '${methodPrefix}_$i'; + setupBuf.writeln(' SC_METHOD($methodName);'); + for (final sig in group.sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + bodyBuf + ..writeln(' void $methodName() {') + ..writeln(group.bodyLines.join('\n')) + ..writeln(' }') + ..writeln(); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Conditional → SystemC + // ──────────────────────────────────────────────────────────────────── + + String _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}); +} + +class _ScMethodAssignment { + final List bodyLines; + final Set sensitivities; + final Set destinations; + + const _ScMethodAssignment({ + required this.bodyLines, + required this.sensitivities, + required this.destinations, + }); +} + +class _ScMethodAssignmentGroup { + final List bodyLines = []; + final Set sensitivities = {}; + final Set destinations = {}; + + bool canAdd(_ScMethodAssignment assignment) => + !assignment.sensitivities.any(destinations.contains) && + !assignment.destinations.any(sensitivities.contains); + + void add(_ScMethodAssignment assignment) { + bodyLines.addAll(assignment.bodyLines); + sensitivities.addAll(assignment.sensitivities); + destinations.addAll(assignment.destinations); + } +} + +/// Collects clocked process data for consolidation by (clock, reset) pair. +class _ClockedGroupData { + final String? resetName; + bool isAsyncReset; + + /// All distinct trigger events (signal name, edge, and whether it's a port). + final List<({String signalName, bool isPosedge, bool isPort})> triggers = []; + + final List resetLines = []; + final List whileBodyLines = []; + _ClockedGroupData({this.resetName, this.isAsyncReset = false}); +} diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 4fa0e8d69..5cd689882 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -588,7 +588,10 @@ class SynthModuleDefinition { // as completely undriven). // make a new const node, it will merge away if not needed - final newReceiverConst = getSynthLogic(Const(receiver.value))!; + final newReceiverConst = getSynthLogic(Const( + receiver.value, + preferredRadix: receiver.preferredRadix, + ))!; internalSignals.add(newReceiverConst); assignments.add(SynthAssignment(newReceiverConst, synthReceiver)); } @@ -810,13 +813,65 @@ class SynthModuleDefinition { } } - return inlineableSubmoduleInstantiations.where((subModuleInstantiation) { - final resultSynthLogic = _inlineResultLogic(subModuleInstantiation); + final collapseCandidates = inlineableSubmoduleInstantiations.where( + (subModuleInstantiation) { + final resultSynthLogic = _inlineResultLogic(subModuleInstantiation); - return resultSynthLogic != null && - singleUseSignals.contains(resultSynthLogic) && - subModuleInstantiation.needsInstantiation; - }); + return resultSynthLogic != null && + singleUseSignals.contains(resultSynthLogic) && + subModuleInstantiation.needsInstantiation; + }, + ).toList(); + + // Keep every module in an inline dependency cycle materialized so that + // rendering expressions cannot recurse through the cycle indefinitely. + final candidateByResult = { + for (final candidate in collapseCandidates) + _inlineResultLogic(candidate)!.resolved: candidate, + }; + final visited = {}; + final activeIndices = {}; + final activePath = []; + final cyclicCandidates = {}; + + void findCycles(SynthSubModuleInstantiation candidate) { + visited.add(candidate); + activeIndices[candidate] = activePath.length; + activePath.add(candidate); + + final resultSignalName = + (candidate.module as InlineSystemVerilog).resultSignalName; + for (final input in [ + ...candidate.inputMapping.values, + ...candidate.inOutMapping.entries + .where((entry) => entry.key != resultSignalName) + .map((entry) => entry.value), + ]) { + final dependency = candidateByResult[input.resolved]; + if (dependency == null) { + continue; + } + + final cycleStart = activeIndices[dependency]; + if (cycleStart != null) { + cyclicCandidates.addAll(activePath.skip(cycleStart)); + } else if (!visited.contains(dependency)) { + findCycles(dependency); + } + } + + activePath.removeLast(); + activeIndices.remove(candidate); + } + + for (final candidate in collapseCandidates) { + if (!visited.contains(candidate)) { + findCycles(candidate); + } + } + + return collapseCandidates + .where((candidate) => !cyclicCandidates.contains(candidate)); } SynthLogic? _inlineResultLogic(SynthSubModuleInstantiation instantiation) { diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index dc585612d..d5f20e783 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -165,6 +165,12 @@ class Namer { bool constNameDisallowed = false, }) { if (constValue != null && !constNameDisallowed) { + final preferredRadix = constValue.preferredRadix; + if (preferredRadix != null && constValue.value.isValid) { + return constValue.value + .toRadixString(radix: preferredRadix, sepChar: ''); + } + return constValue.value.toString(); } diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index f8c07eb71..8d52eebde 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -2,22 +2,25 @@ // SPDX-License-Identifier: BSD-3-Clause // // simcompare.dart -// Helper functionality for unit testing (sv testbench generation, iverilog simulation, vectors, checking/comparison, etc.) +// Helper functionality for unit testing (sv testbench generation, +// iverilog simulation, vectors, checking/comparison, etc.) // // 2021 May 7 // Author: Max Korbel -// ignore_for_file: avoid_print - import 'dart:async'; import 'dart:io'; import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; import 'package:rohd/src/utilities/web.dart'; import 'package:test/test.dart'; +part 'systemverilog_simcompare.dart'; +part 'systemc_simcompare.dart'; + /// Represents a single test case to check in a single clock cycle. /// /// Useful for testing equivalent behavior in different simulation environments. @@ -43,96 +46,9 @@ class Vector { @override String toString() => '$inputValues => $expectedOutputValues'; - /// Computes a SystemVerilog code string that checks in a SystemVerilog - /// simulation whether a signal [sigName] has the [expected] value given - /// the [inputValues]. - static String _errorCheckString(String sigName, dynamic expected, - LogicValue expectedVal, String inputValues) { - if (expected is! int && - expected is! LogicValue && - expected is! BigInt && - expected is! String) { - throw NonSupportedTypeException(expected); - } - - String expectedHexStr; - if (expected is int) { - expectedHexStr = - BigInt.from(expected).toUnsigned(expectedVal.width).toRadixString(16); - expectedHexStr = '0x$expectedHexStr'; - } else if (expected is BigInt) { - expectedHexStr = expected.toUnsigned(expectedVal.width).toRadixString(16); - expectedHexStr = '0x$expectedHexStr'; - } else { - expectedHexStr = expected.toString(); - } - - final expectedValStr = expectedVal.toString(); - - return 'if($sigName !== $expectedValStr) ' - '\$error(\$sformatf("Expected $sigName=$expectedHexStr,' - ' but found $sigName=0x%x (0b%b) with inputs $inputValues",' - ' $sigName, $sigName));'; - } - /// Converts this vector into a SystemVerilog check. - String toTbVerilog(Module module) { - final assignments = inputValues.keys.map((signalName) { - final signal = module.tryInOut(signalName) ?? module.input(signalName); - - if (signal is LogicArray) { - final arrAssigns = StringBuffer(); - var index = 0; - final fullVal = - LogicValue.of(inputValues[signalName], width: signal.width); - for (final leaf in signal.leafElements) { - final subVal = fullVal.getRange(index, index + leaf.width); - arrAssigns.writeln('${leaf.structureName} = $subVal;'); - index += leaf.width; - } - return arrAssigns.toString(); - } else { - final signalVal = - LogicValue.of(inputValues[signalName], width: signal.width); - return '$signalName = $signalVal;'; - } - }).join('\n'); - - final checksList = []; - for (final expectedOutput in expectedOutputValues.entries) { - final outputName = expectedOutput.key; - final outputPort = - module.tryInOut(outputName) ?? module.output(outputName); - final expected = expectedOutput.value; - final expectedValue = LogicValue.of( - expected, - width: outputPort.width, - ); - final inputStimulus = inputValues.toString(); - - if (outputPort is LogicArray) { - var index = 0; - for (final leaf in outputPort.leafElements) { - final subVal = expectedValue.getRange(index, index + leaf.width); - checksList.add(_errorCheckString( - leaf.structureName, subVal, subVal, inputStimulus)); - index += leaf.width; - } - } else { - checksList.add(_errorCheckString( - outputName, expected, expectedValue, inputStimulus)); - } - } - final checks = checksList.join('\n'); - - final tbVerilog = [ - assignments, - '#$_offset', - checks, - '#${_period - _offset}', - ].join('\n'); - return tbVerilog; - } + String toTbVerilog(Module module) => + _SystemVerilogVectorTestbench(this, module).toTbVerilog(); } /// A utility class for checking a collection of [Vector]s against @@ -202,12 +118,10 @@ abstract class SimCompare { throw NonSupportedTypeException(value); } } - }).catchError( - test: (error) => error is Exception, - (Object err, StackTrace stackTrace) { - Simulator.throwException(err as Exception, stackTrace); - }, - )); + }).catchError(test: (error) => error is Exception, + (Object err, StackTrace stackTrace) { + Simulator.throwException(err as Exception, stackTrace); + })); } }); timestamp += Vector._period; @@ -218,15 +132,6 @@ abstract class SimCompare { await Simulator.run(); } - /// A collection of warnings that are fine to ignore usually. - static final List _knownWarnings = [ - RegExp('sorry: Case unique/unique0 qualities are ignored.'), - RegExp(r'sorry: constant selects in always_\* processes' - ' are not currently supported'), - RegExp('warning: always_comb process has no sensitivities'), - RegExp('finish called at'), - ]; - /// Executes [vectors] against the Icarus Verilog simulator and checks /// that it passes. static void checkIverilogVector( @@ -242,20 +147,20 @@ abstract class SimCompare { bool buildOnly = false, SystemVerilogSynthesizerConfiguration synthesizerConfiguration = const SystemVerilogSynthesizerConfiguration(), - }) { - final result = iverilogVector(module, vectors, + }) => + _SystemVerilogSimCompare.checkIverilogVector( + module, + vectors, moduleName: moduleName, dontDeleteTmpFiles: dontDeleteTmpFiles, dumpWaves: dumpWaves, iverilogExtraArgs: iverilogExtraArgs, allowWarnings: allowWarnings, maskKnownWarnings: maskKnownWarnings, + enableChecking: enableChecking, buildOnly: buildOnly, - synthesizerConfiguration: synthesizerConfiguration); - if (enableChecking) { - expect(result, true); - } - } + synthesizerConfiguration: synthesizerConfiguration, + ); /// Executes [vectors] against the Icarus Verilog simulator. static bool iverilogVector( @@ -270,177 +175,173 @@ abstract class SimCompare { bool buildOnly = false, SystemVerilogSynthesizerConfiguration synthesizerConfiguration = const SystemVerilogSynthesizerConfiguration(), - }) { - if (kIsWeb) { - // if running in web mode, then we can't run icarus verilog - return true; - } - - String signalDeclaration(String signalName, - {String Function(String original)? adjust, - String? signalTypeOverride}) { - final signal = module.signals.firstWhere((e) => e.name == signalName); - - final signalType = signalTypeOverride ?? - ((signal is LogicNet || (signal is LogicArray && signal.isNet)) - ? 'wire' - : 'logic'); - - if (adjust != null) { - signalName = adjust(signalName); - } - - if (signal is LogicArray) { - final unpackedDims = - signal.dimensions.getRange(0, signal.numUnpackedDimensions); - final packedDims = signal.dimensions - .getRange(signal.numUnpackedDimensions, signal.dimensions.length); - // ignore: prefer_interpolation_to_compose_strings - return signalType + - ' ' + - // ignore: prefer_interpolation_to_compose_strings - packedDims.map((d) => '[${d - 1}:0]').join() + - ' [${signal.elementWidth - 1}:0] $signalName' + - unpackedDims.map((d) => '[${d - 1}:0]').join(); - } else if (signal.width != 1) { - return '$signalType [${signal.width - 1}:0] $signalName'; - } else { - return '$signalType $signalName'; - } - } - - final topModule = moduleName ?? module.definitionName; - final allSignals = { - for (final v in vectors) ...v.inputValues.keys, - for (final v in vectors) ...v.expectedOutputValues.keys, - }; - - late final tbWireUniquifier = Uniquifier(); - late final alreadyMappedLogicToWires = {}; - String toTbWireName(String name) => alreadyMappedLogicToWires.putIfAbsent( - name, () => tbWireUniquifier.getUniqueName(initialName: 'wire__$name')); - - final logicToWireMapping = Map.fromEntries(vectors - .map((v) => v.inputValues.keys) - .flattened - .where((name) => module.tryInOut(name) != null) - .map((name) => MapEntry(name, toTbWireName(name)))); - - final localDeclarations = [ - ...allSignals.map((e) { - final sigDecl = signalDeclaration(e, - signalTypeOverride: - logicToWireMapping.containsKey(e) ? 'logic' : null); - return '$sigDecl;'; - }), - ...logicToWireMapping.entries.map((e) { - final logicName = e.key; - final wireName = e.value; - - final sigDecl = signalDeclaration(logicName, - adjust: toTbWireName, signalTypeOverride: 'wire'); - return '$sigDecl; assign $wireName = $logicName;'; - }), - ].join('\n'); - - final moduleConnections = - allSignals.map((e) => '.$e(${logicToWireMapping[e] ?? e})').join(', '); - final moduleInstance = '$topModule dut($moduleConnections);'; - final stimulus = vectors.map((e) => e.toTbVerilog(module)).join('\n'); - final generatedVerilog = module.generateSynth( - configuration: synthesizerConfiguration, - ); - - // so that when they run in parallel, they dont step on each other - final uniqueId = - (generatedVerilog + localDeclarations + stimulus + moduleInstance) - .hashCode; - - const dir = 'tmp_test'; - final tmpTestFile = '$dir/tmp_test$uniqueId.sv'; - final tmpOutput = '$dir/tmp_out$uniqueId'; - final tmpVcdFile = '$dir/tmp_waves_$uniqueId.vcd'; - - final waveDumpCode = ''' -\$dumpfile("$tmpVcdFile"); -\$dumpvars(0,dut); -'''; - - final testbench = [ - generatedVerilog, - 'module tb;', - localDeclarations, - moduleInstance, - 'initial begin', - if (dumpWaves) waveDumpCode, - '#1', - stimulus, - r'$finish;', // so the test doesn't run forever if there's a clock gen - 'end', - 'endmodule', - ].join('\n'); + }) => + _SystemVerilogSimCompare.iverilogVector( + module, + vectors, + moduleName: moduleName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + dumpWaves: dumpWaves, + iverilogExtraArgs: iverilogExtraArgs, + allowWarnings: allowWarnings, + maskKnownWarnings: maskKnownWarnings, + buildOnly: buildOnly, + synthesizerConfiguration: synthesizerConfiguration, + ); - Directory(dir).createSync(recursive: true); - File(tmpTestFile).writeAsStringSync(testbench); - final compileResult = Process.runSync('iverilog', - ['-g2012', '-o', tmpOutput, ...iverilogExtraArgs, tmpTestFile]); - bool printIfContentsAndCheckError(dynamic output) { - final maskedOutput = output - .toString() - .split('\n') - .where((element) => element.isNotEmpty) - .map((line) { - for (final knownWarning in _knownWarnings) { - if (knownWarning.hasMatch(line)) { - return null; - } - } - return line; - }) - .nonNulls - .join('\n'); - if (maskedOutput.isNotEmpty) { - print(maskedOutput); - } + /// Cleans up all cached SystemC executables and the precompiled header. + /// Call from `tearDownAll` in tests. + /// + /// If [keepPch] is true (the default), the precompiled header is preserved + /// for faster subsequent runs. Pass `keepPch: false` to remove everything. + static void cleanupSystemCCache({bool keepPch = true}) => + _SystemCSimCompare.cleanupSystemCCache(keepPch: keepPch); - return output.toString().contains(RegExp( - [ - 'error', - 'unable', - if (!allowWarnings) 'warning', - ].join('|'), - caseSensitive: false)); - } + /// Compiles a SystemC module into a reusable stdin-driven vector-testbench + /// executable. + /// + /// Returns a [SystemCVectorExecutable] that can be used to run multiple + /// vector sets without recompilation. Use in `setUpAll` for test groups. + /// Results are cached — calling this with the same module definition + /// returns the previously compiled binary. + static SystemCVectorExecutable? buildSystemCVectorExecutable( + Module module, { + String? moduleName, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + }) => + _SystemCSimCompare.buildSystemCVectorExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); - if (printIfContentsAndCheckError(compileResult.stdout)) { - return false; - } - if (printIfContentsAndCheckError(compileResult.stderr)) { - return false; - } + /// Legacy name for [buildSystemCVectorExecutable]. + @Deprecated('Use buildSystemCVectorExecutable instead.') + static SystemCVectorExecutable? buildSystemCExecutable( + Module module, { + String? moduleName, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + }) => + buildSystemCVectorExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); - if (!buildOnly) { - final simResult = Process.runSync('vvp', [tmpOutput]); - if (printIfContentsAndCheckError(simResult.stdout)) { - return false; - } - if (printIfContentsAndCheckError(simResult.stderr)) { - return false; - } - } + /// Runs [vectors] against a pre-compiled [SystemCVectorExecutable]. + /// + /// Returns `true` if all vectors pass. + static bool runSystemCVectors( + SystemCVectorExecutable exe, List vectors) => + _SystemCSimCompare.runSystemCVectors(exe, vectors); + + /// Convenience: runs [vectors] against a pre-compiled executable and + /// asserts the result. + static void checkSystemCVectors( + SystemCVectorExecutable exe, List vectors) => + _SystemCSimCompare.checkSystemCVectors(exe, vectors); + + /// Executes [vectors] against a SystemC simulator compiled with g++ and + /// checks that it passes (single-shot, compiles each time). + static void checkSystemCVector(Module module, List vectors, + {String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false}) => + _SystemCSimCompare.checkSystemCVector(module, vectors, + moduleName: moduleName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + buildOnly: buildOnly); + + /// Legacy API — returns bool. + static bool systemcVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false, + }) => + _SystemCSimCompare.systemcVector( + module, + vectors, + moduleName: moduleName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + buildOnly: buildOnly, + ); - if (!dontDeleteTmpFiles) { - try { - File(tmpOutput).deleteSync(); - File(tmpTestFile).deleteSync(); - if (dumpWaves) { - File(tmpVcdFile).deleteSync(); - } - } on Exception catch (e) { - print("Couldn't delete: $e"); - return false; - } - } - return true; - } + /// Runs the ROHD simulation using [stimulus], records input/output values + /// at every posedge of [clk], then replays the captured vectors through + /// the SystemC-synthesized version of [module] and compares results. + /// + /// [stimulus] is an async function that sets up and drives the simulation + /// (inject signals, register actions, etc.) but does NOT call + /// [Simulator.run] — that is done internally. + /// + /// [inputNames] and [outputNames] specify which ports to record. If null, + /// all module inputs (excluding clock) and all module outputs are used. + /// + /// Example usage with an existing test: + /// ```dart + /// await SimCompare.systemcSimCompare( + /// counter, + /// clk, + /// stimulus: () async { + /// reset.inject(1); + /// en.inject(0); + /// Simulator.registerAction(25, () { reset.put(0); en.put(1); }); + /// Simulator.setMaxSimTime(100); + /// }, + /// ); + /// ``` + static Future systemcSimCompare( + Module module, + Logic clk, { + required Future Function() stimulus, + List? inputNames, + List? outputNames, + String? clockName, + String? resetName, + bool dontDeleteTmpFiles = false, + String? systemcHome, + String? systemcLib, + }) => + _SystemCSimCompare.systemcSimCompare( + module, + clk, + stimulus: stimulus, + inputNames: inputNames, + outputNames: outputNames, + clockName: clockName, + resetName: resetName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); } diff --git a/lib/src/utilities/systemc_cosim_ffi.dart b/lib/src/utilities/systemc_cosim_ffi.dart new file mode 100644 index 000000000..f6c397943 --- /dev/null +++ b/lib/src/utilities/systemc_cosim_ffi.dart @@ -0,0 +1,986 @@ +// 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/simcompare.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 live peek/poke-style 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]. +/// +/// This is intended for tests that interact with signals procedurally through +/// `inject`, `put`, `await clk.nextPosedge`, and `expect`. Batch [Vector] +/// replay uses the native SystemC vector-testbench executable from +/// [SimCompare.checkSystemCVector] instead of FFI. +/// +/// ## How it works +/// +/// 1. The ROHD module is synthesized to SystemC C++ via +/// [Module.generateSystemC] +/// 2. A C-linkage FFI wrapper is generated around the SystemC module +/// 3. The wrapper is compiled to a `.so` shared library +/// 4. On each [Simulator] tick (at the `clkStable` phase): +/// - Current ROHD input values are pushed to SystemC via FFI +/// - SystemC is advanced by one half clock period (`sc_start`) +/// - SystemC output values are pulled back and `put()` onto ROHD outputs +/// +/// ## Timing compatibility with existing tests +/// +/// The synchronization point at `clkStable` means: +/// - `inject()` calls have already executed (mainTick phase) +/// - Clock has already toggled (mainTick) +/// - `previousValue` was snapshot at preTick (before this tick started) +/// - After clkStable, outputs are updated, then `postTick` fires +/// - `await clk.nextPosedge` resumes after postTick +/// +/// This preserves the same timing semantics as native ROHD Sequential blocks. +/// +/// ## Example +/// +/// ```dart +/// final counter = SimpleCounter(clk, reset, en); +/// await counter.build(); +/// +/// final cosim = await SystemCFfiCosim.create(counter, clk: clk); +/// if (cosim == null) return; // SystemC not installed +/// +/// // Now run your test exactly as before: +/// unawaited(Simulator.run()); +/// reset.inject(1); +/// await clk.nextPosedge; +/// // ... counter.output('val') is driven by SystemC +/// ``` +class SystemCFfiCosim { + /// The ROHD module whose SystemC synthesis is being co-simulated. + final Module module; + + /// The clock signal (null for combinational/clockless mode). + final Logic? clk; + + /// Clock period in nanoseconds (matches SimpleClockGenerator's period). + /// Ignored in combinational mode. + final int clockPeriodNs; + + /// Whether this cosim operates in combinational (clockless) mode. + /// In this mode, inputs are propagated immediately via delta cycles + /// (sc_start(SC_ZERO_TIME)) whenever any input changes. + bool get isCombinational => clk == null; + + /// Handle to the loaded shared library. + DynamicLibrary? _lib; + + /// Opaque handle to the SystemC simulation context. + Pointer _handle = nullptr; + + /// Path to the compiled .so file. + late String? _soPath; + + /// Input port names and widths. + final Map _inputWidths = {}; + + /// Output port names and widths. + final Map _outputWidths = {}; + + /// Clock port name(s) to skip when driving inputs. + final Set _clockNames = {}; + + /// Whether the cosim is actively stepping. + bool _active = false; + + /// Subscription to Simulator.clkStable for per-tick stepping (clocked mode). + StreamSubscription? _clkStableSubscription; + + /// Synchronous subscriptions on input glitches (combinational mode). + final List> + _inputGlitchSubscriptions = []; + + /// Whether a combinational step is already pending in this propagation wave. + /// Prevents re-entrant stepping when multiple inputs change in the same + /// event (e.g. Swizzle feeding a bus). + bool _combStepPending = false; + + // FFI function handles + late final _SetInputDart _setInput; + late final _SetInputWideDart _setInputWide; + late final _GetOutputDart _getOutput; + late final _GetOutputWideDart _getOutputWide; + late final _AdvanceDart _advance; + late final _DestroyDart _destroy; + + // Cached C-string pointers for signal names (allocated once, reused every + // step) + final Map> _inputNamePtrs = {}; + final Map> _outputNamePtrs = {}; + + // Cached signal references (avoid module.input/output map lookups per step) + final Map _inputSignals = {}; + final Map _outputSignals = {}; + + // Pre-allocated buffer for wide hex strings (avoids malloc/free per step). + // 512 chars covers up to 2048-bit signals. + Pointer _hexBuf = nullptr; + static const _hexBufSize = 512; + + // Native memory management + static final _free = DynamicLibrary.process().lookupFunction< + Void Function(Pointer), void Function(Pointer)>('free'); + static final _malloc = DynamicLibrary.process().lookupFunction< + Pointer Function(IntPtr), Pointer Function(int)>('malloc'); + + /// Cache of loaded libraries and handles keyed by .so path. + /// Prevents re-loading a .so that's already in the process, which + /// would crash SystemC's singleton kernel (E113). + static final _loadedLibs = {}; + + /// Deletes all `cosim_ffi_*` source and shared-library files from + /// `tmp_test/` and clears the in-process cache. + /// + /// Call from `tearDownAll` in tests to satisfy `check_tmp_test.sh`. + static void cleanupCache() { + _loadedLibs.clear(); + const dir = 'tmp_test'; + final d = Directory(dir); + if (!d.existsSync()) { + return; + } + for (final entity in d.listSync()) { + final name = entity.uri.pathSegments.last; + if (name.startsWith('cosim_ffi_') || name.startsWith('libcosim_ffi_')) { + try { + entity.deleteSync(recursive: true); + } on Exception catch (_) { + // ignore deletion errors (file may be locked or already removed) + } + } + } + } + + SystemCFfiCosim._(this.module, this.clk, {required this.clockPeriodNs}); + + /// Compiles the module's SystemC output to a shared library, loads it, + /// and begins co-simulation. + /// + /// Returns `null` if SystemC is not installed or compilation fails. + /// + /// If [clk] is provided, the cosim operates in clocked mode — stepping + /// SystemC at each clock edge via `Simulator.clkStable`. + /// + /// If [clk] is omitted (null), the cosim operates in combinational mode — + /// propagating inputs through SystemC delta cycles immediately whenever + /// any input signal changes. This gives the same semantics as native ROHD + /// [Combinational] blocks. + static Future create( + Module module, { + Logic? clk, + int clockPeriodNs = 10, + String? systemcHome, + String? systemcLib, + }) async { + if (kIsWeb) { + return null; + } + + final cosim = SystemCFfiCosim._(module, clk, clockPeriodNs: clockPeriodNs); + + if (!cosim._compileAndLoad( + systemcHome: systemcHome ?? '', + systemcLib: systemcLib ?? '', + )) { + return null; + } + + cosim + .._cachePortInfo() + .._start(); + return cosim; + } + + /// Pre-elaborates the module's SystemC code without starting co-simulation. + /// + /// Call this in `setUpAll` for every module configuration that will be + /// cosim-tested in the file. This ensures all SystemC module types are + /// instantiated during the elaboration phase (before `sc_start`), which + /// avoids E113 errors when multiple configurations are tested. + /// + /// Returns `false` if SystemC is not installed or compilation fails. + static Future preElaborate( + Module module, { + Logic? clk, + int clockPeriodNs = 10, + String? systemcHome, + String? systemcLib, + }) async { + if (kIsWeb) { + return false; + } + + final cosim = SystemCFfiCosim._(module, clk, clockPeriodNs: clockPeriodNs); + + return cosim._compileAndLoad( + systemcHome: systemcHome ?? '', + systemcLib: systemcLib ?? '', + ); + } + + /// Compiles the SystemC wrapper to .so and loads it. + /// Uses a static cache to avoid re-loading the same .so (which would + /// crash SystemC's singleton kernel with E113). + bool _compileAndLoad({ + required String systemcHome, + required String systemcLib, + }) { + final resolvedHome = _resolveHome(systemcHome); + final resolvedLib = _resolveLib(systemcLib); + if (resolvedHome == null || resolvedLib == null) { + // ignore: avoid_print + print('SystemC FFI cosim: SystemC installation not found'); + return false; + } + + // Collect port widths — treat clocks as regular 1-bit inputs driven + // manually via sc_signal (avoids sc_clock phase alignment issues + // when reusing the cached SystemC kernel across tests). + for (final entry in module.inputs.entries) { + final name = entry.key; + if (name == 'clk' || name.contains('clock')) { + _clockNames.add(name); + } + _inputWidths[name] = entry.value.width; + } + for (final entry in module.outputs.entries) { + _outputWidths[entry.key] = entry.value.width; + } + + // Generate wrapper C++ source + final generatedSC = module.generateSystemC(); + + // Compute a content hash to distinguish modules with the same + // definitionName but different logic (e.g., DAZ/FTZ variants). + // Strip non-deterministic lines (e.g. timestamps) before hashing so + // that repeated instantiations of the same module share one .so. + final stableCode = generatedSC + .split('\n') + .where((line) => !line.contains('Generation time:')) + .join('\n'); + final contentHash = stableCode.hashCode.toUnsigned(32).toRadixString(16); + final uniqueName = '${module.definitionName}_$contentHash'; + + // Rename the top-level SC_MODULE in the generated code to the unique name + // so that different logic variants don't collide in the SystemC linker. + final renamedSC = generatedSC.replaceAll(module.definitionName, uniqueName); + final wrapperSrc = _generateWrapper(renamedSC, uniqueName); + + const dir = 'tmp_test'; + Directory(dir).createSync(recursive: true); + final cacheKey = uniqueName; + final cppFile = '$dir/cosim_ffi_$cacheKey.cpp'; + _soPath = '$dir/libcosim_ffi_$cacheKey.so'; + + // Check cache — if already loaded in this process, reuse it + if (_loadedLibs.containsKey(cacheKey)) { + final cached = _loadedLibs[cacheKey]!; + _lib = cached.lib; + _handle = cached.handle; + _setInput = cached.setInput; + _setInputWide = cached.setInputWide; + _getOutput = cached.getOutput; + _getOutputWide = cached.getOutputWide; + _advance = cached.advance; + _destroy = cached.destroy; + + // Reset all inputs to 0 (including clock) so the DUT starts fresh. + // The writes are committed by the first sc_start in _step(). + // Note: _cachePortInfo() is called after this, so use temp pointers here. + for (final name in _inputWidths.keys) { + if (_inputNamePtrs.containsKey(name)) { + _setInput(_handle, _inputNamePtrs[name]!.cast(), 0); + } else { + final namePtr = _toCString(name); + _setInput(_handle, namePtr.cast(), 0); + _free(namePtr); + } + } + + return true; + } + + // Compile (only if .so doesn't exist on disk) + if (!File(_soPath!).existsSync()) { + File(cppFile).writeAsStringSync(wrapperSrc); + + final cxxStd = _detectCxxStd(resolvedLib); + final result = Process.runSync('g++', [ + '-std=$cxxStd', + '-shared', + '-fPIC', + '-O2', + '-I$resolvedHome', + '-L$resolvedLib', + '-Wl,-rpath,$resolvedLib', + '-o', + _soPath!, + cppFile, + '-lsystemc', + ]); + + if (result.exitCode != 0) { + // ignore: avoid_print + print('SystemC FFI: compilation failed:\n${result.stderr}'); + return false; + } + } + + // Load the shared library + _lib = DynamicLibrary.open(_soPath!); + + // Bind function pointers + final create = _lib!.lookupFunction Function(Pointer), + Pointer Function(Pointer)>('sc_cosim_create'); + _setInput = _lib!.lookupFunction< + Void Function(Pointer, Pointer, Uint64), + _SetInputDart>('sc_cosim_set_input'); + _setInputWide = _lib!.lookupFunction< + Void Function(Pointer, Pointer, Pointer), + _SetInputWideDart>('sc_cosim_set_input_wide'); + _getOutput = _lib!.lookupFunction< + Uint64 Function(Pointer, Pointer), + _GetOutputDart>('sc_cosim_get_output'); + _getOutputWide = _lib!.lookupFunction< + Pointer Function(Pointer, Pointer), + _GetOutputWideDart>('sc_cosim_get_output_wide'); + _advance = _lib! + .lookupFunction, Uint64), _AdvanceDart>( + 'sc_cosim_advance'); + _destroy = _lib!.lookupFunction), _DestroyDart>( + 'sc_cosim_destroy'); + + // Create the SystemC context (elaborates the design) + final namePtr = _toCString(module.definitionName); + _handle = create(namePtr.cast()); + _free(namePtr); + + if (_handle == nullptr) { + // ignore: avoid_print + print('SystemC FFI: sc_cosim_create returned null'); + return false; + } + + // Cache for reuse + _loadedLibs[cacheKey] = _LoadedCosimLib( + lib: _lib!, + handle: _handle, + setInput: _setInput, + setInputWide: _setInputWide, + getOutput: _getOutput, + getOutputWide: _getOutputWide, + advance: _advance, + destroy: _destroy, + ); + + return true; + } + + /// Pre-allocates cached C-string pointers and signal references. + /// Call once after _compileAndLoad succeeds. + void _cachePortInfo() { + for (final entry in _inputWidths.entries) { + _inputNamePtrs[entry.key] = _toCString(entry.key); + _inputSignals[entry.key] = module.input(entry.key); + } + for (final entry in _outputWidths.entries) { + _outputNamePtrs[entry.key] = _toCString(entry.key); + _outputSignals[entry.key] = module.output(entry.key); + } + // Pre-allocate hex buffer for wide signals + _hexBuf = _malloc(_hexBufSize); + } + + /// Whether an edge occurred in this tick (set by glitch listener). + bool _edgePending = false; + + /// Subscription to clock glitch for edge detection. + SynchronousSubscription? _glitchSubscription; + + /// Starts the co-simulation by hooking into the clock's glitch and + /// Simulator.clkStable — mirroring how ROHD's Sequential works. + /// + /// In clocked mode: Steps SystemC on BOTH posedge and negedge, advancing + /// by half-period each time. This keeps the SystemC clock perfectly aligned + /// with ROHD's: + /// + /// ROHD posedge → sc_start(T/2) → SystemC posedge occurs → read outputs + /// ROHD negedge → sc_start(T/2) → SystemC negedge occurs → read outputs + /// + /// In combinational mode: Listens to input signal glitches and immediately + /// propagates through SystemC via delta cycles (sc_start(0)). This gives + /// the same timing semantics as native ROHD [Combinational] blocks. + void _start() { + _active = true; + + if (isCombinational) { + _startCombinational(); + } else { + _startClocked(); + } + } + + /// Starts clocked mode — step at each clock edge via clkStable. + void _startClocked() { + // Detect any clock edge (0→1 or 1→0) by listening to the glitch stream. + _glitchSubscription = clk!.glitch.listen((event) { + if (!_active) { + return; + } + // Any valid transition on the clock (posedge or negedge) + final isPosedge = event.previousValue == LogicValue.zero && + event.newValue == LogicValue.one; + final isNegedge = event.previousValue == LogicValue.one && + event.newValue == LogicValue.zero; + if ((isPosedge || isNegedge) && !_edgePending) { + _edgePending = true; + // Wait for clkStable (all inputs settled) then step SystemC. + unawaited(Simulator.clkStable.first.then((_) { + if (!_active) { + return; + } + _edgePending = false; + _step(); + })); + } + }); + } + + /// Starts combinational mode — step on any input change (synchronous). + /// + /// Uses synchronous glitch subscriptions so that output values are + /// available immediately after `put()` — matching native ROHD behavior. + /// + /// Does NOT call sc_start here — the kernel transition from ELABORATION + /// to RUNNING is deferred to the first actual `_stepCombinational()` call. + /// This allows multiple module variants to be pre-elaborated before the + /// kernel starts (avoiding E113 errors). + void _startCombinational() { + for (final entry in _inputWidths.entries) { + final name = entry.key; + // Skip clock-like signals (shouldn't exist in combinational mode, + // but guard against it) + if (_clockNames.contains(name)) { + continue; + } + + final signal = module.input(name); + final sub = signal.glitch.listen((event) { + if (!_active) { + return; + } + if (_combStepPending) { + return; + } + _combStepPending = true; + + // Push all current inputs, advance by 1 ps (triggers delta cycles), + // and pull outputs. The _combStepPending flag prevents re-entrant + // calls during the same propagation wave. + _stepCombinational(); + _combStepPending = false; + }); + _inputGlitchSubscriptions.add(sub); + } + } + + /// One co-simulation step: push inputs, advance time, pull outputs. + void _step() { + _pushInputs(); + + // Advance SystemC to process the signal writes (delta cycle). + // We advance T/2 per edge for timing consistency. The clock signal + // is driven manually (not sc_clock), so posedge/negedge detection + // in SystemC relies on the sc_signal transitions we just wrote. + _advance(_handle, clockPeriodNs * 1000 ~/ 2); + + _pullOutputs(); + } + + /// Combinational step: push inputs, advance minimally, pull outputs. + /// + /// Advances by 1 ps — the minimum non-zero time to trigger the full + /// SystemC evaluate→update→notify loop. Per IEEE 1666 §4.3.4.2, + /// sc_start(SC_ZERO_TIME) explicitly does NOT process delta notifications, + /// so external signal writes cannot trigger SC_METHOD evaluation without + /// a non-zero time advancement. + void _stepCombinational() { + _pushInputs(); + _advance(_handle, 1); // 1 ps — minimum to trigger full eval loop + _pullOutputs(); + } + + /// Pushes all current ROHD input values to the SystemC model via FFI. + void _pushInputs() { + for (final entry in _inputWidths.entries) { + final name = entry.key; + final width = entry.value; + final signal = _inputSignals[name]!; + final val = signal.value; + + if (width <= 64) { + final intVal = val.isValid ? val.toInt() : 0; + _setInput(_handle, _inputNamePtrs[name]!.cast(), intVal); + } else { + final bigVal = + val.isValid ? val.toBigInt().toUnsigned(width) : BigInt.zero; + var hex = bigVal.toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + // Write hex into pre-allocated buffer (no malloc/free per step) + final fullHex = '0x$hex'; + final bytes = utf8.encode(fullHex); + final buf = _hexBuf.cast(); + for (var i = 0; i < bytes.length && i < _hexBufSize - 1; i++) { + (buf + i).value = bytes[i]; + } + (buf + bytes.length).value = 0; + _setInputWide(_handle, _inputNamePtrs[name]!.cast(), _hexBuf.cast()); + } + } + } + + /// Pulls all SystemC output values back to ROHD signals. + void _pullOutputs() { + for (final entry in _outputWidths.entries) { + final name = entry.key; + final width = entry.value; + final signal = _outputSignals[name]!; + + if (width <= 64) { + final intVal = _getOutput(_handle, _outputNamePtrs[name]!.cast()); + signal.put(LogicValue.ofInt(intVal, width)); + } else { + final hexCharPtr = + _getOutputWide(_handle, _outputNamePtrs[name]!.cast()); + final hexStr = _fromCString(hexCharPtr); + final bigVal = BigInt.parse( + hexStr.startsWith('0x') ? hexStr.substring(2) : hexStr, + radix: 16); + signal.put(LogicValue.of(bigVal.toUnsigned(width), width: width)); + } + } + } + + /// Stops co-simulation and releases all resources. + Future dispose() async { + _active = false; + await _clkStableSubscription?.cancel(); + _clkStableSubscription = null; + _glitchSubscription?.cancel(); + _glitchSubscription = null; + for (final sub in _inputGlitchSubscriptions) { + sub.cancel(); + } + _inputGlitchSubscriptions.clear(); + // Free cached name pointers + _inputNamePtrs.values.forEach(_free); + _inputNamePtrs.clear(); + _outputNamePtrs.values.forEach(_free); + _outputNamePtrs.clear(); + if (_hexBuf != nullptr) { + _free(_hexBuf); + _hexBuf = nullptr; + } + _inputSignals.clear(); + _outputSignals.clear(); + if (_handle != nullptr) { + _destroy(_handle); + _handle = nullptr; + } + _lib = null; + } + + // ══════════════════════════════════════════════════════════════════════ + // C++ Code Generation + // ══════════════════════════════════════════════════════════════════════ + + static void _writeCode(StringBuffer sb, String code, {String prefix = ''}) { + final lines = code.substring(1).split('\n'); + final nonEmptyLines = lines.where((line) => line.trim().isNotEmpty); + final indent = nonEmptyLines.isEmpty + ? 0 + : nonEmptyLines + .map((line) => line.length - line.trimLeft().length) + .reduce((current, next) => current < next ? current : next); + + sb.write(lines + .map((line) => + line.length < indent ? '' : '$prefix${line.substring(indent)}') + .join('\n')); + } + + /// Generates the C++ wrapper with extern "C" API around the ROHD-generated + /// SystemC module code. + String _generateWrapper(String generatedSystemC, String topModule) { + final sb = StringBuffer(); + + _writeCode(sb, ''' + // Auto-generated SystemC FFI Cosim Wrapper + // Module: $topModule + + #include + #include + #include + #include + using namespace std; + + // ═══ ROHD-Generated SystemC Module(s) ═══ + + '''); + sb.writeln(generatedSystemC); + _writeCode(sb, ''' + // ═══ FFI Cosim Context ═══ + + struct CosimContext { + '''); + + // All input signal declarations (including clocks as sc_signal) + for (final entry in _inputWidths.entries) { + final type = SystemCSynthesisResult.systemCType(entry.value); + sb.writeln(' sc_signal<$type> ${entry.key};'); + } + // Output signal declarations + for (final entry in _outputWidths.entries) { + final type = SystemCSynthesisResult.systemCType(entry.value); + sb.writeln(' sc_signal<$type> ${entry.key};'); + } + + _writeCode(sb, ''' + $topModule* dut; + }; + + extern "C" { + + // Required by SystemC linker — we never call it directly + int sc_main(int, char*[]) { return 0; } + + // Track whether the kernel has been initialized + static CosimContext* _active_ctx = nullptr; + + void* sc_cosim_create(const char* name) { + // If a context already exists (same process, new test), + // just return the existing one after resetting signals. + if (_active_ctx != nullptr) { + // Reset all input signals to 0 + '''); + + for (final entry in _inputWidths.entries) { + final type = SystemCSynthesisResult.systemCType(entry.value); + sb.writeln(' _active_ctx->${entry.key}.write($type(0));'); + } + + _writeCode(sb, ''' + return static_cast(_active_ctx); + } + + // Guard: cannot create sc_signal after kernel starts + if (sc_get_status() != SC_ELABORATION && sc_get_status() != SC_BEFORE_END_OF_ELABORATION) { + return nullptr; // E113 prevention + } + + auto* ctx = new CosimContext(); + ctx->dut = new $topModule("dut"); + '''); + + // Bind all inputs (including clocks — driven via sc_signal) + for (final name in _inputWidths.keys) { + sb.writeln(' ctx->dut->$name(ctx->$name);'); + } + // Bind outputs + for (final name in _outputWidths.keys) { + sb.writeln(' ctx->dut->$name(ctx->$name);'); + } + + _writeCode(sb, ''' + // Store context — do NOT call sc_start here. + // Deferring sc_start to the first advance allows + // multiple module types to be elaborated before + // the kernel starts (avoids E113). + _active_ctx = ctx; + return static_cast(ctx); + } + + void sc_cosim_set_input(void* handle, const char* name, uint64_t value) { + auto* ctx = static_cast(handle); + '''); + + _generateInputDispatch(sb, narrow: true); + + _writeCode(sb, ''' + } + + void sc_cosim_set_input_wide(void* handle, const char* name, const char* hex_value) { + auto* ctx = static_cast(handle); + '''); + + _generateInputDispatch(sb, narrow: false); + + _writeCode(sb, ''' + } + + uint64_t sc_cosim_get_output(void* handle, const char* name) { + auto* ctx = static_cast(handle); + '''); + + _generateOutputDispatch(sb, narrow: true); + + _writeCode(sb, ''' + return 0; + } + + const char* sc_cosim_get_output_wide(void* handle, const char* name) { + auto* ctx = static_cast(handle); + static char _buf[512]; + '''); + + _generateOutputDispatch(sb, narrow: false); + + _writeCode(sb, ''' + _buf[0] = '0'; _buf[1] = 0; + return _buf; + } + + void sc_cosim_advance(void* handle, uint64_t time_ps) { + // End elaboration on first advance (allows multiple + // module types to be instantiated before starting). + if (sc_get_status() == SC_ELABORATION) { + sc_start(SC_ZERO_TIME); + } + if (time_ps == 0) { + // Zero-time advance: process delta cycles only. + // Use SC_ZERO_TIME explicitly (some implementations + // treat sc_time(0,SC_PS) differently). + sc_start(SC_ZERO_TIME); + } else { + sc_start(sc_time(static_cast(time_ps), SC_PS)); + } + } + + void sc_cosim_destroy(void* handle) { + // Do NOT delete or sc_stop — the SystemC kernel is a + // process-wide singleton. The context is reused if + // sc_cosim_create is called again (same module). + // This avoids E113 "insert primitive channel failed". + } + + } // extern "C" + '''); + + return sb.toString(); + } + + /// Generates the if-else chain for setting input signals. + void _generateInputDispatch(StringBuffer sb, {required bool narrow}) { + var first = true; + for (final entry in _inputWidths.entries) { + final name = entry.key; + final width = entry.value; + + if (narrow && width > 64) { + continue; + } + if (!narrow && width <= 64) { + continue; + } + + final ifStr = first ? ' if' : ' } else if'; + first = false; + + sb.writeln('$ifStr (strcmp(name, "$name") == 0) {'); + if (narrow) { + final type = SystemCSynthesisResult.systemCType(width); + sb.writeln(' ctx->$name.write(static_cast<$type>(value));'); + } else { + _writeCode( + sb, + ''' + sc_biguint<$width> v(hex_value); + ctx->$name.write(v); + ''', + prefix: ' '); + } + } + if (!first) { + sb.writeln(' }'); + } + } + + /// Generates the if-else chain for reading output signals. + void _generateOutputDispatch(StringBuffer sb, {required bool narrow}) { + var first = true; + for (final entry in _outputWidths.entries) { + final name = entry.key; + final width = entry.value; + + if (narrow && width > 64) { + continue; + } + if (!narrow && width <= 64) { + continue; + } + + final ifStr = first ? ' if' : ' } else if'; + first = false; + + sb.writeln('$ifStr (strcmp(name, "$name") == 0) {'); + if (narrow) { + sb.writeln(' return static_cast(ctx->$name.read());'); + } else { + _writeCode( + sb, + ''' + sc_biguint<$width> v = ctx->$name.read(); + string s = v.to_string(SC_HEX_US); + strncpy(_buf, s.c_str(), sizeof(_buf)-1); + _buf[sizeof(_buf)-1] = 0; + return _buf; + ''', + prefix: ' '); + } + } + if (!first) { + sb.writeln(' }'); + } + } + + // ══════════════════════════════════════════════════════════════════════ + // String/Memory Utilities (no package:ffi dependency) + // ══════════════════════════════════════════════════════════════════════ + + /// Allocates a null-terminated C string from a Dart string. + static Pointer _toCString(String s) { + final bytes = utf8.encode(s); + final ptr = _malloc(bytes.length + 1); + final charPtr = ptr.cast(); + for (var i = 0; i < bytes.length; i++) { + (charPtr + i).value = bytes[i]; + } + (charPtr + bytes.length).value = 0; + return ptr; + } + + /// Reads a null-terminated C string into a Dart string. + static String _fromCString(Pointer ptr) { + final bytes = []; + var i = 0; + while (true) { + final byte = (ptr.cast() + i).value; + if (byte == 0) { + break; + } + bytes.add(byte); + i++; + } + return utf8.decode(bytes); + } + + // ══════════════════════════════════════════════════════════════════════ + // SystemC Path Resolution (mirrors SimCompare) + // ══════════════════════════════════════════════════════════════════════ + + static const _defaultHome = '/opt/systemc/include'; + static const _defaultLib = '/opt/systemc/lib'; + + static String? _resolveHome(String scHome) { + if (scHome.isNotEmpty && Directory(scHome).existsSync()) { + return scHome; + } + if (Directory(_defaultHome).existsSync()) { + return _defaultHome; + } + return null; + } + + static String? _resolveLib(String scLib) { + if (scLib.isNotEmpty && Directory(scLib).existsSync()) { + return scLib; + } + if (Directory(_defaultLib).existsSync()) { + return _defaultLib; + } + return null; + } + + static String _detectCxxStd(String scLib) { + try { + final r = Process.runSync('nm', ['-D', '$scLib/libsystemc.so']); + if (r.exitCode == 0) { + final out = r.stdout as String; + if (out.contains('cxx202002L')) { + return 'c++20'; + } + if (out.contains('cxx201703L')) { + return 'c++17'; + } + } + } on Object { + // ignore + } + return 'c++20'; + } +} + +/// Cached state for a loaded SystemC cosim shared library. +class _LoadedCosimLib { + final DynamicLibrary lib; + final Pointer handle; + final _SetInputDart setInput; + final _SetInputWideDart setInputWide; + final _GetOutputDart getOutput; + final _GetOutputWideDart getOutputWide; + final _AdvanceDart advance; + final _DestroyDart destroy; + + _LoadedCosimLib({ + required this.lib, + required this.handle, + required this.setInput, + required this.setInputWide, + required this.getOutput, + required this.getOutputWide, + required this.advance, + required this.destroy, + }); +} diff --git a/lib/src/utilities/systemc_simcompare.dart b/lib/src/utilities/systemc_simcompare.dart new file mode 100644 index 000000000..650d6be75 --- /dev/null +++ b/lib/src/utilities/systemc_simcompare.dart @@ -0,0 +1,841 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_simcompare.dart +// SystemC simulation comparison support for SimCompare. +// +// 2026 July 20 +// Author: Desmond A. Kirkpatrick + +// ignore_for_file: avoid_print + +part of 'simcompare.dart'; + +class _SystemCSimCompare { + /// The default SystemC installation path (Accellera). + static const _systemCDefaultHome = '/opt/systemc/include'; + static const _systemCDefaultLib = '/opt/systemc/lib'; + + /// Cache of compiled SystemC vector-testbench executables keyed by generated + /// code hash. + static final _compilationCache = {}; + + /// Prefix for SystemC artifacts owned by this test process. + static final String tempPrefix = + 'tmp_sc_${pid}_${DateTime.now().microsecondsSinceEpoch}_' + '${Object().hashCode}'; + + /// Path to the precompiled header, built lazily on first compilation. + static String? _pchPath; + + /// Builds the precompiled header for systemc.h if not already done. + /// Returns the directory containing systemc.h.gch, or null on failure. + /// + /// In CI, the PCH is pre-built by `tool/gh_actions/setup_systemc_pch.sh` + /// before tests run, so this just finds it on disk. Locally it builds + /// on first use (safe because local runs are typically sequential). + static String? _ensurePch(String scHome, String cxxStd) { + if (_pchPath != null) { + return _pchPath; + } + + const dir = 'tmp_test'; + const pchDir = '$dir/pch'; + const gchFile = '$pchDir/systemc.h.gch'; + + // Reuse if already on disk (pre-built by CI or a previous run) + if (File(gchFile).existsSync()) { + return _pchPath = pchDir; + } + + Directory(pchDir).createSync(recursive: true); + + // Copy the original header next to the .gch so g++ matches them + File('$scHome/systemc.h').copySync('$pchDir/systemc.h'); + + final args = [ + '-std=$cxxStd', + '-I$scHome', + '-x', + 'c++-header', + '-o', + gchFile, + '$scHome/systemc.h', + ]; + final result = Process.runSync('g++', args); + if (result.exitCode != 0) { + print('PCH compilation failed (falling back to normal headers):'); + print(result.stderr); + return null; + } + + return _pchPath = pchDir; + } + + /// Resolves SystemC home/lib paths. If explicit paths are given, uses them. + /// Otherwise uses the default Accellera install paths. + static (String?, String?) _resolveSystemCPaths(String scHome, String scLib) { + if (scHome.isNotEmpty && scLib.isNotEmpty) { + if (Directory(scHome).existsSync()) { + return (scHome, scLib); + } + return (null, null); + } + if (Directory(_systemCDefaultHome).existsSync()) { + return (_systemCDefaultHome, _systemCDefaultLib); + } + return (null, null); + } + + /// Detects the C++ standard the SystemC library was compiled with + /// by inspecting the `sc_api_version` symbol in libsystemc.so. + static String _detectCxxStandard(String scLib) { + try { + final result = Process.runSync('nm', ['-D', '$scLib/libsystemc.so']); + if (result.exitCode == 0) { + final output = result.stdout as String; + if (output.contains('cxx202002L')) { + return 'c++20'; + } + if (output.contains('cxx201703L')) { + return 'c++17'; + } + } + } on Object { + // Fall through to default + } + return 'c++20'; + } + + /// Cleans up all cached SystemC executables and the precompiled header. + /// Call from `tearDownAll` in tests. + /// + /// If [keepPch] is true (the default), the precompiled header is preserved + /// for faster subsequent runs. Pass `keepPch: false` to remove everything. + static void cleanupSystemCCache({bool keepPch = true}) { + _compilationCache.clear(); + _pchPath = null; + if (kIsWeb) { + return; + } + try { + final dir = Directory('tmp_test'); + if (dir.existsSync()) { + for (final entity in dir.listSync()) { + // Use entity.path (not entity.uri) to get the basename: Directory.uri + // always appends a trailing slash, making pathSegments.last == "". + final name = entity.path.split('/').last; + + // Remove only SystemC artifacts owned by this test process. Other + // test isolates may be compiling or running from the same tmp_test + // directory concurrently. + if (name.startsWith(tempPrefix) || name == 'Makefile_sc') { + entity.deleteSync(recursive: true); + continue; + } + + // Remove pch/ directory only when keepPch is false + if (!keepPch && entity is Directory && entity.path.endsWith('/pch')) { + entity.deleteSync(recursive: true); + continue; + } + + // Leave everything else (iverilog files from parallel tests) alone + } + } + } on Exception catch (_) {} + } + + /// Compiles a SystemC module into a reusable stdin-driven vector-testbench + /// executable. + /// + /// Returns a [SystemCVectorExecutable] that can be used to run multiple + /// vector sets without recompilation. Use in `setUpAll` for test groups. + /// Results are cached — calling this with the same module definition + /// returns the previously compiled binary. + static SystemCVectorExecutable? buildSystemCVectorExecutable( + Module module, { + String? moduleName, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + }) { + if (kIsWeb) { + return null; + } + + final scHome = systemcHome ?? ''; + final scLib = systemcLib ?? ''; + final (resolvedHome, resolvedLib) = _resolveSystemCPaths(scHome, scLib); + + if (resolvedHome == null || resolvedLib == null) { + print('SystemC installation not found'); + return null; + } + + final topModule = moduleName ?? module.definitionName; + final generatedSystemC = module.generateSystemC(); + + // Check compilation cache + final cacheKey = generatedSystemC.hashCode; + if (_compilationCache.containsKey(cacheKey)) { + final cached = _compilationCache[cacheKey]!; + if (File(cached.binaryPath).existsSync()) { + return cached; + } + // Binary was removed; recompile. + _compilationCache.remove(cacheKey); + } + + // Identify clock signals + final clockSignals = {}; + if (clockName != null) { + clockSignals.add(clockName); + } + for (final input in module.inputs.entries) { + final name = input.key; + if (clockSignals.isEmpty && (name == 'clk' || name.contains('clock'))) { + clockSignals.add(name); + } + } + final promotedClocks = {}; + for (final sub in module.subModules) { + if (sub is SimpleClockGenerator) { + final clkSigName = sub.clk.name; + promotedClocks.add(clkSigName); + clockSignals.add(clkSigName); + } + } + + // Collect ALL module ports for the stdin-driven harness + final inputPorts = {}; + for (final input in module.inputs.entries) { + if (promotedClocks.contains(input.key)) { + continue; + } + inputPorts[input.key] = input.value.width; + } + final outputPorts = {}; + for (final output in module.outputs.entries) { + outputPorts[output.key] = output.value.width; + } + final inOutPorts = {}; + for (final inOut in module.inOuts.entries) { + inOutPorts[inOut.key] = inOut.value.width; + } + + // Generate stdin-driven testbench + final tb = StringBuffer() + ..write(''' +#include +#include +#include +#include +#include +#include +using namespace std; + +''') + ..writeln(generatedSystemC) + ..write(''' +int sc_main(int argc, char* argv[]) { +'''); + + // Clock + for (final clkName in clockSignals) { + tb.writeln( + ' sc_clock $clkName("$clkName", ${Vector._period}, SC_NS);', + ); + } + + // Signals for all non-clock input ports + for (final entry in inputPorts.entries) { + if (clockSignals.contains(entry.key)) { + continue; + } + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};', + ); + } + + // Signals for all output ports + for (final entry in outputPorts.entries) { + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};', + ); + } + + // Signals for all inout ports + for (final entry in inOutPorts.entries) { + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};', + ); + } + + tb + ..writeln() + // DUT instantiation and port binding + ..writeln(' $topModule dut("dut");'); + for (final name in inputPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + for (final clkName in clockSignals) { + if (!inputPorts.containsKey(clkName)) { + tb.writeln(' dut.$clkName($clkName);'); + } + } + for (final name in outputPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + for (final name in inOutPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + + tb.write(''' + int _tb_errors = 0; + + // Initial offset + sc_start(sc_time(1, SC_NS)); + + // Read number of vectors + int _tb_nvec; + cin >> _tb_nvec; + + for (int _tb_v = 0; _tb_v < _tb_nvec; _tb_v++) { +'''); + + // Read and drive each non-clock input + final drivableInputs = + inputPorts.keys.where((k) => !clockSignals.contains(k)).toList(); + for (final name in drivableInputs) { + final w = inputPorts[name]!; + if (w > 64) { + // BigInt — read as hex string + tb + ..writeln(' { string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _v(_h.c_str());') + ..writeln(' $name.write(_v); }'); + } else { + tb + ..writeln(' { uint64_t _v; cin >> _v;') + ..writeln(' $name.write(_v); }'); + } + } + for (final entry in inOutPorts.entries) { + final name = entry.key; + final w = entry.value; + tb.writeln(' { int _drive; cin >> _drive;'); + if (w > 64) { + tb + ..writeln(' if (_drive) { string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _v(_h.c_str());') + ..writeln(' $name.write(_v); } }'); + } else { + tb + ..writeln(' if (_drive) { uint64_t _v; cin >> _v;') + ..writeln(' $name.write(_v); } }'); + } + } + + // Advance to check point + tb.write(''' + sc_start(sc_time(${Vector._offset}, SC_NS)); + + // Read number of outputs to check + int _tb_nchk; + cin >> _tb_nchk; + + for (int _tb_c = 0; _tb_c < _tb_nchk; _tb_c++) { + string _tb_pn; + cin >> _tb_pn; +'''); + + // Generate if-else chain for each output and inout port + var first = true; + final checkablePorts = {...outputPorts, ...inOutPorts}; + for (final entry in checkablePorts.entries) { + final name = entry.key; + final w = entry.value; + final ifKey = first ? 'if' : '} else if'; + first = false; + tb.writeln(' $ifKey (_tb_pn == "$name") {'); + if (w > 64) { + tb + ..writeln(' string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _tb_exp(_h.c_str());') + ..writeln(' if ($name.read() != _tb_exp) {'); + } else { + tb + ..writeln(' uint64_t _tb_exp; cin >> _tb_exp;') + ..writeln(' if ($name.read() != _tb_exp) {'); + } + tb + ..writeln( + ' cout << "ERROR vector " << _tb_v' + ' << ": expected $name=" << _tb_exp' + ' << ", got " << $name.read() << endl;', + ) + ..writeln(' _tb_errors++;') + ..writeln(' }'); + } + if (checkablePorts.isNotEmpty) { + tb + ..writeln(' } else {') + ..writeln(' string _d; cin >> _d; // skip unknown') + ..writeln(' }'); + } + + tb.write(''' + } + + sc_start(sc_time(${Vector._period - Vector._offset}, SC_NS)); + } + + if (_tb_errors == 0) { + cout << "PASS" << endl; + } else { + cout << "FAIL: " << _tb_errors << " errors" << endl; + } + return _tb_errors > 0 ? 1 : 0; +} +'''); + + final testbenchCode = tb.toString(); + + // Write and compile + const dir = 'tmp_test'; + Directory(dir).createSync(recursive: true); + final compileDir = Directory( + dir, + ).createTempSync('${tempPrefix}_${generatedSystemC.hashCode}_'); + final tmpCppFile = '${compileDir.path}/main.cpp'; + final tmpOutput = '${compileDir.path}/sim'; + File(tmpCppFile).writeAsStringSync(testbenchCode); + + // Detect C++ standard for this installation + final cxxStd = _detectCxxStandard(resolvedLib); + + // Build precompiled header on first use + final pchDir = _ensurePch(resolvedHome, cxxStd); + final pchArgs = pchDir != null ? ['-I$pchDir'] : []; + + final compileResult = Process.runSync('g++', [ + '-std=$cxxStd', + '-pipe', + ...pchArgs, + '-I$resolvedHome', + '-o', + tmpOutput, + tmpCppFile, + '-L$resolvedLib', + '-lsystemc', + ]); + if (compileResult.exitCode != 0) { + print('SystemC compilation failed:'); + print(compileResult.stdout); + print(compileResult.stderr); + return null; + } + + final exe = SystemCVectorExecutable._( + binaryPath: tmpOutput, + cppFile: tmpCppFile, + scLib: resolvedLib, + clockSignals: clockSignals, + inputPorts: inputPorts, + outputPorts: outputPorts, + inOutPorts: inOutPorts, + ); + _compilationCache[cacheKey] = exe; + return exe; + } + + /// Runs [vectors] against a pre-compiled [SystemCVectorExecutable]. + /// + /// Returns `true` if all vectors pass. + static bool runSystemCVectors( + SystemCVectorExecutable exe, List vectors) { + if (!File(exe.binaryPath).existsSync()) { + print('SystemC binary not found: ${exe.binaryPath}'); + return false; + } + + // Build stdin data + final sb = StringBuffer()..writeln(vectors.length); + + final drivableInputs = exe.inputPorts.keys + .where((k) => !exe.clockSignals.contains(k)) + .toList(); + + // Track last-driven values (persist across vectors like iverilog) + final lastValues = { + for (final name in drivableInputs) name: '0', + }; + + for (final vector in vectors) { + // Update last-driven values with this vector's inputs + for (final name in drivableInputs) { + final value = vector.inputValues[name]; + if (value != null) { + final w = exe.inputPorts[name]!; + if (w > 64) { + final lv = LogicValue.of(value, width: w); + var hex = lv.toBigInt().toUnsigned(w).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + lastValues[name] = '0x$hex'; + } else { + lastValues[name] = '${_systemcIntValue(value, w)}'; + } + } + } + // Write all input values (using persisted values for unspecified) + for (final name in drivableInputs) { + sb.write('${lastValues[name]} '); + } + for (final name in exe.inOutPorts.keys) { + final value = vector.inputValues[name]; + if (value != null) { + final w = exe.inOutPorts[name]!; + final formattedValue = w > 64 + ? _systemcHexValue(value, w) + : '${_systemcIntValue(value, w)}'; + lastValues[name] = formattedValue; + } + final lastValue = lastValues[name]; + if (lastValue == null) { + sb.write('0 '); + } else { + sb.write('1 $lastValue '); + } + } + sb.writeln(); + + // Write expected outputs: count then name/value pairs + // Skip x/z outputs + final checks = {}; + for (final entry in vector.expectedOutputValues.entries) { + final name = entry.key; + final checkablePorts = {...exe.outputPorts, ...exe.inOutPorts}; + final w = checkablePorts[name]!; + final expectedLV = LogicValue.of(entry.value, width: w); + if (expectedLV.toString().contains('x') || + expectedLV.toString().contains('z')) { + continue; + } + if (w > 64) { + checks[name] = _systemcHexValue(entry.value, w); + } else { + checks[name] = '${_systemcIntValue(entry.value, w)}'; + } + } + sb.write('${checks.length} '); + for (final entry in checks.entries) { + sb.write('${entry.key} ${entry.value} '); + } + sb.writeln(); + } + + // Write vectors to a unique temp file, redirect as stdin. + final stdinDir = Directory('tmp_test').createTempSync('sc_input_'); + final stdinFile = '${stdinDir.path}/input.txt'; + late final ProcessResult result; + try { + File(stdinFile).writeAsStringSync(sb.toString()); + + result = Process.runSync( + 'sh', + ['-c', '${exe.binaryPath} < $stdinFile'], + environment: { + 'LD_LIBRARY_PATH': exe.scLib, + 'SC_COPYRIGHT_MESSAGE': 'DISABLE', + }, + ); + } finally { + if (stdinDir.existsSync()) { + stdinDir.deleteSync(recursive: true); + } + } + + final stdout = result.stdout.toString(); + final stderr = result.stderr.toString(); + + if (stdout.isNotEmpty && !stdout.contains('PASS')) { + print(stdout); + } + if (stderr.isNotEmpty && !stderr.contains('Info:')) { + print(stderr); + } + + return stdout.contains('PASS') && !stdout.contains('FAIL'); + } + + /// Convenience: runs [vectors] against a pre-compiled executable and + /// asserts the result. + static void checkSystemCVectors( + SystemCVectorExecutable exe, List vectors) { + expect(runSystemCVectors(exe, vectors), true); + } + + /// Converts a value to an integer for stdin. + static int _systemcIntValue(dynamic value, int width) { + if (value is int) { + return value; + } + if (value is LogicValue) { + if (!value.isValid) { + return 0; + } + return value.toBigInt().toUnsigned(width).toInt(); + } + if (value is BigInt) { + return value.toUnsigned(width).toInt(); + } + if (value is String) { + final lv = LogicValue.of(value, width: width); + if (!lv.isValid) { + return 0; + } + return lv.toBigInt().toUnsigned(width).toInt(); + } + return 0; + } + + /// Converts a value to a hex string for stdin. + static String _systemcHexValue(dynamic value, int width) { + final lv = LogicValue.of(value, width: width); + var hex = lv.toBigInt().toUnsigned(width).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + return '0x$hex'; + } + + /// Executes [vectors] against a SystemC simulator compiled with g++ and + /// checks that it passes (single-shot, compiles each time). + static void checkSystemCVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false, + }) { + if (buildOnly) { + // Just verify SystemC code generation succeeds + module.generateSystemC(); + return; + } + final exe = buildSystemCVectorExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + if (exe == null) { + // SystemC not available — skip gracefully. + return; + } + final passed = runSystemCVectors(exe, vectors); + if (!dontDeleteTmpFiles) { + // Single-shot path: clean up this process's compiled artifacts now so + // tests that call checkSystemCVector do not require a tearDownAll. + // The PCH is kept to avoid rebuilding it for subsequent calls. + cleanupSystemCCache(); + } + expect(passed, true); + } + + /// Legacy API — returns bool. + static bool systemcVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false, + }) { + if (kIsWeb) { + return true; + } + final exe = buildSystemCVectorExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + if (exe == null) { + return false; + } + if (buildOnly) { + return true; + } + return runSystemCVectors(exe, vectors); + } + + /// Runs the ROHD simulation using [stimulus], records input/output values + /// at every posedge of [clk], then replays the captured vectors through + /// the SystemC-synthesized version of [module] and compares results. + static Future systemcSimCompare( + Module module, + Logic clk, { + required Future Function() stimulus, + List? inputNames, + List? outputNames, + String? clockName, + String? resetName, + bool dontDeleteTmpFiles = false, + String? systemcHome, + String? systemcLib, + }) async { + // Determine which signals to record + final clkName = clockName ?? + module.inputs.keys.firstWhere( + (n) => n == 'clk' || n.contains('clock'), + orElse: () => 'clk', + ); + + final inputs = + inputNames ?? module.inputs.keys.where((n) => n != clkName).toList(); + final outputs = outputNames ?? module.outputs.keys.toList(); + + // Record snapshots at each posedge. + // Use previousValue for outputs — this gives us the output state from + // BEFORE the clock edge, which matches what the SystemC testbench sees + // when it checks at offset (before the posedge). + // Use current value for inputs — these are the values being presented + // to the DUT when the clock edge fires. + final recordings = []; + + clk.posedge.listen((_) { + // Sample inputs (current value — what's being driven now) + final inputValues = {}; + for (final name in inputs) { + final sig = module.input(name); + final val = sig.value; + inputValues[name] = val.isValid ? val.toBigInt().toInt() : 0; + } + + // Sample outputs using previousValue — the settled output + // from before this tick started, which is what a testbench + // checking before the clock edge would observe. + final outputValues = {}; + for (final name in outputs) { + final sig = module.output(name); + final prev = sig.previousValue; + if (prev != null && prev.isValid) { + outputValues[name] = prev.toBigInt().toInt(); + } + // Skip null/x/z — no check for this output + } + + recordings.add(Vector(inputValues, outputValues)); + }); + + // Run the user's stimulus setup + await stimulus(); + + // Run the ROHD simulation + await Simulator.run(); + + if (recordings.length < 2) { + print( + 'Warning: only ${recordings.length} clock edges recorded,' + ' need at least 2 for comparison', + ); + return true; + } + + // No shifting needed — previousValue already gives us the output + // state from before the posedge, which matches systemcVector's + // check-before-edge timing. Just pass recordings directly as vectors. + + // Run through SystemC + return systemcVector( + module, + recordings, + clockName: clkName, + resetName: resetName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + } +} + +/// Holds the compiled state of a native SystemC vector-testbench executable for +/// reuse across tests. +class SystemCVectorExecutable { + /// Path to the compiled binary. + final String binaryPath; + + /// Path to the generated C++ source. + final String cppFile; + + /// Path to the SystemC library (for LD_LIBRARY_PATH). + final String scLib; + + /// Clock signal names. + final Set clockSignals; + + /// Input port names and widths (excluding promoted clocks). + final Map inputPorts; + + /// Output port names and widths. + final Map outputPorts; + + /// Inout port names and widths. + final Map inOutPorts; + + SystemCVectorExecutable._({ + required this.binaryPath, + required this.cppFile, + required this.scLib, + required this.clockSignals, + required this.inputPorts, + required this.outputPorts, + required this.inOutPorts, + }); + + /// Deletes the compiled binary and source. + void cleanup() { + void tryDelete(String path) { + final f = File(path); + if (f.existsSync()) { + f.deleteSync(); + } + } + + try { + final compileDir = File(cppFile).parent; + if (compileDir.existsSync() && + compileDir.uri.pathSegments.last.startsWith( + _SystemCSimCompare.tempPrefix, + )) { + compileDir.deleteSync(recursive: true); + return; + } + tryDelete(cppFile); + tryDelete(binaryPath); + } on Exception catch (_) {} + } +} + +/// Legacy name for [SystemCVectorExecutable]. +@Deprecated('Use SystemCVectorExecutable instead.') +typedef SystemCExecutable = SystemCVectorExecutable; diff --git a/lib/src/utilities/systemverilog_simcompare.dart b/lib/src/utilities/systemverilog_simcompare.dart new file mode 100644 index 000000000..2151d4046 --- /dev/null +++ b/lib/src/utilities/systemverilog_simcompare.dart @@ -0,0 +1,380 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemverilog_simcompare.dart +// SystemVerilog testbench generation and simulation comparison support for +// SimCompare. +// +// 2026 July 20 +// Author: Desmond A. Kirkpatrick + +// ignore_for_file: avoid_print + +part of 'simcompare.dart'; + +class _SystemVerilogVectorTestbench { + final Vector vector; + final Module module; + + _SystemVerilogVectorTestbench(this.vector, this.module); + + /// Computes a SystemVerilog code string that checks in a SystemVerilog + /// simulation whether a signal [sigName] has the [expected] value given + /// the [inputValues]. + static String _errorCheckString( + String sigName, + dynamic expected, + LogicValue expectedVal, + String inputValues, + ) { + if (expected is! int && + expected is! LogicValue && + expected is! BigInt && + expected is! String) { + throw NonSupportedTypeException(expected); + } + + String expectedHexStr; + if (expected is int) { + expectedHexStr = BigInt.from( + expected, + ).toUnsigned(expectedVal.width).toRadixString(16); + expectedHexStr = '0x$expectedHexStr'; + } else if (expected is BigInt) { + expectedHexStr = expected.toUnsigned(expectedVal.width).toRadixString(16); + expectedHexStr = '0x$expectedHexStr'; + } else { + expectedHexStr = expected.toString(); + } + + final expectedValStr = expectedVal.toString(); + + return 'if($sigName !== $expectedValStr) ' + '\$error(\$sformatf("Expected $sigName=$expectedHexStr,' + ' but found $sigName=0x%x (0b%b) with inputs $inputValues",' + ' $sigName, $sigName));'; + } + + String toTbVerilog() { + final assignments = vector.inputValues.keys.map((signalName) { + final signal = module.tryInOut(signalName) ?? module.input(signalName); + + if (signal is LogicArray) { + final arrAssigns = StringBuffer(); + var index = 0; + final fullVal = LogicValue.of( + vector.inputValues[signalName], + width: signal.width, + ); + for (final leaf in signal.leafElements) { + final subVal = fullVal.getRange(index, index + leaf.width); + arrAssigns.writeln('${leaf.structureName} = $subVal;'); + index += leaf.width; + } + return arrAssigns.toString(); + } else { + final signalVal = LogicValue.of( + vector.inputValues[signalName], + width: signal.width, + ); + return '$signalName = $signalVal;'; + } + }).join('\n'); + + final checksList = []; + for (final expectedOutput in vector.expectedOutputValues.entries) { + final outputName = expectedOutput.key; + final outputPort = + module.tryInOut(outputName) ?? module.output(outputName); + final expected = expectedOutput.value; + final expectedValue = LogicValue.of(expected, width: outputPort.width); + final inputStimulus = vector.inputValues.toString(); + + if (outputPort is LogicArray) { + var index = 0; + for (final leaf in outputPort.leafElements) { + final subVal = expectedValue.getRange(index, index + leaf.width); + checksList.add( + _errorCheckString( + leaf.structureName, + subVal, + subVal, + inputStimulus, + ), + ); + index += leaf.width; + } + } else { + checksList.add( + _errorCheckString(outputName, expected, expectedValue, inputStimulus), + ); + } + } + final checks = checksList.join('\n'); + + return [ + assignments, + '#${Vector._offset}', + checks, + '#${Vector._period - Vector._offset}', + ].join('\n'); + } +} + +class _SystemVerilogSimCompare { + /// A collection of warnings that are fine to ignore usually. + static final List _knownWarnings = [ + RegExp('sorry: Case unique/unique0 qualities are ignored.'), + RegExp( + r'sorry: constant selects in always_\* processes' + ' are not currently supported', + ), + RegExp('warning: always_comb process has no sensitivities'), + RegExp('finish called at'), + ]; + + static void checkIverilogVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + bool dumpWaves = false, + List iverilogExtraArgs = const [], + bool allowWarnings = false, + bool maskKnownWarnings = true, + bool enableChecking = true, + bool buildOnly = false, + SystemVerilogSynthesizerConfiguration synthesizerConfiguration = + const SystemVerilogSynthesizerConfiguration(), + }) { + final result = iverilogVector( + module, + vectors, + moduleName: moduleName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + dumpWaves: dumpWaves, + iverilogExtraArgs: iverilogExtraArgs, + allowWarnings: allowWarnings, + maskKnownWarnings: maskKnownWarnings, + buildOnly: buildOnly, + synthesizerConfiguration: synthesizerConfiguration, + ); + if (enableChecking) { + expect(result, true); + } + } + + static bool iverilogVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + bool dumpWaves = false, + List iverilogExtraArgs = const [], + bool allowWarnings = false, + bool maskKnownWarnings = true, + bool buildOnly = false, + SystemVerilogSynthesizerConfiguration synthesizerConfiguration = + const SystemVerilogSynthesizerConfiguration(), + }) { + if (kIsWeb) { + // if running in web mode, then we can't run icarus verilog + return true; + } + + String signalDeclaration( + String signalName, { + String Function(String original)? adjust, + String? signalTypeOverride, + }) { + final signal = module.signals.firstWhere((e) => e.name == signalName); + + final signalType = signalTypeOverride ?? + ((signal is LogicNet || (signal is LogicArray && signal.isNet)) + ? 'wire' + : 'logic'); + + if (adjust != null) { + signalName = adjust(signalName); + } + + if (signal is LogicArray) { + final unpackedDims = signal.dimensions.getRange( + 0, + signal.numUnpackedDimensions, + ); + final packedDims = signal.dimensions.getRange( + signal.numUnpackedDimensions, + signal.dimensions.length, + ); + // ignore: prefer_interpolation_to_compose_strings + return signalType + + ' ' + + // ignore: prefer_interpolation_to_compose_strings + packedDims.map((d) => '[${d - 1}:0]').join() + + ' [${signal.elementWidth - 1}:0] $signalName' + + unpackedDims.map((d) => '[${d - 1}:0]').join(); + } else if (signal.width != 1) { + return '$signalType [${signal.width - 1}:0] $signalName'; + } else { + return '$signalType $signalName'; + } + } + + final topModule = moduleName ?? module.definitionName; + final allSignals = { + for (final v in vectors) ...v.inputValues.keys, + for (final v in vectors) ...v.expectedOutputValues.keys, + }; + + late final tbWireUniquifier = Uniquifier(); + late final alreadyMappedLogicToWires = {}; + String toTbWireName(String name) => alreadyMappedLogicToWires.putIfAbsent( + name, + () => tbWireUniquifier.getUniqueName(initialName: 'wire__$name'), + ); + + final logicToWireMapping = Map.fromEntries( + vectors + .map((v) => v.inputValues.keys) + .flattened + .where((name) => module.tryInOut(name) != null) + .map((name) => MapEntry(name, toTbWireName(name))), + ); + + final localDeclarations = [ + ...allSignals.map((e) { + final sigDecl = signalDeclaration( + e, + signalTypeOverride: + logicToWireMapping.containsKey(e) ? 'logic' : null, + ); + return '$sigDecl;'; + }), + ...logicToWireMapping.entries.map((e) { + final logicName = e.key; + final wireName = e.value; + + final sigDecl = signalDeclaration( + logicName, + adjust: toTbWireName, + signalTypeOverride: 'wire', + ); + return '$sigDecl; assign $wireName = $logicName;'; + }), + ].join('\n'); + + final moduleConnections = + allSignals.map((e) => '.$e(${logicToWireMapping[e] ?? e})').join(', '); + final moduleInstance = '$topModule dut($moduleConnections);'; + final stimulus = vectors.map((e) => e.toTbVerilog(module)).join('\n'); + final generatedVerilog = module.generateSynth( + configuration: synthesizerConfiguration, + ); + + // so that when they run in parallel, they dont step on each other + final uniqueId = + (generatedVerilog + localDeclarations + stimulus + moduleInstance) + .hashCode; + + const dir = 'tmp_test'; + final tmpTestFile = '$dir/tmp_test$uniqueId.sv'; + final tmpOutput = '$dir/tmp_out$uniqueId'; + final tmpVcdFile = '$dir/tmp_waves_$uniqueId.vcd'; + + final waveDumpCode = ''' +\$dumpfile("$tmpVcdFile"); +\$dumpvars(0,dut); +'''; + + final testbench = [ + generatedVerilog, + 'module tb;', + localDeclarations, + moduleInstance, + 'initial begin', + if (dumpWaves) waveDumpCode, + '#1', + stimulus, + r'$finish;', // so the test doesn't run forever if there's a clock gen + 'end', + 'endmodule', + ].join('\n'); + + Directory(dir).createSync(recursive: true); + File(tmpTestFile).writeAsStringSync(testbench); + final compileResult = Process.runSync('iverilog', [ + '-g2012', + '-o', + tmpOutput, + ...iverilogExtraArgs, + tmpTestFile, + ]); + bool printIfContentsAndCheckError(dynamic output) { + final maskedOutput = output + .toString() + .split('\n') + .where((element) => element.isNotEmpty) + .map((line) { + for (final knownWarning in _knownWarnings) { + if (knownWarning.hasMatch(line)) { + return null; + } + } + return line; + }) + .nonNulls + .join('\n'); + if (maskedOutput.isNotEmpty) { + print(maskedOutput); + } + + return output.toString().contains( + RegExp( + ['error', 'unable', if (!allowWarnings) 'warning'].join('|'), + caseSensitive: false, + ), + ); + } + + if (printIfContentsAndCheckError(compileResult.stdout)) { + return false; + } + if (printIfContentsAndCheckError(compileResult.stderr)) { + return false; + } + + if (!buildOnly) { + final simResult = Process.runSync('vvp', [tmpOutput]); + if (printIfContentsAndCheckError(simResult.stdout)) { + return false; + } + if (printIfContentsAndCheckError(simResult.stderr)) { + return false; + } + } + + if (!dontDeleteTmpFiles) { + try { + final outFile = File(tmpOutput); + if (outFile.existsSync()) { + outFile.deleteSync(); + } + final testFile = File(tmpTestFile); + if (testFile.existsSync()) { + testFile.deleteSync(); + } + if (dumpWaves) { + final vcdFile = File(tmpVcdFile); + if (vcdFile.existsSync()) { + vcdFile.deleteSync(); + } + } + } on Exception catch (e) { + print("Couldn't delete: $e"); + return false; + } + } + return true; + } +} diff --git a/lib/src/values/logic_value.dart b/lib/src/values/logic_value.dart index 81fc7304b..4efeef4ab 100644 --- a/lib/src/values/logic_value.dart +++ b/lib/src/values/logic_value.dart @@ -607,12 +607,16 @@ abstract class LogicValue implements Comparable { static String _reverse(String inString) => String.fromCharCodes(inString.runes.toList().reversed); - /// Return the radix encoding of the current [LogicValue] as a sequence - /// of radix characters prefixed by the length and encoding format. - /// Output format is: `'`. + /// Return the radix encoding of the current [LogicValue] as a sequence of + /// radix characters. By default, the output is prefixed by the length and + /// encoding format: `'`. /// - /// [ofRadixString] can parse a [String] produced by [toRadixString] and - /// construct a [LogicValue]. + /// If [includeWidth] is `false`, the length and encoding format are omitted. + /// The resulting string is not self-describing and cannot be parsed by + /// [ofRadixString] without restoring that information. + /// + /// [ofRadixString] can parse a decorated [String] produced by + /// [toRadixString] and construct a [LogicValue]. /// /// Here is the number 1492 printed as a radix string: /// - Binary: `15'b101_1101_0100` @@ -624,7 +628,8 @@ abstract class LogicValue implements Comparable { /// Separators are output according to [chunkSize] starting from the /// LSB(right), default is 4 characters. The default separator is '_'. /// [sepChar] can be set to another character, but not in [radixStringChars], - /// otherwise it will throw an exception. + /// or to an empty string to disable separators. Otherwise, it will throw an + /// exception. /// - [chunkSize] = default: `61'h2_9ebc_5f06_5bf7` /// - [chunkSize] = 10: `61'h29e_bc5f065bf7` /// @@ -650,8 +655,9 @@ abstract class LogicValue implements Comparable { {int radix = 2, int chunkSize = 4, bool leadingZeros = false, + bool includeWidth = true, String sepChar = '_'}) { - if (radixStringChars.contains(sepChar)) { + if (sepChar.isNotEmpty && radixStringChars.contains(sepChar)) { throw LogicValueConversionException('separation character invalid'); } final radixStr = switch (radix) { @@ -730,7 +736,7 @@ abstract class LogicValue implements Comparable { ? spaceString.substring(1, spaceString.length) : spaceString : '0'; - return '$width$radixStr$fullString'; + return '${includeWidth ? '$width$radixStr' : ''}$fullString'; } /// Create a [LogicValue] from a length/radix-encoded string of the diff --git a/test/arithmetic_shift_right_test.dart b/test/arithmetic_shift_right_test.dart index 1d31f4a51..1ba137d1f 100644 --- a/test/arithmetic_shift_right_test.dart +++ b/test/arithmetic_shift_right_test.dart @@ -41,5 +41,6 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); } diff --git a/test/assignment_test.dart b/test/assignment_test.dart index 712ebd9ee..e9566f518 100644 --- a/test/assignment_test.dart +++ b/test/assignment_test.dart @@ -95,6 +95,7 @@ void main() { allowWarnings: true, // since always_comb has no sensitivities ); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(exampleModule, vectors); }); group('assign subset', () { @@ -110,6 +111,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('multiple bits', () async { diff --git a/test/bus_test.dart b/test/bus_test.dart index 08ccb4c9b..695070146 100644 --- a/test/bus_test.dart +++ b/test/bus_test.dart @@ -379,6 +379,23 @@ void main() { }); group('simcompare', () { + SystemCVectorExecutable? busSystemCExe; + + setUpAll(() async { + final gtm = BusTestModule(Logic(width: 8), Logic(width: 8)); + await gtm.build(); + busSystemCExe = SimCompare.buildSystemCVectorExecutable(gtm); + }); + + tearDownAll(SimCompare.cleanupSystemCCache); + + void checkBusSystemC(List vectors) { + final exe = busSystemCExe; + if (exe != null) { + SimCompare.checkSystemCVectors(exe, vectors); + } + } + group('const sv gen', () { test('Subset of a const', () async { final mod = ConstBusModule(0xabcd, subset: true); @@ -389,6 +406,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors, dontDeleteTmpFiles: true); }); test('Assignment of a const', () async { @@ -400,6 +418,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors, dontDeleteTmpFiles: true); final sv = mod.generateSynth(); expect(sv.contains("assign const_subset = 16'habcd;"), true); @@ -418,6 +437,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('And2Gate bus', () async { @@ -434,6 +454,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Operator indexing', () async { @@ -450,6 +471,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + checkBusSystemC(vectors); }); test('Bus shrink', () async { @@ -487,6 +509,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus reverse slice', () async { @@ -524,6 +547,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus reversed', () async { @@ -537,6 +561,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus range', () async { @@ -582,6 +607,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus swizzle', () async { @@ -597,6 +623,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus bit', () async { @@ -610,6 +637,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('add busses', () async { @@ -625,6 +653,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('expression bit select', () async { @@ -635,6 +664,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + checkBusSystemC(vectors); }); test('selectFrom and selectIndex', () async { @@ -652,6 +682,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(gtm, vectors, dontDeleteTmpFiles: true); }); test('selectFrom with default Value', () async { @@ -666,6 +697,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(gtm, vectors, dontDeleteTmpFiles: true); }); }); } diff --git a/test/collapse_test.dart b/test/collapse_test.dart index 0ef7e00c5..90b5dc5e2 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // collapse_test.dart @@ -38,6 +38,15 @@ class CollapseTestModule extends Module { } } +class CombinationalLoopCollapseModule extends Module { + CombinationalLoopCollapseModule(Logic a) { + a = addInput('a', a); + final loop = Logic(); + loop <= loop | a; + addOutput('out') <= loop.eq(a); + } +} + void main() { tearDown(() async { await Simulator.reset(); @@ -52,6 +61,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('collapse pretty', () async { @@ -64,4 +74,13 @@ void main() { expect(sv, contains(RegExp('internal.*=.*~z'))); }); + + test('combinational loop does not recurse while collapsing', () async { + final mod = CombinationalLoopCollapseModule(Logic()); + await mod.build(); + + final sv = mod.generateSynth(); + expect(sv, contains(' | a')); + expect(sv, contains(' == a')); + }); } diff --git a/test/comparison_test.dart b/test/comparison_test.dart index 1a3e5e98c..a3e24ff25 100644 --- a/test/comparison_test.dart +++ b/test/comparison_test.dart @@ -136,6 +136,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(gtm, vectors); }); }); } diff --git a/test/conditionals_test.dart b/test/conditionals_test.dart index d35a8e5bb..37044cb38 100644 --- a/test/conditionals_test.dart +++ b/test/conditionals_test.dart @@ -490,6 +490,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -564,6 +565,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('iffblock comb', () async { @@ -578,6 +580,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('if invalid ', () async { @@ -600,6 +603,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('elseifblock comb', () async { @@ -614,6 +618,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('Conditional assign module with invalid inputs', () async { @@ -654,6 +659,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('case comb', () async { @@ -668,6 +674,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('Unique case', () async { @@ -696,6 +703,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('should return exception if a conditional is used multiple times.', @@ -717,6 +725,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test( @@ -731,6 +740,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test( @@ -745,6 +755,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test( @@ -780,6 +791,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test( diff --git a/test/const_radix_test.dart b/test/const_radix_test.dart new file mode 100644 index 000000000..4e9beb4eb --- /dev/null +++ b/test/const_radix_test.dart @@ -0,0 +1,164 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// const_radix_test.dart +// Tests for preferred radix formatting of constants. +// +// 2026 July 14 +// Author: Max Korbel + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +/// Exposes constants in each supported radix through generated outputs. +class _ConstRadixModule extends Module { + _ConstRadixModule() : super(name: 'constRadix') { + addOutput('autoHex', width: 8) <= Const(42, width: 8); + addOutput('binaryValue', width: 8) <= + Const(42, width: 8, preferredRadix: 2); + addOutput('octalValue', width: 8) <= Const(42, width: 8, preferredRadix: 8); + addOutput('decimalValue', width: 8) <= + Const(42, width: 8, preferredRadix: 10); + addOutput('hexValue', width: 8) <= Const(42, width: 8, preferredRadix: 16); + addOutput('invalidValue', width: 4) <= + Const( + LogicValue.ofString('10xz'), + preferredRadix: 16, + ); + } +} + +/// Requires its input connection to be a signal rather than an expression. +class _ExpressionlessRadixSub extends Module with SystemVerilog { + @override + List get expressionlessInputs => const ['in']; + + _ExpressionlessRadixSub(Logic input) : super(name: 'expressionlessRadix') { + input = addInput('in', input, width: input.width); + addOutput('out', width: input.width) <= input; + } +} + +/// Drives an expressionless submodule input with a radix-preferred constant. +class _ExpressionlessRadixTop extends Module { + _ExpressionlessRadixTop() : super(name: 'expressionlessRadixTop') { + final sub = _ExpressionlessRadixSub( + Const(42, width: 8, preferredRadix: 10), + ); + addOutput('out', width: 8) <= sub.output('out'); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('Const preferred radix', () { + test('normalizes names using the displayed radix', () { + expect(Const(42, width: 8).name, 'const_42'); + expect( + Const(42, width: 8, preferredRadix: 2).name, + 'const_0b101010', + ); + expect( + Const(42, width: 8, preferredRadix: 8).name, + 'const_0o52', + ); + expect( + Const(42, width: 8, preferredRadix: 10).name, + 'const_42', + ); + expect( + Const(42, width: 8, preferredRadix: 16).name, + 'const_0x2a', + ); + }); + + test('derives names from the normalized stored value', () { + expect( + Const(-1, width: 8, preferredRadix: 16).name, + 'const_0xff', + ); + expect( + Const(BigInt.from(42), width: 8, preferredRadix: 8).name, + 'const_0o52', + ); + expect( + Const( + LogicValue.ofInt(42, 8), + preferredRadix: 10, + ).name, + 'const_42', + ); + expect( + Const(1, width: 8, fill: true, preferredRadix: 16).name, + 'const_0xff', + ); + expect( + Const( + LogicValue.ofString('10xz'), + preferredRadix: 16, + ).name, + 'const_0b10xz', + ); + }); + + test('rejects unsupported radices', () { + for (final radix in [3, 4, 12]) { + expect( + () => Const(1, preferredRadix: radix), + throwsA(isA()), + ); + } + }); + + test('clone preserves the preference and normalized name', () { + final original = Const(42, width: 8, preferredRadix: 2); + final clone = original.clone(); + + expect(clone.preferredRadix, 2); + expect(clone.name, original.name); + expect(clone.value, original.value); + }); + + test('toString remains a Logic diagnostic', () { + expect( + Const(42, width: 8, preferredRadix: 10).toString(), + 'Logic(8): const_42', + ); + }); + + test('normalized names compose into expression names', () { + final result = + Logic(name: 'a', width: 8) + Const(42, width: 8, preferredRadix: 16); + + expect(result.name, contains('const_0x2a')); + }); + + test('controls generated SystemVerilog literals', () async { + final module = _ConstRadixModule(); + await module.build(); + + final systemVerilog = module.generateSynth(); + + expect(systemVerilog, contains("assign autoHex = 8'h2a;")); + expect(systemVerilog, contains("assign binaryValue = 8'b101010;")); + expect(systemVerilog, contains("assign octalValue = 8'o52;")); + expect(systemVerilog, contains("assign decimalValue = 8'd42;")); + expect(systemVerilog, contains("assign hexValue = 8'h2a;")); + expect(systemVerilog, contains("assign invalidValue = 4'b10xz;")); + }); + + test('preserves the preference for expressionless inputs', () async { + final module = _ExpressionlessRadixTop(); + await module.build(); + + final systemVerilog = module.generateSynth(); + + expect(systemVerilog, contains("assign in = 8'd42;")); + expect(systemVerilog, contains('.in(in)')); + expect(systemVerilog, isNot(contains(".in(8'd42)"))); + }); + }); +} diff --git a/test/counter_test.dart b/test/counter_test.dart index 8f59b58d7..3a5aedf2b 100644 --- a/test/counter_test.dart +++ b/test/counter_test.dart @@ -69,6 +69,7 @@ void main() { await SimCompare.checkFunctionalVector(counter, vectors); final simResult = SimCompare.iverilogVector(counter, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(counter, vectors); }); }); } diff --git a/test/extend_test.dart b/test/extend_test.dart index 54f608598..771d12661 100644 --- a/test/extend_test.dart +++ b/test/extend_test.dart @@ -52,6 +52,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); } test('zero extend with same width returns same thing', () async { @@ -117,6 +118,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); } test('setting with bigger number throws exception', () async { diff --git a/test/flop_test.dart b/test/flop_test.dart index 4e3def505..85d41958c 100644 --- a/test/flop_test.dart +++ b/test/flop_test.dart @@ -53,6 +53,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bit with enable', () async { @@ -74,6 +75,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus', () async { @@ -88,6 +90,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus with enable', () async { @@ -111,6 +114,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, no reset value', () async { @@ -124,6 +128,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, const reset value', () async { @@ -141,6 +146,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, logic reset value', () async { @@ -158,6 +164,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus no reset, const reset value', () async { @@ -174,6 +181,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus, enable, reset, const reset value', () async { @@ -194,6 +202,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); }); } diff --git a/test/net_test.dart b/test/net_test.dart index c8d15b7d7..7efe1dd4e 100644 --- a/test/net_test.dart +++ b/test/net_test.dart @@ -689,6 +689,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('build fails with missing inout port', () async { diff --git a/test/systemc_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..9374b479a --- /dev/null +++ b/test/systemc_simcompare_test.dart @@ -0,0 +1,259 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_simcompare_test.dart +// Tests for SystemC synthesis and simulation comparison. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +/// A simple module with basic gates for testing SystemC synthesis. +class GateModule extends Module { + GateModule(Logic a, Logic b) : super(name: 'GateModule') { + a = addInput('a', a); + b = addInput('b', b); + final aAndB = addOutput('a_and_b'); + final aOrB = addOutput('a_or_b'); + final notA = addOutput('not_a'); + + aAndB <= a & b; + aOrB <= a | b; + notA <= ~a; + } +} + +/// A simple counter for testing sequential SystemC synthesis. +class SimpleCounter extends Module { + SimpleCounter(Logic clk, Logic reset, Logic en) : super(name: 'Counter') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + en = addInput('en', en); + final val = addOutput('val', width: 8); + + final nextVal = Logic(name: 'nextVal', width: 8); + + Sequential(clk, reset: reset, [ + If(en, then: [nextVal < nextVal + 1], orElse: [nextVal < nextVal]), + ]); + + val <= nextVal; + } +} + +/// A flip-flop module for testing. +class FlopModule extends Module { + FlopModule(Logic clk, Logic reset, Logic d) : super(name: 'FlopModule') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + d = addInput('d', d, width: 8); + final q = addOutput('q', width: 8); + q <= flop(clk, d, reset: reset); + } +} + +/// A flip-flop with enable. +class FlopEnModule extends Module { + FlopEnModule(Logic clk, Logic reset, Logic en, Logic d) + : super(name: 'FlopEnModule') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + en = addInput('en', en); + d = addInput('d', d, width: 8); + final q = addOutput('q', width: 8); + q <= flop(clk, d, reset: reset, en: en); + } +} + +/// A chained combinational module for checking generated sensitivity lists. +class ChainedGateModule extends Module { + ChainedGateModule(Logic a, Logic b, Logic c) + : super(name: 'ChainedGateModule') { + a = addInput('a', a); + b = addInput('b', b); + c = addInput('c', c); + final y = addOutput('y'); + + final mid = (a & b).named('mid'); + y <= mid | c; + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + tearDownAll(SimCompare.cleanupSystemCCache); + + group('SimCompare SystemC', () { + test('gate module passes vectors', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = GateModule(a, b); + await mod.build(); + + final vectors = [ + Vector({'a': 0, 'b': 0}, {'a_and_b': 0, 'a_or_b': 0, 'not_a': 1}), + Vector({'a': 1, 'b': 0}, {'a_and_b': 0, 'a_or_b': 1, 'not_a': 0}), + Vector({'a': 0, 'b': 1}, {'a_and_b': 0, 'a_or_b': 1, 'not_a': 1}), + Vector({'a': 1, 'b': 1}, {'a_and_b': 1, 'a_or_b': 1, 'not_a': 0}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('counter module passes vectors', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final mod = SimpleCounter(clk, reset, en); + await mod.build(); + + // Same vectors as counter_test.dart (iverilog-compatible timing) + final vectors = [ + Vector({'en': 0, 'reset': 0}, {}), + Vector({'en': 0, 'reset': 1}, {'val': 0}), + Vector({'en': 1, 'reset': 1}, {'val': 0}), + Vector({'en': 1, 'reset': 0}, {'val': 0}), + Vector({'en': 1, 'reset': 0}, {'val': 1}), + Vector({'en': 1, 'reset': 0}, {'val': 2}), + Vector({'en': 1, 'reset': 0}, {'val': 3}), + Vector({'en': 0, 'reset': 0}, {'val': 4}), + Vector({'en': 0, 'reset': 0}, {'val': 4}), + Vector({'en': 1, 'reset': 0}, {'val': 4}), + Vector({'en': 0, 'reset': 0}, {'val': 5}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('flip-flop module passes vectors', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final d = Logic(name: 'd', width: 8); + final mod = FlopModule(clk, reset, d); + await mod.build(); + + // Flop: output follows input with 1-cycle latency + final vectors = [ + Vector({'d': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0xAA, 'reset': 0}, {'q': 0}), + Vector({'d': 0xBB, 'reset': 0}, {'q': 0xAA}), + Vector({'d': 0xCC, 'reset': 0}, {'q': 0xBB}), + Vector({'d': 0xDD, 'reset': 0}, {'q': 0xCC}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('flip-flop with enable passes vectors', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final d = Logic(name: 'd', width: 8); + final mod = FlopEnModule(clk, reset, en, d); + await mod.build(); + + // When en=0, q holds; when en=1, q follows d with 1-cycle latency + final vectors = [ + Vector({'d': 0, 'en': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0, 'en': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0x42, 'en': 1, 'reset': 0}, {'q': 0}), + Vector({'d': 0x55, 'en': 1, 'reset': 0}, {'q': 0x42}), + Vector({'d': 0xFF, 'en': 0, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0x00, 'en': 0, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0x99, 'en': 1, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0xAA, 'en': 1, 'reset': 0}, {'q': 0x99}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('independent inline gates share a method', () async { + final mod = GateModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final systemc = mod.generateSystemC(); + final assignMethods = + RegExp(r'SC_METHOD\(assign_\d+\);').allMatches(systemc).toList(); + + expect(assignMethods, hasLength(1)); + expect(systemc, contains('sensitive << a;')); + expect(systemc, contains('sensitive << b;')); + expect(systemc, contains('a_and_b = a.read() & b.read();')); + expect(systemc, contains('a_or_b = a.read() | b.read();')); + expect(systemc, contains('not_a = !a.read();')); + }); + + test('chained inline gates use minimal sensitivity lists', () async { + final mod = ChainedGateModule( + Logic(name: 'a'), + Logic(name: 'b'), + Logic(name: 'c'), + ); + await mod.build(); + + final systemc = mod.generateSystemC(); + final sensitivityBlocks = + RegExp(r'SC_METHOD\(assign_\d+\);\n((?: sensitive << .+;\n)+)') + .allMatches(systemc) + .map((match) => match.group(1)!) + .toList(); + + expect(sensitivityBlocks, hasLength(2)); + expect( + sensitivityBlocks, + contains(allOf( + contains('sensitive << a;'), + contains('sensitive << b;'), + isNot(contains('sensitive << mid;')), + )), + ); + expect( + sensitivityBlocks, + contains(allOf( + contains('sensitive << mid;'), + contains('sensitive << c;'), + isNot(contains('sensitive << a;')), + isNot(contains('sensitive << b;')), + )), + ); + }); + + test('counter trace-based comparison', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final mod = SimpleCounter(clk, reset, en); + await mod.build(); + + // Use the trace-based approach: just write normal simulation code, + // no vectors needed. The method records all I/O at every clock edge + // and replays through SystemC. + final result = await SimCompare.systemcSimCompare( + mod, + clk, + stimulus: () async { + reset.inject(1); + en.inject(0); + Simulator.registerAction(25, () { + reset.put(0); + en.put(1); + }); + Simulator.registerAction(65, () { + en.put(0); + }); + Simulator.registerAction(85, () { + en.put(1); + }); + Simulator.setMaxSimTime(120); + }, + ); + expect(result, isTrue); + }); + }); +} diff --git a/tool/gh_actions/check_tmp_test.sh b/tool/gh_actions/check_tmp_test.sh index a21154d8d..f0e5e92bc 100755 --- a/tool/gh_actions/check_tmp_test.sh +++ b/tool/gh_actions/check_tmp_test.sh @@ -13,9 +13,13 @@ set -euo pipefail declare -r folder_name='tmp_test' -# The "tmp_test" folder after performing the tests should be empty. +# The "tmp_test" folder after performing the tests should be empty, +# except for the precompiled-header cache (pch/) which is intentionally +# persistent and pre-built by CI before the test run. if [ -d "${folder_name}" ]; then - output=$(find ${folder_name} | wc --lines | tee) + output=$(find ${folder_name} -not -path "${folder_name}/pch" \ + -not -path "${folder_name}/pch/*" \ + | wc --lines | tee) if [ "${output}" -eq 1 ]; then echo "Success: directory \"${folder_name}\" is empty!" else diff --git a/tool/gh_actions/cleanup_systemc_tmp.sh b/tool/gh_actions/cleanup_systemc_tmp.sh new file mode 100755 index 000000000..d91149adb --- /dev/null +++ b/tool/gh_actions/cleanup_systemc_tmp.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# cleanup_systemc_tmp.sh +# GitHub Actions step helper: remove SystemC temporary build caches. +# +# 2026 June +# Author: Desmond Kirkpatrick + +set -euo pipefail + +declare -r folder_name='tmp_test' + +if [ ! -d "${folder_name}" ]; then + exit 0 +fi + +find "${folder_name}" -mindepth 1 \ + \( \ + -name 'pch' -o \ + -name 'pch.lock' -o \ + -name 'tmp_sc_*' -o \ + -name 'sc_input_*' -o \ + -name 'Makefile_sc' \ + \) \ + -exec rm -rf {} + + +mkdir -p "${folder_name}" \ No newline at end of file diff --git a/tool/gh_actions/install_systemc.sh b/tool/gh_actions/install_systemc.sh new file mode 100755 index 000000000..5c3fb82bf --- /dev/null +++ b/tool/gh_actions/install_systemc.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# Copyright (C) 2024-2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# install_systemc.sh +# GitHub Actions step: Install Accellera SystemC library. +# +# Downloads, builds, and installs SystemC to /opt/systemc. +# Uses a cache-friendly layout so the install directory can be +# cached across CI runs. +# +# 2026 May +# Author: Desmond Kirkpatrick + +set -euo pipefail + +SYSTEMC_VERSION="${SYSTEMC_VERSION:-3.0.2}" +INSTALL_PREFIX="${SYSTEMC_INSTALL_PREFIX:-/opt/systemc}" + +if [ "$(id -u)" -eq 0 ]; then + SUDO=() +else + SUDO=(sudo) +fi + +# Skip if already installed (e.g. from cache) +if [ -f "$INSTALL_PREFIX/lib/libsystemc.so" ]; then + echo "SystemC already installed at $INSTALL_PREFIX — skipping build." + exit 0 +fi + +echo "Installing Accellera SystemC $SYSTEMC_VERSION to $INSTALL_PREFIX ..." + +# Install build dependencies +"${SUDO[@]}" apt-get update -qq +"${SUDO[@]}" apt-get install --yes --no-install-recommends cmake g++ make + +# Download source +TARBALL="systemc-$SYSTEMC_VERSION.tar.gz" +DOWNLOAD_URL="https://github.com/accellera-official/systemc/archive/refs/tags/$SYSTEMC_VERSION.tar.gz" + +cd /tmp +curl -fsSL -o "$TARBALL" "$DOWNLOAD_URL" +tar xzf "$TARBALL" +cd "systemc-$SYSTEMC_VERSION" + +# Build with CMake +mkdir -p build && cd build +cmake .. \ + -DCMAKE_INSTALL_PREFIX="$INSTALL_PREFIX" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_STANDARD=17 \ + -DBUILD_SHARED_LIBS=ON \ + -DENABLE_EXAMPLES=OFF \ + -DENABLE_REGRESSION=OFF \ + -DDISABLE_COPYRIGHT_MESSAGE=ON + +make -j"$(nproc)" +"${SUDO[@]}" make install + +echo "SystemC $SYSTEMC_VERSION installed to $INSTALL_PREFIX" diff --git a/tool/gh_actions/run_tests.sh b/tool/gh_actions/run_tests.sh index 160352cd2..5e2804edc 100755 --- a/tool/gh_actions/run_tests.sh +++ b/tool/gh_actions/run_tests.sh @@ -11,8 +11,9 @@ set -euo pipefail -dart test +# Exclude FFI-dependent tests (dart:ffi unavailable on some CI platforms). +dart test $(find test -name '*_test.dart' ! -name 'systemc_ffi_cosim_test.dart' | sort) # run tests in JS (increase heap size also) export NODE_OPTIONS="--max-old-space-size=8192" -dart test --platform node \ No newline at end of file +dart test --platform node $(find test -name '*_test.dart' ! -name 'systemc_ffi_cosim_test.dart' | sort) \ No newline at end of file diff --git a/tool/gh_actions/setup_systemc_pch.sh b/tool/gh_actions/setup_systemc_pch.sh new file mode 100755 index 000000000..25d1ff93f --- /dev/null +++ b/tool/gh_actions/setup_systemc_pch.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Copyright (C) 2024-2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# setup_systemc_pch.sh +# GitHub Actions step: Pre-build SystemC precompiled header and Makefile. +# +# Run this after install_systemc.sh and before tests to avoid race +# conditions when multiple test isolates run in parallel. +# +# 2026 May +# Author: Desmond Kirkpatrick + +set -euo pipefail + +SC_HOME="${SYSTEMC_INCLUDE:-/opt/systemc/include}" +SC_LIB="${SYSTEMC_LIB:-/opt/systemc/lib}" + +if [ ! -d "$SC_HOME" ]; then + echo "SystemC not found at $SC_HOME — skipping PCH setup." + exit 0 +fi + +# Detect C++ standard from the installed library +CXX_STD="c++17" +if command -v nm &>/dev/null && [ -f "$SC_LIB/libsystemc.so" ]; then + if nm -D "$SC_LIB/libsystemc.so" 2>/dev/null | grep -q 'cxx202002L'; then + CXX_STD="c++20" + fi +fi + +echo "Setting up SystemC PCH ($CXX_STD) ..." + +# Build precompiled header +PCH_DIR="tmp_test/pch" +mkdir -p "$PCH_DIR" +cp "$SC_HOME/systemc.h" "$PCH_DIR/systemc.h" +g++ -std="$CXX_STD" -I"$SC_HOME" -x c++-header \ + -o "$PCH_DIR/systemc.h.gch" "$SC_HOME/systemc.h" + +echo "PCH built: $PCH_DIR/systemc.h.gch" diff --git a/tool/run_checks.sh b/tool/run_checks.sh index 6a0ea0080..939750a47 100755 --- a/tool/run_checks.sh +++ b/tool/run_checks.sh @@ -63,6 +63,10 @@ fi print_step 'Run project tests' tool/gh_actions/run_tests.sh +# Clean SystemC temporary files +print_step 'Clean SystemC temporary files' +tool/gh_actions/cleanup_systemc_tmp.sh + # Check temporary test files print_step 'Check temporary test files' tool/gh_actions/check_tmp_test.sh