diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 3d485094f..780efba84 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: Run SystemC tests + run: dart test test/systemc_vector_test.dart + - 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 + dart test test/systemc_vector_test.dart + tool/gh_actions/check_tmp_test.sh deploy-documentation: name: Deploy Documentation diff --git a/README.md b/README.md index b812acc2f..1863edac9 100644 --- a/README.md +++ b/README.md @@ -44,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. diff --git a/dart_test.yaml b/dart_test.yaml new file mode 100644 index 000000000..30eaa9f01 --- /dev/null +++ b/dart_test.yaml @@ -0,0 +1,15 @@ +# Test configuration for ROHD. +# +# To exclude FFI-dependent tests (e.g. in CI without native code support): +# dart test --preset no-ffi +# +# To run all tests including FFI (requires native shared libraries): +# dart test + +tags: + ffi: + # Tests requiring dart:ffi and native shared libraries. + +presets: + no-ffi: + exclude_tags: ffi diff --git a/doc/architecture.md b/doc/architecture.md index cc1e775ae..aacf29c35 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -24,7 +24,7 @@ The `Simulator` acts as a statically accessible driver of the overall simulation ### Synthesizer -A separate type of object responsible for taking a `Module` and converting it to some output, such as SystemVerilog. +A separate type of object responsible for taking a `Module` and converting it to some output, such as SystemVerilog or SystemC. ## Organization @@ -44,7 +44,7 @@ Contains a collection of `Module` implementations that can be used as primitive ### Synthesizers -Contains logic for synthesizing `Module`s into some output. It is structured to maximize reusability across different output types (including those not yet supported). +Contains logic for synthesizing `Module`s into some output (e.g. SystemVerilog, SystemC). It is structured to maximize reusability across different output types. ### Utilities diff --git a/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart b/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart index 5765f08bf..18efb5a2d 100644 --- a/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart +++ b/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart @@ -7,7 +7,6 @@ import '../../chapter_3/answers/helper.dart'; import '../../chapter_5/answers/full_subtractor.dart'; class FullSubtractorComb extends FullSubtractor { - @override FullSubtractorComb(super.a, super.b, super.borrowIn) { // Declare input and output final a = input('a'); diff --git a/doc/user_guide/_docs/A21-generation.md b/doc/user_guide/_docs/A21-generation.md index 00d3d25bb..27135a53f 100644 --- a/doc/user_guide/_docs/A21-generation.md +++ b/doc/user_guide/_docs/A21-generation.md @@ -5,7 +5,7 @@ last_modified_at: 2023-11-13 toc: true --- -Hardware in ROHD is convertible to an output format via `Synthesizer`s, the most popular of which is SystemVerilog. Hardware in ROHD can be converted to logically equivalent, human-readable SystemVerilog with structure, hierarchy, ports, and names maintained. +Hardware in ROHD is convertible to an output format via `Synthesizer`s. The most popular output format is SystemVerilog, with SystemC also available. Hardware in ROHD can be converted to logically equivalent, human-readable SystemVerilog or SystemC with structure, hierarchy, ports, and names maintained. The simplest way to generate SystemVerilog is with the helper method `generateSynth` in `Module`: @@ -28,6 +28,26 @@ void main() async { The `generateSynth` function will return a `String` with the SystemVerilog `module` definitions for the top-level it is called on, as well as any sub-modules (recursively). You can dump the entire contents to a file and use it anywhere you would any other SystemVerilog. +## SystemC generation + +ROHD can also generate SystemC (C++ with the SystemC library) from the same hardware description. Use the `generateSystemC` helper method: + +```dart +void main() async { + final myModule = MyModule(); + await myModule.build(); + + final generatedSc = myModule.generateSystemC(); + + // write it to a file + File('myHardware.h').writeAsStringSync(generatedSc); +} +``` + +The generated SystemC uses `SC_MODULE`, `SC_METHOD`, and `SC_CTHREAD` constructs. Combinational logic becomes `SC_METHOD` processes, sequential logic (flip-flops and `Sequential` blocks) sharing the same clock and reset are consolidated into a single `SC_CTHREAD`, and sub-modules are instantiated with port bindings. All signal types map to SystemC equivalents (`bool`, `sc_uint`, `sc_biguint`). + +For more control over SystemC generation, use `SynthBuilder` with `SystemCSynthesizer()` directly. + ## Controlling naming ### Modules diff --git a/doc/user_guide/_get-started/01-overview.md b/doc/user_guide/_get-started/01-overview.md index c1a98cdc1..c30c9f87f 100644 --- a/doc/user_guide/_get-started/01-overview.md +++ b/doc/user_guide/_get-started/01-overview.md @@ -19,7 +19,7 @@ Features of ROHD include: - **Simple and fast build**, free of complex build systems and EDA vendor tools - Can use the excellent pub.dev **package manager** and all the packages it has to offer - Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform dumper** to .vcd file format -- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption +- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** and **SystemC** for integration or downstream tool consumption - **Run-time dynamic** module port definitions (numbers, names, widths, etc.) and internal module logic, including recursive module contents - Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components. - Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. diff --git a/lib/rohd.dart b/lib/rohd.dart index 841505590..d0ea2a266 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -1,6 +1,7 @@ // Copyright (C) 2021-2023 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'src/diagnostics/module_services.dart'; export 'src/exceptions/exceptions.dart'; export 'src/external.dart'; export 'src/finite_state_machine.dart'; diff --git a/lib/src/diagnostics/module_services.dart b/lib/src/diagnostics/module_services.dart new file mode 100644 index 000000000..a0e583b59 --- /dev/null +++ b/lib/src/diagnostics/module_services.dart @@ -0,0 +1,78 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services.dart +// Singleton service registry for DevTools integration. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/diagnostics/inspector_service.dart'; + +/// Singleton service registry that provides a unified query surface for +/// DevTools and other inspection tools. +/// +/// Services register themselves here on construction; DevTools evaluates +/// getters on [instance] via `EvalOnDartLibrary` to pull data. +/// +/// **Auto-registered:** +/// - [rootModule] / [hierarchyJSON] — set by [Module.build]. +/// +/// **Opt-in (registered by service constructors):** +/// - [svService] — SystemVerilog synthesis results. +/// +/// Additional services (netlist, trace, waveform) can be added by setting +/// the corresponding field after construction. +class ModuleServices { + ModuleServices._(); + + /// The singleton instance. + static final ModuleServices instance = ModuleServices._(); + + // ─── Hierarchy (auto-registered by Module.build) ────────────── + + /// The most recently built top-level [Module]. + /// + /// Set automatically at the end of [Module.build]. + Module? rootModule; + + /// Returns the module hierarchy as a JSON string. + /// + /// DevTools evaluates this via `EvalOnDartLibrary` to display + /// the module hierarchy. + String get hierarchyJSON { + ModuleTree.rootModuleInstance = rootModule; + return ModuleTree.instance.hierarchyJSON; + } + + /// Returns the primary inspector JSON for DevTools. + /// + /// Returns the hierarchy JSON. Downstream branches (e.g. netlist) may + /// override this to return richer data when available. + String get inspectorJSON => hierarchyJSON; + + // ─── SystemVerilog service (opt-in) ─────────────────────────── + + /// The active [SvService], if one has been registered. + SvService? svService; + + /// Returns SV synthesis metadata as JSON, or an unavailable status. + String get svJSON => + svService != null ? jsonEncode(svService!.toJson()) : _unavailable('sv'); + + // ─── Helpers ────────────────────────────────────────────────── + + static String _unavailable(String service) => jsonEncode({ + 'status': 'unavailable', + 'reason': '$service service not registered', + }); + + /// Resets all services. Intended for test teardown. + void reset() { + rootModule = null; + svService = null; + } +} diff --git a/lib/src/module.dart b/lib/src/module.dart index 92fc410e0..d4e31fbd5 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // module.dart @@ -11,11 +11,10 @@ import 'dart:async'; import 'dart:collection'; import 'package:meta/meta.dart'; - import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; -import 'package:rohd/src/diagnostics/inspector_service.dart'; import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; import 'package:rohd/src/utilities/timestamper.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; @@ -52,6 +51,22 @@ abstract class Module { /// An internal mapping of input names to their sources to this [Module]. late final Map _inputSources = {}; + // ─── Central naming (Namer) ───────────────────────────────────── + + /// Central namer that owns both the signal and instance namespaces. + /// Initialized lazily on first access (after build). + @internal + late final Namer namer = _createNamer(); + + Namer _createNamer() { + assert(hasBuilt, 'Module must be built before canonical names are bound.'); + return Namer.forModule( + inputs: _inputs, + outputs: _outputs, + inOuts: _inOuts, + ); + } + /// An internal mapping of inOut names to their sources to this [Module]. late final Map _inOutSources = {}; @@ -317,7 +332,7 @@ abstract class Module { _hasBuilt = true; - ModuleTree.rootModuleInstance = this; + ModuleServices.instance.rootModule = this; } /// Confirms that the post-[build] hierarchy is valid. @@ -1131,6 +1146,27 @@ abstract class Module { .getSynthFileContents() .join('\n\n////////////////////\n\n'); } + + /// Returns a synthesized SystemC version of this [Module]. + /// + /// Generates SystemC code that is equivalent to the hardware described by + /// this module, using the same naming strategy as [generateSynth]. + String generateSystemC() { + if (!_hasBuilt) { + throw ModuleNotBuiltException(this); + } + + final synthBuilder = SynthBuilder(this, SystemCSynthesizer()); + final moduleContents = + synthBuilder.getSynthFileContents().map((e) => e.contents).join('\n'); + return '// Generated by ROHD - www.github.com/intel/rohd\n' + '// Generation time: ${Timestamper.stamp()}\n' + '// ROHD Version: ${Config.version}\n' + '\n' + '#include \n' + '\n' + '$moduleContents'; + } } extension on LogicStructure { diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index cd9aa8750..3e3f4acdf 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -88,6 +88,11 @@ class FlipFlop extends Module with SystemVerilog { /// Only initialized if a constant value is provided. late LogicValue _resetValueConst; + /// Returns the constant reset value if one was provided, or null if the + /// reset value is a port or no reset exists. + LogicValue? get constantResetValue => + _reset != null && _resetValuePort == null ? _resetValueConst : null; + /// Indicates whether provided `reset` signals should be treated as an async /// reset. If no `reset` is provided, this will have no effect. final bool asyncReset; diff --git a/lib/src/modules/conditionals/sequential.dart b/lib/src/modules/conditionals/sequential.dart index 62a7c1129..8871202fd 100644 --- a/lib/src/modules/conditionals/sequential.dart +++ b/lib/src/modules/conditionals/sequential.dart @@ -135,6 +135,14 @@ class Sequential extends Always { /// The input edge triggers used in this block. final List<_SequentialTrigger> _triggers = []; + /// Returns the edge polarity for each trigger input port. + /// + /// Each entry pairs the trigger input port name with whether the trigger + /// fires on a positive edge (`true`) or negative edge (`false`). + List<({String portName, bool isPosedge})> get triggerEdges => _triggers + .map((t) => (portName: t.signal.name, isPosedge: t.isPosedge)) + .toList(); + /// When `false`, an [SignalRedrivenException] will be thrown during /// simulation if the same signal is driven multiple times within this /// [Sequential]. diff --git a/lib/src/synthesizers/synth_builder.dart b/lib/src/synthesizers/synth_builder.dart index 54e312ab3..f9d0a0d08 100644 --- a/lib/src/synthesizers/synth_builder.dart +++ b/lib/src/synthesizers/synth_builder.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synth_builder.dart diff --git a/lib/src/synthesizers/synthesizer.dart b/lib/src/synthesizers/synthesizer.dart index b70c9338e..687bbab03 100644 --- a/lib/src/synthesizers/synthesizer.dart +++ b/lib/src/synthesizers/synthesizer.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synthesizer.dart @@ -6,7 +6,6 @@ // // 2021 August 26 // Author: Max Korbel -// import 'package:rohd/rohd.dart'; diff --git a/lib/src/synthesizers/synthesizers.dart b/lib/src/synthesizers/synthesizers.dart index b8c8523ec..5246eb98a 100644 --- a/lib/src/synthesizers/synthesizers.dart +++ b/lib/src/synthesizers/synthesizers.dart @@ -5,4 +5,5 @@ export 'synth_builder.dart'; export 'synth_file_contents.dart'; export 'synthesis_result.dart'; export 'synthesizer.dart'; +export 'systemc/systemc.dart'; export 'systemverilog/systemverilog.dart'; diff --git a/lib/src/synthesizers/systemc/systemc.dart b/lib/src/synthesizers/systemc/systemc.dart new file mode 100644 index 000000000..7bf0f1211 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc.dart @@ -0,0 +1,29 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synthesizer.dart +// Definition for SystemC Synthesizer +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; + +/// A [Synthesizer] which generates equivalent SystemC as the given [Module]. +/// +/// Attempts to maintain signal naming and structure as much as possible, +/// using the same naming strategy as the SystemVerilog synthesizer. +class SystemCSynthesizer extends Synthesizer { + @override + bool generatesDefinition(Module module) => + // ignore: deprecated_member_use_from_same_package + !((module is CustomSystemVerilog) || + (module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none)); + + @override + SynthesisResult synthesize(Module module, + String Function(Module module) getInstanceTypeOfModule) => + SystemCSynthesisResult(module, getInstanceTypeOfModule); +} diff --git a/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart b/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart new file mode 100644 index 000000000..e670279d4 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart @@ -0,0 +1,31 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synth_module_definition.dart +// Definition for SystemCSynthModuleDefinition +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// A special [SynthModuleDefinition] for SystemC modules. +class SystemCSynthModuleDefinition extends SynthModuleDefinition { + /// Creates a new [SystemCSynthModuleDefinition] for the given [module]. + SystemCSynthModuleDefinition(super.module); + + @override + void process() { + // For now, do not collapse inline modules. Each InlineSystemVerilog gate + // remains as a sub-module instantiation and gets emitted as an assign-style + // expression in the generated SystemC (similar to SV `assign x = a & b`). + // + // Future: implement chain-collapsing for compound expressions. + } + + @override + SynthSubModuleInstantiation createSubModuleInstantiation(Module m) => + SystemCSynthSubModuleInstantiation(m); +} diff --git a/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart new file mode 100644 index 000000000..7e692ff8a --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart @@ -0,0 +1,113 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synth_sub_module_instantiation.dart +// Definition for SystemCSynthSubModuleInstantiation +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// Represents a submodule instantiation for SystemC. +class SystemCSynthSubModuleInstantiation extends SynthSubModuleInstantiation { + /// Creates a new [SystemCSynthSubModuleInstantiation] for the given + /// [module]. + SystemCSynthSubModuleInstantiation(super.module); + + /// If [module] is [InlineSystemVerilog], this will be the [SynthLogic] that + /// is the `result` of that module. Otherwise, `null`. + SynthLogic? get inlineResultLogic => module is! InlineSystemVerilog + ? null + : (outputMapping[(module as InlineSystemVerilog).resultSignalName] ?? + inOutMapping[(module as InlineSystemVerilog).resultSignalName]); + + /// Mapping from [SynthLogic]s which are outputs of inlineable modules to + /// those inlineable modules. + Map? + synthLogicToInlineableSynthSubmoduleMap; + + /// Provides a mapping from ports of this module to a string that can be fed + /// into that port, which may include inline expressions. + Map _modulePortsMapWithInline( + Map plainPorts) => + plainPorts.map((name, synthLogic) => MapEntry( + name, + synthLogicToInlineableSynthSubmoduleMap?[synthLogic] + ?.inlineSystemC() ?? + (synthLogic.declarationCleared ? '' : synthLogic.name))); + + /// Provides the inline SystemC expression for this module. + /// + /// Should only be called if [module] is [InlineSystemVerilog]. + String inlineSystemC() { + final portNameToValueMapping = _modulePortsMapWithInline( + {...inputMapping, ...inOutMapping} + ..remove((module as InlineSystemVerilog).resultSignalName), + ); + + final inlineRepresentation = + _inlineSystemCExpression(portNameToValueMapping); + + return '($inlineRepresentation)'; + } + + /// Generates the inline SystemC expression for the gate module. + String _inlineSystemCExpression(Map inputs) { + final m = module; + + if (m is NotGate) { + final inVal = inputs.values.first; + return '~$inVal'; + } else if (m is And2Gate) { + return '${inputs.values.first} & ${inputs.values.last}'; + } else if (m is Or2Gate) { + return '${inputs.values.first} | ${inputs.values.last}'; + } else if (m is Xor2Gate) { + return '${inputs.values.first} ^ ${inputs.values.last}'; + } else if (m is Mux) { + // Mux has inputs: control, d0, d1 → output: y + // In SystemC: control ? d1 : d0 + final entries = inputs.entries.toList(); + final control = entries[0].value; + final d0 = entries[1].value; + final d1 = entries[2].value; + return '$control ? $d1 : $d0'; + } else if (m is InlineSystemVerilog) { + // Fallback: use the verilog inline expression as a reasonable + // approximation (many operators are identical between SV and C++) + return m.inlineVerilog(inputs); + } + + throw SynthException('Unsupported inline module type: ${m.runtimeType}'); + } + + /// Provides the full SystemC instantiation for this module as a member + /// declaration and port binding in the constructor. + /// + /// Returns null if this module does not need instantiation. + String? memberDeclaration(String instanceType) { + if (!needsInstantiation) { + return null; + } + return '$instanceType $name{"$name"};'; + } + + /// Generates port binding statements for the constructor body. + String? portBindings() { + if (!needsInstantiation) { + return null; + } + final bindings = []; + final allPorts = {...inputMapping, ...outputMapping, ...inOutMapping}; + for (final entry in allPorts.entries) { + final portName = entry.key; + final synthLogic = entry.value; + if (!synthLogic.declarationCleared) { + bindings.add('$name.$portName(${synthLogic.name});'); + } + } + return bindings.join('\n'); + } +} diff --git a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart new file mode 100644 index 000000000..fa73b7f30 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart @@ -0,0 +1,1678 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synthesis_result.dart +// Definition for SystemCSynthesisResult +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:collection/collection.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/conditionals/always.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// A [SynthesisResult] representing a conversion of a [Module] to SystemC. +class SystemCSynthesisResult extends SynthesisResult { + /// A cached copy of the generated ports. + late final String _portsString; + + /// A cached copy of the generated module body (used for matching). + late final String _moduleBodyString; + + /// The main [SynthModuleDefinition] for this. + final SynthModuleDefinition _synthModuleDefinition; + + @override + List get supportingModules => + _synthModuleDefinition.supportingModules; + + // Cached sections for final assembly + late final String _internalSigs; + late final String _subMembers; + late final String _ctorBody; + late final String _methodBodies; + + /// Creates a new [SystemCSynthesisResult] for the given [module]. + SystemCSynthesisResult(super.module, super.getInstanceTypeOfModule) + : _synthModuleDefinition = SystemCSynthModuleDefinition(module) { + _findClockResetSignals(); + _portsString = _systemCPorts(); + _buildModuleBody(getInstanceTypeOfModule); + _moduleBodyString = '$_ctorBody|$_methodBodies'; + } + + @override + bool matchesImplementation(SynthesisResult other) => + other is SystemCSynthesisResult && + other._portsString == _portsString && + other._moduleBodyString == _moduleBodyString; + + @override + int get matchHashCode => _portsString.hashCode ^ _moduleBodyString.hashCode; + + @override + String toFileContents() => _toSystemC(); + + @override + List toSynthFileContents() => List.unmodifiable([ + SynthFileContents( + name: instanceTypeName, + description: 'SystemC module definition for $instanceTypeName', + contents: _toSystemC(), + ) + ]); + + // ──────────────────────────────────────────────────────────────────── + // Line/column position tracking for debug tracing + // ──────────────────────────────────────────────────────────────────── + + /// SystemC line map: signal/instance name → list of `'line:col'` positions + /// in the generated SystemC output (both 1-based). + /// + /// Each name's list contains the first occurrence (the declaration / port / + /// submodule member line) followed by each assignment LHS line where that + /// name appears on the left of `=` in a method body. Positions are in + /// textual (source) order; consumers that need the "assignments first, + /// declaration last" convention should reorder at emit time. + /// + /// Populated by [_buildScLineMap] after the final text is assembled. + /// Keys match the names used in the FLC trace data: canonical signal + /// names (from [SynthLogic.name]) for signals and + /// [Module.uniqueInstanceName] for submodule instances. + Map> get scLineMap => Map.unmodifiable( + _scLineMap.map((k, v) => MapEntry(k, List.unmodifiable(v)))); + final Map> _scLineMap = {}; + + /// Walks the already-generated [scText] counting newlines, and records + /// the 1-based `line:col` of each signal declaration, port, submodule + /// instance member, and assignment LHS. + /// + /// This mirrors the approach used by the SystemVerilog synthesizer's + /// `_buildSvLineMap` in the `source_debug` branch, enabling the + /// `SignalSourceTracer` to emit FLC data with both SV and SC positions. + void _buildScLineMap(String scText) { + _scLineMap.clear(); + + final targets = { + for (final sig in _synthModuleDefinition.inputs) sig.name, + for (final sig in _synthModuleDefinition.outputs) sig.name, + for (final sig in _synthModuleDefinition.inOuts) sig.name, + for (final sig in _synthModuleDefinition.internalSignals + .where((e) => e.needsDeclaration)) + sig.name, + for (final smi in _synthModuleDefinition.subModuleInstantiations + .where((s) => s.needsInstantiation)) + smi.name, + }; + + if (targets.isEmpty) { + return; + } + + // Single-pass: tokenize each line once, check tokens against target set. + // Record the first occurrence (declaration) and any subsequent occurrence + // that is an assignment LHS (identifier followed by `=` but not `==`). + final identRe = RegExp(r'[A-Za-z_]\w*'); + var lineNum = 1; + var lineStart = 0; + final len = scText.length; + + for (var i = 0; i <= len; i++) { + if (i == len || scText[i] == '\n') { + final lineText = scText.substring(lineStart, i); + for (final match in identRe.allMatches(lineText)) { + final word = match.group(0)!; + if (!targets.contains(word)) { + continue; + } + final pos = '$lineNum:${match.start + 1}'; + final list = _scLineMap[word]; + if (list == null) { + // First occurrence — declaration / port / sub-module member. + _scLineMap[word] = [pos]; + } else if (_isAssignmentLhs(lineText, match.end) && + !list.contains(pos)) { + // Subsequent occurrence on an assignment LHS — record it. + list.add(pos); + } + } + lineNum++; + lineStart = i + 1; + } + } + } + + /// Returns true if the identifier ending at [afterIdent] in [lineText] is + /// followed (after optional whitespace) by a single `=` (and not `==`). + static bool _isAssignmentLhs(String lineText, int afterIdent) { + var j = afterIdent; + while (j < lineText.length && + (lineText.codeUnitAt(j) == 0x20 || lineText.codeUnitAt(j) == 0x09)) { + j++; + } + if (j >= lineText.length || lineText[j] != '=') { + return false; + } + if (j + 1 < lineText.length && lineText[j + 1] == '=') { + return false; + } + return true; + } + + // ──────────────────────────────────────────────────────────────────── + // Clock/reset detection + // ──────────────────────────────────────────────────────────────────── + + /// Internal clock signals promoted to ports (from SimpleClockGenerator). + late final Set _promotedClockSignals; + + /// Pre-scans sub-module instantiations to identify clock/reset signals + /// and internal clocks that should be promoted to ports. + void _findClockResetSignals() { + final promotedClocks = {}; + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + final m = ssmi.module; + // Detect SimpleClockGenerator and promote its output to a port + if (m is SimpleClockGenerator) { + for (final entry in ssmi.outputMapping.entries) { + promotedClocks.add(entry.value.name); + } + } + } + _promotedClockSignals = promotedClocks; + } + + // ──────────────────────────────────────────────────────────────────── + // Type mapping + // ──────────────────────────────────────────────────────────────────── + + /// Sanitize a signal/port name to be a valid C++ identifier. + /// Replaces `[N]` with `_N_` (LogicArray element indexing). + static String _scName(String name) => + name.replaceAllMapped(RegExp(r'\[(\d+)\]'), (m) => '_${m[1]}_'); + + /// Maps a signal width to the appropriate SystemC data type. + static String systemCType(int width) { + if (width == 1) { + return 'bool'; + } else if (width <= 64) { + return 'sc_uint<$width>'; + } else { + return 'sc_biguint<$width>'; + } + } + + /// SystemC input port type for a given width. + static String systemCInType(int width) => 'sc_in<${systemCType(width)}>'; + + /// SystemC output port type for a given width. + static String systemCOutType(int width) => 'sc_out<${systemCType(width)}>'; + + /// SystemC signal type for a given width. + static String systemCSignalType(int width) => + 'sc_signal<${systemCType(width)}>'; + + // ──────────────────────────────────────────────────────────────────── + // Port declarations + // ──────────────────────────────────────────────────────────────────── + + String _systemCPorts() { + final lines = []; + for (final sig in _synthModuleDefinition.inputs) { + final n = _scName(sig.name); + lines.add(' ${systemCInType(sig.width)} $n{"$n"};'); + } + // Promote internal clock signals (from SimpleClockGenerator) to ports + for (final clkName in _promotedClockSignals) { + final n = _scName(clkName); + lines.add(' ${systemCInType(1)} $n{"$n"};'); + } + for (final sig in _synthModuleDefinition.outputs) { + final n = _scName(sig.name); + lines.add(' ${systemCOutType(sig.width)} $n{"$n"};'); + } + return lines.join('\n'); + } + + // ──────────────────────────────────────────────────────────────────── + // Internal signals + // ──────────────────────────────────────────────────────────────────── + + String _buildInternalSignals() { + final declarations = []; + for (final sig in _synthModuleDefinition.internalSignals + .where((e) => e.needsDeclaration) + .where((e) => !_promotedClockSignals.contains(e.name)) + .sorted((a, b) => a.name.compareTo(b.name))) { + final n = _scName(sig.name); + declarations.add(' ${systemCSignalType(sig.width)} $n{"$n"};'); + } + + // Declare individual signals for array elements that are written to + // (FlipFlop/Sequential outputs targeting array elements) + for (final elemName in _arrayElementsWritten.keys) { + final n = _scName(elemName); + final width = _arrayElementsWritten[elemName]!; + declarations.add(' ${systemCSignalType(width)} $n{"$n"};'); + } + return declarations.join('\n'); + } + + /// Maps array element names (e.g. "delayLine[0]") to their widths. + /// These need separate signal declarations because SystemC can't do + /// partial writes to sc_signal. + late final Map _arrayElementsWritten = + _findArrayElementsWritten(); + + /// Groups array elements by parent: parentName → list of (index, elemWidth). + late final Map> + _arrayElementsByParent = _groupArrayElementsByParent(); + + Map _findArrayElementsWritten() { + final result = {}; + + void addIfArrayElement(SynthLogic sl) { + if (sl is SynthLogicArrayElement) { + result[sl.name] = sl.logic.width; + } + } + + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + final m = ssmi.module; + + // All submodule output mappings + ssmi.outputMapping.values.forEach(addIfArrayElement); + + // Inline gate result logics + if (ssmi is SystemCSynthSubModuleInstantiation) { + final rl = ssmi.inlineResultLogic; + if (rl != null) { + addIfArrayElement(rl); + } + } + + // Scan conditionals for nested array element receivers + if (m is Combinational) { + _collectArrayReceiversFromConditionals(m.conditionals, result); + } else if (m is Sequential) { + _collectArrayReceiversFromConditionals(m.conditionals, result); + } + } + + // Wire assignments targeting array elements + for (final assignment in _synthModuleDefinition.assignments) { + addIfArrayElement(assignment.dst); + } + + return result; + } + + /// Recursively walks a conditionals tree to find all receivers that + /// are array elements and adds them to [result]. + void _collectArrayReceiversFromConditionals( + List conditionals, Map result) { + for (final c in conditionals) { + for (final receiver in c.receivers) { + final sl = _synthModuleDefinition.logicToSynthMap[receiver]; + if (sl is SynthLogicArrayElement && !result.containsKey(sl.name)) { + result[sl.name] = sl.logic.width; + } + } + // Recurse into sub-conditionals + _collectArrayReceiversFromConditionals(c.conditionals, result); + } + } + + /// Groups array elements by their root parent signal, + /// computing flat bit offsets for nested elements. + Map> + _groupArrayElementsByParent() { + final result = >{}; + + void addElement(SynthLogicArrayElement sl) { + // Walk up to root and compute flat bit offset + var flatOffset = 0; + SynthLogic current = sl; + while (current is SynthLogicArrayElement) { + final idx = current.logic.arrayIndex; + if (idx == null) { + return; // pruned element — skip + } + flatOffset += idx * current.logic.width; + current = current.parentArray.replacement ?? current.parentArray; + } + final rootName = current.name; + + final entry = ( + // Use flat bit offset as "index" for assembly ordering + index: flatOffset, + width: sl.logic.width, + elemName: sl.name, + ); + // Avoid duplicates + final list = result.putIfAbsent(rootName, () => []); + if (!list.any((e) => e.elemName == entry.elemName)) { + list.add(entry); + } + } + + // Use logicToSynthMap to find the SynthLogicArrayElement for each written + // element, rather than re-scanning submodule instantiations. + for (final sl in _synthModuleDefinition.logicToSynthMap.values) { + if (sl is SynthLogicArrayElement && sl.replacement == null) { + // Skip elements whose parent has been pruned or not named + final parent = sl.parentArray.replacement ?? sl.parentArray; + if (parent.declarationCleared) { + continue; + } + if (_arrayElementsWritten.containsKey(sl.name)) { + addElement(sl); + } + } + } + + // Sort each list by flat bit offset + for (final list in result.values) { + list.sort((a, b) => a.index.compareTo(b.index)); + } + return result; + } + + // ──────────────────────────────────────────────────────────────────── + // Inline gate expressions + // ──────────────────────────────────────────────────────────────────── + + /// Returns true if a module is a SystemVerilog gate that generates no + /// definition and should be inlined (like Add). + static bool _isInlinableSystemVerilogGate(Module m) => + m is SystemVerilog && + m is! InlineSystemVerilog && + m is! Always && + m is! FlipFlop && + m.generatedDefinitionType == DefinitionGenerationType.none; + + /// Converts a [SynthLogic] to a SystemC read expression. + /// Constants become typed literals; signals get `.read()`. + /// Array elements become range expressions on their parent. + static String _synthLogicReadExpr(SynthLogic sl) { + if (sl.isConstant) { + final c = sl.logics.whereType().first; + return _typedConstExpr(c.value, c.width); + } + if (sl is SynthLogicArrayElement) { + return _arrayElementReadExpr(sl); + } + return '${_scName(sl.name)}.read()'; + } + + /// Generates a typed constant expression for SystemC. + /// Handles x/z values by treating them as 0. + static String _typedConstExpr(LogicValue val, int width) { + if (val.isValid) { + if (width == 0) { + return '0'; + } + final bigVal = val.toBigInt(); + if (width > 64) { + // Use hex string constructor for sc_biguint + var hex = bigVal.toUnsigned(width).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + return '${systemCType(width)}("0x$hex")'; + } + // For uint64 values above INT64_MAX, add ULL suffix + if (bigVal > (BigInt.one << 63) - BigInt.one) { + return '${systemCType(width)}' + '(${bigVal.toUnsigned(width)}ULL)'; + } + return '${systemCType(width)}(${bigVal.toUnsigned(width)})'; + } + // For values with x/z, use 0 (SystemC doesn't have x/z) + return '${systemCType(width)}(0)'; + } + + /// Generates a range read expression for an array element. e.g. + /// deserialized[0] (8-bit in 32-bit parent) → deserialized.read().range(7, 0) + /// Generates a range read expression for an array element, handling + /// arbitrary nesting depth. e.g. `laIn[2][1]` in a `[3,2]x8` array + /// → `laIn.read().range(47, 40)`. + static String _arrayElementReadExpr(SynthLogicArrayElement sl) { + final elemWidth = sl.logic.width; + + // Walk up the parent chain to find the root signal and accumulate + // the flat bit offset. + var flatOffset = 0; + SynthLogic current = sl; + while (current is SynthLogicArrayElement) { + final idx = current.logic.arrayIndex!; + final w = current.logic.width; + flatOffset += idx * w; + current = current.parentArray.replacement ?? current.parentArray; + } + final rootName = _scName(current.name); + final rootWidth = current.width; + + final lo = flatOffset; + final hi = lo + elemWidth - 1; + + // If the root is 1-bit (bool), subscript/range is not valid + if (rootWidth == 1) { + return '$rootName.read()'; + } + if (elemWidth == 1) { + return 'static_cast($rootName.read()[$lo])'; + } + final rangeType = elemWidth <= 64 ? 'sc_uint' : 'sc_biguint'; + return '$rangeType<$elemWidth>($rootName.read().range($hi, $lo))'; + } + + /// Returns the sensitivity signal name for a SynthLogic. + /// For array elements, walks up to the root (non-array-element) parent. + static String _sensitivityName(SynthLogic sl) { + var current = sl; + while (current is SynthLogicArrayElement) { + current = current.parentArray.replacement ?? current.parentArray; + } + return _scName(current.name); + } + + /// Generates an SC_METHOD for inline gates (like SV `assign` stmts). + _MethodResult? _buildInlineGates() { + final inlineGates = _synthModuleDefinition.subModuleInstantiations + .where((s) => + s.needsInstantiation && + (s.module is InlineSystemVerilog || + _isInlinableSystemVerilogGate(s.module))) + .cast() + .toList(); + + if (inlineGates.isEmpty) { + return null; + } + + final sensitivities = {}; + final bodyLines = []; + + for (final ssmi in inlineGates) { + final m = ssmi.module; + + // Collect inputs — constants become literals, signals get .read() + final inputExprs = {}; + for (final entry in ssmi.inputMapping.entries) { + final sl = entry.value; + if (!sl.isConstant) { + sensitivities.add(_sensitivityName(sl)); + } + inputExprs[entry.key] = _synthLogicReadExpr(sl); + } + + if (m is InlineSystemVerilog) { + final resultSynthLogic = ssmi.inlineResultLogic; + if (resultSynthLogic == null) { + continue; + } + final expr = _gateExpression(m, inputExprs); + final dst = _scName(resultSynthLogic.name); + bodyLines.add(' $dst = $expr;'); + } else if (m is Add) { + // Add has two outputs: sum and carry. + // Emit inline expressions for each used output. + final vals = inputExprs.values.toList(); + final sumPortName = m.sum.name; + for (final entry in ssmi.outputMapping.entries) { + final portName = entry.key; + final dst = _scName(entry.value.name); + if (portName == sumPortName) { + bodyLines.add(' $dst = ${vals[0]} + ${vals[1]};'); + } else { + // carry: high bit of (width+1)-bit addition + final w = m.width; + final w1 = w + 1; + final utype = systemCType(w1); + final carryExpr = 'static_cast' + '($utype($utype(${vals[0]})' + ' + $utype(${vals[1]}))[$w])'; + bodyLines.add(' $dst = $carryExpr;'); + } + } + } + ssmi.clearInstantiation(); + } + + if (bodyLines.isEmpty) { + return null; + } + + final setupBuf = StringBuffer()..writeln(' SC_METHOD(assign_method);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: ' void assign_method() {\n' + '${bodyLines.join('\n')}\n' + ' }', + ); + } + + /// Maps an InlineSystemVerilog gate to a C++ expression. + /// + /// Handles all gate types that have SV-specific syntax which needs + /// translation to valid SystemC/C++. + String _gateExpression(InlineSystemVerilog m, Map inputs) { + // ── Single-output bitwise gates (C++ operators identical to SV) ── + if (m is NotGate) { + // For bool (width-1), use logical not; for wider, bitwise not + if ((m as Module).outputs.values.first.width == 1) { + return '!${inputs.values.first}'; + } + return '~${inputs.values.first}'; + } + + // ── Binary operator gates (C++ operators identical to SV) ── + const binaryOps = { + And2Gate: '&', + Or2Gate: '|', + Xor2Gate: '^', + Subtract: '-', + Multiply: '*', + }; + final binOp = binaryOps[m.runtimeType]; + if (binOp != null) { + final vals = inputs.values.toList(); + return '${vals[0]} $binOp ${vals[1]}'; + } + if (m is Divide || m is Modulo) { + final vals = inputs.values.toList(); + final op = m is Divide ? '/' : '%'; + // Guard against zero divisor (sc_uint defaults to 0 at time-0) + return '(${vals[1]} != 0 ? ${vals[0]} $op ${vals[1]} : 0)'; + } + if (m is Power) { + final vals = inputs.values.toList(); + final w = (m as Module).inputs.values.first.width; + return '${systemCType(w)}' + '(static_cast' + '(pow(static_cast(${vals[0]}),' + ' static_cast(${vals[1]}))))'; + } + + // ── Comparison (operators identical) ── + const cmpOps = { + Equals: '==', + NotEquals: '!=', + LessThan: '<', + GreaterThan: '>', + LessThanOrEqual: '<=', + GreaterThanOrEqual: '>=', + }; + final cmpOp = cmpOps[m.runtimeType]; + if (cmpOp != null) { + final vals = inputs.values.toList(); + return '${vals[0]} $cmpOp ${vals[1]}'; + } + + // ── Shifts ── + // Cast shift amount to int to avoid ambiguous overloads. + // Width 1 maps to bool in SystemC (no .to_int()), so use (int) cast. + // Clamp: if shift amount >= operand width, result is 0 (or sign-fill + // for arshift), avoiding .to_int() overflow on huge shift amounts. + if (m is LShift || m is RShift || m is ARShift) { + final vals = inputs.values.toList(); + final w = (m as Module).inputs.values.first.width; + final outType = systemCType(w); + final shiftAmtWidth = (m as Module).inputs.values.toList()[1].width; + final shiftExpr = + shiftAmtWidth == 1 ? '(int)(${vals[1]})' : '(${vals[1]}).to_int()'; + if (m is ARShift) { + final signedType = w <= 64 ? 'sc_int<$w>' : 'sc_bigint<$w>'; + final shiftOp = '$outType(($signedType(${vals[0]})) >> $shiftExpr)'; + if (shiftAmtWidth > 31) { + // Sign-fill: shift by width-1 to replicate MSB when shift >= width + final overflow = '$outType(($signedType(${vals[0]})) >> ${w - 1})'; + return '(${vals[1]} >= $w) ? $overflow : $shiftOp'; + } + return shiftOp; + } + final op = m is LShift ? '<<' : '>>'; + final shiftOp = '$outType(${vals[0]} $op $shiftExpr)'; + if (shiftAmtWidth > 31) { + return '(${vals[1]} >= $w) ? $outType(0) : $shiftOp'; + } + return shiftOp; + } + + // ── Unary reductions ── + if (m is AndUnary || m is OrUnary || m is XorUnary) { + final inputWidth = (m as Module).inputs.values.first.width; + // 1-bit: reduce is identity (and bool has no .xor_reduce() in SystemC) + if (inputWidth == 1) { + return 'static_cast(${inputs.values.first})'; + } + if (m is AndUnary) { + return '${inputs.values.first}.and_reduce()'; + } else if (m is OrUnary) { + return '${inputs.values.first}.or_reduce()'; + } else { + return '${inputs.values.first}.xor_reduce()'; + } + } + + // ── Bus subset (slice / index) ── + if (m is BusSubset) { + final a = inputs.values.first; + final inputWidth = (m as Module).inputs.values.first.width; + // If input is already 1-bit (bool), extracting bit 0 is identity + if (inputWidth == 1 && m.startIndex == 0 && m.endIndex == 0) { + return a; + } + if (m.startIndex == m.endIndex) { + return 'static_cast($a[${m.startIndex}])'; + } + if (m.startIndex > m.endIndex) { + // Reverse order — build bit-by-bit concat + // bits[0]=a[endIndex], ..., bits[N]=a[startIndex] + // SystemC concat is MSB-first: output MSB = input[endIndex] + // Use sc_uint<1> (not bool) so SystemC concat operator is invoked + final bits = List.generate(m.startIndex - m.endIndex + 1, + (i) => 'sc_uint<1>($a[${m.endIndex + i}])'); + return '(${bits.join(', ')})'; + } + final w = m.endIndex - m.startIndex + 1; + final rangeType = w <= 64 ? 'sc_uint' : 'sc_biguint'; + return '$rangeType<$w>($a.range(${m.endIndex}, ${m.startIndex}))'; + } + + // ── Dynamic bit index ── + if (m is IndexGate) { + final vals = inputs.values.toList(); + return 'static_cast(${vals[0]}[${vals[1]}])'; + } + + // ── Mux (ternary) ── + if (m is Mux) { + final vals = inputs.values.toList(); + final w = m.out.width; + final utype = systemCType(w); + // Cast both branches to avoid C++ ternary type mismatch + // (e.g., when one branch is bool and the other is sc_uint<1>) + return '${vals[0]}' + ' ? $utype(${vals[2]})' + ' : $utype(${vals[1]})'; + } + + // ── Replication ── + if (m is ReplicationOp) { + final a = inputs.values.first; + final inputWidth = (m as Module).inputs.values.first.width; + final outputWidth = m.replicated.width; + final numReps = outputWidth ~/ inputWidth; + if (inputWidth == 1) { + // Single-bit replicate: all-1s or all-0s + final utype = systemCType(outputWidth); + return '$utype(' + '$a ' + '? $utype(-1) ' + ': $utype(0))'; + } + // Multi-bit replicate: concat N copies + final copies = List.filled(numReps, a); + return '(${copies.join(', ')})'; + } + + // ── Swizzle (concatenation) ── + if (m is Swizzle) { + // SystemC concatenation: (sig1, sig2, sig3) + // bool operands must be cast to sc_uint<1> to use SystemC concat + // (otherwise C++ comma operator is invoked instead) + final modInputs = (m as Module).inputs.values.toList(); + final exprList = []; + var i = 0; + for (final expr in inputs.values) { + final w = modInputs[i].width; + if (w == 0) { + i++; + continue; // skip zero-width padding + } + // Wrap 1-bit (bool) operands in sc_uint<1>() for concat + if (w == 1) { + exprList.add('sc_uint<1>($expr)'); + } else { + exprList.add(expr); + } + i++; + } + if (exprList.length == 1) { + return exprList.first; + } + // Swizzle stores inputs LSB-first (in0=LSB), but SystemC concat + // is MSB-first: (msb, ..., lsb). So reverse. + return '(${exprList.reversed.join(', ')})'; + } + + // Fallback: use SV inline (may not be valid C++ — flag for review) + return '/* TODO: ${m.runtimeType} */ ${m.inlineVerilog(inputs)}'; + } + + // ──────────────────────────────────────────────────────────────────── + // Clock / trigger edge resolution + // ──────────────────────────────────────────────────────────────────── + + /// Resolves a trigger [SynthLogic] to the effective clock port and edge. + /// + /// If the trigger signal is a module input port, it can be used directly + /// with `SC_CTHREAD`. If it is an internal signal derived from a [NotGate], + /// the method traces through the inversion chain to find the original port + /// and flips the edge accordingly (`negedge(~clk) = posedge(clk)`). + ({String clockName, bool isPort, bool isPosedge}) _resolveClockAndEdge( + SynthLogic triggerSL, bool isPosedge) { + final sl = triggerSL.replacement ?? triggerSL; + + if (sl.isPort(_synthModuleDefinition.module)) { + return (clockName: sl.name, isPort: true, isPosedge: isPosedge); + } + + // Try to trace through a NotGate inversion + for (final logic in sl.logics) { + final src = logic.srcConnection; + if (src != null && src.parentModule is NotGate) { + final notInput = src.parentModule!.inputs.values.first; + final notInputSrc = notInput.srcConnection; + if (notInputSrc != null) { + final srcSL = _synthModuleDefinition.logicToSynthMap[notInputSrc]; + if (srcSL != null) { + // Inversion flips the edge + return _resolveClockAndEdge(srcSL, !isPosedge); + } + } + } + } + + // Fallback — use the signal as-is (SC_THREAD will be needed) + return (clockName: sl.name, isPort: false, isPosedge: isPosedge); + } + + // ──────────────────────────────────────────────────────────────────── + // Combinational / Sequential processes + // ──────────────────────────────────────────────────────────────────── + + _MethodResult? _buildProcesses() { + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + var idx = 0; + + // Collect clocked processes for consolidation by (clock, reset) pair. + // Sequentials and FlipFlops sharing the same clock/reset are merged + // into a single SC_CTHREAD, eliminating repeated async_reset_signal_is. + final clockedGroups = {}; + + for (final ssmi + in _synthModuleDefinition.subModuleInstantiations.toList()) { + ssmi as SystemCSynthSubModuleInstantiation; + final m = ssmi.module; + + if (m is Combinational) { + final name = 'comb_$idx'; + idx++; + + final sensitivities = ssmi.inputMapping.values + .where((sl) => !sl.declarationCleared && !sl.isConstant) + .map(_sensitivityName) + .toSet(); + + setupBuf.writeln(' SC_METHOD($name);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + // Build maps keyed by port name (what verilogContents expects) + final inputsMap = ssmi.inputMapping + .map((k, sl) => MapEntry(k, _synthLogicReadExpr(sl))); + final outputsMap = + ssmi.outputMapping.map((k, sl) => MapEntry(k, _scName(sl.name))); + + bodyBuf.writeln(' void $name() {'); + for (final c in m.conditionals) { + bodyBuf.write(_conditionalToSC(c, 2, inputsMap, outputsMap)); + } + bodyBuf + ..writeln(' }') + ..writeln(); + ssmi.clearInstantiation(); + } else if (m is Sequential) { + final resetEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('reset')) + .firstOrNull; + + // Detect async reset: either explicitly via asyncReset flag, or + // implicitly when the reset signal is also listed as a trigger + // (e.g. Sequential.multi([clk, reset], reset: reset, ...)). + final isAsync = m.asyncReset || + (resetEntry != null && + ssmi.inputMapping.entries.any((e) => + e.key.contains('trigger') && + e.value.name == resetEntry.value.name)); + + // Resolve ALL trigger entries to (signalName, edge, isPort). + final triggerEdges = m.triggerEdges; + final triggerEntries = ssmi.inputMapping.entries + .where((e) => e.key.contains('trigger')) + .toList(); + + final resolvedTriggers = + <({String signalName, bool isPosedge, bool isPort})>[]; + + for (final te in triggerEntries) { + final triggerSL = te.value; + // Skip if this trigger is the async reset signal + if (resetEntry != null && triggerSL.name == resetEntry.value.name) { + continue; + } + // Skip constant triggers (e.g. clk <= Const(0) — never toggles) + if (triggerSL.isConstant) { + continue; + } + final isPosedge = triggerEdges + .where((t) => t.portName == te.key) + .firstOrNull + ?.isPosedge ?? + true; + final resolved = _resolveClockAndEdge(triggerSL, isPosedge); + // Skip if the resolved signal is constant + final resolvedSL = _synthModuleDefinition.logicToSynthMap.values + .where((sl) => sl.replacement == null && !sl.declarationCleared) + .where((sl) => sl.name == resolved.clockName) + .firstOrNull; + if (resolvedSL != null && resolvedSL.isConstant) { + continue; + } + resolvedTriggers.add(( + signalName: resolved.clockName, + isPosedge: resolved.isPosedge, + isPort: resolved.isPort, + )); + } + + // Deduplicate by (signalName, isPosedge) + final seen = {}; + final uniqueTriggers = + <({String signalName, bool isPosedge, bool isPort})>[]; + for (final t in resolvedTriggers) { + final key = '${t.signalName}|${t.isPosedge}'; + if (seen.add(key)) { + uniqueTriggers.add(t); + } + } + + // Build group key from all trigger signals + reset + final triggerKey = uniqueTriggers + .map((t) => '${t.signalName}:${t.isPosedge}') + .join(','); + final groupKey = '$triggerKey|${resetEntry?.value.name ?? '_none_'}'; + final group = clockedGroups.putIfAbsent( + groupKey, + () => _ClockedGroupData( + resetName: resetEntry?.value.name, + isAsyncReset: isAsync, + )); + // Add all triggers to the group (dedup handled by emission) + for (final t in uniqueTriggers) { + if (!group.triggers.any((existing) => + existing.signalName == t.signalName && + existing.isPosedge == t.isPosedge)) { + group.triggers.add(t); + } + } + if (isAsync) { + group.isAsyncReset = true; + } + + final inputsMap = ssmi.inputMapping + .map((k, sl) => MapEntry(k, _synthLogicReadExpr(sl))); + final outputsMap = + ssmi.outputMapping.map((k, sl) => MapEntry(k, _scName(sl.name))); + + for (final outName in outputsMap.values) { + group.resetLines.add(' $outName = 0;'); + } + final condBuf = StringBuffer(); + for (final c in m.conditionals) { + condBuf.write(_conditionalToSC(c, 3, inputsMap, outputsMap)); + } + group.whileBodyLines.add(condBuf.toString()); + ssmi.clearInstantiation(); + } else if (m is FlipFlop) { + // Resolve port signals via the input/output mapping + final clkSl = ssmi.inputMapping.entries + .firstWhere((e) => e.key.contains('clk')) + .value; + final dSl = ssmi.inputMapping.entries + .firstWhere((e) => e.key.contains('d')) + .value; + final resetEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('reset') && !e.key.contains('Value')) + .firstOrNull; + final enEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('en')) + .firstOrNull; + final resetValueEntry = ssmi.inputMapping.entries + .where( + (e) => e.key.contains('resetValue') || e.key.contains('Value')) + .firstOrNull; + final qSl = ssmi.outputMapping.values.first; + + final groupKey = + '${clkSl.name}:true|${resetEntry?.value.name ?? '_none_'}'; + final group = clockedGroups.putIfAbsent( + groupKey, + () => _ClockedGroupData( + resetName: resetEntry?.value.name, + isAsyncReset: m.asyncReset, + )); + // FlipFlop always posedge + if (!group.triggers + .any((t) => t.signalName == clkSl.name && t.isPosedge)) { + group.triggers.add(( + signalName: clkSl.name, + isPosedge: true, + isPort: clkSl.isPort(_synthModuleDefinition.module), + )); + } + if (m.asyncReset) { + group.isAsyncReset = true; + } + + // Reset value + String resetValExpr; + if (resetValueEntry != null) { + resetValExpr = _synthLogicReadExpr(resetValueEntry.value); + } else if (m.constantResetValue != null) { + resetValExpr = m.constantResetValue!.toBigInt().toString(); + } else { + resetValExpr = '0'; + } + group.resetLines.add(' ${_scName(qSl.name)} = $resetValExpr;'); + + // Build the data assignment (with optional enable gate) + final assignExpr = + ' ${_scName(qSl.name)} = ${_synthLogicReadExpr(dSl)};\n'; + final bodyLine = enEntry != null + ? ' if (${_synthLogicReadExpr(enEntry.value)}) {\n' + ' $assignExpr' + ' }\n' + : assignExpr; + + // Wrap in sync reset check if needed + if (resetEntry != null && !m.asyncReset) { + group.whileBodyLines + .add(' if (${_scName(resetEntry.value.name)}.read()) {\n' + ' ${_scName(qSl.name)} = $resetValExpr;\n' + ' } else {\n' + ' $bodyLine' + ' }\n'); + } else { + group.whileBodyLines.add(bodyLine); + } + ssmi.clearInstantiation(); + } + } + + // Emit one SC_CTHREAD or SC_THREAD per (clock, reset) group + for (final group in clockedGroups.values) { + final name = 'clocked_$idx'; + idx++; + + final triggers = group.triggers; + + if (triggers.isEmpty) { + // All triggers were constant — skip this group + continue; + } + + // Determine if we can use SC_CTHREAD: + // - exactly one trigger signal + // - that signal is a port (sc_in) + // - only one edge direction + final distinctSignals = triggers.map((t) => t.signalName).toSet(); + final useCthread = distinctSignals.length == 1 && + triggers.first.isPort && + triggers.length == 1; + + if (useCthread) { + final t = triggers.first; + final clockRef = _scName(t.signalName); + final edge = t.isPosedge ? '.pos()' : '.neg()'; + setupBuf.writeln(' SC_CTHREAD($name, $clockRef$edge);'); + if (group.resetName != null && group.isAsyncReset) { + setupBuf.writeln(' async_reset_signal_is(' + '${_scName(group.resetName!)}, true);'); + } + + bodyBuf.writeln(' void $name() {'); + group.resetLines.forEach(bodyBuf.writeln); + bodyBuf + ..writeln(' wait();') + ..writeln(' while (true) {'); + group.whileBodyLines.forEach(bodyBuf.write); + bodyBuf + ..writeln(' wait();') + ..writeln(' }') + ..writeln(' }') + ..writeln(); + } else { + // SC_THREAD with explicit wait on events + setupBuf.writeln(' SC_THREAD($name);'); + + // Build wait expression from all trigger events + String waitExpr; + if (distinctSignals.length == 1) { + // Same signal, but both edges + final sig = _scName(triggers.first.signalName); + final edges = triggers.map((t) => t.isPosedge).toSet(); + if (edges.length == 2) { + waitExpr = '$sig.value_changed_event()'; + } else if (edges.first) { + waitExpr = '$sig.posedge_event()'; + } else { + waitExpr = '$sig.negedge_event()'; + } + } else { + // Multiple distinct trigger signals — OR them together + final eventExprs = []; + for (final t in triggers) { + final sig = _scName(t.signalName); + eventExprs + .add('$sig.${t.isPosedge ? 'posedge' : 'negedge'}_event()'); + } + waitExpr = eventExprs.join(' | '); + } + + bodyBuf.writeln(' void $name() {'); + group.resetLines.forEach(bodyBuf.writeln); + bodyBuf + ..writeln(' while (true) {') + ..writeln(' wait($waitExpr);'); + group.whileBodyLines.forEach(bodyBuf.write); + bodyBuf + ..writeln(' }') + ..writeln(' }') + ..writeln(); + } + } + + if (setupBuf.isEmpty && bodyBuf.isEmpty) { + return null; + } + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Regular sub-module instantiations + // ──────────────────────────────────────────────────────────────────── + + /// Returns true if the sub-module is handled inline (not a real child + /// instantiation) — i.e. it is an inline gate, Always, FlipFlop, or clock. + static bool _isHandledInline(SystemCSynthSubModuleInstantiation ssmi) => + !ssmi.needsInstantiation || + ssmi.module is InlineSystemVerilog || + ssmi.module is Always || + ssmi.module is FlipFlop || + ssmi.module is SimpleClockGenerator || + _isInlinableSystemVerilogGate(ssmi.module); + + String _buildSubModuleMembers( + String Function(Module module) getInstanceTypeOfModule) { + final lines = []; + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + ssmi as SystemCSynthSubModuleInstantiation; + if (_isHandledInline(ssmi)) { + continue; + } + final instanceType = getInstanceTypeOfModule(ssmi.module); + lines.add(' $instanceType ${ssmi.name}{"${ssmi.name}"};'); + } + return lines.join('\n'); + } + + /// Dummy signal declarations needed for unconnected submodule output ports. + /// Populated by [_buildSubModuleBindings]. + final List _unconnectedOutputSignals = []; + + /// Signal declarations for constants bound to submodule input ports. + /// Populated by [_buildSubModuleBindings]. + final List _constInputSignals = []; + + /// Initialization statements for constant signals (in constructor body). + /// Populated by [_buildSubModuleBindings]. + final List _constInputInits = []; + + String _buildSubModuleBindings( + String Function(Module module) getInstanceTypeOfModule) { + final lines = []; + var unconnIdx = 0; + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + ssmi as SystemCSynthSubModuleInstantiation; + if (_isHandledInline(ssmi)) { + continue; + } + + // Bind connected ports (inputs, outputs, inouts) + final allPorts = { + ...ssmi.inputMapping, + ...ssmi.outputMapping, + ...ssmi.inOutMapping, + }; + for (final entry in allPorts.entries) { + if (!entry.value.declarationCleared) { + if (entry.value.isConstant) { + // Constants can't be bound directly to sc_in ports; + // create a signal, initialize it, and bind that. + final constName = _scName('_const_${ssmi.name}' + '_${entry.key}_${_constInputSignals.length}'); + final w = entry.value.width; + final c = entry.value.logics.whereType().first; + final constVal = _typedConstExpr(c.value, c.width); + _constInputSignals + .add(' ${systemCSignalType(w)} $constName{"$constName"};'); + _constInputInits.add(' $constName.write($constVal);'); + lines.add(' ${ssmi.name}.${entry.key}($constName);'); + } else { + lines.add(' ' + '${ssmi.name}.${entry.key}(${_scName(entry.value.name)});'); + } + } + } + + // Bind unconnected ports to dummy signals + // (SystemC requires all sc_in/sc_out ports to be bound) + for (final entry in [ + ...ssmi.outputMapping.entries, + ...ssmi.inputMapping.entries, + ]) { + if (entry.value.declarationCleared) { + final dummyName = '_unused_${ssmi.name}_${entry.key}_$unconnIdx'; + final w = entry.value.width; + _unconnectedOutputSignals + .add(' ${systemCSignalType(w)} $dummyName{"$dummyName"};'); + lines.add(' ${ssmi.name}.${entry.key}($dummyName);'); + unconnIdx++; + } + } + } + return lines.join('\n'); + } + + // ──────────────────────────────────────────────────────────────────── + // Wire assignments + // ──────────────────────────────────────────────────────────────────── + + _MethodResult? _buildWireAssignments() { + if (_synthModuleDefinition.assignments.isEmpty) { + return null; + } + + final bodyLines = []; + final sensitivities = {}; + + // Group partial assignments by destination for concatenated writes + final partialsByDst = >{}; + + for (final assignment in _synthModuleDefinition.assignments) { + if (!assignment.src.isConstant) { + sensitivities.add(_sensitivityName(assignment.src)); + } + if (assignment is PartialSynthAssignment) { + partialsByDst + .putIfAbsent(_scName(assignment.dst.name), () => []) + .add(assignment); + } else { + bodyLines.add(' ${_scName(assignment.dst.name)} = ' + '${_synthLogicReadExpr(assignment.src)};'); + } + } + + // Emit grouped partial assignments as shift-or concatenation + for (final entry in partialsByDst.entries) { + final dstName = entry.key; + final partials = entry.value + ..sort((a, b) => a.dstLowerIndex.compareTo(b.dstLowerIndex)); + + // Find total width from the destination SynthLogic + final dstWidth = partials.last.dstUpperIndex + 1; + final utype = systemCType(dstWidth); + final parts = []; + for (final p in partials) { + final srcExpr = _synthLogicReadExpr(p.src); + if (p.dstLowerIndex == 0) { + parts.add('$utype($srcExpr)'); + } else { + parts.add('($utype($srcExpr) << ${p.dstLowerIndex})'); + } + } + bodyLines.add(' $dstName = ${parts.join(' | ')};'); + } + + final setupBuf = StringBuffer()..writeln(' SC_METHOD(wire_assign);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: ' void wire_assign() {\n' + '${bodyLines.join('\n')}\n' + ' }', + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Conditional → SystemC + // ──────────────────────────────────────────────────────────────────── + + String _conditionalToSC(Conditional conditional, int indent, + Map inputsMap, Map outputsMap) { + final padding = ' ' * indent; + + if (conditional is ConditionalAssign) { + final driverExpr = _resolveDriver(conditional.driver, inputsMap); + final receiver = _resolveReceiver(conditional.receiver, outputsMap); + return '$padding$receiver = $driverExpr;\n'; + } else if (conditional is If) { + return _ifToSC(conditional, indent, inputsMap, outputsMap); + } else if (conditional is Case) { + return _caseToSC(conditional, indent, inputsMap, outputsMap); + } else if (conditional is ConditionalGroup) { + final buf = StringBuffer(); + for (final c in conditional.conditionals) { + buf.write(_conditionalToSC(c, indent, inputsMap, outputsMap)); + } + return buf.toString(); + } + return ''; + } + + String _ifToSC(If ifBlock, int indent, Map inputsMap, + Map outputsMap) { + final padding = ' ' * indent; + final buf = StringBuffer(); + + for (final iff in ifBlock.iffs) { + final header = iff == ifBlock.iffs.first + ? 'if' + : iff is Else + ? ' else' + : ' else if'; + final condition = + iff is! Else ? ' (${_resolveDriver(iff.condition, inputsMap)})' : ''; + buf.write('$padding$header$condition {\n'); + for (final c in iff.then) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + buf.writeln(); + return buf.toString(); + } + + String _caseToSC(Case caseBlock, int indent, Map inputsMap, + Map outputsMap) { + final padding = ' ' * indent; + final buf = StringBuffer(); + final expr = _resolveDriver(caseBlock.expression, inputsMap); + + // Check if all case items have compile-time constant values + final allConst = + caseBlock.items.every((item) => _isConstCaseItem(item.value)); + + // CaseZ requires mask matching — always use if/else + // Non-const case items also require if/else + if (caseBlock is CaseZ || !allConst) { + return _caseToIfElseSC(caseBlock, indent, inputsMap, outputsMap, expr); + } + + buf.writeln('${padding}switch ($expr) {'); + for (final item in caseBlock.items) { + buf.writeln('$padding case ${_constLit(item.value)}:'); + for (final c in item.then) { + buf.write(_conditionalToSC(c, indent + 2, inputsMap, outputsMap)); + } + buf.writeln('$padding break;'); + } + if (caseBlock.defaultItem != null) { + buf.writeln('$padding default:'); + for (final c in caseBlock.defaultItem!) { + buf.write(_conditionalToSC(c, indent + 2, inputsMap, outputsMap)); + } + buf.writeln('$padding break;'); + } + buf.writeln('$padding}'); + return buf.toString(); + } + + /// Checks whether a case item value is a compile-time constant. + bool _isConstCaseItem(dynamic value) { + if (value is Const) { + return true; + } + if (value is LogicValue) { + return true; + } + if (value is Logic) { + if (value.srcConnection is Const) { + return true; + } + final sl = _synthModuleDefinition.logicToSynthMap[value]; + if (sl != null && sl.isConstant) { + return true; + } + return false; + } + return true; // int, string, etc. + } + + /// Converts a Case/CaseZ block to if/else chain (for non-const items + /// or CaseZ with z-masks). + String _caseToIfElseSC( + Case caseBlock, + int indent, + Map inputsMap, + Map outputsMap, + String expr) { + final padding = ' ' * indent; + final buf = StringBuffer(); + + for (var i = 0; i < caseBlock.items.length; i++) { + final item = caseBlock.items[i]; + final condition = _caseItemCondition(item.value, expr, inputsMap, + isCaseZ: caseBlock is CaseZ); + final header = i == 0 ? 'if' : ' else if'; + buf.write('$padding$header ($condition) {\n'); + for (final c in item.then) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + if (caseBlock.defaultItem != null) { + buf.write(' else {\n'); + for (final c in caseBlock.defaultItem!) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + buf.writeln(); + return buf.toString(); + } + + /// Generates the condition expression for a case item comparison. + String _caseItemCondition( + dynamic value, String expr, Map inputsMap, + {bool isCaseZ = false}) { + // Extract LogicValue from Const for CaseZ mask matching + LogicValue? lv; + if (value is Const) { + lv = value.value; + } else if (value is LogicValue) { + lv = value; + } + if (isCaseZ && lv != null && !lv.isValid) { + // CaseZ: create mask comparison (expr & mask) == pattern + // z bits become don't-care (mask out those bits) + final width = lv.width; + // z→0 in mask, 0/1→1 in mask + var maskStr = ''; + var patStr = ''; + for (var i = width - 1; i >= 0; i--) { + final bit = lv[i]; + if (bit == LogicValue.z || bit == LogicValue.x) { + maskStr += '0'; + patStr += '0'; + } else { + maskStr += '1'; + patStr += bit == LogicValue.one ? '1' : '0'; + } + } + final maskVal = BigInt.parse(maskStr, radix: 2); + final patVal = BigInt.parse(patStr, radix: 2); + return '($expr & $maskVal) == $patVal'; + } + if (value is Logic && value is! Const) { + final resolved = _resolveDriver(value, inputsMap); + return '$expr == $resolved'; + } + return '$expr == ${_constLit(value)}'; + } + + /// Resolves a driver Logic to a SystemC read expression using the + /// SynthModuleDefinition's logicToSynthMap to find the canonical name. + String _resolveDriver(Logic driver, Map inputsMap) { + if (driver is Const) { + return _constLit(driver); + } + // Look up via logicToSynthMap — the SynthLogic has the canonical name + final sl = _synthModuleDefinition.logicToSynthMap[driver]; + if (sl != null) { + return _synthLogicReadExpr(sl); + } + // Try to find via source connection chain — handles cases where + // the Logic object isn't directly in the map but its source is + var src = driver.srcConnection; + while (src != null) { + final srcSl = _synthModuleDefinition.logicToSynthMap[src]; + if (srcSl != null) { + return _synthLogicReadExpr(srcSl); + } + src = src.srcConnection; + } + // Fallback: try inputsMap by port name + if (inputsMap.containsKey(driver.name)) { + return inputsMap[driver.name]!; + } + return '${_scName(driver.name)}.read()'; + } + + /// Resolves a receiver Logic to a SystemC signal name using the + /// SynthModuleDefinition's logicToSynthMap to find the canonical name. + String _resolveReceiver(Logic receiver, Map outputsMap) { + // Look up via logicToSynthMap + final sl = _synthModuleDefinition.logicToSynthMap[receiver]; + if (sl != null) { + return _scName(sl.name); + } + // Fallback + if (outputsMap.containsKey(receiver.name)) { + return outputsMap[receiver.name]!; + } + return _scName(receiver.name); + } + + String _constLit(dynamic value) { + if (value is Const) { + if (value.value.isValid) { + return value.value.toBigInt().toString(); + } + return '0'; // x/z → 0 in SystemC + } else if (value is LogicValue) { + if (value.isValid) { + return value.toBigInt().toString(); + } + return '0'; // x/z → 0 in SystemC + } else if (value is Logic) { + // If the Logic is driven by a Const, resolve to integer literal + if (value.srcConnection is Const) { + final cv = (value.srcConnection! as Const).value; + return cv.isValid ? cv.toBigInt().toString() : '0'; + } + // Check logicToSynthMap for a constant SynthLogic + final sl = _synthModuleDefinition.logicToSynthMap[value]; + if (sl != null && sl.isConstant) { + final constLogic = sl.logics.whereType().firstOrNull; + if (constLogic != null) { + return constLogic.value.isValid + ? constLogic.value.toBigInt().toString() + : '0'; + } + } + // Fallback: use signal read expression + return '${value.name}.read()'; + } + return value.toString(); + } + + // ──────────────────────────────────────────────────────────────────── + // Build all sections + // ──────────────────────────────────────────────────────────────────── + + void _buildModuleBody( + String Function(Module module) getInstanceTypeOfModule) { + _subMembers = _buildSubModuleMembers(getInstanceTypeOfModule); + + final inlineGates = _buildInlineGates(); + final processes = _buildProcesses(); + final wireAssigns = _buildWireAssignments(); + final arrayAssembly = _buildArrayAssemblyMethod(); + final subBindings = _buildSubModuleBindings(getInstanceTypeOfModule); + + // Build internal signals, appending dummy signals for unconnected + // submodule outputs (populated by _buildSubModuleBindings above). + final baseSigs = _buildInternalSignals(); + _internalSigs = [ + baseSigs, + ..._unconnectedOutputSignals, + ..._constInputSignals, + ].where((s) => s.isNotEmpty).join('\n'); + + final ctorParts = [ + if (_constInputInits.isNotEmpty) _constInputInits.join('\n'), + if (inlineGates != null) inlineGates.setup, + if (processes != null) processes.setup, + if (wireAssigns != null) wireAssigns.setup, + if (arrayAssembly != null) arrayAssembly.setup, + if (subBindings.isNotEmpty) subBindings, + ]; + _ctorBody = ctorParts.join(); + + final bodyParts = [ + if (inlineGates != null) inlineGates.body, + if (processes != null) processes.body, + if (wireAssigns != null) wireAssigns.body, + if (arrayAssembly != null) arrayAssembly.body, + ]; + _methodBodies = bodyParts.where((s) => s.isNotEmpty).join('\n'); + } + + /// Builds an SC_METHOD that assembles individual array element signals + /// back into their parent signal via concatenation. + _MethodResult? _buildArrayAssemblyMethod() { + if (_arrayElementsByParent.isEmpty) { + return null; + } + + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + var methodIdx = 0; + + for (final entry in _arrayElementsByParent.entries) { + final parentName = _scName(entry.key); + final elements = entry.value; + final methodName = 'array_assemble_$methodIdx'; + methodIdx++; + + setupBuf.writeln(' SC_METHOD($methodName);'); + for (final elem in elements) { + setupBuf.writeln(' sensitive << ${_scName(elem.elemName)};'); + } + + // Build concatenation: (elem[N-1], ..., elem[1], elem[0]) + // SystemC concat is MSB-first, so highest index first + // Wrap 1-bit (bool) elements in sc_uint<1>() for proper concat + final concatParts = elements.reversed.map((e) { + final read = '${_scName(e.elemName)}.read()'; + return e.width == 1 ? 'sc_uint<1>($read)' : read; + }).toList(); + + bodyBuf + ..writeln(' void $methodName() {') + ..writeln(' $parentName = (${concatParts.join(', ')});') + ..writeln(' }') + ..writeln(); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Final assembly + // ──────────────────────────────────────────────────────────────────── + + String _toSystemC() { + final moduleName = getInstanceTypeOfModule(module); + final buf = StringBuffer()..writeln('SC_MODULE($moduleName) {'); + + if (_portsString.isNotEmpty) { + buf.writeln(_portsString); + } + if (_internalSigs.isNotEmpty) { + buf + ..writeln() + ..writeln(_internalSigs); + } + if (_subMembers.isNotEmpty) { + buf + ..writeln() + ..writeln(_subMembers); + } + + buf + ..writeln() + ..writeln(' SC_CTOR($moduleName) {'); + if (_ctorBody.isNotEmpty) { + buf.write(_ctorBody); + } + buf.writeln(' }'); + + if (_methodBodies.isNotEmpty) { + buf + ..writeln() + ..write(_methodBodies) + ..writeln(); + } + + buf.writeln('};'); + final text = buf.toString(); + + _buildScLineMap(text); + + return text; + } +} + +/// Helper to hold a constructor setup string and method body string. +class _MethodResult { + final String setup; + final String body; + const _MethodResult({required this.setup, required this.body}); +} + +/// Collects clocked process data for consolidation by (clock, reset) pair. +class _ClockedGroupData { + final String? resetName; + bool isAsyncReset; + + /// All distinct trigger events (signal name, edge, and whether it's a port). + final List<({String signalName, bool isPosedge, bool isPort})> triggers = []; + + final List resetLines = []; + final List whileBodyLines = []; + _ClockedGroupData({this.resetName, this.isAsyncReset = false}); +} diff --git a/lib/src/synthesizers/systemverilog/sv_service.dart b/lib/src/synthesizers/systemverilog/sv_service.dart new file mode 100644 index 000000000..e1adf43cd --- /dev/null +++ b/lib/src/synthesizers/systemverilog/sv_service.dart @@ -0,0 +1,114 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// sv_service.dart +// Service wrapper for SystemVerilog synthesis. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:rohd/rohd.dart'; + +/// A service that wraps SystemVerilog synthesis of a [Module] hierarchy. +/// +/// Provides access to the generated SV file contents and per-module +/// synthesis results, and optionally registers with [ModuleServices] +/// for DevTools inspection. +/// +/// Example: +/// ```dart +/// final dut = MyModule(...); +/// await dut.build(); +/// final sv = SvService(dut); +/// +/// // Write individual .sv files: +/// sv.writeFiles('build/'); +/// +/// // Or get the concatenated output (like generateSynth): +/// print(sv.allContents); +/// ``` +class SvService { + /// The top-level [Module] being synthesized. + final Module module; + + /// The underlying [SynthBuilder] that drove synthesis. + late final SynthBuilder synthBuilder; + + /// The generated file contents (one per unique module definition). + late final List fileContents; + + /// Creates an [SvService] for [module]. + /// + /// [module] must already be built. Set [register] to `true` (the + /// default) to register this service with [ModuleServices] for + /// DevTools access. + SvService(this.module, {bool register = true}) { + if (!module.hasBuilt) { + throw Exception('Module must be built before creating SvService. ' + 'Call build() first.'); + } + + synthBuilder = SynthBuilder(module, SystemVerilogSynthesizer()); + fileContents = synthBuilder.getSynthFileContents(); + + if (register) { + ModuleServices.instance.svService = this; + } + } + + /// All [SynthesisResult]s produced by synthesis. + Set get synthesisResults => synthBuilder.synthesisResults; + + /// Returns the concatenated SystemVerilog output as a single string, + /// matching the format of [Module.generateSynth]. + String get allContents => fileContents.map((fc) => fc.contents).join('\n\n'); + + /// Returns a map from module definition name to its SV file contents. + /// + /// Keys are [SynthesisResult.instanceTypeName] (the uniquified definition + /// name used in the generated SV). + Map get contentsByName => { + for (final fc in fileContents) fc.name: fc.contents, + }; + + /// Returns a map from module definition name + /// ([Module.definitionName]) to its SV file contents. + /// + /// This uses the original definition name (not uniquified), matching + /// the keys used by FLC trace data. + Map get contentsByDefinitionName { + final result = {}; + for (final sr in synthesisResults) { + final defName = sr.module.definitionName; + final instanceName = sr.instanceTypeName; + // Find the file content matching this instance type name. + final fc = fileContents.firstWhereOrNull((f) => f.name == instanceName); + if (fc != null) { + result[defName] = fc.contents; + } + } + return result; + } + + /// Writes each module's SV to a separate file in [directory]. + /// + /// Files are named `.sv`. + void writeFiles(String directory) { + final dir = Directory(directory)..createSync(recursive: true); + for (final fc in fileContents) { + File('${dir.path}/${fc.name}.sv').writeAsStringSync(fc.contents); + } + } + + /// Returns a JSON-serialisable summary of the SV synthesis. + /// + /// Contains the list of generated module definition names. + Map toJson() => { + 'modules': [ + for (final fc in fileContents) fc.name, + ], + }; +} diff --git a/lib/src/synthesizers/systemverilog/systemverilog.dart b/lib/src/synthesizers/systemverilog/systemverilog.dart index 281b05df9..e5f772e44 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog.dart @@ -1,5 +1,6 @@ // Copyright (C) 2021-2024 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'sv_service.dart'; export 'systemverilog_mixins.dart'; export 'systemverilog_synthesizer.dart'; diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index d8b5bae36..062647ac3 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // systemverilog_synthesizer.dart diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index c3026a0d5..8fcbc014a 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -11,8 +11,8 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; /// Represents a logic signal in the generated code within a module. @internal @@ -212,92 +212,25 @@ class SynthLogic { /// The name of this, if it has been picked. String? _name; - /// Picks a [name]. + /// Picks a [name] using the module's signal namer. /// /// Must be called exactly once. - void pickName(Uniquifier uniquifier) { + void pickName() { assert(_name == null, 'Should only pick a name once.'); - _name = _findName(uniquifier); + _name = _findName(); } /// Finds the best name from the collection of [Logic]s. - String _findName(Uniquifier uniquifier) { - // check for const - if (_constLogic != null) { - if (!_constNameDisallowed) { - return _constLogic!.value.toString(); - } else { - assert( - logics.length > 1, - 'If there is a constant, but the const name is not allowed, ' - 'there needs to be another option', - ); - } - } - - // check for reserved - if (_reservedLogic != null) { - return uniquifier.getUniqueName( - initialName: _reservedLogic!.name, - reserved: true, - ); - } - - // check for renameable - if (_renameableLogic != null) { - return uniquifier.getUniqueName( - initialName: _renameableLogic!.preferredSynthName, - ); - } - - // pick a preferred, available, mergeable name, if one exists - final unpreferredMergeableLogics = []; - final uniquifiableMergeableLogics = []; - for (final mergeableLogic in _mergeableLogics) { - if (Naming.isUnpreferred(mergeableLogic.preferredSynthName)) { - unpreferredMergeableLogics.add(mergeableLogic); - } else if (!uniquifier.isAvailable(mergeableLogic.preferredSynthName)) { - uniquifiableMergeableLogics.add(mergeableLogic); - } else { - return uniquifier.getUniqueName( - initialName: mergeableLogic.preferredSynthName, - ); - } - } - - // uniquify a preferred, mergeable name, if one exists - if (uniquifiableMergeableLogics.isNotEmpty) { - return uniquifier.getUniqueName( - initialName: uniquifiableMergeableLogics.first.preferredSynthName, - ); - } - - // pick an available unpreferred mergeable name, if one exists, otherwise - // uniquify an unpreferred mergeable name - if (unpreferredMergeableLogics.isNotEmpty) { - return uniquifier.getUniqueName( - initialName: unpreferredMergeableLogics - .firstWhereOrNull( - (element) => - uniquifier.isAvailable(element.preferredSynthName), - ) - ?.preferredSynthName ?? - unpreferredMergeableLogics.first.preferredSynthName, + /// + /// Delegates to signal namer which handles constant value naming, priority + /// selection, and uniquification via the module's shared namespace. + String _findName() => + parentSynthModuleDefinition.module.namer.signalNameOfBest( + logics, + constValue: _constLogic, + constNameDisallowed: _constNameDisallowed, ); - } - - // pick anything (unnamed) and uniquify as necessary (considering preferred) - // no need to prefer an available one here, since it's all unnamed - return uniquifier.getUniqueName( - initialName: _unnamedLogics - .firstWhereOrNull( - (element) => !Naming.isUnpreferred(element.preferredSynthName), - ) - ?.preferredSynthName ?? - _unnamedLogics.first.preferredSynthName, - ); - } /// Creates an instance to represent [initialLogic] and any that merge /// into it. @@ -404,7 +337,7 @@ class SynthLogic { @override String toString() => '${_name == null ? 'null' : '"$name"'}, ' - 'logics contained: ${logics.map((e) => e.preferredSynthName).toList()}'; + 'logics contained: ${logics.map(Namer.baseName).toList()}'; /// Provides a definition for a range in SV from a width. static String _widthToRangeDef(int width, {bool forceRange = false}) { @@ -551,17 +484,3 @@ class SynthLogicArrayElement extends SynthLogic { ' parentArray=($parentArray), element ${logic.arrayIndex}, logic: $logic' ' logics contained: ${logics.map((e) => e.name).toList()}'; } - -extension on Logic { - /// Returns the preferred name for this [Logic] while generating in the synth - /// stack. - String get preferredSynthName => naming == Naming.reserved - // if reserved, keep the exact name - ? name - : isArrayMember - // arrays nicely name their elements already - ? name - // sanitize to remove any `.` in struct names - // the base `name` will be returned if not a structure. - : Sanitizer.sanitizeSV(structureName); -} diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 37ebfb323..9ea120646 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -14,7 +14,6 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; /// A version of [BusSubset] that can be used for slicing on [LogicStructure] /// ports. @@ -110,10 +109,6 @@ class SynthModuleDefinition { @override String toString() => "module name: '${module.name}'"; - /// Used to uniquify any identifiers, including signal names - /// and module instances. - final Uniquifier _synthInstantiationNameUniquifier; - /// Indicates whether [logic] has a corresponding present [SynthLogic] in /// this definition. @internal @@ -289,14 +284,7 @@ class SynthModuleDefinition { /// Creates a new definition representation for this [module]. SynthModuleDefinition(this.module) - : _synthInstantiationNameUniquifier = Uniquifier( - reservedNames: { - ...module.inputs.keys, - ...module.outputs.keys, - ...module.inOuts.keys, - }, - ), - assert( + : assert( !(module is SystemVerilog && module.generatedDefinitionType == DefinitionGenerationType.none), @@ -465,6 +453,7 @@ class SynthModuleDefinition { final receiverIsSubModuleOutput = receiver.isOutput && (receiver.parentModule?.parent == module); + if (receiverIsSubModuleOutput) { final subModule = receiver.parentModule!; @@ -513,6 +502,7 @@ class SynthModuleDefinition { _collapseArrays(); _collapseAssignments(); _assignSubmodulePortMapping(); + _pruneUnused(); process(); _pickNames(); @@ -767,49 +757,59 @@ class SynthModuleDefinition { } /// Picks names of signals and sub-modules. + /// + /// Signal names are read from `Namer.signalNameOf` (for user-created + /// [Logic] objects) or kept as literal constants and are allocated from + /// `Namer.signalNameOf`. Submodule instance names are allocated + /// from `Namer.allocateRawName`. All names share a single + /// namespace managed by the module's `Namer`. void _pickNames() { - // first ports get priority + // Name allocation order matters — earlier claims get the unsuffixed name + // when there are collisions. This matches production ROHD priority: + // 1. Ports (reserved by _initNamespace, claimed via signalName) + // 2. Reserved submodule instances + // 3. Reserved internal signals + // 4. Non-reserved submodule instances + // 5. Non-reserved internal signals for (final input in inputs) { - input.pickName(_synthInstantiationNameUniquifier); + input.pickName(); } for (final output in outputs) { - output.pickName(_synthInstantiationNameUniquifier); + output.pickName(); } for (final inOut in inOuts) { - inOut.pickName(_synthInstantiationNameUniquifier); + inOut.pickName(); } - // pick names of *reserved* submodule instances - final nonReservedSubmodules = []; + // Reserved submodule instances first (they assert their exact name). for (final submodule in subModuleInstantiations) { if (submodule.module.reserveName) { - submodule.pickName(_synthInstantiationNameUniquifier); + submodule.pickName(module); assert(submodule.module.name == submodule.name, 'Expect reserved names to retain their name.'); - } else { - nonReservedSubmodules.add(submodule); } } - // then *reserved* internal signals get priority + // Reserved internal signals next. final nonReservedSignals = []; for (final signal in internalSignals) { if (signal.isReserved) { - signal.pickName(_synthInstantiationNameUniquifier); + signal.pickName(); } else { nonReservedSignals.add(signal); } } - // then submodule instances - for (final submodule in nonReservedSubmodules - .where((element) => element.needsInstantiation)) { - submodule.pickName(_synthInstantiationNameUniquifier); + // Then non-reserved submodule instances. + for (final submodule in subModuleInstantiations) { + if (!submodule.module.reserveName && submodule.needsInstantiation) { + submodule.pickName(module); + } } - // then the rest of the internal signals + // Then the rest of the internal signals. for (final signal in nonReservedSignals) { - signal.pickName(_synthInstantiationNameUniquifier); + signal.pickName(); } } diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 80a415a09..cf7da28e8 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synth_sub_module_instantiation.dart @@ -11,7 +11,6 @@ import 'dart:collection'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; /// Represents an instantiation of a module within another module. class SynthSubModuleInstantiation { @@ -25,13 +24,15 @@ class SynthSubModuleInstantiation { String get name => _name!; /// Selects a name for this module instance. Must be called exactly once. - void pickName(Uniquifier uniquifier) { + /// + /// Names are allocated from [parentModule]'s `Namer`'s shared namespace + /// via `Namer.allocateName`. + void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = uniquifier.getUniqueName( - initialName: module.uniqueInstanceName, + _name = parentModule.namer.allocateRawName( + module.uniqueInstanceName, reserved: module.reserveName, - nullStarter: 'm', ); } diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart new file mode 100644 index 000000000..efbe8e3e4 --- /dev/null +++ b/lib/src/utilities/namer.dart @@ -0,0 +1,233 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// namer.dart +// Central collision-free naming for signals and instances within a module. +// +// 2026 April 10 +// Author: Desmond Kirkpatrick + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; +import 'package:rohd/src/utilities/uniquifier.dart'; + +/// Central namer that manages collision-free names for both signals and +/// submodule instances within a single module scope. +/// +/// All identifiers (signals and instances) share a single namespace, +/// ensuring no name collisions in the generated SystemVerilog. +/// +/// Port names are reserved at construction time. Internal signal names +/// are assigned lazily on the first [signalNameOf] call. Instance names +/// are allocated explicitly via [allocateRawName]. +@internal +class Namer { + // ─── Shared namespace ─────────────────────────────────────────── + + final Uniquifier _uniquifier; + + /// Cache of resolved names for internal (non-port) signals only. + /// Port names are returned directly from [_portLogics] and never cached here. + final Map _signalNames = {}; + + /// The set of port [Logic] objects, for O(1) port membership tests. + final Set _portLogics; + + // ─── Construction ─────────────────────────────────────────────── + + Namer._({ + required Uniquifier uniquifier, + required Set portLogics, + }) : _uniquifier = uniquifier, + _portLogics = portLogics; + + /// Creates a [Namer] for the given module ports. + /// + /// Port names are reserved in the shared namespace. Port names are + /// guaranteed sanitary by [Module]'s `_checkForSafePortName`. + factory Namer.forModule({ + required Map inputs, + required Map outputs, + required Map inOuts, + }) { + final portLogics = { + ...inputs.values, + ...outputs.values, + ...inOuts.values, + }; + + final uniquifier = Uniquifier(); + for (final logic in portLogics) { + uniquifier.getUniqueName(initialName: logic.name, reserved: true); + } + + return Namer._( + uniquifier: uniquifier, + portLogics: portLogics, + ); + } + + // ─── Name availability / allocation ───────────────────────────── + + /// Returns `true` if [name] has not yet been claimed in the namespace. + bool isAvailable(String name) => _uniquifier.isAvailable(name); + + /// Allocates a collision-free name in the shared namespace. + /// + /// When [reserved] is `true`, the exact [baseName] (after sanitization) + /// is claimed without modification; an exception is thrown if it collides. + String allocateRawName(String baseName, {bool reserved = false}) => + _uniquifier.getUniqueName( + initialName: Sanitizer.sanitizeSV(baseName), + reserved: reserved, + ); + + // ─── Signal naming (Logic → String) ───────────────────────────── + + /// Returns the canonical name for [logic]. + /// + /// The first call for a given [logic] allocates a collision-free name + /// via the underlying [Uniquifier]. Subsequent calls return the cached + /// result in O(1). + String signalNameOf(Logic logic) { + final cached = _signalNames[logic]; + if (cached != null) { + return cached; + } + + if (_portLogics.contains(logic)) { + return logic.name; + } + + String base; + final isReservedInternal = logic.naming == Naming.reserved && !logic.isPort; + if (logic.naming == Naming.reserved || logic.isArrayMember) { + base = logic.name; + } else { + base = Sanitizer.sanitizeSV(logic.structureName); + } + + final name = _uniquifier.getUniqueName( + initialName: base, + reserved: isReservedInternal, + ); + _signalNames[logic] = name; + return name; + } + + /// The base name that would be used for [logic] before uniquification. + static String baseName(Logic logic) => + (logic.naming == Naming.reserved || logic.isArrayMember) + ? logic.name + : Sanitizer.sanitizeSV(logic.structureName); + + /// Chooses the best name from a pool of merged [Logic] signals. + /// + /// When [constValue] is provided and [constNameDisallowed] is `false`, + /// the constant's value string is used directly as the name (no + /// uniquification). When [constNameDisallowed] is `true`, the constant + /// is excluded from the candidate pool and the normal priority applies. + /// + /// Priority (after constant handling): + /// 1. Port of this module (always wins — its name is already reserved). + /// 2. Reserved internal signal (exact name, throws on collision). + /// 3. Renameable signal. + /// 4. Preferred-available mergeable (base name not yet taken). + /// 5. Preferred-uniquifiable mergeable. + /// 6. Available-unpreferred mergeable. + /// 7. First unpreferred mergeable. + /// 8. Unnamed (prefer non-unpreferred base name). + /// + /// The winning name is allocated once and cached for the chosen [Logic]. + /// All other non-port [Logic]s in [candidates] are also cached to the + /// same name. + String signalNameOfBest( + Iterable candidates, { + Const? constValue, + bool constNameDisallowed = false, + }) { + if (constValue != null && !constNameDisallowed) { + return constValue.value.toString(); + } + + Logic? port; + Logic? reserved; + Logic? renameable; + final preferredMergeable = []; + final unpreferredMergeable = []; + final unnamed = []; + + for (final logic in candidates) { + if (_portLogics.contains(logic)) { + port = logic; + } else if (logic.isPort) { + if (Naming.isUnpreferred(baseName(logic))) { + unpreferredMergeable.add(logic); + } else { + preferredMergeable.add(logic); + } + } else if (logic.naming == Naming.reserved) { + reserved = logic; + } else if (logic.naming == Naming.renameable) { + renameable = logic; + } else if (logic.naming == Naming.mergeable) { + if (Naming.isUnpreferred(baseName(logic))) { + unpreferredMergeable.add(logic); + } else { + preferredMergeable.add(logic); + } + } else { + unnamed.add(logic); + } + } + + if (port != null) { + return _nameAndCacheAll(port, candidates); + } + + if (reserved != null) { + return _nameAndCacheAll(reserved, candidates); + } + + if (renameable != null) { + return _nameAndCacheAll(renameable, candidates); + } + + if (preferredMergeable.isNotEmpty) { + final best = preferredMergeable + .firstWhereOrNull((e) => isAvailable(baseName(e))) ?? + preferredMergeable.first; + return _nameAndCacheAll(best, candidates); + } + + if (unpreferredMergeable.isNotEmpty) { + final best = unpreferredMergeable + .firstWhereOrNull((e) => isAvailable(baseName(e))) ?? + unpreferredMergeable.first; + return _nameAndCacheAll(best, candidates); + } + + if (unnamed.isNotEmpty) { + final best = + unnamed.firstWhereOrNull((e) => !Naming.isUnpreferred(baseName(e))) ?? + unnamed.first; + return _nameAndCacheAll(best, candidates); + } + + throw StateError('No Logic candidates to name.'); + } + + /// Names [chosen] via [signalNameOf], then caches the same name for all + /// other non-port [Logic]s in [all]. + String _nameAndCacheAll(Logic chosen, Iterable all) { + final name = signalNameOf(chosen); + for (final logic in all) { + if (!identical(logic, chosen) && !_portLogics.contains(logic)) { + _signalNames[logic] = name; + } + } + return name; + } +} diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index d7850df4e..f9267ba4a 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -14,6 +14,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; import 'package:rohd/src/utilities/web.dart'; import 'package:test/test.dart'; @@ -424,16 +425,776 @@ abstract class SimCompare { if (!dontDeleteTmpFiles) { try { - File(tmpOutput).deleteSync(); - File(tmpTestFile).deleteSync(); + final outFile = File(tmpOutput); + if (outFile.existsSync()) { + outFile.deleteSync(); + } + final testFile = File(tmpTestFile); + if (testFile.existsSync()) { + testFile.deleteSync(); + } if (dumpWaves) { - File(tmpVcdFile).deleteSync(); + final vcdFile = File(tmpVcdFile); + if (vcdFile.existsSync()) { + vcdFile.deleteSync(); + } } } on Exception catch (e) { print("Couldn't delete: $e"); - return false; } } return true; } + + // ══════════════════════════════════════════════════════════════════════ + // SystemC simulation (Accellera SystemC) + // ══════════════════════════════════════════════════════════════════════ + + /// The default SystemC installation path (Accellera). + static const _systemCDefaultHome = '/opt/systemc/include'; + static const _systemCDefaultLib = '/opt/systemc/lib'; + + /// Cache of compiled SystemC executables keyed by generated code hash. + static final _compilationCache = {}; + + /// 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()) { + final name = entity.uri.pathSegments.last; + + // Always remove SystemC artifacts (tmp_sc_*, Makefile_sc) + if (name.startsWith('tmp_sc_') || name == 'Makefile_sc') { + entity.deleteSync(recursive: true); + continue; + } + + // Remove pch/ directory only when keepPch is false + if (!keepPch && entity is Directory && entity.path.endsWith('/pch')) { + entity.deleteSync(recursive: true); + continue; + } + + // Leave everything else (iverilog files from parallel tests) alone + } + } + } on Exception catch (_) {} + } + + /// Compiles a SystemC module into a reusable stdin-driven executable. + /// + /// Returns a [SystemCExecutable] that can be used to run multiple vector + /// sets without recompilation. Use in `setUpAll` for test groups. + /// Results are cached — calling this with the same module definition + /// returns the previously compiled binary. + static SystemCExecutable? buildSystemCExecutable( + Module module, { + String? moduleName, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + }) { + if (kIsWeb) { + return null; + } + + final scHome = systemcHome ?? ''; + final scLib = systemcLib ?? ''; + final (resolvedHome, resolvedLib) = _resolveSystemCPaths(scHome, scLib); + + if (resolvedHome == null || resolvedLib == null) { + print('SystemC installation not found'); + return null; + } + + final topModule = moduleName ?? module.definitionName; + final generatedSystemC = module.generateSystemC(); + + // Check compilation cache + final cacheKey = generatedSystemC.hashCode; + if (_compilationCache.containsKey(cacheKey)) { + final cached = _compilationCache[cacheKey]!; + if (File(cached.binaryPath).existsSync()) { + return cached; + } + // Binary was removed; recompile. + _compilationCache.remove(cacheKey); + } + + // Identify clock signals + final clockSignals = {}; + if (clockName != null) { + clockSignals.add(clockName); + } + for (final input in module.inputs.entries) { + final name = input.key; + if (clockSignals.isEmpty && (name == 'clk' || name.contains('clock'))) { + clockSignals.add(name); + } + } + final promotedClocks = {}; + for (final sub in module.subModules) { + if (sub is SimpleClockGenerator) { + final clkSigName = sub.clk.name; + promotedClocks.add(clkSigName); + clockSignals.add(clkSigName); + } + } + + // Collect ALL module ports for the stdin-driven harness + final inputPorts = {}; + for (final input in module.inputs.entries) { + if (promotedClocks.contains(input.key)) { + continue; + } + inputPorts[input.key] = input.value.width; + } + final outputPorts = {}; + for (final output in module.outputs.entries) { + outputPorts[output.key] = output.value.width; + } + + // Generate stdin-driven testbench + final tb = StringBuffer() + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('using namespace std;') + ..writeln() + ..writeln(generatedSystemC) + ..writeln() + ..writeln('int sc_main(int argc, char* argv[]) {'); + + // Clock + for (final clkName in clockSignals) { + tb.writeln( + ' sc_clock $clkName("$clkName", ${Vector._period}, SC_NS);'); + } + + // Signals for all non-clock input ports + for (final entry in inputPorts.entries) { + if (clockSignals.contains(entry.key)) { + continue; + } + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};'); + } + + // Signals for all output ports + for (final entry in outputPorts.entries) { + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};'); + } + + tb + ..writeln() + // DUT instantiation and port binding + ..writeln(' $topModule dut("dut");'); + for (final name in inputPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + for (final clkName in clockSignals) { + if (!inputPorts.containsKey(clkName)) { + tb.writeln(' dut.$clkName($clkName);'); + } + } + for (final name in outputPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + + tb + ..writeln() + ..writeln(' int _tb_errors = 0;') + ..writeln() + ..writeln(' // Initial offset') + ..writeln(' sc_start(sc_time(1, SC_NS));') + ..writeln() + ..writeln(' // Read number of vectors') + ..writeln(' int _tb_nvec;') + ..writeln(' cin >> _tb_nvec;') + ..writeln() + ..writeln(' for (int _tb_v = 0; _tb_v < _tb_nvec; _tb_v++) {'); + + // Read and drive each non-clock input + final drivableInputs = + inputPorts.keys.where((k) => !clockSignals.contains(k)).toList(); + for (final name in drivableInputs) { + final w = inputPorts[name]!; + if (w > 64) { + // BigInt — read as hex string + tb + ..writeln(' { string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _v(_h.c_str());') + ..writeln(' $name.write(_v); }'); + } else { + tb + ..writeln(' { uint64_t _v; cin >> _v;') + ..writeln(' $name.write(_v); }'); + } + } + + // Advance to check point + tb + ..writeln() + ..writeln(' sc_start(sc_time(${Vector._offset}, SC_NS));') + ..writeln() + ..writeln(' // Read number of outputs to check') + ..writeln(' int _tb_nchk;') + ..writeln(' cin >> _tb_nchk;') + ..writeln() + ..writeln(' for (int _tb_c = 0; _tb_c < _tb_nchk; _tb_c++) {') + ..writeln(' string _tb_pn;') + ..writeln(' cin >> _tb_pn;'); + + // Generate if-else chain for each output port + var first = true; + for (final entry in outputPorts.entries) { + final name = entry.key; + final w = entry.value; + final ifKey = first ? 'if' : '} else if'; + first = false; + tb.writeln(' $ifKey (_tb_pn == "$name") {'); + if (w > 64) { + tb + ..writeln(' string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _tb_exp(_h.c_str());') + ..writeln(' if ($name.read() != _tb_exp) {'); + } else { + tb + ..writeln(' uint64_t _tb_exp; cin >> _tb_exp;') + ..writeln(' if ($name.read() != _tb_exp) {'); + } + tb + ..writeln(' cout << "ERROR vector " << _tb_v' + ' << ": expected $name=" << _tb_exp' + ' << ", got " << $name.read() << endl;') + ..writeln(' _tb_errors++;') + ..writeln(' }'); + } + if (outputPorts.isNotEmpty) { + tb + ..writeln(' } else {') + ..writeln(' string _d; cin >> _d; // skip unknown') + ..writeln(' }'); + } + + tb + ..writeln(' }') + ..writeln() + ..writeln(' sc_start(sc_time(' + '${Vector._period - Vector._offset}, SC_NS));') + ..writeln(' }') + ..writeln() + ..writeln(' if (_tb_errors == 0) {') + ..writeln(' cout << "PASS" << endl;') + ..writeln(' } else {') + ..writeln(' cout << "FAIL: " << _tb_errors << " errors" << endl;') + ..writeln(' }') + ..writeln(' return _tb_errors > 0 ? 1 : 0;') + ..writeln('}'); + + final testbenchCode = tb.toString(); + + // Write and compile + final uniqueId = generatedSystemC.hashCode; + const dir = 'tmp_test'; + final tmpCppFile = '$dir/tmp_sc_$uniqueId.cpp'; + final tmpOutput = '$dir/tmp_sc_out_$uniqueId'; + + Directory(dir).createSync(recursive: true); + File(tmpCppFile).writeAsStringSync(testbenchCode); + + // Detect C++ standard for this installation + final cxxStd = _detectCxxStandard(resolvedLib); + + // Build precompiled header on first use + final pchDir = _ensurePch(resolvedHome, cxxStd); + final pchArgs = pchDir != null ? ['-I$pchDir'] : []; + + final compileResult = Process.runSync('g++', [ + '-std=$cxxStd', + '-pipe', + ...pchArgs, + '-I$resolvedHome', + '-o', + tmpOutput, + tmpCppFile, + '-L$resolvedLib', + '-lsystemc', + ]); + if (compileResult.exitCode != 0) { + print('SystemC compilation failed:'); + print(compileResult.stdout); + print(compileResult.stderr); + return null; + } + + final exe = SystemCExecutable._( + binaryPath: tmpOutput, + cppFile: tmpCppFile, + scLib: resolvedLib, + clockSignals: clockSignals, + inputPorts: inputPorts, + outputPorts: outputPorts, + ); + _compilationCache[cacheKey] = exe; + return exe; + } + + /// Runs [vectors] against a pre-compiled [SystemCExecutable]. + /// + /// Returns `true` if all vectors pass. + static bool runSystemCVectors( + SystemCExecutable exe, + List vectors, + ) { + if (!File(exe.binaryPath).existsSync()) { + print('SystemC binary not found: ${exe.binaryPath}'); + return false; + } + + // Build stdin data + final sb = StringBuffer()..writeln(vectors.length); + + final drivableInputs = exe.inputPorts.keys + .where((k) => !exe.clockSignals.contains(k)) + .toList(); + + // Track last-driven values (persist across vectors like iverilog) + final lastValues = { + for (final name in drivableInputs) name: '0', + }; + + for (final vector in vectors) { + // Update last-driven values with this vector's inputs + for (final name in drivableInputs) { + final value = vector.inputValues[name]; + if (value != null) { + final w = exe.inputPorts[name]!; + if (w > 64) { + final lv = LogicValue.of(value, width: w); + var hex = lv.toBigInt().toUnsigned(w).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + lastValues[name] = '0x$hex'; + } else { + lastValues[name] = '${_systemcIntValue(value, w)}'; + } + } + } + // Write all input values (using persisted values for unspecified) + for (final name in drivableInputs) { + sb.write('${lastValues[name]} '); + } + sb.writeln(); + + // Write expected outputs: count then name/value pairs + // Skip x/z outputs + final checks = {}; + for (final entry in vector.expectedOutputValues.entries) { + final name = entry.key; + final w = exe.outputPorts[name]!; + final expectedLV = LogicValue.of(entry.value, width: w); + if (expectedLV.toString().contains('x') || + expectedLV.toString().contains('z')) { + continue; + } + if (w > 64) { + var hex = expectedLV.toBigInt().toUnsigned(w).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + checks[name] = '0x$hex'; + } else { + checks[name] = '${_systemcIntValue(entry.value, w)}'; + } + } + sb.write('${checks.length} '); + for (final entry in checks.entries) { + sb.write('${entry.key} ${entry.value} '); + } + sb.writeln(); + } + + // Write vectors to temp file, redirect as stdin + final stdinFile = '${exe.binaryPath}_input.txt'; + File(stdinFile).writeAsStringSync(sb.toString()); + + final result = Process.runSync( + 'sh', + ['-c', '${exe.binaryPath} < $stdinFile'], + environment: { + 'LD_LIBRARY_PATH': exe.scLib, + 'SC_COPYRIGHT_MESSAGE': 'DISABLE', + }, + ); + + File(stdinFile).deleteSync(); + + final stdout = result.stdout.toString(); + final stderr = result.stderr.toString(); + + if (stdout.isNotEmpty && !stdout.contains('PASS')) { + print(stdout); + } + if (stderr.isNotEmpty && !stderr.contains('Info:')) { + print(stderr); + } + + return stdout.contains('PASS') && !stdout.contains('FAIL'); + } + + /// Convenience: runs [vectors] against a pre-compiled executable and + /// asserts the result. + static void checkSystemCVectors( + SystemCExecutable exe, + List vectors, + ) { + expect(runSystemCVectors(exe, vectors), true); + } + + /// Converts a value to an integer for stdin. + static int _systemcIntValue(dynamic value, int width) { + if (value is int) { + return value; + } + if (value is LogicValue) { + if (!value.isValid) { + return 0; + } + return value.toBigInt().toUnsigned(width).toInt(); + } + if (value is BigInt) { + return value.toUnsigned(width).toInt(); + } + if (value is String) { + final lv = LogicValue.of(value, width: width); + if (!lv.isValid) { + return 0; + } + return lv.toBigInt().toUnsigned(width).toInt(); + } + return 0; + } + + /// Executes [vectors] against a SystemC simulator compiled with g++ and + /// checks that it passes (single-shot, compiles each time). + static void checkSystemCVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false, + }) { + if (buildOnly) { + // Just verify SystemC code generation succeeds + module.generateSystemC(); + return; + } + final exe = buildSystemCExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + if (exe == null) { + // SystemC not available — skip gracefully. + return; + } + final passed = runSystemCVectors(exe, vectors); + expect(passed, true); + } + + /// Legacy API — returns bool. + static bool systemcVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false, + }) { + if (kIsWeb) { + return true; + } + final exe = buildSystemCExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + if (exe == null) { + return false; + } + if (buildOnly) { + return true; + } + return runSystemCVectors(exe, vectors); + } + + // ══════════════════════════════════════════════════════════════════════ + // Trace-based SystemC co-simulation + // ══════════════════════════════════════════════════════════════════════ + + /// Runs the ROHD simulation using [stimulus], records input/output values + /// at every posedge of [clk], then replays the captured vectors through + /// the SystemC-synthesized version of [module] and compares results. + /// + /// [stimulus] is an async function that sets up and drives the simulation + /// (inject signals, register actions, etc.) but does NOT call + /// [Simulator.run] — that is done internally. + /// + /// [inputNames] and [outputNames] specify which ports to record. If null, + /// all module inputs (excluding clock) and all module outputs are used. + /// + /// Example usage with an existing test: + /// ```dart + /// await SimCompare.systemcSimCompare( + /// counter, + /// clk, + /// stimulus: () async { + /// reset.inject(1); + /// en.inject(0); + /// Simulator.registerAction(25, () { reset.put(0); en.put(1); }); + /// Simulator.setMaxSimTime(100); + /// }, + /// ); + /// ``` + static Future systemcSimCompare( + Module module, + Logic clk, { + required Future Function() stimulus, + List? inputNames, + List? outputNames, + String? clockName, + String? resetName, + bool dontDeleteTmpFiles = false, + String? systemcHome, + String? systemcLib, + }) async { + // Determine which signals to record + final clkName = clockName ?? + module.inputs.keys.firstWhere((n) => n == 'clk' || n.contains('clock'), + orElse: () => 'clk'); + + final inputs = + inputNames ?? module.inputs.keys.where((n) => n != clkName).toList(); + final outputs = outputNames ?? module.outputs.keys.toList(); + + // Record snapshots at each posedge. + // Use previousValue for outputs — this gives us the output state from + // BEFORE the clock edge, which matches what the SystemC testbench sees + // when it checks at offset (before the posedge). + // Use current value for inputs — these are the values being presented + // to the DUT when the clock edge fires. + final recordings = []; + + clk.posedge.listen((_) { + // Sample inputs (current value — what's being driven now) + final inputValues = {}; + for (final name in inputs) { + final sig = module.input(name); + final val = sig.value; + inputValues[name] = val.isValid ? val.toBigInt().toInt() : 0; + } + + // Sample outputs using previousValue — the settled output + // from before this tick started, which is what a testbench + // checking before the clock edge would observe. + final outputValues = {}; + for (final name in outputs) { + final sig = module.output(name); + final prev = sig.previousValue; + if (prev != null && prev.isValid) { + outputValues[name] = prev.toBigInt().toInt(); + } + // Skip null/x/z — no check for this output + } + + recordings.add(Vector(inputValues, outputValues)); + }); + + // Run the user's stimulus setup + await stimulus(); + + // Run the ROHD simulation + await Simulator.run(); + + if (recordings.length < 2) { + print('Warning: only ${recordings.length} clock edges recorded,' + ' need at least 2 for comparison'); + return true; + } + + // No shifting needed — previousValue already gives us the output + // state from before the posedge, which matches systemcVector's + // check-before-edge timing. Just pass recordings directly as vectors. + + // Run through SystemC + return systemcVector( + module, + recordings, + clockName: clkName, + resetName: resetName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + } +} + +/// Holds the compiled state of a SystemC executable for reuse across tests. +class SystemCExecutable { + /// Path to the compiled binary. + final String binaryPath; + + /// Path to the generated C++ source. + final String cppFile; + + /// Path to the SystemC library (for LD_LIBRARY_PATH). + final String scLib; + + /// Clock signal names. + final Set clockSignals; + + /// Input port names and widths (excluding promoted clocks). + final Map inputPorts; + + /// Output port names and widths. + final Map outputPorts; + + SystemCExecutable._({ + required this.binaryPath, + required this.cppFile, + required this.scLib, + required this.clockSignals, + required this.inputPorts, + required this.outputPorts, + }); + + /// Deletes the compiled binary and source. + void cleanup() { + void tryDelete(String path) { + final f = File(path); + if (f.existsSync()) { + f.deleteSync(); + } + } + + try { + tryDelete(cppFile); + tryDelete(binaryPath); + } on Exception catch (_) {} + } } diff --git a/lib/src/utilities/systemc_cosim_ffi.dart b/lib/src/utilities/systemc_cosim_ffi.dart new file mode 100644 index 000000000..8ac2b47dc --- /dev/null +++ b/lib/src/utilities/systemc_cosim_ffi.dart @@ -0,0 +1,938 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_cosim_ffi.dart +// FFI-based real-time co-simulation with a SystemC compiled module. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; +import 'package:rohd/src/utilities/synchronous_propagator.dart'; +import 'package:rohd/src/utilities/web.dart'; + +// ============================================================================ +// FFI Type Definitions (using only dart:ffi built-in types) +// ============================================================================ + +typedef _DestroyDart = void Function(Pointer); + +typedef _SetInputDart = void Function(Pointer, Pointer, int); + +typedef _SetInputWideDart = void Function( + Pointer, Pointer, Pointer); + +typedef _GetOutputDart = int Function(Pointer, Pointer); + +typedef _GetOutputWideDart = Pointer Function( + Pointer, Pointer); + +typedef _AdvanceDart = void Function(Pointer, int); + +// ============================================================================ +// SystemCFfiCosim — Real-time FFI co-simulation with SystemC +// ============================================================================ + +/// A co-simulation wrapper that compiles an ROHD module's SystemC output to a +/// shared library and drives it in lock-step with the ROHD [Simulator]. +/// +/// ## How it works +/// +/// 1. The ROHD module is synthesized to SystemC C++ via +/// [Module.generateSystemC] +/// 2. A C-linkage FFI wrapper is generated around the SystemC module +/// 3. The wrapper is compiled to a `.so` shared library +/// 4. On each [Simulator] tick (at the `clkStable` phase): +/// - Current ROHD input values are pushed to SystemC via FFI +/// - SystemC is advanced by one half clock period (`sc_start`) +/// - SystemC output values are pulled back and `put()` onto ROHD outputs +/// +/// ## Timing compatibility with existing tests +/// +/// The synchronization point at `clkStable` means: +/// - `inject()` calls have already executed (mainTick phase) +/// - Clock has already toggled (mainTick) +/// - `previousValue` was snapshot at preTick (before this tick started) +/// - After clkStable, outputs are updated, then `postTick` fires +/// - `await clk.nextPosedge` resumes after postTick +/// +/// This preserves the same timing semantics as native ROHD Sequential blocks. +/// +/// ## Example +/// +/// ```dart +/// final counter = SimpleCounter(clk, reset, en); +/// await counter.build(); +/// +/// final cosim = await SystemCFfiCosim.create(counter, clk: clk); +/// if (cosim == null) return; // SystemC not installed +/// +/// // Now run your test exactly as before: +/// unawaited(Simulator.run()); +/// reset.inject(1); +/// await clk.nextPosedge; +/// // ... counter.output('val') is driven by SystemC +/// ``` +class SystemCFfiCosim { + /// The ROHD module whose SystemC synthesis is being co-simulated. + final Module module; + + /// The clock signal (null for combinational/clockless mode). + final Logic? clk; + + /// Clock period in nanoseconds (matches SimpleClockGenerator's period). + /// Ignored in combinational mode. + final int clockPeriodNs; + + /// Whether this cosim operates in combinational (clockless) mode. + /// In this mode, inputs are propagated immediately via delta cycles + /// (sc_start(SC_ZERO_TIME)) whenever any input changes. + bool get isCombinational => clk == null; + + /// Handle to the loaded shared library. + DynamicLibrary? _lib; + + /// Opaque handle to the SystemC simulation context. + Pointer _handle = nullptr; + + /// Path to the compiled .so file. + late String? _soPath; + + /// Input port names and widths. + final Map _inputWidths = {}; + + /// Output port names and widths. + final Map _outputWidths = {}; + + /// Clock port name(s) to skip when driving inputs. + final Set _clockNames = {}; + + /// Whether the cosim is actively stepping. + bool _active = false; + + /// Subscription to Simulator.clkStable for per-tick stepping (clocked mode). + StreamSubscription? _clkStableSubscription; + + /// Synchronous subscriptions on input glitches (combinational mode). + final List> + _inputGlitchSubscriptions = []; + + /// Whether a combinational step is already pending in this propagation wave. + /// Prevents re-entrant stepping when multiple inputs change in the same + /// event (e.g. Swizzle feeding a bus). + bool _combStepPending = false; + + // FFI function handles + late final _SetInputDart _setInput; + late final _SetInputWideDart _setInputWide; + late final _GetOutputDart _getOutput; + late final _GetOutputWideDart _getOutputWide; + late final _AdvanceDart _advance; + late final _DestroyDart _destroy; + + // Cached C-string pointers for signal names (allocated once, reused every + // step) + final Map> _inputNamePtrs = {}; + final Map> _outputNamePtrs = {}; + + // Cached signal references (avoid module.input/output map lookups per step) + final Map _inputSignals = {}; + final Map _outputSignals = {}; + + // Pre-allocated buffer for wide hex strings (avoids malloc/free per step). + // 512 chars covers up to 2048-bit signals. + Pointer _hexBuf = nullptr; + static const _hexBufSize = 512; + + // Native memory management + static final _free = DynamicLibrary.process().lookupFunction< + Void Function(Pointer), void Function(Pointer)>('free'); + static final _malloc = DynamicLibrary.process().lookupFunction< + Pointer Function(IntPtr), Pointer Function(int)>('malloc'); + + /// Cache of loaded libraries and handles keyed by .so path. + /// Prevents re-loading a .so that's already in the process, which + /// would crash SystemC's singleton kernel (E113). + static final _loadedLibs = {}; + + SystemCFfiCosim._(this.module, this.clk, {required this.clockPeriodNs}); + + /// Compiles the module's SystemC output to a shared library, loads it, + /// and begins co-simulation. + /// + /// Returns `null` if SystemC is not installed or compilation fails. + /// + /// If [clk] is provided, the cosim operates in clocked mode — stepping + /// SystemC at each clock edge via `Simulator.clkStable`. + /// + /// If [clk] is omitted (null), the cosim operates in combinational mode — + /// propagating inputs through SystemC delta cycles immediately whenever + /// any input signal changes. This gives the same semantics as native ROHD + /// [Combinational] blocks. + static Future create( + Module module, { + Logic? clk, + int clockPeriodNs = 10, + String? systemcHome, + String? systemcLib, + }) async { + if (kIsWeb) { + return null; + } + + final cosim = SystemCFfiCosim._(module, clk, clockPeriodNs: clockPeriodNs); + + if (!cosim._compileAndLoad( + systemcHome: systemcHome ?? '', + systemcLib: systemcLib ?? '', + )) { + return null; + } + + cosim + .._cachePortInfo() + .._start(); + return cosim; + } + + /// Pre-elaborates the module's SystemC code without starting co-simulation. + /// + /// Call this in `setUpAll` for every module configuration that will be + /// cosim-tested in the file. This ensures all SystemC module types are + /// instantiated during the elaboration phase (before `sc_start`), which + /// avoids E113 errors when multiple configurations are tested. + /// + /// Returns `false` if SystemC is not installed or compilation fails. + static Future preElaborate( + Module module, { + Logic? clk, + int clockPeriodNs = 10, + String? systemcHome, + String? systemcLib, + }) async { + if (kIsWeb) { + return false; + } + + final cosim = SystemCFfiCosim._(module, clk, clockPeriodNs: clockPeriodNs); + + return cosim._compileAndLoad( + systemcHome: systemcHome ?? '', + systemcLib: systemcLib ?? '', + ); + } + + /// Compiles the SystemC wrapper to .so and loads it. + /// Uses a static cache to avoid re-loading the same .so (which would + /// crash SystemC's singleton kernel with E113). + bool _compileAndLoad({ + required String systemcHome, + required String systemcLib, + }) { + final resolvedHome = _resolveHome(systemcHome); + final resolvedLib = _resolveLib(systemcLib); + if (resolvedHome == null || resolvedLib == null) { + // ignore: avoid_print + print('SystemC FFI cosim: SystemC installation not found'); + return false; + } + + // Collect port widths — treat clocks as regular 1-bit inputs driven + // manually via sc_signal (avoids sc_clock phase alignment issues + // when reusing the cached SystemC kernel across tests). + for (final entry in module.inputs.entries) { + final name = entry.key; + if (name == 'clk' || name.contains('clock')) { + _clockNames.add(name); + } + _inputWidths[name] = entry.value.width; + } + for (final entry in module.outputs.entries) { + _outputWidths[entry.key] = entry.value.width; + } + + // Generate wrapper C++ source + final generatedSC = module.generateSystemC(); + + // Compute a content hash to distinguish modules with the same + // definitionName but different logic (e.g., DAZ/FTZ variants). + // Strip non-deterministic lines (e.g. timestamps) before hashing so + // that repeated instantiations of the same module share one .so. + final stableCode = generatedSC + .split('\n') + .where((line) => !line.contains('Generation time:')) + .join('\n'); + final contentHash = stableCode.hashCode.toUnsigned(32).toRadixString(16); + final uniqueName = '${module.definitionName}_$contentHash'; + + // Rename the top-level SC_MODULE in the generated code to the unique name + // so that different logic variants don't collide in the SystemC linker. + final renamedSC = generatedSC.replaceAll(module.definitionName, uniqueName); + final wrapperSrc = _generateWrapper(renamedSC, uniqueName); + + const dir = 'tmp_test'; + Directory(dir).createSync(recursive: true); + final cacheKey = uniqueName; + final cppFile = '$dir/cosim_ffi_$cacheKey.cpp'; + _soPath = '$dir/libcosim_ffi_$cacheKey.so'; + + // Check cache — if already loaded in this process, reuse it + if (_loadedLibs.containsKey(cacheKey)) { + final cached = _loadedLibs[cacheKey]!; + _lib = cached.lib; + _handle = cached.handle; + _setInput = cached.setInput; + _setInputWide = cached.setInputWide; + _getOutput = cached.getOutput; + _getOutputWide = cached.getOutputWide; + _advance = cached.advance; + _destroy = cached.destroy; + + // Reset all inputs to 0 (including clock) so the DUT starts fresh. + // The writes are committed by the first sc_start in _step(). + // Note: _cachePortInfo() is called after this, so use temp pointers here. + for (final name in _inputWidths.keys) { + if (_inputNamePtrs.containsKey(name)) { + _setInput(_handle, _inputNamePtrs[name]!.cast(), 0); + } else { + final namePtr = _toCString(name); + _setInput(_handle, namePtr.cast(), 0); + _free(namePtr); + } + } + + return true; + } + + // Compile (only if .so doesn't exist on disk) + if (!File(_soPath!).existsSync()) { + File(cppFile).writeAsStringSync(wrapperSrc); + + final cxxStd = _detectCxxStd(resolvedLib); + final result = Process.runSync('g++', [ + '-std=$cxxStd', + '-shared', + '-fPIC', + '-O2', + '-I$resolvedHome', + '-L$resolvedLib', + '-Wl,-rpath,$resolvedLib', + '-o', + _soPath!, + cppFile, + '-lsystemc', + ]); + + if (result.exitCode != 0) { + // ignore: avoid_print + print('SystemC FFI: compilation failed:\n${result.stderr}'); + return false; + } + } + + // Load the shared library + _lib = DynamicLibrary.open(_soPath!); + + // Bind function pointers + final create = _lib!.lookupFunction Function(Pointer), + Pointer Function(Pointer)>('sc_cosim_create'); + _setInput = _lib!.lookupFunction< + Void Function(Pointer, Pointer, Uint64), + _SetInputDart>('sc_cosim_set_input'); + _setInputWide = _lib!.lookupFunction< + Void Function(Pointer, Pointer, Pointer), + _SetInputWideDart>('sc_cosim_set_input_wide'); + _getOutput = _lib!.lookupFunction< + Uint64 Function(Pointer, Pointer), + _GetOutputDart>('sc_cosim_get_output'); + _getOutputWide = _lib!.lookupFunction< + Pointer Function(Pointer, Pointer), + _GetOutputWideDart>('sc_cosim_get_output_wide'); + _advance = _lib! + .lookupFunction, Uint64), _AdvanceDart>( + 'sc_cosim_advance'); + _destroy = _lib!.lookupFunction), _DestroyDart>( + 'sc_cosim_destroy'); + + // Create the SystemC context (elaborates the design) + final namePtr = _toCString(module.definitionName); + _handle = create(namePtr.cast()); + _free(namePtr); + + if (_handle == nullptr) { + // ignore: avoid_print + print('SystemC FFI: sc_cosim_create returned null'); + return false; + } + + // Cache for reuse + _loadedLibs[cacheKey] = _LoadedCosimLib( + lib: _lib!, + handle: _handle, + setInput: _setInput, + setInputWide: _setInputWide, + getOutput: _getOutput, + getOutputWide: _getOutputWide, + advance: _advance, + destroy: _destroy, + ); + + return true; + } + + /// Pre-allocates cached C-string pointers and signal references. + /// Call once after _compileAndLoad succeeds. + void _cachePortInfo() { + for (final entry in _inputWidths.entries) { + _inputNamePtrs[entry.key] = _toCString(entry.key); + _inputSignals[entry.key] = module.input(entry.key); + } + for (final entry in _outputWidths.entries) { + _outputNamePtrs[entry.key] = _toCString(entry.key); + _outputSignals[entry.key] = module.output(entry.key); + } + // Pre-allocate hex buffer for wide signals + _hexBuf = _malloc(_hexBufSize); + } + + /// Whether an edge occurred in this tick (set by glitch listener). + bool _edgePending = false; + + /// Subscription to clock glitch for edge detection. + SynchronousSubscription? _glitchSubscription; + + /// Starts the co-simulation by hooking into the clock's glitch and + /// Simulator.clkStable — mirroring how ROHD's Sequential works. + /// + /// In clocked mode: Steps SystemC on BOTH posedge and negedge, advancing + /// by half-period each time. This keeps the SystemC clock perfectly aligned + /// with ROHD's: + /// + /// ROHD posedge → sc_start(T/2) → SystemC posedge occurs → read outputs + /// ROHD negedge → sc_start(T/2) → SystemC negedge occurs → read outputs + /// + /// In combinational mode: Listens to input signal glitches and immediately + /// propagates through SystemC via delta cycles (sc_start(0)). This gives + /// the same timing semantics as native ROHD [Combinational] blocks. + void _start() { + _active = true; + + if (isCombinational) { + _startCombinational(); + } else { + _startClocked(); + } + } + + /// Starts clocked mode — step at each clock edge via clkStable. + void _startClocked() { + // Detect any clock edge (0→1 or 1→0) by listening to the glitch stream. + _glitchSubscription = clk!.glitch.listen((event) { + if (!_active) { + return; + } + // Any valid transition on the clock (posedge or negedge) + final isPosedge = event.previousValue == LogicValue.zero && + event.newValue == LogicValue.one; + final isNegedge = event.previousValue == LogicValue.one && + event.newValue == LogicValue.zero; + if ((isPosedge || isNegedge) && !_edgePending) { + _edgePending = true; + // Wait for clkStable (all inputs settled) then step SystemC. + unawaited(Simulator.clkStable.first.then((_) { + if (!_active) { + return; + } + _edgePending = false; + _step(); + })); + } + }); + } + + /// Starts combinational mode — step on any input change (synchronous). + /// + /// Uses synchronous glitch subscriptions so that output values are + /// available immediately after `put()` — matching native ROHD behavior. + /// + /// Does NOT call sc_start here — the kernel transition from ELABORATION + /// to RUNNING is deferred to the first actual `_stepCombinational()` call. + /// This allows multiple module variants to be pre-elaborated before the + /// kernel starts (avoiding E113 errors). + void _startCombinational() { + for (final entry in _inputWidths.entries) { + final name = entry.key; + // Skip clock-like signals (shouldn't exist in combinational mode, + // but guard against it) + if (_clockNames.contains(name)) { + continue; + } + + final signal = module.input(name); + final sub = signal.glitch.listen((event) { + if (!_active) { + return; + } + if (_combStepPending) { + return; + } + _combStepPending = true; + + // Push all current inputs, advance by 1 ps (triggers delta cycles), + // and pull outputs. The _combStepPending flag prevents re-entrant + // calls during the same propagation wave. + _stepCombinational(); + _combStepPending = false; + }); + _inputGlitchSubscriptions.add(sub); + } + } + + /// One co-simulation step: push inputs, advance time, pull outputs. + void _step() { + _pushInputs(); + + // Advance SystemC to process the signal writes (delta cycle). + // We advance T/2 per edge for timing consistency. The clock signal + // is driven manually (not sc_clock), so posedge/negedge detection + // in SystemC relies on the sc_signal transitions we just wrote. + _advance(_handle, clockPeriodNs * 1000 ~/ 2); + + _pullOutputs(); + } + + /// Combinational step: push inputs, advance minimally, pull outputs. + /// + /// Advances by 1 ps — the minimum non-zero time to trigger the full + /// SystemC evaluate→update→notify loop. Per IEEE 1666 §4.3.4.2, + /// sc_start(SC_ZERO_TIME) explicitly does NOT process delta notifications, + /// so external signal writes cannot trigger SC_METHOD evaluation without + /// a non-zero time advancement. + void _stepCombinational() { + _pushInputs(); + _advance(_handle, 1); // 1 ps — minimum to trigger full eval loop + _pullOutputs(); + } + + /// Pushes all current ROHD input values to the SystemC model via FFI. + void _pushInputs() { + for (final entry in _inputWidths.entries) { + final name = entry.key; + final width = entry.value; + final signal = _inputSignals[name]!; + final val = signal.value; + + if (width <= 64) { + final intVal = val.isValid ? val.toInt() : 0; + _setInput(_handle, _inputNamePtrs[name]!.cast(), intVal); + } else { + final bigVal = + val.isValid ? val.toBigInt().toUnsigned(width) : BigInt.zero; + var hex = bigVal.toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + // Write hex into pre-allocated buffer (no malloc/free per step) + final fullHex = '0x$hex'; + final bytes = utf8.encode(fullHex); + final buf = _hexBuf.cast(); + for (var i = 0; i < bytes.length && i < _hexBufSize - 1; i++) { + (buf + i).value = bytes[i]; + } + (buf + bytes.length).value = 0; + _setInputWide(_handle, _inputNamePtrs[name]!.cast(), _hexBuf.cast()); + } + } + } + + /// Pulls all SystemC output values back to ROHD signals. + void _pullOutputs() { + for (final entry in _outputWidths.entries) { + final name = entry.key; + final width = entry.value; + final signal = _outputSignals[name]!; + + if (width <= 64) { + final intVal = _getOutput(_handle, _outputNamePtrs[name]!.cast()); + signal.put(LogicValue.ofInt(intVal, width)); + } else { + final hexCharPtr = + _getOutputWide(_handle, _outputNamePtrs[name]!.cast()); + final hexStr = _fromCString(hexCharPtr); + final bigVal = BigInt.parse( + hexStr.startsWith('0x') ? hexStr.substring(2) : hexStr, + radix: 16); + signal.put(LogicValue.of(bigVal.toUnsigned(width), width: width)); + } + } + } + + /// Stops co-simulation and releases all resources. + Future dispose() async { + _active = false; + await _clkStableSubscription?.cancel(); + _clkStableSubscription = null; + _glitchSubscription?.cancel(); + _glitchSubscription = null; + for (final sub in _inputGlitchSubscriptions) { + sub.cancel(); + } + _inputGlitchSubscriptions.clear(); + // Free cached name pointers + _inputNamePtrs.values.forEach(_free); + _inputNamePtrs.clear(); + _outputNamePtrs.values.forEach(_free); + _outputNamePtrs.clear(); + if (_hexBuf != nullptr) { + _free(_hexBuf); + _hexBuf = nullptr; + } + _inputSignals.clear(); + _outputSignals.clear(); + if (_handle != nullptr) { + _destroy(_handle); + _handle = nullptr; + } + _lib = null; + } + + // ══════════════════════════════════════════════════════════════════════ + // C++ Code Generation + // ══════════════════════════════════════════════════════════════════════ + + /// Generates the C++ wrapper with extern "C" API around the ROHD-generated + /// SystemC module code. + String _generateWrapper(String generatedSystemC, String topModule) { + final sb = StringBuffer() + ..writeln('// Auto-generated SystemC FFI Cosim Wrapper') + ..writeln('// Module: $topModule') + ..writeln() + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('using namespace std;') + ..writeln() + ..writeln('// ═══ ROHD-Generated SystemC Module(s) ═══') + ..writeln() + ..writeln(generatedSystemC) + ..writeln() + ..writeln('// ═══ FFI Cosim Context ═══') + ..writeln() + ..writeln('struct CosimContext {'); + + // All input signal declarations (including clocks as sc_signal) + for (final entry in _inputWidths.entries) { + final type = SystemCSynthesisResult.systemCType(entry.value); + sb.writeln(' sc_signal<$type> ${entry.key};'); + } + // Output signal declarations + for (final entry in _outputWidths.entries) { + final type = SystemCSynthesisResult.systemCType(entry.value); + sb.writeln(' sc_signal<$type> ${entry.key};'); + } + + sb + ..writeln(' $topModule* dut;') + ..writeln('};') + ..writeln() + ..writeln('extern "C" {') + ..writeln() + ..writeln('// Required by SystemC linker — we never call it directly') + ..writeln('int sc_main(int, char*[]) { return 0; }') + ..writeln() + ..writeln('// Track whether the kernel has been initialized') + ..writeln('static CosimContext* _active_ctx = nullptr;') + ..writeln() + // ──── sc_cosim_create ──── + ..writeln('void* sc_cosim_create(const char* name) {') + ..writeln(' // If a context already exists (same process, new test),') + ..writeln(' // just return the existing one after resetting signals.') + ..writeln(' if (_active_ctx != nullptr) {') + ..writeln(' // Reset all input signals to 0'); + + for (final entry in _inputWidths.entries) { + final type = SystemCSynthesisResult.systemCType(entry.value); + sb.writeln(' _active_ctx->${entry.key}.write($type(0));'); + } + + sb + ..writeln(' return static_cast(_active_ctx);') + ..writeln(' }') + ..writeln() + ..writeln(' // Guard: cannot create sc_signal after kernel starts') + ..writeln(' if (sc_get_status() != SC_ELABORATION' + ' && sc_get_status() != SC_BEFORE_END_OF_ELABORATION) {') + ..writeln(' return nullptr; // E113 prevention') + ..writeln(' }') + ..writeln() + ..writeln(' auto* ctx = new CosimContext();') + + // Instantiate DUT + ..writeln(' ctx->dut = new $topModule("dut");'); + + // Bind all inputs (including clocks — driven via sc_signal) + for (final name in _inputWidths.keys) { + sb.writeln(' ctx->dut->$name(ctx->$name);'); + } + // Bind outputs + for (final name in _outputWidths.keys) { + sb.writeln(' ctx->dut->$name(ctx->$name);'); + } + + sb + ..writeln() + ..writeln(' // Store context — do NOT call sc_start here.') + ..writeln(' // Deferring sc_start to the first advance allows') + ..writeln(' // multiple module types to be elaborated before') + ..writeln(' // the kernel starts (avoids E113).') + ..writeln(' _active_ctx = ctx;') + ..writeln(' return static_cast(ctx);') + ..writeln('}') + ..writeln() + // ──── sc_cosim_set_input ──── + ..writeln('void sc_cosim_set_input(void* handle, const char* name,' + ' uint64_t value) {') + ..writeln(' auto* ctx = static_cast(handle);'); + + _generateInputDispatch(sb, narrow: true); + + sb + ..writeln('}') + ..writeln() + // ──── sc_cosim_set_input_wide ──── + ..writeln('void sc_cosim_set_input_wide(void* handle, const char* name,' + ' const char* hex_value) {') + ..writeln(' auto* ctx = static_cast(handle);'); + + _generateInputDispatch(sb, narrow: false); + + sb + ..writeln('}') + ..writeln() + // ──── sc_cosim_get_output ──── + ..writeln( + 'uint64_t sc_cosim_get_output(void* handle, const char* name) {') + ..writeln(' auto* ctx = static_cast(handle);'); + + _generateOutputDispatch(sb, narrow: true); + + sb + ..writeln(' return 0;') + ..writeln('}') + ..writeln() + // ──── sc_cosim_get_output_wide ──── + ..writeln('const char* sc_cosim_get_output_wide(void* handle,' + ' const char* name) {') + ..writeln(' auto* ctx = static_cast(handle);') + ..writeln(' static char _buf[512];'); + + _generateOutputDispatch(sb, narrow: false); + + sb + ..writeln(" _buf[0] = '0'; _buf[1] = 0;") + ..writeln(' return _buf;') + ..writeln('}') + ..writeln() + // ──── sc_cosim_advance ──── + ..writeln('void sc_cosim_advance(void* handle, uint64_t time_ps) {') + ..writeln(' // End elaboration on first advance (allows multiple') + ..writeln(' // module types to be instantiated before starting).') + ..writeln(' if (sc_get_status() == SC_ELABORATION) {') + ..writeln(' sc_start(SC_ZERO_TIME);') + ..writeln(' }') + ..writeln(' if (time_ps == 0) {') + ..writeln(' // Zero-time advance: process delta cycles only.') + ..writeln(' // Use SC_ZERO_TIME explicitly (some implementations') + ..writeln(' // treat sc_time(0,SC_PS) differently).') + ..writeln(' sc_start(SC_ZERO_TIME);') + ..writeln(' } else {') + ..writeln( + ' sc_start(sc_time(static_cast(time_ps), SC_PS));') + ..writeln(' }') + ..writeln('}') + ..writeln() + // ──── sc_cosim_destroy ──── + ..writeln('void sc_cosim_destroy(void* handle) {') + ..writeln(' // Do NOT delete or sc_stop — the SystemC kernel is a') + ..writeln(' // process-wide singleton. The context is reused if') + ..writeln(' // sc_cosim_create is called again (same module).') + ..writeln(' // This avoids E113 "insert primitive channel failed".') + ..writeln('}') + ..writeln() + ..writeln('} // extern "C"'); + + return sb.toString(); + } + + /// Generates the if-else chain for setting input signals. + void _generateInputDispatch(StringBuffer sb, {required bool narrow}) { + var first = true; + for (final entry in _inputWidths.entries) { + final name = entry.key; + final width = entry.value; + + if (narrow && width > 64) { + continue; + } + if (!narrow && width <= 64) { + continue; + } + + final ifStr = first ? ' if' : ' } else if'; + first = false; + + sb.writeln('$ifStr (strcmp(name, "$name") == 0) {'); + if (narrow) { + final type = SystemCSynthesisResult.systemCType(width); + sb.writeln(' ctx->$name.write(static_cast<$type>(value));'); + } else { + sb + ..writeln(' sc_biguint<$width> v(hex_value);') + ..writeln(' ctx->$name.write(v);'); + } + } + if (!first) { + sb.writeln(' }'); + } + } + + /// Generates the if-else chain for reading output signals. + void _generateOutputDispatch(StringBuffer sb, {required bool narrow}) { + var first = true; + for (final entry in _outputWidths.entries) { + final name = entry.key; + final width = entry.value; + + if (narrow && width > 64) { + continue; + } + if (!narrow && width <= 64) { + continue; + } + + final ifStr = first ? ' if' : ' } else if'; + first = false; + + sb.writeln('$ifStr (strcmp(name, "$name") == 0) {'); + if (narrow) { + sb.writeln(' return static_cast(ctx->$name.read());'); + } else { + sb + ..writeln(' sc_biguint<$width> v = ctx->$name.read();') + ..writeln(' string s = v.to_string(SC_HEX_US);') + ..writeln(' strncpy(_buf, s.c_str(), sizeof(_buf)-1);') + ..writeln(' _buf[sizeof(_buf)-1] = 0;') + ..writeln(' return _buf;'); + } + } + if (!first) { + sb.writeln(' }'); + } + } + + // ══════════════════════════════════════════════════════════════════════ + // String/Memory Utilities (no package:ffi dependency) + // ══════════════════════════════════════════════════════════════════════ + + /// Allocates a null-terminated C string from a Dart string. + static Pointer _toCString(String s) { + final bytes = utf8.encode(s); + final ptr = _malloc(bytes.length + 1); + final charPtr = ptr.cast(); + for (var i = 0; i < bytes.length; i++) { + (charPtr + i).value = bytes[i]; + } + (charPtr + bytes.length).value = 0; + return ptr; + } + + /// Reads a null-terminated C string into a Dart string. + static String _fromCString(Pointer ptr) { + final bytes = []; + var i = 0; + while (true) { + final byte = (ptr.cast() + i).value; + if (byte == 0) { + break; + } + bytes.add(byte); + i++; + } + return utf8.decode(bytes); + } + + // ══════════════════════════════════════════════════════════════════════ + // SystemC Path Resolution (mirrors SimCompare) + // ══════════════════════════════════════════════════════════════════════ + + static const _defaultHome = '/opt/systemc/include'; + static const _defaultLib = '/opt/systemc/lib'; + + static String? _resolveHome(String scHome) { + if (scHome.isNotEmpty && Directory(scHome).existsSync()) { + return scHome; + } + if (Directory(_defaultHome).existsSync()) { + return _defaultHome; + } + return null; + } + + static String? _resolveLib(String scLib) { + if (scLib.isNotEmpty && Directory(scLib).existsSync()) { + return scLib; + } + if (Directory(_defaultLib).existsSync()) { + return _defaultLib; + } + return null; + } + + static String _detectCxxStd(String scLib) { + try { + final r = Process.runSync('nm', ['-D', '$scLib/libsystemc.so']); + if (r.exitCode == 0) { + final out = r.stdout as String; + if (out.contains('cxx202002L')) { + return 'c++20'; + } + if (out.contains('cxx201703L')) { + return 'c++17'; + } + } + } on Object { + // ignore + } + return 'c++20'; + } +} + +/// Cached state for a loaded SystemC cosim shared library. +class _LoadedCosimLib { + final DynamicLibrary lib; + final Pointer handle; + final _SetInputDart setInput; + final _SetInputWideDart setInputWide; + final _GetOutputDart getOutput; + final _GetOutputWideDart getOutputWide; + final _AdvanceDart advance; + final _DestroyDart destroy; + + _LoadedCosimLib({ + required this.lib, + required this.handle, + required this.setInput, + required this.setInputWide, + required this.getOutput, + required this.getOutputWide, + required this.advance, + required this.destroy, + }); +} diff --git a/test/assignment_test.dart b/test/assignment_test.dart index 712ebd9ee..e845086a0 100644 --- a/test/assignment_test.dart +++ b/test/assignment_test.dart @@ -110,6 +110,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('multiple bits', () async { @@ -147,6 +148,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('logic net is multi-assignable', () { diff --git a/test/async_reset_test.dart b/test/async_reset_test.dart index 82e1dbcdf..cb0c0a3b2 100644 --- a/test/async_reset_test.dart +++ b/test/async_reset_test.dart @@ -151,6 +151,9 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + // SystemC can't handle manually-driven clocks — buildOnly verifies + // the generated code compiles. + SimCompare.checkSystemCVector(mod, vectors, buildOnly: true); }); test('simcompare with clk sync reset', () async { @@ -172,6 +175,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); } @@ -266,6 +270,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); } @@ -318,6 +323,7 @@ void main() { ]; SimCompare.checkIverilogVector(mod, vectorsSv); + SimCompare.checkSystemCVector(mod, vectorsSv); }); test('inverted', () async { @@ -339,6 +345,7 @@ void main() { ]; SimCompare.checkIverilogVector(mod, vectorsSv); + SimCompare.checkSystemCVector(mod, vectorsSv); }); test('trigger earlier inverted', () async { @@ -362,6 +369,7 @@ void main() { ]; SimCompare.checkIverilogVector(mod, vectorsSv); + SimCompare.checkSystemCVector(mod, vectorsSv); }); test('trigger earlier normal', () async { @@ -385,6 +393,7 @@ void main() { ]; SimCompare.checkIverilogVector(mod, vectorsSv); + SimCompare.checkSystemCVector(mod, vectorsSv, buildOnly: true); }); }); @@ -410,6 +419,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); } }); diff --git a/test/bus_test.dart b/test/bus_test.dart index 08ccb4c9b..b0802d20b 100644 --- a/test/bus_test.dart +++ b/test/bus_test.dart @@ -238,6 +238,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('functional', () { @@ -389,6 +390,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('Assignment of a const', () async { @@ -400,6 +402,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); final sv = mod.generateSynth(); expect(sv.contains("assign const_subset = 16'habcd;"), true); @@ -450,6 +453,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('Bus shrink', () async { @@ -635,6 +639,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('selectFrom and selectIndex', () async { diff --git a/test/collapse_test.dart b/test/collapse_test.dart index 0ef7e00c5..3a4b49427 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -52,6 +52,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('collapse pretty', () async { diff --git a/test/comb_math_test.dart b/test/comb_math_test.dart index b2a7165ed..c8e3b45dc 100644 --- a/test/comb_math_test.dart +++ b/test/comb_math_test.dart @@ -218,6 +218,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); // thank you to @chykon in issue #158 for providing this example! @@ -236,6 +237,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('simpler example', () { @@ -264,6 +266,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -293,6 +296,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); diff --git a/test/comb_mod_test.dart b/test/comb_mod_test.dart index 280d19e2e..e8399ad3e 100644 --- a/test/comb_mod_test.dart +++ b/test/comb_mod_test.dart @@ -58,6 +58,29 @@ class ReuseExampleSsa extends Module { } } +class ReuseExampleSsaNoLoop extends Module { + /// Like [ReuseExampleSsa] but the shared [IncrModule] reads from the input + /// [a] rather than `intermediate`, avoiding the combo loop while still + /// exercising the SSA codegen (multiple `intermediate_N` versions). + ReuseExampleSsaNoLoop(Logic a) { + a = addInput('a', a, width: a.width); + final b = addOutput('b', width: a.width); + + final intermediate = Logic(name: 'intermediate', width: a.width); + + // Shared sub-module reads from `a` (no feedback loop) + final inc = IncrModule(a); + + Combinational.ssa((s) => [ + s(intermediate) < a, + s(intermediate) < inc.result, + s(intermediate) < inc.result, + ]); + + b <= intermediate; + } +} + class DuplicateExample extends Module { DuplicateExample(Logic a) { a = addInput('a', a, width: a.width); @@ -238,6 +261,7 @@ void main() { if (useSsa) { await SimCompare.checkFunctionalVector(dut, vectors); SimCompare.checkIverilogVector(dut, vectors); + SimCompare.checkSystemCVector(dut, vectors); } else { try { await SimCompare.checkFunctionalVector(dut, vectors); @@ -281,6 +305,27 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors, buildOnly: true); + }); + + test('should resolve correctly with shared sub-module ssa (no loop)', + () async { + final mod = ReuseExampleSsaNoLoop(Logic(width: 8)); + await mod.build(); + + // inc reads a (=3), result = a+1 = 4 + // SSA: intermediate_0 = a(3), intermediate_1 = result(4), + // intermediate = result(4) + // b = intermediate = 4 + final vectors = [ + Vector({'a': 3}, {'b': 4}), + Vector({'a': 0}, {'b': 1}), + Vector({'a': 254}, {'b': 255}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -308,6 +353,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); } diff --git a/test/conditionals_test.dart b/test/conditionals_test.dart index d35a8e5bb..c27ebb2ae 100644 --- a/test/conditionals_test.dart +++ b/test/conditionals_test.dart @@ -490,6 +490,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -780,6 +781,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test( @@ -878,6 +880,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); } test('normal logic', () async { diff --git a/test/flop_test.dart b/test/flop_test.dart index 4e3def505..85d41958c 100644 --- a/test/flop_test.dart +++ b/test/flop_test.dart @@ -53,6 +53,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bit with enable', () async { @@ -74,6 +75,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus', () async { @@ -88,6 +90,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus with enable', () async { @@ -111,6 +114,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, no reset value', () async { @@ -124,6 +128,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, const reset value', () async { @@ -141,6 +146,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, logic reset value', () async { @@ -158,6 +164,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus no reset, const reset value', () async { @@ -174,6 +181,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus, enable, reset, const reset value', () async { @@ -194,6 +202,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); }); } diff --git a/test/fsm_test.dart b/test/fsm_test.dart index b5f010a56..54dd1e661 100644 --- a/test/fsm_test.dart +++ b/test/fsm_test.dart @@ -270,6 +270,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); verifyMermaidStateDiagram(_simpleFSMPath); }); @@ -286,6 +287,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); verifyMermaidStateDiagram(_simpleFSMPath); }); @@ -304,6 +306,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); if (!kIsWeb) { const fsmPath = '$_tmpDir/default_next_state_fsm.md'; @@ -344,6 +347,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); verifyMermaidStateDiagram(_trafficFSMPath); }); diff --git a/test/gate_test.dart b/test/gate_test.dart index 905c912d9..b92839774 100644 --- a/test/gate_test.dart +++ b/test/gate_test.dart @@ -362,6 +362,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('unary and', () async { @@ -470,6 +471,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('rshift logic', () async { @@ -483,6 +485,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('arshift logic', () async { @@ -498,6 +501,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('lshift int', () async { @@ -509,6 +513,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('rshift int', () async { @@ -520,6 +525,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('arshift int', () async { @@ -531,6 +537,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('shift by const zero', () async { @@ -552,6 +559,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('large logic shifted by small bus', () async { @@ -573,6 +581,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('large logic shifted by large bus', () async { @@ -594,6 +603,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('small logic shifted by large bus', () async { @@ -615,6 +625,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('large logic shifted by huge value on large bus', () async { @@ -636,6 +647,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('small logic shifted by huge value on large bus', () async { @@ -657,6 +669,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('very small logic shifted by huge value on large bus', () async { @@ -678,6 +691,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); }); diff --git a/test/instance_signal_name_collision_test.dart b/test/instance_signal_name_collision_test.dart new file mode 100644 index 000000000..65747204a --- /dev/null +++ b/test/instance_signal_name_collision_test.dart @@ -0,0 +1,87 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// instance_signal_name_collision_test.dart +// Tests that submodule instance names and signal names share a single +// namespace, so a collision between them results in uniquification. +// +// 2026 April 18 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +// ── Minimal repro modules ──────────────────────────────────────────────────── + +/// Leaf module whose default instance name is "inner". +class _Inner extends Module { + _Inner(Logic a) : super(name: 'inner') { + a = addInput('a', a, width: a.width); + addOutput('y', width: a.width) <= a; + } +} + +/// Parent module that: +/// • instantiates [_Inner] (default instance name: "inner") +/// • names an internal wire "inner" as well +/// +/// Because both identifiers live in a single shared namespace, one of them +/// will be suffixed to avoid collision. +class _CollidingParent extends Module { + _CollidingParent(Logic a) : super(name: 'colliding_parent') { + a = addInput('a', a, width: a.width); + + // Internal wire explicitly named "inner". + final inner = Logic(name: 'inner', width: a.width, naming: Naming.reserved) + ..gets(a); + + // Submodule whose uniqueInstanceName will also be "inner". + final sub = _Inner(inner); + + addOutput('y', width: a.width) <= sub.output('y'); + } +} + +// ── Test ───────────────────────────────────────────────────────────────────── + +void main() { + group('instance / signal name collision (shared namespace)', () { + late _CollidingParent mod; + late SynthModuleDefinition def; + + setUpAll(() async { + mod = _CollidingParent(Logic(width: 8)); + await mod.build(); + def = SynthModuleDefinition(mod); + }); + + test('internal signal named "inner" retains its exact name', () { + // The reserved signal should keep its exact name. + final sl = def.internalSignals.cast().firstWhere( + (s) => s!.logics.any((l) => l.name == 'inner'), + orElse: () => null, + ); + expect(sl, isNotNull, reason: 'Expected to find SynthLogic for "inner"'); + expect(sl!.name, 'inner', + reason: 'Reserved signal "inner" must keep its exact name'); + }); + + test( + 'submodule instance is uniquified because signal ' + '"inner" already claimed the name', () { + final inst = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .cast() + .firstWhere( + (s) => s!.module.name == 'inner', + orElse: () => null, + ); + expect(inst, isNotNull, reason: 'Expected submodule instance for inner'); + // The instance should be suffixed since the signal took "inner" first. + expect(inst!.name, isNot('inner'), + reason: 'Instance should be uniquified when signal already ' + 'claims "inner"'); + }); + }); +} diff --git a/test/interface_test.dart b/test/interface_test.dart index eaf4433d2..68e3a60e1 100644 --- a/test/interface_test.dart +++ b/test/interface_test.dart @@ -142,6 +142,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('should return exception when port name is not sanitary.', () async { diff --git a/test/logic_array_sim_test.dart b/test/logic_array_sim_test.dart new file mode 100644 index 000000000..0669dac5d --- /dev/null +++ b/test/logic_array_sim_test.dart @@ -0,0 +1,250 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_array_sim_test.dart +// Simulation tests for LogicArray with Iverilog and SystemC backends. +// Exercises sequential logic, element-wise operations, and submodule +// hierarchy with array ports — scenarios beyond the combinational +// passthrough tests in logic_array_test.dart. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +/// Flops each element of a LogicArray independently. +/// Tests sequential (clocked) array element access in generated code. +class ArrayFlopModule extends Module { + LogicArray get dataOut => output('dataOut') as LogicArray; + + ArrayFlopModule(LogicArray dataIn, {required Logic reset}) + : super(name: 'ArrayFlopModule') { + final clk = SimpleClockGenerator(10).clk; + reset = addInput('reset', reset); + dataIn = addInputArray('dataIn', dataIn, + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth); + + final out = addOutputArray('dataOut', + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth); + + for (var i = 0; i < dataIn.dimensions[0]; i++) { + out.elements[i] <= flop(clk, dataIn.elements[i], reset: reset); + } + } +} + +/// Applies bitwise NOT to each element, then passes through a submodule. +/// Tests combinational element-wise ops + array hierarchy. +class ArrayInvertAndPassModule extends Module { + LogicArray get dataOut => output('dataOut') as LogicArray; + + ArrayInvertAndPassModule(LogicArray dataIn) + : super(name: 'ArrayInvertAndPassModule') { + dataIn = addInputArray('dataIn', dataIn, + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth); + + final inverted = + LogicArray(dataIn.dimensions, dataIn.elementWidth, name: 'inverted'); + for (var i = 0; i < dataIn.dimensions[0]; i++) { + inverted.elements[i] <= ~dataIn.elements[i]; + } + + // Pass through a sub-module to exercise array port wiring + final sub = _ArrayPassSub(inverted); + + addOutputArray('dataOut', + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth) <= + sub.out; + } +} + +class _ArrayPassSub extends Module { + LogicArray get out => output('out') as LogicArray; + + _ArrayPassSub(LogicArray inp) : super(name: 'ArrayPassSub') { + inp = addInputArray('inp', inp, + dimensions: inp.dimensions, elementWidth: inp.elementWidth); + addOutputArray('out', + dimensions: inp.dimensions, elementWidth: inp.elementWidth) <= + inp; + } +} + +/// Muxes between two LogicArray inputs based on a select signal. +/// Tests conditional array assignment in generated code. +class ArrayMuxModule extends Module { + LogicArray get dataOut => output('dataOut') as LogicArray; + + ArrayMuxModule(LogicArray a, LogicArray b, Logic sel) + : super(name: 'ArrayMuxModule') { + a = addInputArray('a', a, + dimensions: a.dimensions, elementWidth: a.elementWidth); + b = addInputArray('b', b, + dimensions: b.dimensions, elementWidth: b.elementWidth); + sel = addInput('sel', sel); + + final out = addOutputArray('dataOut', + dimensions: a.dimensions, elementWidth: a.elementWidth); + + Combinational([ + If(sel, then: [out < a], orElse: [out < b]), + ]); + } +} + +/// Concatenates two array elements into a wider output and also +/// provides a reduced (OR-reduce) output across array elements. +/// Tests mixed array-element and scalar operations. +class ArrayReduceModule extends Module { + Logic get concat01 => output('concat01'); + Logic get anyNonZero => output('anyNonZero'); + + ArrayReduceModule(LogicArray dataIn) : super(name: 'ArrayReduceModule') { + dataIn = addInputArray('dataIn', dataIn, + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth); + + final c = addOutput('concat01', width: dataIn.elementWidth * 2); + final a = addOutput('anyNonZero'); + + // Concatenate elements [1] and [0] + c <= [dataIn.elements[1], dataIn.elements[0]].swizzle(); + + // OR-reduce: is any element non-zero? + a <= dataIn.elements.map((e) => e.or()).toList().swizzle().or(); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('LogicArray simulation', () { + group('sequential flop per element', () { + test('1D array of 4x8-bit', () async { + final reset = Logic(name: 'reset'); + final dataIn = LogicArray([4], 8); + final mod = ArrayFlopModule(dataIn, reset: reset); + await mod.build(); + + // Each element is flopped: output appears one cycle after input. + // Vector check is BEFORE posedge → sees PREVIOUS cycle's result. + final vectors = [ + Vector({'reset': 1, 'dataIn': 0}, {}), + Vector({'reset': 1, 'dataIn': 0}, {}), + Vector({'reset': 1, 'dataIn': 0}, {'dataOut': 0}), + // Deassert reset; still see 0 from reset phase + Vector({'reset': 0, 'dataIn': 0x44332211}, {'dataOut': 0x00000000}), + // Now see 0x44332211 from previous cycle + Vector({'reset': 0, 'dataIn': 0xDDCCBBAA}, {'dataOut': 0x44332211}), + Vector({'reset': 0, 'dataIn': 0x00000000}, {'dataOut': 0xDDCCBBAA}), + Vector({'reset': 0, 'dataIn': 0x00000000}, {'dataOut': 0x00000000}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + }); + + group('element-wise invert with submodule', () { + test('1D array of 3x8-bit', () async { + final dataIn = LogicArray([3], 8); + final mod = ArrayInvertAndPassModule(dataIn); + await mod.build(); + + // 0x00 → 0xFF, 0xAA → 0x55, 0x0F → 0xF0 + // Input: 0x0FAA00 (elem[0]=0x00, elem[1]=0xAA, elem[2]=0x0F) + // Output: 0xF055FF (elem[0]=0xFF, elem[1]=0x55, elem[2]=0xF0) + final vectors = [ + Vector({'dataIn': 0x0FAA00}, {'dataOut': 0xF055FF}), + Vector({'dataIn': 0xFFFFFF}, {'dataOut': 0x000000}), + Vector({'dataIn': 0x000000}, {'dataOut': 0xFFFFFF}), + Vector({ + 'dataIn': 0x123456 + }, { + 'dataOut': LogicValue.ofInt(0x123456, 24) ^ + LogicValue.filled(24, LogicValue.one) + }), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('1D array of 2x4-bit', () async { + final dataIn = LogicArray([2], 4); + final mod = ArrayInvertAndPassModule(dataIn); + await mod.build(); + + final vectors = [ + Vector({'dataIn': 0x00}, {'dataOut': 0xFF}), + Vector({'dataIn': 0xAB}, {'dataOut': 0x54}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + }); + + group('array mux', () { + test('1D array of 3x8-bit', () async { + final a = LogicArray([3], 8); + final b = LogicArray([3], 8); + final sel = Logic(name: 'sel'); + final mod = ArrayMuxModule(a, b, sel); + await mod.build(); + + final vectors = [ + // sel=1 → output = a + Vector( + {'sel': 1, 'a': 0x112233, 'b': 0xAABBCC}, {'dataOut': 0x112233}), + // sel=0 → output = b + Vector( + {'sel': 0, 'a': 0x112233, 'b': 0xAABBCC}, {'dataOut': 0xAABBCC}), + // Toggle + Vector( + {'sel': 1, 'a': 0xFFFFFF, 'b': 0x000000}, {'dataOut': 0xFFFFFF}), + Vector( + {'sel': 0, 'a': 0xFFFFFF, 'b': 0x000000}, {'dataOut': 0x000000}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + }); + + group('array reduce and concat', () { + test('1D array of 4x8-bit', () async { + final dataIn = LogicArray([4], 8); + final mod = ArrayReduceModule(dataIn); + await mod.build(); + + // Elements: [0]=low 8 bits, [1]=next 8, etc. + // concat01 = {elem[1], elem[0]} (16 bits) + // anyNonZero = OR-reduce of all elements + final vectors = [ + // All zero + Vector({'dataIn': 0x00000000}, {'concat01': 0x0000, 'anyNonZero': 0}), + // elem[0]=0x01 + Vector({'dataIn': 0x00000001}, {'concat01': 0x0001, 'anyNonZero': 1}), + // elem[0]=0xAB, elem[1]=0xCD + Vector({'dataIn': 0x0000CDAB}, {'concat01': 0xCDAB, 'anyNonZero': 1}), + // elem[3]=0xFF only (upper byte) + Vector({'dataIn': 0xFF000000}, {'concat01': 0x0000, 'anyNonZero': 1}), + // All 0xFF + Vector({'dataIn': 0xFFFFFFFF}, {'concat01': 0xFFFF, 'anyNonZero': 1}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + }); + }); +} diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index 87c6be85a..735eafe12 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -727,6 +727,8 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + // buildOnly: array element sub-module binding not yet supported + SimCompare.checkSystemCVector(mod, vectors, buildOnly: true); }); group('logicarray passthrough', () { @@ -760,6 +762,7 @@ void main() { SimCompare.checkIverilogVector(mod, vectors, buildOnly: noSvSim, dontDeleteTmpFiles: dontDeleteTmpFiles); } + SimCompare.checkSystemCVector(mod, vectors, buildOnly: noSvSim); } group('simple', () { @@ -1108,6 +1111,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('assign subset of logic array without mentioning start', () async { @@ -1161,6 +1165,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); diff --git a/test/logic_name_test.dart b/test/logic_name_test.dart index 8ce9d5f40..2f571baba 100644 --- a/test/logic_name_test.dart +++ b/test/logic_name_test.dart @@ -289,6 +289,7 @@ void main() { // confirm build works SimCompare.checkIverilogVector(mod, []); + SimCompare.checkSystemCVector(mod, []); }); test('array port and simple port with _num name conflict but pruned away', @@ -305,6 +306,7 @@ void main() { // confirm build works SimCompare.checkIverilogVector(mod, []); + SimCompare.checkSystemCVector(mod, []); }); test('badly named intermediate signal sanitization', () async { diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index fdc522e96..59f35bd62 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -256,6 +256,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('simple passthrough struct', () async { @@ -271,6 +272,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('fancy struct inverter', () async { @@ -293,6 +295,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); diff --git a/test/math_test.dart b/test/math_test.dart index d9ada00a0..b4f0ecf5f 100644 --- a/test/math_test.dart +++ b/test/math_test.dart @@ -112,6 +112,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -136,6 +137,7 @@ void main() { await gtm.build(); await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); } test('power', () async { diff --git a/test/module_services_test.dart b/test/module_services_test.dart new file mode 100644 index 000000000..527601154 --- /dev/null +++ b/test/module_services_test.dart @@ -0,0 +1,132 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services_test.dart +// Unit tests for ModuleServices and SvService. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +class SimpleModule extends Module { + SimpleModule(Logic a) : super(name: 'simple') { + a = addInput('a', a); + addOutput('b') <= ~a; + } +} + +void main() { + tearDown(ModuleServices.instance.reset); + + group('ModuleServices', () { + test('rootModule is set after build', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(ModuleServices.instance.rootModule, equals(mod)); + }); + + test('hierarchyJSON returns valid JSON', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final json = ModuleServices.instance.hierarchyJSON; + expect(() => jsonDecode(json), returnsNormally); + }); + + test('inspectorJSON matches hierarchyJSON', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(ModuleServices.instance.inspectorJSON, + equals(ModuleServices.instance.hierarchyJSON)); + }); + + test('svJSON returns unavailable when no service registered', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final result = + jsonDecode(ModuleServices.instance.svJSON) as Map; + expect(result['status'], equals('unavailable')); + }); + + test('reset clears all services', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(ModuleServices.instance.rootModule, isNotNull); + ModuleServices.instance.reset(); + expect(ModuleServices.instance.rootModule, isNull); + expect(ModuleServices.instance.svService, isNull); + }); + }); + + group('SvService', () { + test('registers with ModuleServices on creation', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(ModuleServices.instance.svService, equals(sv)); + }); + + test('allContents is non-empty', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.allContents, isNotEmpty); + }); + + test('contentsByName has entries', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.contentsByName, isNotEmpty); + }); + + test('contentsByDefinitionName has entries', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.contentsByDefinitionName, isNotEmpty); + expect(sv.contentsByDefinitionName.containsKey('SimpleModule'), isTrue); + }); + + test('svJSON returns valid JSON after registration', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + SvService(mod); + final result = + jsonDecode(ModuleServices.instance.svJSON) as Map; + expect(result['modules'], isList); + }); + + test('writeFiles creates SV files', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + sv.writeFiles(dir.path); + final files = dir.listSync().whereType().toList(); + expect(files, isNotEmpty); + expect(files.any((f) => f.path.endsWith('.sv')), isTrue); + } finally { + dir.deleteSync(recursive: true); + } + }, testOn: 'vm'); + + test('register false does not register', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + ModuleServices.instance.reset(); + SvService(mod, register: false); + expect(ModuleServices.instance.svService, isNull); + }); + + test('throws if module not built', () { + final mod = SimpleModule(Logic()); + expect(() => SvService(mod), throwsException); + }); + }); +} diff --git a/test/name_test.dart b/test/name_test.dart index 2742c0ec8..bde8a9c9f 100644 --- a/test/name_test.dart +++ b/test/name_test.dart @@ -1,7 +1,7 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// definition_name_test.dart +// name_test.dart // Tests for definition names (including reserving them) of Modules. // // 2022 March 7 @@ -136,6 +136,11 @@ void main() { final nameTypes = [nameType1, nameType2]; // skip ones that actually *should* cause a failure + // + // Note: SystemVerilog allows using the same identifier for a signal + // and an instance because they are different namespaces. However, + // Icarus Verilog rejects that pattern, so ROHD treats those as + // conflicts for simulator compatibility. final shouldConflict = [ { NameType.internalModuleDefinition, diff --git a/test/naming_cases_test.dart b/test/naming_cases_test.dart new file mode 100644 index 000000000..fbc1d9536 --- /dev/null +++ b/test/naming_cases_test.dart @@ -0,0 +1,583 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// naming_cases_test.dart +// Systematic test of all signal-naming cases in the synthesis pipeline. +// +// 2026 April 10 +// Author: Desmond Kirkpatrick + +// ════════════════════════════════════════════════════════ +// NAMING CROSS-PRODUCT TABLE +// ════════════════════════════════════════════════════════ +// +// Axis 1 — Naming enum (set at Logic construction time): +// reserved Exact name required; collision → exception. +// renameable Keeps name, uniquified on collision; never merged. +// mergeable May merge with equivalent signals; any merged name chosen. +// unnamed No user name; system generates one. +// +// Axis 2 — Context role (per SynthModuleDefinition): +// this-port Port of module being synthesized +// (namingOverride → reserved). +// sub-port Port of a child submodule +// (namingOverride → mergeable). +// internal Non-port signal inside the module (no override). +// const Const object (separate path via constValue). +// +// Axis 3 — Name preference: +// preferred baseName does NOT start with '_' +// unpreferred baseName starts with '_' +// +// Axis 4 — Constant context (only for Const): +// allowed Literal value string used as name. +// disallowed Feeding expressionlessInput; +// must use a wire name. +// +// ────────────────────────────────────────────────────── +// Row Naming Context Pref? Test Valid? +// Effective class → Outcome +// ────────────────────────────────────────────────────── +// 1 reserved this-port pref T1 ✓ +// port (in _portLogics) → exact sanitized name +// 2 reserved this-port unpref T2 ✓ unusual +// port → exact _-prefixed port name +// 3 reserved sub-port pref T3 ✓ +// preferred mergeable → merged, uniquified +// 4 reserved sub-port unpref T4 ✓ +// unpreferred mergeable → low-priority merge +// 5 reserved internal pref T5 ✓ +// reserved internal → exact name, throw on clash +// 6 reserved internal unpref T6 ✓ unusual +// reserved internal → exact _-prefixed name +// 7 renameable this-port pref — can't happen* +// port → exact port name +// 8 renameable sub-port pref — can't happen* +// preferred mergeable → merged +// 9 renameable internal pref T9 ✓ +// renameable → base name, uniquified +// 10 renameable internal unpref T10 ✓ unusual +// renameable → uniquified _-prefixed +// 11 mergeable this-port pref T11 ✓ +// port → exact port name (Logic.port()) +// 12 mergeable this-port unpref T12 ✓ unusual +// port → exact _-prefixed port name +// 13 mergeable sub-port pref T3 ✓ (=row 3) +// preferred mergeable → best-available merge +// 14 mergeable sub-port unpref T4 ✓ (=row 4) +// unpreferred mergeable → low-priority merge +// 15 mergeable internal pref T15 ✓ +// preferred mergeable → prefer available name +// 16 mergeable internal unpref T16 ✓ +// unpreferred mergeable → low-priority merge +// 17 unnamed this-port — — ✗ impossible** +// port → exact port name +// 18 unnamed sub-port — — ✗ impossible** +// mergeable → merged +// 19 unnamed internal (unpf) T19 ✓ +// unnamed → generated _s name +// 20 —(Const) — — T20 ✓ +// const allowed → literal value e.g. 8'h42 +// 21 —(Const) — — T21 ✓ +// const disallowed → wire name (not literal) +// ────────────────────────────────────────────────────── +// +// * Rows 7-8: addInput/addOutput always create +// Logic with Naming.reserved, so a port can +// never have intrinsic Naming.renameable. +// The namingOverride makes it moot anyway. +// +// ** Rows 17-18: addInput/addOutput require a +// non-null, non-empty name. chooseName() only +// yields Naming.unnamed for null/empty names, +// so a port can never be unnamed. +// +// ✗ unnamed + reserved: Logic(naming: reserved) +// with null/empty name throws +// NullReservedNameException / +// EmptyReservedNameException at construction +// time. Never reaches synthesizer. +// +// Additional cross-cutting concerns: +// COL Collision between mergeables +// → uniquified suffix (_0) +// MG Merge: directly-connected signals +// share SynthLogic +// INST Submodule instance names: unique, +// don't collide with ports +// ST Structure element: structureName +// = "parent.field" → sanitized ("_") +// AR Array element: isArrayMember +// → uses logic.name (index-based) +// +// ════════════════════════════════════════════════════════ + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +// ── Leaf sub-modules ────────────────────────────── + +/// A leaf module whose `in0` is an "expressionless input" — +/// meaning any constant driving it must get a real wire name, not a literal. +class _ExpressionlessSub extends Module with SystemVerilog { + @override + List get expressionlessInputs => const ['in0']; + + _ExpressionlessSub(Logic a, Logic b) : super(name: 'exprsub') { + a = addInput('in0', a, width: a.width); + b = addInput('in1', b, width: b.width); + addOutput('out', width: a.width) <= a & b; + } +} + +/// A simple sub-module with preferred-name ports. +class _SimpleSub extends Module { + _SimpleSub(Logic x) : super(name: 'simplesub') { + x = addInput('x', x, width: x.width); + addOutput('y', width: x.width) <= ~x; + } +} + +/// A sub-module with an unpreferred-name port. +class _UnprefSub extends Module { + _UnprefSub(Logic a) : super(name: 'unprefsub') { + a = addInput('_uport', a, width: a.width); + addOutput('uout', width: a.width) <= ~a; + } +} + +// ── Main test module ────────────────────────────── +// One module that exercises every valid naming case in a minimal design. +// Each signal is tagged with the row number from the table above. + +class _AllNamingCases extends Module { + // Exposed for test inspection. + // Row 1 / Row 2: ports (accessed via mod.input / mod.output). + // Row 5: + late final Logic reservedInternal; + // Row 6: + late final Logic reservedInternalUnpref; + // Row 9: + late final Logic renameableInternal; + // Row 10: + late final Logic renameableInternalUnpref; + // Row 15: + late final Logic mergeablePref; + // Row 15 collision partner: + late final Logic mergeablePrefCollide; + // Row 16: + late final Logic mergeableUnpref; + // Row 19: + late final Logic unnamed; + // Row 20: + late final Logic constAllowed; + // Row 21: + late final Logic constDisallowed; + // MG: + late final Logic mergeTarget; + + // Structure/array elements (ST, AR): + late final LogicStructure structPort; + late final LogicArray arrayPort; + + _AllNamingCases() : super(name: 'allcases') { + // ── Row 1: reserved + this-port + preferred ────────────────── + final inp = addInput('inp', Logic(width: 8), width: 8); + final out = addOutput('out', width: 8); + + // ── Row 2: reserved + this-port + unpreferred ──────────────── + final uInp = addInput('_uinp', Logic(width: 8), width: 8); + + // ── Row 11: mergeable + this-port + preferred ──────────────── + // (This is the Logic.port() → connectIO path. addInput forces + // Naming.reserved regardless of the source's naming, so intrinsic + // mergeable is overridden to reserved. We test the port keeps its + // exact name.) + final mPortInp = addInput('mport', Logic(width: 8), width: 8); + + // ── Row 12: mergeable + this-port + unpreferred ────────────── + final mPortUnpref = addInput('_muprt', Logic(width: 8), width: 8); + + // ── Row 5: reserved + internal + preferred ─────────────────── + reservedInternal = Logic(name: 'resv', width: 8, naming: Naming.reserved) + ..gets(inp ^ Const(0x01, width: 8)); + + // ── Row 6: reserved + internal + unpreferred ───────────────── + reservedInternalUnpref = + Logic(name: '_resvu', width: 8, naming: Naming.reserved) + ..gets(inp ^ Const(0x02, width: 8)); + + // ── Row 9: renameable + internal + preferred ───────────────── + renameableInternal = Logic(name: 'ren', width: 8, naming: Naming.renameable) + ..gets(inp ^ Const(0x03, width: 8)); + + // ── Row 10: renameable + internal + unpreferred ────────────── + renameableInternalUnpref = + Logic(name: '_renu', width: 8, naming: Naming.renameable) + ..gets(inp ^ Const(0x04, width: 8)); + + // ── Row 15: mergeable + internal + preferred ───────────────── + mergeablePref = Logic(name: 'mname', width: 8, naming: Naming.mergeable) + ..gets(inp ^ Const(0x05, width: 8)); + + // ── COL: collision partner — same base name 'mname' ────────── + mergeablePrefCollide = + Logic(name: 'mname', width: 8, naming: Naming.mergeable) + ..gets(inp ^ Const(0x06, width: 8)); + + // ── Row 16: mergeable + internal + unpreferred ─────────────── + mergeableUnpref = Logic(name: '_hidden', width: 8, naming: Naming.mergeable) + ..gets(inp ^ Const(0x07, width: 8)); + + // ── Row 19: unnamed + internal ─────────────────────────────── + unnamed = Logic(width: 8)..gets(inp ^ Const(0x08, width: 8)); + + // ── Rows 3/13: sub-port preferred (via _SimpleSub.x / .y) ─── + // ── Row 4/14: sub-port unpreferred (via _UnprefSub._uport) ── + final sub = _SimpleSub(renameableInternal); + final subOut = sub.output('y'); + // Use a distinct expression so the submodule port doesn't merge with + // renameableInternal (which is renameable and would win). + final unpSub = _UnprefSub(inp ^ Const(0x0a, width: 8)); + + // ── MG: merge behavior — mergeTarget merges with subOut ────── + mergeTarget = Logic(name: 'mmerge', width: 8, naming: Naming.mergeable) + ..gets(subOut); + + // ── Row 20: constant with name allowed ─────────────────────── + constAllowed = + Const(0x42, width: 8).named('const_ok', naming: Naming.mergeable); + + // ── Row 21: constant with name disallowed (expressionlessInput) + constDisallowed = + Const(0x09, width: 8).named('const_wire', naming: Naming.mergeable); + // ignore: unused_local_variable + final exprSub = _ExpressionlessSub(constDisallowed, inp); + + // ── ST: structure element (structureName = "parent.field") ──── + structPort = _SimpleStruct(); + addInput('stIn', structPort, width: structPort.width); + + // ── AR: array element (isArrayMember, uses logic.name) ─────── + arrayPort = LogicArray([3], 8, name: 'arIn'); + addInputArray('arIn', arrayPort, dimensions: [3], elementWidth: 8); + + // Drive output to use all signals (prevents pruning). + out <= + mergeTarget | + mergeablePrefCollide | + mergeableUnpref | + unnamed | + constAllowed | + uInp | + mPortInp | + mPortUnpref | + reservedInternalUnpref | + renameableInternalUnpref | + unpSub.output('uout'); + } +} + +/// A minimal LogicStructure for testing structureName sanitization. +class _SimpleStruct extends LogicStructure { + final Logic field1; + final Logic field2; + + factory _SimpleStruct({String name = 'st'}) => _SimpleStruct._( + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + name: name, + ); + + _SimpleStruct._(this.field1, this.field2, {required super.name}) + : super([field1, field2]); + + @override + LogicStructure clone({String? name}) => + _SimpleStruct(name: name ?? this.name); +} + +// ── Helpers ─────────────────────────────────────── + +/// Collects a map from Logic → picked name for all SynthLogics. +Map _collectNames(SynthModuleDefinition def) { + final names = {}; + for (final sl in [ + ...def.inputs, + ...def.outputs, + ...def.inOuts, + ...def.internalSignals, + ]) { + try { + final n = sl.name; + for (final logic in sl.logics) { + names[logic] = n; + } + // ignore: avoid_catches_without_on_clauses + } catch (_) { + // name not picked (pruned/replaced) + } + } + return names; +} + +/// Finds a SynthLogic that contains [logic]. +SynthLogic? _findSynthLogic(SynthModuleDefinition def, Logic logic) { + for (final sl in [ + ...def.inputs, + ...def.outputs, + ...def.inOuts, + ...def.internalSignals, + ]) { + if (sl.logics.contains(logic)) { + return sl; + } + } + return null; +} + +// ── Tests ──────────────────────────────────────── + +void main() { + late _AllNamingCases mod; + late SynthModuleDefinition def; + late Map names; + + setUp(() async { + mod = _AllNamingCases(); + await mod.build(); + def = SynthModuleDefinition(mod); + names = _collectNames(def); + }); + + group('naming cases', () { + // ── Row 1: reserved + this-port + preferred ──────────────── + + test('T1: reserved preferred port keeps exact name', () { + expect(names[mod.input('inp')], 'inp'); + expect(names[mod.output('out')], 'out'); + }); + + // ── Row 2: reserved + this-port + unpreferred ────────────── + + test('T2: reserved unpreferred port keeps exact _-prefixed name', () { + expect(names[mod.input('_uinp')], '_uinp'); + }); + + // ── Rows 3/13: sub-port + preferred (reserved or mergeable) ─ + + test('T3: submodule preferred port gets a name in parent', () { + final subX = mod.subModules.whereType<_SimpleSub>().first.input('x'); + final n = names[subX]; + expect(n, isNotNull, reason: 'Submodule port must be named'); + // Treated as preferred mergeable — name should not start with _. + expect(n, isNot(startsWith('_')), + reason: 'Preferred submodule port name should not be unpreferred'); + }); + + // ── Row 4/14: sub-port + unpreferred ──────────────────────── + + test('T4: submodule unpreferred port gets an unpreferred name', () { + final subUPort = + mod.subModules.whereType<_UnprefSub>().first.input('_uport'); + final n = names[subUPort]; + expect(n, isNotNull, reason: 'Submodule port must be named'); + expect(n, startsWith('_'), + reason: 'Unpreferred submodule port should keep _-prefix'); + }); + + // ── Row 5: reserved + internal + preferred ────────────────── + + test('T5: reserved preferred internal keeps exact name', () { + expect(names[mod.reservedInternal], 'resv'); + }); + + // ── Row 6: reserved + internal + unpreferred ──────────────── + + test('T6: reserved unpreferred internal keeps exact _-prefixed name', () { + expect(names[mod.reservedInternalUnpref], '_resvu'); + }); + + // ── Row 9: renameable + internal + preferred ──────────────── + + test('T9: renameable preferred internal gets its name', () { + final n = names[mod.renameableInternal]; + expect(n, isNotNull); + expect(n, contains('ren')); + }); + + // ── Row 10: renameable + internal + unpreferred ───────────── + + test('T10: renameable unpreferred internal keeps _-prefix', () { + final n = names[mod.renameableInternalUnpref]; + expect(n, isNotNull); + expect(n, startsWith('_'), + reason: 'Unpreferred renameable should keep _-prefix'); + expect(n, contains('renu')); + }); + + // ── Row 11: mergeable + this-port + preferred ─────────────── + + test('T11: mergeable-origin port (Logic.port) keeps exact port name', () { + // addInput overrides naming to reserved; the port name is exact. + expect(names[mod.input('mport')], 'mport'); + }); + + // ── Row 12: mergeable + this-port + unpreferred ───────────── + + test('T12: mergeable-origin unpreferred port keeps exact name', () { + expect(names[mod.input('_muprt')], '_muprt'); + }); + + // ── Row 15: mergeable + internal + preferred ──────────────── + + test('T15: mergeable preferred internal gets its name', () { + final n = names[mod.mergeablePref]; + expect(n, isNotNull); + expect(n, contains('mname')); + }); + + // ── COL: name collision → uniquified suffix ───────────────── + + test('COL: collision between two mergeables gets uniquified', () { + final n1 = names[mod.mergeablePref]; + final n2 = names[mod.mergeablePrefCollide]; + expect(n1, isNot(n2), reason: 'Colliding names must be uniquified'); + expect({n1, n2}, containsAll(['mname', 'mname_0'])); + }); + + // ── Row 16: mergeable + internal + unpreferred ────────────── + + test('T16: mergeable unpreferred internal keeps _-prefix', () { + final n = names[mod.mergeableUnpref]; + expect(n, isNotNull); + expect(n, startsWith('_'), + reason: 'Unpreferred mergeable should keep _-prefix'); + }); + + // ── Row 19: unnamed + internal ────────────────────────────── + + test('T19: unnamed signal gets a generated name', () { + final n = names[mod.unnamed]; + expect(n, isNotNull, reason: 'Unnamed signal must still get a name'); + // chooseName() gives unnamed signals a name starting with '_s'. + expect(n, startsWith('_'), + reason: 'Unnamed signals get unpreferred generated names'); + }); + + // ── Row 20: constant with name allowed ────────────────────── + + test('T20: constant with name allowed uses literal value', () { + final sl = _findSynthLogic(def, mod.constAllowed); + expect(sl, isNotNull); + if (sl != null && !sl.constNameDisallowed) { + expect(sl.name, contains("8'h42"), + reason: 'Allowed constant should use value literal'); + } + }); + + // ── Row 21: constant with name disallowed ─────────────────── + + test('T21: constant with name disallowed uses wire name', () { + final sl = _findSynthLogic(def, mod.constDisallowed); + expect(sl, isNotNull); + if (sl != null) { + if (sl.constNameDisallowed) { + expect(sl.name, isNot(contains("8'h09")), + reason: 'Disallowed constant should not use value literal'); + expect(sl.name, isNotEmpty); + } + } + }); + + // ── MG: merge behavior ────────────────────────────────────── + + test('MG: merged signals share the same SynthLogic', () { + final sl = _findSynthLogic(def, mod.mergeTarget); + expect(sl, isNotNull); + if (sl != null && sl.logics.length > 1) { + expect(sl.name, isNotEmpty); + } + }); + + // ── INST: submodule instance naming ───────────────────────── + + test('INST: submodule instances get collision-free names', () { + final instNames = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toList(); + expect(instNames.toSet().length, instNames.length, + reason: 'Instance names must be unique'); + final portNames = {...mod.inputs.keys, ...mod.outputs.keys}; + for (final name in instNames) { + expect(portNames, isNot(contains(name)), + reason: 'Instance "$name" should not collide with a port'); + } + }); + + // ── ST: structure element naming ──────────────────────────── + + test('ST: structure element structureName is sanitized', () { + // structureName for field1 is "st.a" → sanitized to "st_a". + final stIn = mod.input('stIn'); + final n = names[stIn]; + expect(n, isNotNull); + // The port itself should keep its reserved name 'stIn'. + expect(n, 'stIn'); + }); + + // ── AR: array element naming ──────────────────────────────── + + test('AR: array port keeps its name', () { + // Array ports are registered via addInputArray with Naming.reserved. + final arIn = mod.input('arIn'); + final n = names[arIn]; + expect(n, isNotNull); + expect(n, 'arIn'); + }); + + // ── Impossible cases ──────────────────────────────────────── + + test('unnamed + reserved throws at construction time', () { + expect( + () => Logic(naming: Naming.reserved), + throwsA(isA()), + ); + expect( + () => Logic(name: '', naming: Naming.reserved), + throwsA(isA()), + ); + }); + + // ── Golden SV snapshot ────────────────────────────────────── + + test('golden SV output snapshot', () { + final sv = mod.generateSynth(); + + // Port declarations. + expect(sv, contains('input logic [7:0] inp')); + expect(sv, contains('output logic [7:0] out')); + expect(sv, contains('_uinp')); + expect(sv, contains('mport')); + expect(sv, contains('_muprt')); + + // Reserved internals. + expect(sv, contains('resv')); + expect(sv, contains('_resvu')); + + // Renameable internals. + expect(sv, contains('ren')); + expect(sv, contains('_renu')); + + // Constant literal (T20). + expect(sv, contains("8'h42")); + + // Submodule instantiations. + expect(sv, contains('simplesub')); + expect(sv, contains('exprsub')); + expect(sv, contains('unprefsub')); + }); + }); +} diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart new file mode 100644 index 000000000..f0d7b2d31 --- /dev/null +++ b/test/naming_consistency_test.dart @@ -0,0 +1,247 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// naming_consistency_test.dart +// Validates that both the SystemVerilog synthesizer and a base +// SynthModuleDefinition (used by the netlist synthesizer) produce +// consistent signal names via the shared Module.namer. +// +// 2026 April 10 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +// ── Helper modules ────────────────────────────────────────────────── + +/// A simple module with ports, internal wires, and a sub-module. +class _Inner extends Module { + _Inner(Logic a, Logic b) : super(name: 'inner') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + addOutput('y', width: a.width) <= a & b; + } +} + +class _Outer extends Module { + _Outer(Logic a, Logic b) : super(name: 'outer') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + final inner = _Inner(a, b); + addOutput('y', width: a.width) <= inner.output('y'); + } +} + +/// A module with a constant assignment (exercises const naming). +class _ConstModule extends Module { + _ConstModule(Logic a) : super(name: 'constmod') { + a = addInput('a', a, width: 8); + final c = Const(0x42, width: 8).named('myConst', naming: Naming.mergeable); + addOutput('y', width: 8) <= a + c; + } +} + +/// A module with Naming.renameable and Naming.mergeable signals. +class _MixedNaming extends Module { + _MixedNaming(Logic a) : super(name: 'mixednaming') { + a = addInput('a', a, width: 8); + final r = Logic(name: 'renamed', width: 8, naming: Naming.renameable) + ..gets(a); + final m = Logic(name: 'merged', width: 8, naming: Naming.mergeable) + ..gets(r); + addOutput('y', width: 8) <= m; + } +} + +/// A module with a FlipFlop sub-module. +class _FlopOuter extends Module { + _FlopOuter(Logic clk, Logic d) : super(name: 'flopouter') { + clk = addInput('clk', clk); + d = addInput('d', d, width: 8); + addOutput('q', width: 8) <= flop(clk, d); + } +} + +/// Builds [SynthModuleDefinition]s from both bases and collects a +/// Logic→name mapping for all present SynthLogics. +/// +/// Returns maps from Logic to its resolved signal name. +Map _collectNames(SynthModuleDefinition def) { + final names = {}; + for (final sl in [ + ...def.inputs, + ...def.outputs, + ...def.inOuts, + ...def.internalSignals, + ]) { + // Skip SynthLogics whose name was never picked (replaced/pruned). + try { + final n = sl.name; + for (final logic in sl.logics) { + names[logic] = n; + } + // ignore: avoid_catches_without_on_clauses + } catch (_) { + // name not picked — skip + } + } + return names; +} + +void main() { + group('naming consistency', () { + test('SV and base SynthModuleDefinition agree on port names', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + // SV synthesizer path + final svDef = SystemVerilogSynthModuleDefinition(mod); + + // Base path (same as netlist synthesizer uses) + // Since namer is late final, the second constructor reuses + // the same naming state — names must be consistent. + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + // Every Logic present in both must have the same name. + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect(baseNames[logic], svNames[logic], + reason: 'Name mismatch for ${logic.name} ' + '(${logic.runtimeType}, naming=${logic.naming})'); + } + } + + // Port names specifically must match. + for (final port in [...mod.inputs.values, ...mod.outputs.values]) { + expect(svNames[port], isNotNull, + reason: 'SV def should have port ${port.name}'); + expect(baseNames[port], isNotNull, + reason: 'Base def should have port ${port.name}'); + expect(svNames[port], baseNames[port], + reason: 'Port name must match for ${port.name}'); + } + }); + + test('constant naming is consistent', () async { + final mod = _ConstModule(Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect(baseNames[logic], svNames[logic], + reason: 'Name mismatch for ${logic.name}'); + } + } + }); + + test('mixed naming (renameable + mergeable) is consistent', () async { + final mod = _MixedNaming(Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect(baseNames[logic], svNames[logic], + reason: 'Name mismatch for ${logic.name}'); + } + } + }); + + test('flop module naming is consistent', () async { + final mod = _FlopOuter(Logic(), Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect(baseNames[logic], svNames[logic], + reason: 'Name mismatch for ${logic.name}'); + } + } + }); + + test('namer is shared across multiple SynthModuleDefinitions', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + // Build one def, then build another — same namer instance. + final def1 = SynthModuleDefinition(mod); + final def2 = SynthModuleDefinition(mod); + + final names1 = _collectNames(def1); + final names2 = _collectNames(def2); + + for (final logic in names1.keys) { + if (names2.containsKey(logic)) { + expect(names2[logic], names1[logic], + reason: 'Shared namer should produce same name for ' + '${logic.name}'); + } + } + }); + + test('Namer.signalNameOf matches SynthLogic.name for ports', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final def = SynthModuleDefinition(mod); + final synthNames = _collectNames(def); + + // Module.namer.signalNameOf uses Namer directly + for (final port in [...mod.inputs.values, ...mod.outputs.values]) { + final moduleName = mod.namer.signalNameOf(port); + final synthName = synthNames[port]; + expect(synthName, moduleName, + reason: 'SynthLogic.name and Module.namer.signalNameOf must agree ' + 'for port ${port.name}'); + } + }); + + test('submodule instance names are allocated from the shared namespace', + () async { + // Instance names come from Module.namer.allocateName, which + // shares the same namespace as signal names. + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final def = SynthModuleDefinition(mod); + + final instNames = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toSet(); + + // The inner module instance should have a name + expect(instNames, isNotEmpty, + reason: 'Should have at least one submodule instance'); + + // Instance names are claimed in the shared namespace. + for (final name in instNames) { + expect(mod.namer.isAvailable(name), isFalse, + reason: 'Instance name "$name" should be claimed in the ' + 'namespace'); + } + }); + }); +} diff --git a/test/naming_namespace_test.dart b/test/naming_namespace_test.dart new file mode 100644 index 000000000..a5263a998 --- /dev/null +++ b/test/naming_namespace_test.dart @@ -0,0 +1,129 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// naming_namespace_test.dart +// Tests for constant naming via nameOfBest and shared instance/signal +// namespace uniquification. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +/// A simple submodule whose instance name can collide with a signal name. +class _Inner extends Module { + _Inner(Logic a, {super.name = 'inner'}) { + a = addInput('a', a); + addOutput('b') <= ~a; + } +} + +/// Top module that has a signal named the same as a submodule instance. +class _InstanceSignalCollision extends Module { + _InstanceSignalCollision({String instanceName = 'inner'}) + : super(name: 'top') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + // Create a signal whose name matches the submodule instance name. + final sig = Logic(name: instanceName); + sig <= ~a; + + final sub = _Inner(sig, name: instanceName); + o <= sub.output('b'); + } +} + +/// Top module with two submodule instances that have the same name. +class _DuplicateInstances extends Module { + _DuplicateInstances() : super(name: 'top') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + final sub1 = _Inner(a, name: 'blk'); + final sub2 = _Inner(sub1.output('b'), name: 'blk'); + o <= sub2.output('b'); + } +} + +/// Module that uses a constant in a connection chain, exercising constant +/// naming through nameOfBest. +class _ConstantNamingModule extends Module { + _ConstantNamingModule() : super(name: 'const_mod') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + // A constant "1" drives one input of the AND gate. + o <= a & Const(1); + } +} + +/// Module with a mux where one input is a constant, exercising the +/// constNameDisallowed path — the mux output cannot use the constant's +/// literal as its name because it also carries non-constant values. +class _ConstNameDisallowedModule extends Module { + _ConstNameDisallowedModule() : super(name: 'const_disallow') { + final a = addInput('a', Logic()); + final sel = addInput('sel', Logic()); + final o = addOutput('o'); + + // mux output can be the constant OR a, so the constant name is disallowed. + o <= mux(sel, Const(1), a); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('constant naming via nameOfBest', () { + test('constant value appears as literal in SV output', () async { + final dut = _ConstantNamingModule(); + await dut.build(); + final sv = dut.generateSynth(); + + // The constant "1" should appear as a literal 1'h1 in the output, + // not as a declared signal. + expect(sv, contains("1'h1")); + }); + + test('constNameDisallowed falls through to signal naming', () async { + final dut = _ConstNameDisallowedModule(); + await dut.build(); + final sv = dut.generateSynth(); + + // The output assignment should NOT use the raw constant literal + // as a wire name; a proper signal name should be used instead. + // The constant still appears as a literal in the mux expression. + expect(sv, contains("1'h1")); + // The output 'o' should be assigned from something. + expect(sv, contains('o')); + }); + }); + + group('shared instance and signal namespace', () { + test( + 'signal and instance with same name get uniquified ' + 'in the shared namespace', () async { + final dut = _InstanceSignalCollision(); + await dut.build(); + final sv = dut.generateSynth(); + + // With a single shared namespace, one of the two "inner" identifiers + // must be suffixed to avoid collision. + expect(sv, contains('inner_0')); + }); + + test('duplicate instance names get uniquified', () async { + final dut = _DuplicateInstances(); + await dut.build(); + final sv = dut.generateSynth(); + + // Two instances named 'blk' — one should be 'blk', the other 'blk_0'. + expect(sv, contains('blk')); + expect(sv, contains(RegExp(r'blk_\d'))); + }); + }); +} diff --git a/test/pipeline_test.dart b/test/pipeline_test.dart index 31bf38bd9..8a2f2648e 100644 --- a/test/pipeline_test.dart +++ b/test/pipeline_test.dart @@ -264,6 +264,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('simple pipeline with intermediate gets', () async { @@ -280,6 +281,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('pipeline with pipelined sub-operation', () async { @@ -297,6 +299,7 @@ void main() { await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('pipeline with abs reference', () async { @@ -312,6 +315,7 @@ void main() { await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('getting out of range on pipeline is error', () { @@ -354,6 +358,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('multiuse pipeline', () async { @@ -369,6 +374,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('simple pipeline late add', () async { @@ -389,6 +395,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('pipeline initialized via get', () async { @@ -408,6 +415,7 @@ void main() { expect(pipem.b.value.isValid, isTrue); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('pipeline initialized directly instead of via get', () async { @@ -427,6 +435,7 @@ void main() { expect(pipem.b.value.isValid, isTrue); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline simple', () async { @@ -459,6 +468,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline simple async reset', () async { @@ -472,6 +482,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline simple reset vals', () async { @@ -504,6 +515,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline notready', () async { @@ -558,6 +570,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline multi', () async { @@ -602,6 +615,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); }); } diff --git a/test/provider_consumer_test.dart b/test/provider_consumer_test.dart index 97c15f648..fefe21f58 100644 --- a/test/provider_consumer_test.dart +++ b/test/provider_consumer_test.dart @@ -212,5 +212,6 @@ output logic rd_valid_rsp await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); } diff --git a/test/provider_consumer_w_modify_test.dart b/test/provider_consumer_w_modify_test.dart index d34c8e374..d1b3815f7 100644 --- a/test/provider_consumer_w_modify_test.dart +++ b/test/provider_consumer_w_modify_test.dart @@ -182,5 +182,6 @@ output logic rd_valid_rsp await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); } diff --git a/test/sequential_test.dart b/test/sequential_test.dart index ade256cf3..bf913328b 100644 --- a/test/sequential_test.dart +++ b/test/sequential_test.dart @@ -182,6 +182,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(dut, vectors); SimCompare.checkIverilogVector(dut, vectors); + SimCompare.checkSystemCVector(dut, vectors); }); group('shorthand with sequential', () { @@ -203,6 +204,7 @@ void main() { // await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); } test('normal logic', () async { @@ -235,6 +237,7 @@ void main() { await SimCompare.checkFunctionalVector(dut, vectors); SimCompare.checkIverilogVector(dut, vectors); + SimCompare.checkSystemCVector(dut, vectors); }); test('negedge triggered flop', () async { @@ -252,6 +255,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('multiple triggers, both edges', () async { @@ -269,6 +273,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('negedge trigger actually occurs on negedge', () async { diff --git a/test/signal_registry_test.dart b/test/signal_registry_test.dart new file mode 100644 index 000000000..d1719c85e --- /dev/null +++ b/test/signal_registry_test.dart @@ -0,0 +1,143 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_registry_test.dart +// Tests for Module canonical naming (Namer). +// +// 2026 April 14 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +// ──────────────────────────────────────────────────────────────── +// Simple test modules +// ──────────────────────────────────────────────────────────────── + +class _GateMod extends Module { + _GateMod(Logic a, Logic b) : super(name: 'gatetestmodule') { + a = addInput('a', a); + b = addInput('b', b); + final aBar = addOutput('a_bar'); + final aAndB = addOutput('a_and_b'); + aBar <= ~a; + aAndB <= a & b; + } +} + +class _Counter extends Module { + _Counter(Logic en, Logic reset, {int width = 8}) : super(name: 'counter') { + en = addInput('en', en); + reset = addInput('reset', reset); + final val = addOutput('val', width: width); + final nextVal = Logic(name: 'nextVal', width: width); + nextVal <= val + 1; + Sequential.multi([ + SimpleClockGenerator(10).clk, + reset, + ], [ + If(reset, then: [ + val < 0, + ], orElse: [ + If(en, then: [val < nextVal]), + ]), + ]); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('signalName basics', () { + test('returns port names after build', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.signalNameOf(mod.input('a')), equals('a')); + expect(mod.namer.signalNameOf(mod.input('b')), equals('b')); + expect(mod.namer.signalNameOf(mod.output('a_bar')), equals('a_bar')); + expect(mod.namer.signalNameOf(mod.output('a_and_b')), equals('a_and_b')); + }); + + test('returns internal signal names', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.signalNameOf(mod.input('en')), equals('en')); + expect(mod.namer.signalNameOf(mod.input('reset')), equals('reset')); + expect(mod.namer.signalNameOf(mod.output('val')), equals('val')); + }); + + test('agrees with signalName after synth', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + for (final entry in mod.inputs.entries) { + expect( + mod.namer.signalNameOf(entry.value), + isNotNull, + reason: 'signalName should work for input ${entry.key}', + ); + } + for (final entry in mod.outputs.entries) { + expect( + mod.namer.signalNameOf(entry.value), + isNotNull, + reason: 'signalName should work for output ${entry.key}', + ); + } + }); + }); + + group('allocateName', () { + test('avoids collision with existing names', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + final allocated = mod.namer.allocateRawName('en'); + expect(allocated, isNot(equals('en')), + reason: 'Should not collide with existing port name'); + expect(allocated, contains('en'), + reason: 'Should be based on the requested name'); + }); + + test('successive allocations are unique', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + final a = mod.namer.allocateRawName('wire'); + final b = mod.namer.allocateRawName('wire'); + expect(a, isNot(equals(b)), reason: 'Each allocation should be unique'); + }); + }); + + group('sparse storage', () { + test('identity names not stored in renames', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.signalNameOf(mod.input('a')), equals('a')); + expect(mod.input('a').name, equals('a')); + }); + }); + + group('determinism', () { + test('same module produces identical canonical names', () async { + Future> buildAndGetNames() async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + return { + for (final sig in mod.signals) sig.name: mod.namer.signalNameOf(sig), + }; + } + + final names1 = await buildAndGetNames(); + await Simulator.reset(); + final names2 = await buildAndGetNames(); + + expect(names1, equals(names2)); + }); + }); +} diff --git a/test/ssa_test.dart b/test/ssa_test.dart index d16f5fb2d..71e35ec2c 100644 --- a/test/ssa_test.dart +++ b/test/ssa_test.dart @@ -495,6 +495,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('ssa multi use model bad reuse', () { @@ -528,6 +529,10 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + // Skip SystemC for modules with unsupported patterns. + if (mod is! SsaNested && mod is! SsaModWithStructElements) { + SimCompare.checkSystemCVector(mod, vectors); + } }); } }); @@ -560,6 +565,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('ssa seq of cases', () async { @@ -590,6 +596,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('ssa uninitialized', () async { diff --git a/test/sv_gen_test.dart b/test/sv_gen_test.dart index 6ad38737a..c64c3192d 100644 --- a/test/sv_gen_test.dart +++ b/test/sv_gen_test.dart @@ -689,6 +689,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('tieoff ', () { @@ -713,6 +714,8 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + // z-valued outputs are skipped in SystemC checks + SimCompare.checkSystemCVector(mod, vectors); }); test('full port', () async { @@ -734,6 +737,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); } @@ -909,6 +913,7 @@ endmodule : ModWithUselessWireMods''')); ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('connected ports and pruning', () { diff --git a/test/systemc_ffi_cosim_test.dart b/test/systemc_ffi_cosim_test.dart new file mode 100644 index 000000000..6a8428ba2 --- /dev/null +++ b/test/systemc_ffi_cosim_test.dart @@ -0,0 +1,265 @@ +// 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(); + }); + + /// 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_naming_consistency_test.dart b/test/systemc_naming_consistency_test.dart new file mode 100644 index 000000000..334f7b7fa --- /dev/null +++ b/test/systemc_naming_consistency_test.dart @@ -0,0 +1,394 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_naming_consistency_test.dart +// Validates that the SystemC synthesizer produces signal names consistent +// with the SystemVerilog synthesizer via the shared Module.namer. +// +// 2026 May +// Author: Desmond Kirkpatrick + +@TestOn('vm') +library; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +// ── Helper modules ────────────────────────────────────────────────── + +/// Simple combinational logic with an inner sub-module. +class _Inner extends Module { + _Inner(Logic a, Logic b) : super(name: 'inner') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + addOutput('y', width: a.width) <= a & b; + } +} + +class _Outer extends Module { + _Outer(Logic a, Logic b) : super(name: 'outer') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + final inner = _Inner(a, b); + addOutput('y', width: a.width) <= inner.output('y'); + } +} + +/// A module with a constant assignment. +class _ConstModule extends Module { + _ConstModule(Logic a) : super(name: 'constmod') { + a = addInput('a', a, width: 8); + final c = Const(0x42, width: 8).named('myConst', naming: Naming.mergeable); + addOutput('y', width: 8) <= a + c; + } +} + +/// A module with mixed naming priorities. +class _MixedNaming extends Module { + _MixedNaming(Logic a) : super(name: 'mixednaming') { + a = addInput('a', a, width: 8); + final r = Logic(name: 'renamed', width: 8, naming: Naming.renameable) + ..gets(a); + final m = Logic(name: 'merged', width: 8, naming: Naming.mergeable) + ..gets(r); + addOutput('y', width: 8) <= m; + } +} + +/// A module with a FlipFlop (exercises Sequential/clocked naming). +class _FlopModule extends Module { + _FlopModule(Logic clk, Logic d) : super(name: 'flopmod') { + clk = addInput('clk', clk); + d = addInput('d', d, width: 8); + addOutput('q', width: 8) <= flop(clk, d); + } +} + +/// A module with a Combinational block. +class _CombModule extends Module { + _CombModule(Logic a, Logic b) : super(name: 'combmod') { + a = addInput('a', a, width: 8); + b = addInput('b', b, width: 8); + final out = addOutput('y', width: 8); + Combinational([ + If(a.eq(b), then: [out < a], orElse: [out < b]), + ]); + } +} + +/// A module with multiple internal signals that may collide. +class _CollisionModule extends Module { + _CollisionModule(Logic a) : super(name: 'collision') { + a = addInput('a', a, width: 8); + final x = Logic(name: 'sig', width: 8)..gets(a); + final y = Logic(name: 'sig', width: 8)..gets(x); + addOutput('out', width: 8) <= y; + } +} + +// ── Utilities ──────────────────────────────────────────────────────── + +/// Collects Logic→name mappings from a SynthModuleDefinition for all +/// signals that have had their names picked (alive, not pruned/replaced). +Map _collectNames(SynthModuleDefinition def) { + final names = {}; + for (final sl in [ + ...def.inputs, + ...def.outputs, + ...def.inOuts, + ...def.internalSignals, + ]) { + try { + final n = sl.name; + for (final logic in sl.logics) { + names[logic] = n; + } + // ignore: avoid_catches_without_on_clauses + } catch (_) { + // name not picked — skip (replaced/pruned signal) + } + } + return names; +} + +/// Verifies that all signals present in both maps have the same name. +void _expectConsistentNames( + Map svNames, + Map scNames, { + required String context, +}) { + for (final logic in svNames.keys) { + if (scNames.containsKey(logic)) { + expect(scNames[logic], svNames[logic], + reason: '$context: Name mismatch for Logic "${logic.name}" ' + '(${logic.runtimeType}, naming=${logic.naming}). ' + 'SV="${svNames[logic]}", SC="${scNames[logic]}"'); + } + } +} + +// ── Tests ──────────────────────────────────────────────────────────── + +void main() { + group('SystemC vs SystemVerilog naming consistency', () { + test('simple hierarchy - port and internal signal names match', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final scDef = SystemCSynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final scNames = _collectNames(scDef); + + _expectConsistentNames(svNames, scNames, context: '_Outer'); + + // Port names must be present in both. + for (final port in [...mod.inputs.values, ...mod.outputs.values]) { + expect(svNames[port], isNotNull, + reason: 'SV should have port ${port.name}'); + expect(scNames[port], isNotNull, + reason: 'SC should have port ${port.name}'); + expect(scNames[port], svNames[port], + reason: 'Port "${port.name}" must match between SV and SC'); + } + }); + + test('constant module - names match', () async { + final mod = _ConstModule(Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final scDef = SystemCSynthModuleDefinition(mod); + + _expectConsistentNames( + _collectNames(svDef), + _collectNames(scDef), + context: '_ConstModule', + ); + }); + + test('mixed naming priorities - names match', () async { + final mod = _MixedNaming(Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final scDef = SystemCSynthModuleDefinition(mod); + + _expectConsistentNames( + _collectNames(svDef), + _collectNames(scDef), + context: '_MixedNaming', + ); + }); + + test('flop module - clocked signal names match', () async { + final mod = _FlopModule(Logic(), Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final scDef = SystemCSynthModuleDefinition(mod); + + _expectConsistentNames( + _collectNames(svDef), + _collectNames(scDef), + context: '_FlopModule', + ); + }); + + test('combinational module - names match', () async { + final mod = _CombModule(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final scDef = SystemCSynthModuleDefinition(mod); + + _expectConsistentNames( + _collectNames(svDef), + _collectNames(scDef), + context: '_CombModule', + ); + }); + + test('name collisions resolved identically', () async { + final mod = _CollisionModule(Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final scDef = SystemCSynthModuleDefinition(mod); + + _expectConsistentNames( + _collectNames(svDef), + _collectNames(scDef), + context: '_CollisionModule', + ); + }); + + test('generateSystemC does not crash after generateSynth', () async { + final mod = _FlopModule(Logic(), Logic(width: 8)); + await mod.build(); + + // SV first, then SC — validates shared Namer state is safe. + final sv = mod.generateSynth(); + expect(sv, contains('module')); + + final sc = mod.generateSystemC(); + expect(sc, contains('SC_MODULE')); + }); + + test('generateSynth does not crash after generateSystemC', () async { + final mod = _FlopModule(Logic(), Logic(width: 8)); + await mod.build(); + + // SC first, then SV — reverse order. + final sc = mod.generateSystemC(); + expect(sc, contains('SC_MODULE')); + + final sv = mod.generateSynth(); + expect(sv, contains('module')); + }); + + test('signal names in generated output match between SV and SC', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final sv = mod.generateSynth(); + final sc = mod.generateSystemC(); + + // Port names must appear in both outputs. + for (final portName in [...mod.inputs.keys, ...mod.outputs.keys]) { + expect(sv, contains(portName), + reason: 'SV output should contain port "$portName"'); + expect(sc, contains(portName), + reason: 'SC output should contain port "$portName"'); + } + }); + + test('scLineMap is populated with port and signal positions', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + // Access the SystemCSynthesisResult to check scLineMap. + final synthBuilder = SynthBuilder(mod, SystemCSynthesizer()); + final results = synthBuilder.synthesisResults; + + // The top-level module's result should have line map entries. + final topResult = results.firstWhere((r) => r.module == mod); + expect(topResult, isA()); + final scResult = topResult as SystemCSynthesisResult + + // Force text generation (which populates the line map). + ..toFileContents(); + + final lineMap = scResult.scLineMap; + + // Port names should have entries. + for (final portName in [...mod.inputs.keys, ...mod.outputs.keys]) { + expect(lineMap, contains(portName), + reason: 'scLineMap should contain port "$portName"'); + // Each entry should be a non-empty list of 'line:col' strings. + final positions = lineMap[portName]!; + expect(positions, isNotEmpty, + reason: 'Entry for "$portName" should have at least one position'); + for (final p in positions) { + expect(p, matches(RegExp(r'^\d+:\d+$')), + reason: 'Position "$p" for "$portName" ' + 'should be "line:col" format'); + } + } + }); + + test('scLineMap positions match actual text positions', () async { + final mod = _CombModule(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final synthBuilder = SynthBuilder(mod, SystemCSynthesizer()); + final results = synthBuilder.synthesisResults; + final topResult = + results.firstWhere((r) => r.module == mod) as SystemCSynthesisResult; + final text = topResult.toFileContents(); + final lineMap = topResult.scLineMap; + final lines = text.split('\n'); + + // Verify that every recorded position actually contains the symbol name. + for (final entry in lineMap.entries) { + final name = entry.key; + for (final lineCol in entry.value) { + final parts = lineCol.split(':'); + final line = int.parse(parts[0]) - 1; // 0-based + final col = int.parse(parts[1]) - 1; // 0-based + + expect(line, lessThan(lines.length), + reason: 'Line for "$name" should be within text'); + expect(lines[line], contains(name), + reason: 'Line ${line + 1} should contain "$name".\n' + 'Actual line: "${lines[line]}"'); + // Verify column position points to the name. + final colEnd = col + name.length; + if (colEnd <= lines[line].length) { + expect(lines[line].substring(col, colEnd), equals(name), + reason: 'Column position for "$name" should point to the name'); + } + } + } + }); + + test('scLineMap records multiple positions for re-assigned signals', + () async { + // _CombModule drives output `y` from two arms of an If/Else, so the + // SystemC output contains the declaration line plus two assignment + // LHS lines for `y`. All three should be recorded. + final mod = _CombModule(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final synthBuilder = SynthBuilder(mod, SystemCSynthesizer()); + final results = synthBuilder.synthesisResults; + final topResult = + results.firstWhere((r) => r.module == mod) as SystemCSynthesisResult; + final text = topResult.toFileContents(); + final lineMap = topResult.scLineMap; + final lines = text.split('\n'); + + final yPositions = lineMap['y']; + expect(yPositions, isNotNull, reason: 'output `y` must be recorded'); + expect(yPositions!.length, greaterThanOrEqualTo(3), + reason: 'Expected the declaration plus at least two assignment ' + 'positions for `y`, got: $yPositions'); + + // Positions must be in textual (line-number) order. + final lineNumbers = + yPositions.map((p) => int.parse(p.split(':')[0])).toList(); + final sorted = [...lineNumbers]..sort(); + expect(lineNumbers, equals(sorted), + reason: 'scLineMap positions must be in source order'); + + // No duplicates. + expect(yPositions.toSet().length, equals(yPositions.length), + reason: 'scLineMap should not record duplicate positions'); + + // Each recorded line must literally contain `y` at the recorded col. + for (final p in yPositions) { + final parts = p.split(':'); + final line = int.parse(parts[0]) - 1; + final col = int.parse(parts[1]) - 1; + expect(lines[line].substring(col, col + 1), equals('y'), + reason: 'Position $p should point to `y`'); + } + + // Verify at least two of the recorded lines are assignment LHS + // (i.e. line text matches `y=`). + final assignLhsRe = RegExp(r'^\s*y\s*=(?!=)'); + final assignmentLines = yPositions.where((p) { + final line = int.parse(p.split(':')[0]) - 1; + return assignLhsRe.hasMatch(lines[line]); + }).toList(); + expect(assignmentLines.length, greaterThanOrEqualTo(2), + reason: 'Expected at least two assignment LHS lines for `y`, ' + 'got: $assignmentLines'); + }); + }); +} diff --git a/test/systemc_simcompare_test.dart b/test/systemc_simcompare_test.dart new file mode 100644 index 000000000..b396c6709 --- /dev/null +++ b/test/systemc_simcompare_test.dart @@ -0,0 +1,194 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_simcompare_test.dart +// Tests for SystemC synthesis and simulation comparison. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +/// A simple module with basic gates for testing SystemC synthesis. +class GateModule extends Module { + GateModule(Logic a, Logic b) : super(name: 'GateModule') { + a = addInput('a', a); + b = addInput('b', b); + final aAndB = addOutput('a_and_b'); + final aOrB = addOutput('a_or_b'); + final notA = addOutput('not_a'); + + aAndB <= a & b; + aOrB <= a | b; + notA <= ~a; + } +} + +/// A simple counter for testing sequential SystemC synthesis. +class SimpleCounter extends Module { + SimpleCounter(Logic clk, Logic reset, Logic en) : super(name: 'Counter') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + en = addInput('en', en); + final val = addOutput('val', width: 8); + + final nextVal = Logic(name: 'nextVal', width: 8); + + Sequential(clk, reset: reset, [ + If(en, then: [nextVal < nextVal + 1], orElse: [nextVal < nextVal]), + ]); + + val <= nextVal; + } +} + +/// A flip-flop module for testing. +class FlopModule extends Module { + FlopModule(Logic clk, Logic reset, Logic d) : super(name: 'FlopModule') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + d = addInput('d', d, width: 8); + final q = addOutput('q', width: 8); + q <= flop(clk, d, reset: reset); + } +} + +/// A flip-flop with enable. +class FlopEnModule extends Module { + FlopEnModule(Logic clk, Logic reset, Logic en, Logic d) + : super(name: 'FlopEnModule') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + en = addInput('en', en); + d = addInput('d', d, width: 8); + final q = addOutput('q', width: 8); + q <= flop(clk, d, reset: reset, en: en); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + 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, dontDeleteTmpFiles: true); + }); + + test('flip-flop module passes vectors', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final d = Logic(name: 'd', width: 8); + final mod = FlopModule(clk, reset, d); + await mod.build(); + + // Flop: output follows input with 1-cycle latency + final vectors = [ + Vector({'d': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0xAA, 'reset': 0}, {'q': 0}), + Vector({'d': 0xBB, 'reset': 0}, {'q': 0xAA}), + Vector({'d': 0xCC, 'reset': 0}, {'q': 0xBB}), + Vector({'d': 0xDD, 'reset': 0}, {'q': 0xCC}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('flip-flop with enable passes vectors', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final d = Logic(name: 'd', width: 8); + final mod = FlopEnModule(clk, reset, en, d); + await mod.build(); + + // When en=0, q holds; when en=1, q follows d with 1-cycle latency + final vectors = [ + Vector({'d': 0, 'en': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0, 'en': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0x42, 'en': 1, 'reset': 0}, {'q': 0}), + Vector({'d': 0x55, 'en': 1, 'reset': 0}, {'q': 0x42}), + Vector({'d': 0xFF, 'en': 0, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0x00, 'en': 0, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0x99, 'en': 1, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0xAA, 'en': 1, 'reset': 0}, {'q': 0x99}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('counter trace-based comparison', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final mod = SimpleCounter(clk, reset, en); + await mod.build(); + + // Use the trace-based approach: just write normal simulation code, + // no vectors needed. The method records all I/O at every clock edge + // and replays through SystemC. + final result = await SimCompare.systemcSimCompare( + mod, + clk, + stimulus: () async { + reset.inject(1); + en.inject(0); + Simulator.registerAction(25, () { + reset.put(0); + en.put(1); + }); + Simulator.registerAction(65, () { + en.put(0); + }); + Simulator.registerAction(85, () { + en.put(1); + }); + Simulator.setMaxSimTime(120); + }, + dontDeleteTmpFiles: true, + ); + expect(result, isTrue); + }); + }); +} diff --git a/test/systemc_struct_naming_test.dart b/test/systemc_struct_naming_test.dart new file mode 100644 index 000000000..a1754060c --- /dev/null +++ b/test/systemc_struct_naming_test.dart @@ -0,0 +1,937 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_struct_naming_test.dart +// Tests for SystemC generation with various LogicStructure/Interface port +// patterns that exercise _BusSubsetForStructSlice and SynthLogic naming. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +@TestOn('vm') +library; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +// ───────────────────────────────────────────────────────────────────────────── +// Test structures and modules +// ───────────────────────────────────────────────────────────────────────────── + +/// A simple 2-field LogicStructure. +class TwoFieldStruct extends LogicStructure { + late final Logic data; + late final Logic valid; + + TwoFieldStruct({int dataWidth = 4, String? name}) + : super([ + Logic(name: 'data', width: dataWidth), + Logic(name: 'valid'), + ], name: name ?? 'twoField') { + data = elements[0]; + valid = elements[1]; + } + + @override + TwoFieldStruct clone({String? name}) => TwoFieldStruct( + dataWidth: data.width, + name: name ?? this.name, + ); +} + +/// A 3-field struct to test wider slicing. +class ThreeFieldStruct extends LogicStructure { + late final Logic a; + late final Logic b; + late final Logic c; + + ThreeFieldStruct({int width = 4, String? name}) + : super([ + Logic(name: 'a', width: width), + Logic(name: 'b', width: width), + Logic(name: 'c', width: width), + ], name: name ?? 'threeField') { + a = elements[0]; + b = elements[1]; + c = elements[2]; + } + + @override + ThreeFieldStruct clone({String? name}) => + ThreeFieldStruct(width: a.width, name: name ?? this.name); +} + +/// A nested struct: outer contains an inner TwoFieldStruct plus extra signal. +class NestedStruct extends LogicStructure { + late final Logic inner; + late final Logic extra; + + NestedStruct({int dataWidth = 4, String? name}) + : super([ + TwoFieldStruct(dataWidth: dataWidth, name: 'inner'), + Logic(name: 'extra', width: 2), + ], name: name ?? 'nested') { + inner = elements[0]; + extra = elements[1]; + } + + @override + NestedStruct clone({String? name}) => NestedStruct( + dataWidth: (elements[0] as LogicStructure).elements[0].width, + name: name ?? this.name); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Case 1: Module with a LogicStructure INPUT port +// ───────────────────────────────────────────────────────────────────────────── + +/// Module that takes a LogicStructure input and uses individual fields. +class StructInputModule extends Module { + Logic get dataOut => output('dataOut'); + Logic get validOut => output('validOut'); + + StructInputModule(TwoFieldStruct structIn) + : super( + name: 'structInputMod', + definitionName: 'StructInputModule_W${structIn.data.width}') { + structIn = addTypedInput('structIn', structIn); + final dataOut = addOutput('dataOut', width: structIn.data.width); + final validOut = addOutput('validOut'); + dataOut <= structIn.data; + validOut <= structIn.valid; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Case 2: Module with a LogicStructure OUTPUT port +// ───────────────────────────────────────────────────────────────────────────── + +/// Module that produces a LogicStructure output from scalar inputs. +class StructOutputModule extends Module { + late final TwoFieldStruct structOut; + + StructOutputModule(Logic data, Logic valid) + : super( + name: 'structOutputMod', + definitionName: 'StructOutputModule_W${data.width}') { + data = addInput('data', data, width: data.width); + valid = addInput('valid', valid); + structOut = addTypedOutput( + 'structOut', TwoFieldStruct(dataWidth: data.width).clone); + structOut.data <= data; + structOut.valid <= valid; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Case 3: Sub-module with LogicStructure output consumed by parent +// ───────────────────────────────────────────────────────────────────────────── + +/// Parent module that instantiates StructOutputModule and reads its struct out. +class ParentOfStructOutput extends Module { + Logic get dataOut => output('dataOut'); + Logic get validOut => output('validOut'); + + ParentOfStructOutput(Logic data, Logic valid) + : super( + name: 'parentOfStructOutput', + definitionName: 'ParentOfStructOutput_W${data.width}') { + data = addInput('data', data, width: data.width); + valid = addInput('valid', valid); + + final sub = StructOutputModule(data, valid); + final dataOut = addOutput('dataOut', width: data.width); + final validOut = addOutput('validOut'); + dataOut <= sub.structOut.data; + validOut <= sub.structOut.valid; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Case 4: Sub-module with LogicStructure input fed by parent +// ───────────────────────────────────────────────────────────────────────────── + +/// Parent module that builds a struct and feeds it to StructInputModule. +class ParentOfStructInput extends Module { + Logic get dataOut => output('dataOut'); + Logic get validOut => output('validOut'); + + ParentOfStructInput(Logic data, Logic valid) + : super( + name: 'parentOfStructInput', + definitionName: 'ParentOfStructInput_W${data.width}') { + data = addInput('data', data, width: data.width); + valid = addInput('valid', valid); + + final structSig = TwoFieldStruct(dataWidth: data.width, name: 'myStruct'); + structSig.data <= data; + structSig.valid <= valid; + + final sub = StructInputModule(structSig); + final dataOut = addOutput('dataOut', width: data.width); + final validOut = addOutput('validOut'); + dataOut <= sub.dataOut; + validOut <= sub.validOut; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Case 5: Chained sub-modules with struct passthrough +// ───────────────────────────────────────────────────────────────────────────── + +/// Sub-module that passes through a struct (input struct → output struct). +class StructPassthrough extends Module { + late final TwoFieldStruct structOut; + + StructPassthrough(TwoFieldStruct structIn) + : super( + name: 'structPass', + definitionName: 'StructPassthrough_W${structIn.data.width}') { + structIn = addTypedInput('structIn', structIn); + structOut = addTypedOutput('structOut', structIn.clone); + structOut <= structIn; + } +} + +/// Parent with two chained StructPassthrough sub-modules. +class ChainedStructPassthrough extends Module { + Logic get dataOut => output('dataOut'); + + ChainedStructPassthrough(Logic data, Logic valid) + : super( + name: 'chainedStruct', + definitionName: 'ChainedStructPassthrough_W${data.width}') { + data = addInput('data', data, width: data.width); + valid = addInput('valid', valid); + + final struct1 = TwoFieldStruct(dataWidth: data.width, name: 'struct1'); + struct1.data <= data; + struct1.valid <= valid; + + final pass1 = StructPassthrough(struct1); + final pass2 = StructPassthrough(pass1.structOut); + + final dataOut = addOutput('dataOut', width: data.width); + dataOut <= pass2.structOut.data; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Case 6: Three-field struct (wider slicing pattern) +// ───────────────────────────────────────────────────────────────────────────── + +/// Module with a 3-field struct output, partially consumed by parent. +class ThreeFieldOutputModule extends Module { + late final ThreeFieldStruct structOut; + + ThreeFieldOutputModule(Logic a, Logic b, Logic c) + : super( + name: 'threeFieldOut', + definitionName: 'ThreeFieldOutputModule_W${a.width}') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + c = addInput('c', c, width: c.width); + structOut = + addTypedOutput('structOut', ThreeFieldStruct(width: a.width).clone); + structOut.a <= a; + structOut.b <= b; + structOut.c <= c; + } +} + +/// Parent that only uses SOME fields of the 3-field struct output. +class PartialStructConsumer extends Module { + Logic get sumOut => output('sumOut'); + + PartialStructConsumer(Logic a, Logic b, Logic c) + : super( + name: 'partialConsumer', + definitionName: 'PartialStructConsumer_W${a.width}') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + c = addInput('c', c, width: c.width); + + final sub = ThreeFieldOutputModule(a, b, c); + // Only consume .a and .c, leaving .b unused + final sumOut = addOutput('sumOut', width: a.width); + sumOut <= sub.structOut.a + sub.structOut.c; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Case 7: Struct with gates operating on fields +// ───────────────────────────────────────────────────────────────────────────── + +/// Module that takes a struct input and performs gate operations on fields. +class StructGateModule extends Module { + Logic get result => output('result'); + + StructGateModule(TwoFieldStruct structIn) + : super( + name: 'structGate', + definitionName: 'StructGateModule_W${structIn.data.width}') { + structIn = addTypedInput('structIn', structIn); + final result = addOutput('result', width: structIn.data.width); + // Gate operations on struct fields + result <= mux(structIn.valid, structIn.data, Const(0, width: result.width)); + } +} + +/// Parent that creates a struct and feeds it into StructGateModule. +class ParentOfStructGate extends Module { + Logic get result => output('result'); + + ParentOfStructGate(Logic data, Logic valid) + : super( + name: 'parentStructGate', + definitionName: 'ParentOfStructGate_W${data.width}') { + data = addInput('data', data, width: data.width); + valid = addInput('valid', valid); + + final s = TwoFieldStruct(dataWidth: data.width, name: 'mySig'); + s.data <= data; + s.valid <= valid; + + final sub = StructGateModule(s); + final result = addOutput('result', width: data.width); + result <= sub.result; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Case 8: Sequential module with struct input +// ───────────────────────────────────────────────────────────────────────────── + +/// Module with a struct input and a flop on the data field. +class StructFlopModule extends Module { + Logic get qOut => output('qOut'); + + StructFlopModule(TwoFieldStruct structIn, {Logic? reset}) + : super( + name: 'structFlop', + definitionName: 'StructFlopModule_W${structIn.data.width}') { + if (reset != null) { + reset = addInput('reset', reset); + } + structIn = addTypedInput('structIn', structIn); + final qOut = addOutput('qOut', width: structIn.data.width); + final clk = SimpleClockGenerator(10).clk; + // Only flop the data field when valid is high + qOut <= flop(clk, structIn.data, reset: reset, en: structIn.valid); + } +} + +/// Parent that drives StructFlopModule from scalar signals. +class ParentOfStructFlop extends Module { + Logic get qOut => output('qOut'); + + ParentOfStructFlop(Logic data, Logic valid, {Logic? reset}) + : super( + name: 'parentStructFlop', + definitionName: 'ParentOfStructFlop_W${data.width}') { + if (reset != null) { + reset = addInput('reset', reset); + } + data = addInput('data', data, width: data.width); + valid = addInput('valid', valid); + + final s = TwoFieldStruct(dataWidth: data.width, name: 'flopIn'); + s.data <= data; + s.valid <= valid; + + final sub = StructFlopModule(s, reset: reset); + final qOut = addOutput('qOut', width: data.width); + qOut <= sub.qOut; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Case 9: Multiple sub-modules sharing struct signals +// ───────────────────────────────────────────────────────────────────────────── + +/// Parent with two sub-modules both reading the same struct output. +class SharedStructConsumer extends Module { + Logic get sum => output('sum'); + Logic get xorResult => output('xorResult'); + + SharedStructConsumer(Logic data, Logic valid) + : super( + name: 'sharedConsumer', + definitionName: 'SharedStructConsumer_W${data.width}') { + data = addInput('data', data, width: data.width); + valid = addInput('valid', valid); + + // Producer sub-module with struct output + final producer = StructOutputModule(data, valid); + + // Two consumers reading different fields of the same struct + final sub1 = StructInputModule(producer.structOut); + final sub2 = StructGateModule(producer.structOut); + + final sum = addOutput('sum', width: data.width); + final xorResult = addOutput('xorResult', width: data.width); + sum <= sub1.dataOut; + xorResult <= sub2.result; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Case 10: Struct output partially unused (pruning stress test) +// ───────────────────────────────────────────────────────────────────────────── + +/// Module with a struct output where only one field is consumed downstream. +class SingleFieldConsumer extends Module { + Logic get validOnly => output('validOnly'); + + SingleFieldConsumer(Logic data, Logic valid) + : super( + name: 'singleFieldConsumer', + definitionName: 'SingleFieldConsumer_W${data.width}') { + data = addInput('data', data, width: data.width); + valid = addInput('valid', valid); + + final sub = StructOutputModule(data, valid); + // Only use .valid, ignore .data entirely + final validOnly = addOutput('validOnly'); + validOnly <= sub.structOut.valid; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Case 11-14: Interface patterns that stress pruning + naming +// ───────────────────────────────────────────────────────────────────────────── + +/// Sub-module that has BOTH a used output AND an unused struct output. +/// The unused struct output exercises the path where BusSubset slices +/// are created in the parent but the resulting signals are never consumed. +class SubModWithUnusedStructOut extends Module { + Logic get usedOut => output('usedOut'); + TwoFieldStruct get unusedStructOut => + output('unusedStructOut') as TwoFieldStruct; + + SubModWithUnusedStructOut(Logic inp) + : super( + name: 'subModUnused', definitionName: 'SubModWithUnusedStructOut') { + inp = addInput('inp', inp, width: inp.width); + + final usedOut = addOutput('usedOut', width: inp.width); + usedOut <= inp; + + final structOut = addTypedOutput( + 'unusedStructOut', + ({name = 'unusedStructOut'}) => + TwoFieldStruct(dataWidth: inp.width, name: name)); + structOut.data <= inp; + structOut.valid <= Const(1); + } +} + +/// Parent that instantiates SubModWithUnusedStructOut but only uses +/// the scalar output, leaving the struct output entirely unconsumed. +class ParentWithUnusedStructOutput extends Module { + Logic get usedOut => output('usedOut'); + + ParentWithUnusedStructOutput(Logic inp) + : super( + name: 'parentUnused', + definitionName: 'ParentWithUnusedStructOutput') { + inp = addInput('inp', inp, width: inp.width); + + final sub = SubModWithUnusedStructOut(inp); + + // Only consume the scalar output, completely ignore the struct output + final usedOut = addOutput('usedOut', width: inp.width); + usedOut <= sub.usedOut; + } +} + +/// Sub-module that outputs a 2-field struct. +class StructProducer extends Module { + TwoFieldStruct get structOut => output('structOut') as TwoFieldStruct; + + StructProducer(Logic inp) + : super(name: 'producer', definitionName: 'StructProducer') { + inp = addInput('inp', inp, width: inp.width); + + final structOut = addTypedOutput( + 'structOut', + ({name = 'structOut'}) => + TwoFieldStruct(dataWidth: inp.width, name: name)); + structOut.data <= inp; + structOut.valid <= Const(1); + } +} + +/// Parent that only consumes the `data` field of the sub-module's struct +/// output, leaving `valid` unconsumed. +class ParentPartialStructConsumption extends Module { + Logic get dataOnly => output('dataOnly'); + + ParentPartialStructConsumption(Logic inp) + : super( + name: 'partialConsume', + definitionName: 'ParentPartialStructConsumption') { + inp = addInput('inp', inp, width: inp.width); + + final sub = StructProducer(inp); + + // Only consume .data, leave .valid unused + final dataOnly = addOutput('dataOnly', width: inp.width); + dataOnly <= sub.structOut.data; + } +} + +/// Parent that connects sub-module struct output elements to +/// Naming.mergeable locals (which should be merged/pruned). +class MergeableStructConsumer extends Module { + Logic get result => output('result'); + + MergeableStructConsumer(Logic inp) + : super( + name: 'mergeableConsumer', + definitionName: 'MergeableStructConsumer') { + inp = addInput('inp', inp, width: inp.width); + + final sub = StructProducer(inp); + + // Connect struct output data via a mergeable intermediate + final intermediate = + Logic(name: 'tmp', width: inp.width, naming: Naming.mergeable); + intermediate <= sub.structOut.data; + + final result = addOutput('result', width: inp.width); + result <= intermediate; + } +} + +/// Simple PairInterface with data+valid ports. +class SimplePairIntf extends PairInterface { + Logic get data => port('data'); + Logic get valid => port('valid'); + + SimplePairIntf({int dataWidth = 8}) + : super(portsFromProvider: [ + Logic.port('data', dataWidth), + Logic.port('valid'), + ]); + + @override + SimplePairIntf clone() => SimplePairIntf(dataWidth: data.width); +} + +/// Module using PairInterface that exercises port connection naming. +class PairInterfaceModule extends Module { + Logic get dataOut => output('dataOut'); + Logic get validOut => output('validOut'); + + PairInterfaceModule(Logic data, Logic valid) + : super(name: 'pairIntfMod', definitionName: 'PairInterfaceModule') { + data = addInput('data', data, width: data.width); + valid = addInput('valid', valid); + + // Create a PairInterface and connect as provider + final intf = SimplePairIntf(dataWidth: data.width); + intf.data <= data; + intf.valid <= valid; + + // Sub-module consumes the interface + final sub = PairIntfConsumer(intf); + + final dataOut = addOutput('dataOut', width: data.width); + final validOut = addOutput('validOut'); + dataOut <= sub.dataThru; + validOut <= sub.validThru; + } +} + +/// Sub-module that consumes a PairInterface. +class PairIntfConsumer extends Module { + Logic get dataThru => output('dataThru'); + Logic get validThru => output('validThru'); + + PairIntfConsumer(SimplePairIntf intf) + : super(name: 'pairConsumer', definitionName: 'PairIntfConsumer') { + intf = SimplePairIntf(dataWidth: intf.data.width) + ..pairConnectIO(this, intf, PairRole.consumer); + + final dataThru = addOutput('dataThru', width: intf.data.width); + final validThru = addOutput('validThru'); + dataThru <= intf.data; + validThru <= intf.valid; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Tests +// ───────────────────────────────────────────────────────────────────────────── + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('SystemC generation with LogicStructure ports', () { + test('Case 1: struct input - generates without assertion failure', + () async { + final s = TwoFieldStruct(name: 'inp'); + final mod = StructInputModule(s); + await mod.build(); + + final sc = mod.generateSynth(); + expect(sc, contains('StructInputModule_W4')); + + // Also verify functional correctness via vectors + final vectors = [ + // struct packed: [valid(1), data(4)] = 5 bits total + // valid is MSB, data is lower 4 bits + Vector({'structIn': 0x1F}, {'dataOut': 0xF, 'validOut': 1}), + Vector({'structIn': 0x05}, {'dataOut': 0x5, 'validOut': 0}), + Vector({'structIn': 0x10}, {'dataOut': 0x0, 'validOut': 1}), + Vector({'structIn': 0x00}, {'dataOut': 0x0, 'validOut': 0}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('Case 2: struct output - generates without assertion failure', + () async { + final data = Logic(name: 'data', width: 4); + final valid = Logic(name: 'valid'); + final mod = StructOutputModule(data, valid); + await mod.build(); + + final sc = mod.generateSynth(); + expect(sc, contains('StructOutputModule_W4')); + + final vectors = [ + Vector({'data': 0xA, 'valid': 1}, {'structOut': 0x1A}), + Vector({'data': 0x5, 'valid': 0}, {'structOut': 0x05}), + Vector({'data': 0xF, 'valid': 1}, {'structOut': 0x1F}), + Vector({'data': 0x0, 'valid': 0}, {'structOut': 0x00}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('Case 3: sub-module struct output consumed by parent', () async { + final data = Logic(name: 'data', width: 4); + final valid = Logic(name: 'valid'); + final mod = ParentOfStructOutput(data, valid); + await mod.build(); + + final sc = mod.generateSynth(); + expect(sc, contains('ParentOfStructOutput_W4')); + + final vectors = [ + Vector({'data': 0xA, 'valid': 1}, {'dataOut': 0xA, 'validOut': 1}), + Vector({'data': 0x5, 'valid': 0}, {'dataOut': 0x5, 'validOut': 0}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('Case 4: parent builds struct and feeds to sub-module input', + () async { + final data = Logic(name: 'data', width: 4); + final valid = Logic(name: 'valid'); + final mod = ParentOfStructInput(data, valid); + await mod.build(); + + final sc = mod.generateSynth(); + expect(sc, contains('ParentOfStructInput_W4')); + + final vectors = [ + Vector({'data': 0xC, 'valid': 1}, {'dataOut': 0xC, 'validOut': 1}), + Vector({'data': 0x3, 'valid': 0}, {'dataOut': 0x3, 'validOut': 0}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('Case 5: chained struct passthrough sub-modules', () async { + final data = Logic(name: 'data', width: 4); + final valid = Logic(name: 'valid'); + final mod = ChainedStructPassthrough(data, valid); + await mod.build(); + + final sc = mod.generateSynth(); + expect(sc, contains('ChainedStructPassthrough_W4')); + + final vectors = [ + Vector({'data': 0x7, 'valid': 1}, {'dataOut': 0x7}), + Vector({'data': 0xE, 'valid': 0}, {'dataOut': 0xE}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('Case 6: three-field struct with partial consumption', () async { + final a = Logic(name: 'a', width: 4); + final b = Logic(name: 'b', width: 4); + final c = Logic(name: 'c', width: 4); + final mod = PartialStructConsumer(a, b, c); + await mod.build(); + + final sc = mod.generateSynth(); + expect(sc, contains('PartialStructConsumer_W4')); + + // sumOut = a + c (b is unused) + final vectors = [ + Vector({'a': 1, 'b': 99, 'c': 2}, {'sumOut': 3}), + Vector({'a': 5, 'b': 0, 'c': 3}, {'sumOut': 8}), + Vector({'a': 0xF, 'b': 7, 'c': 1}, {'sumOut': 0}), // overflow wraps + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('Case 7: struct input with gate operations on fields', () async { + final data = Logic(name: 'data', width: 4); + final valid = Logic(name: 'valid'); + final mod = ParentOfStructGate(data, valid); + await mod.build(); + + final sc = mod.generateSynth(); + expect(sc, contains('ParentOfStructGate_W4')); + + // result = mux(valid, data, 0) + final vectors = [ + Vector({'data': 0xA, 'valid': 1}, {'result': 0xA}), + Vector({'data': 0xA, 'valid': 0}, {'result': 0x0}), + Vector({'data': 0xF, 'valid': 1}, {'result': 0xF}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('Case 8: struct input with sequential logic (flop)', () async { + final data = Logic(name: 'data', width: 4); + final valid = Logic(name: 'valid'); + final reset = Logic(name: 'reset'); + final mod = ParentOfStructFlop(data, valid, reset: reset); + await mod.build(); + + final sc = mod.generateSynth(); + expect(sc, contains('ParentOfStructFlop_W4')); + + // Flop with enable: qOut follows data when valid=1 + // Clock is internal (SimpleClockGenerator), not in vectors + final vectors = [ + Vector({'data': 0, 'valid': 0, 'reset': 1}, {}), + Vector({'data': 0xA, 'valid': 1, 'reset': 0}, {'qOut': 0}), + Vector({'data': 0xB, 'valid': 1, 'reset': 0}, {'qOut': 0xA}), + Vector({'data': 0xC, 'valid': 0, 'reset': 0}, {'qOut': 0xB}), + Vector({'data': 0xD, 'valid': 0, 'reset': 0}, {'qOut': 0xB}), + Vector({'data': 0xE, 'valid': 1, 'reset': 0}, {'qOut': 0xB}), + Vector({'data': 0xF, 'valid': 1, 'reset': 0}, {'qOut': 0xE}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + // Note: SystemC check skipped — SimpleClockGenerator inside a sub-module + // causes unbound clk port, unrelated to struct naming. + }); + + test('Case 9: multiple sub-modules sharing one struct output', () async { + final data = Logic(name: 'data', width: 4); + final valid = Logic(name: 'valid'); + final mod = SharedStructConsumer(data, valid); + await mod.build(); + + final sc = mod.generateSynth(); + expect(sc, contains('SharedStructConsumer_W4')); + + // sum = data (passthrough via StructInputModule) + // xorResult = mux(valid, data, 0) (via StructGateModule) + final vectors = [ + Vector({'data': 0xA, 'valid': 1}, {'sum': 0xA, 'xorResult': 0xA}), + Vector({'data': 0xA, 'valid': 0}, {'sum': 0xA, 'xorResult': 0x0}), + Vector({'data': 0x5, 'valid': 1}, {'sum': 0x5, 'xorResult': 0x5}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('Case 10: struct output with only one field consumed (pruning stress)', + () async { + final data = Logic(name: 'data', width: 4); + final valid = Logic(name: 'valid'); + final mod = SingleFieldConsumer(data, valid); + await mod.build(); + + final sc = mod.generateSynth(); + expect(sc, contains('SingleFieldConsumer_W4')); + + // Only .valid is consumed; .data should be prunable + final vectors = [ + Vector({'data': 0xF, 'valid': 1}, {'validOnly': 1}), + Vector({'data': 0xF, 'valid': 0}, {'validOnly': 0}), + Vector({'data': 0x0, 'valid': 1}, {'validOnly': 1}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('Case 3 - build-only: struct output sub-module generateSynth', + () async { + final data = Logic(name: 'data', width: 8); + final valid = Logic(name: 'valid'); + final mod = ParentOfStructOutput(data, valid); + await mod.build(); + + // Just verify it doesn't throw during SystemC generation + SimCompare.checkSystemCVector(mod, [], buildOnly: true); + }); + + test('Case 5 - build-only: chained struct passthrough generateSynth', + () async { + final data = Logic(name: 'data', width: 8); + final valid = Logic(name: 'valid'); + final mod = ChainedStructPassthrough(data, valid); + await mod.build(); + + SimCompare.checkSystemCVector(mod, [], buildOnly: true); + }); + + test('Case 6 - build-only: partial struct consumer generateSynth', + () async { + final a = Logic(name: 'a', width: 8); + final b = Logic(name: 'b', width: 8); + final c = Logic(name: 'c', width: 8); + final mod = PartialStructConsumer(a, b, c); + await mod.build(); + + SimCompare.checkSystemCVector(mod, [], buildOnly: true); + }); + + test('Case 10 - build-only: single field consumer generateSynth', () async { + final data = Logic(name: 'data', width: 8); + final valid = Logic(name: 'valid'); + final mod = SingleFieldConsumer(data, valid); + await mod.build(); + + SimCompare.checkSystemCVector(mod, [], buildOnly: true); + }); + }); + + // ───────────────────────────────────────────────────────────────────────── + // Interface/PairInterface patterns that stress pruning + naming + // ───────────────────────────────────────────────────────────────────────── + group('SystemC generation with Interface struct patterns', () { + test('Case 11: sub-module with unused struct output (pruning path)', + () async { + // Pattern from TopWithUnusedSubModPorts: sub-module has a struct output + // that the parent doesn't consume. The struct output's leaf elements + // should be prunable without causing naming assertions. + final inp = Logic(name: 'inp', width: 8); + final mod = ParentWithUnusedStructOutput(inp); + await mod.build(); + + final sc = mod.generateSynth(); + expect(sc, contains('ParentWithUnusedStructOutput')); + // Should NOT assert on signal naming + + final vectors = [ + Vector({'inp': 0xAB}, {'usedOut': 0xAB}), + Vector({'inp': 0xCD}, {'usedOut': 0xCD}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test( + 'Case 12: sub-module struct output partially consumed (pruning stress)', + () async { + // Sub-module outputs a struct, parent only uses one field. + // The unused field's BusSubset slice should be properly handled. + final inp = Logic(name: 'inp', width: 8); + final mod = ParentPartialStructConsumption(inp); + await mod.build(); + + final sc = mod.generateSynth(); + expect(sc, contains('ParentPartialStructConsumption')); + + final vectors = [ + Vector({'inp': 0xAB}, {'dataOnly': 0xAB}), + Vector({'inp': 0x12}, {'dataOnly': 0x12}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('Case 13: mergeable signals connected to struct output elements', + () async { + // Tests that when a struct output element is connected to a + // Naming.mergeable local, the merged signal still gets a name. + final inp = Logic(name: 'inp', width: 8); + final mod = MergeableStructConsumer(inp); + await mod.build(); + + final sc = mod.generateSynth(); + expect(sc, contains('MergeableStructConsumer')); + + final vectors = [ + Vector({'inp': 0x55}, {'result': 0x55}), + Vector({'inp': 0xFF}, {'result': 0xFF}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('Case 14: PairInterface with struct-like multi-port pattern', + () async { + // Exercises the PairInterface pattern where multiple ports form a + // logical struct-like group, stressing the naming system. + final data = Logic(name: 'data', width: 8); + final valid = Logic(name: 'valid'); + final mod = PairInterfaceModule(data, valid); + await mod.build(); + + final sc = mod.generateSynth(); + expect(sc, contains('PairInterfaceModule')); + + final vectors = [ + Vector({'data': 0xAA, 'valid': 1}, {'dataOut': 0xAA, 'validOut': 1}), + Vector({'data': 0x55, 'valid': 0}, {'dataOut': 0x55, 'validOut': 0}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('Case 11 - build-only: unused struct output generateSynth', () async { + final inp = Logic(name: 'inp', width: 8); + final mod = ParentWithUnusedStructOutput(inp); + await mod.build(); + SimCompare.checkSystemCVector(mod, [], buildOnly: true); + }); + + test('Case 12 - build-only: partial struct consumption generateSynth', + () async { + final inp = Logic(name: 'inp', width: 8); + final mod = ParentPartialStructConsumption(inp); + await mod.build(); + SimCompare.checkSystemCVector(mod, [], buildOnly: true); + }); + + test('Case 13 - build-only: mergeable struct consumer generateSynth', + () async { + final inp = Logic(name: 'inp', width: 8); + final mod = MergeableStructConsumer(inp); + await mod.build(); + SimCompare.checkSystemCVector(mod, [], buildOnly: true); + }); + }); +} diff --git a/test/systemc_vector_test.dart b/test/systemc_vector_test.dart new file mode 100644 index 000000000..8d516663b --- /dev/null +++ b/test/systemc_vector_test.dart @@ -0,0 +1,1279 @@ +// Copyright (C) 2024-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_vector_test.dart +// Parallel SystemC simulation tests for all modules tested with iverilog. +// +// 2026 May 7 +// Author: Desmond A. Kirkpatrick + +import 'dart:math'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +// ===== Modules from flop_test.dart ===== + +class FlopTestModule extends Module { + FlopTestModule(Logic a, {Logic? en, Logic? reset, dynamic resetValue}) + : super(name: 'floptestmodule') { + a = addInput('a', a, width: a.width); + if (en != null) { + en = addInput('en', en); + } + if (reset != null) { + reset = addInput('reset', reset); + } + if (resetValue != null && resetValue is Logic) { + resetValue = addInput('resetValue', resetValue, width: a.width); + } + final y = addOutput('y', width: a.width); + final clk = SimpleClockGenerator(10).clk; + y <= flop(clk, a, en: en, reset: reset, resetValue: resetValue); + } +} + +// ===== Modules from counter_test.dart ===== + +class Counter extends Module { + final int width; + Logic get val => output('val'); + Counter(Logic en, Logic reset, {this.width = 8}) : super(name: 'counter') { + en = addInput('en', en); + reset = addInput('reset', reset); + final val = addOutput('val', width: width); + final nextVal = Logic(name: 'nextVal', width: width); + nextVal <= val + 1; + Sequential.multi([ + SimpleClockGenerator(10).clk, + reset + ], [ + If(reset, then: [ + val < 0 + ], orElse: [ + If(en, then: [val < nextVal]) + ]) + ]); + } +} + +// ===== Modules from comparison_test.dart ===== + +class ComparisonTestModule extends Module { + final int c; + ComparisonTestModule(Logic a, Logic b, {this.c = 5}) + : super(name: 'gatetestmodule') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + + final aEqB = addOutput('a_eq_b'); + final aNeqB = addOutput('a_neq_b'); + final aLtB = addOutput('a_lt_b'); + final aLteB = addOutput('a_lte_b'); + final aGtB = addOutput('a_gt_b'); + final aGteB = addOutput('a_gte_b'); + final aGtOperatorB = addOutput('a_gt_operator_b'); + final aGteOperatorB = addOutput('a_gte_operator_b'); + + final aEqC = addOutput('a_eq_c'); + final aNeqC = addOutput('a_neq_c'); + final aLtC = addOutput('a_lt_c'); + final aLteC = addOutput('a_lte_c'); + final aGtC = addOutput('a_gt_c'); + final aGteC = addOutput('a_gte_c'); + final aGtOperatorC = addOutput('a_gt_operator_c'); + final aGteOperatorC = addOutput('a_gte_operator_c'); + + aEqB <= a.eq(b); + aNeqB <= a.neq(b); + aLtB <= a.lt(b); + aLteB <= a.lte(b); + aGtB <= a.gt(b); + aGteB <= a.gte(b); + aGtOperatorB <= (a > b); + aGteOperatorB <= (a >= b); + + aEqC <= a.eq(c); + aNeqC <= a.neq(c); + aLtC <= a.lt(c); + aLteC <= a.lte(c); + aGtC <= a.gt(c); + aGteC <= a.gte(c); + aGtOperatorC <= (a > c); + aGteOperatorC <= (a >= c); + } +} + +// ===== Modules from arithmetic_shift_right_test.dart ===== + +class SraUnsignedTestModule extends Module { + Logic get result => output('result'); + SraUnsignedTestModule(Logic toShift, Logic shiftAmount, Logic maskBit) { + toShift = addInput('toShift', toShift, width: toShift.width); + shiftAmount = + addInput('shiftAmount', shiftAmount, width: shiftAmount.width); + maskBit = addInput('maskBit', maskBit); + addOutput('result', width: toShift.width); + result <= (toShift >> shiftAmount) & maskBit.replicate(toShift.width); + } +} + +// ===== Modules from collapse_test.dart ===== + +class CollapseTestModule extends Module { + CollapseTestModule(Logic a, Logic b) : super(name: 'collapsetestmodule') { + a = addInput('a', a); + b = addInput('b', b); + final c = addOutput('c'); + final d = addOutput('d'); + final e = addOutput('e'); + final f = addOutput('f'); + + final x = Logic(name: 'x'); + final y = Logic(name: 'y'); + final z = Logic(name: 'z', naming: Naming.mergeable); + c <= a & b; + d <= a & b; + x <= a; + y <= x; + e <= a & b & c & x & y; + z <= b & y; + f <= a & z; + + Logic(name: 'internal') <= ~z; + } +} + +// ===== Modules from extend_test.dart ===== + +class ExtendModule extends Module { + ExtendModule(Logic a, int newWidth, ExtendType extendType) { + a = addInput('a', a, width: a.width); + final b = addOutput('b', width: newWidth); + if (extendType == ExtendType.zero) { + b <= a.zeroExtend(newWidth); + } else { + b <= a.signExtend(newWidth); + } + } +} + +enum ExtendType { zero, sign } + +class WithSetModule extends Module { + WithSetModule(Logic a, int startIndex, Logic b) { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + final c = addOutput('c', width: a.width); + c <= a.withSet(startIndex, b); + } +} + +// ===== Modules from bus_test.dart ===== + +class BusTestModule extends Module { + BusTestModule(Logic a, Logic b) : super(name: 'bustestmodule') { + if (a.width != b.width) { + throw Exception('a and b must be same width.'); + } + if (a.width <= 3) { + throw Exception('a must be more than width 3.'); + } + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + + final aBar = addOutput('a_bar', width: a.width); + final aAndB = addOutput('a_and_b', width: a.width); + final aBJoined = addOutput('a_b_joined', width: a.width + b.width); + final aPlusB = addOutput('a_plus_b', width: a.width); + final a1 = addOutput('a1'); + final expressionBitSelect = addOutput('expression_bit_select', width: 4); + + final aReversed = addOutput('a_reversed', width: a.width); + final aShrunk1 = addOutput('a_shrunk1', width: 3); + final aShrunk2 = addOutput('a_shrunk2', width: 2); + final aShrunk3 = addOutput('a_shrunk3'); + final aNegativeShrunk1 = addOutput('a_neg_shrunk1', width: 3); + final aNegativeShrunk2 = addOutput('a_neg_shrunk2', width: 2); + final aNegativeShrunk3 = addOutput('a_neg_shrunk3'); + final aRSliced1 = addOutput('a_rsliced1', width: 5); + final aRSliced2 = addOutput('a_rsliced2', width: 2); + final aRSliced3 = addOutput('a_rsliced3'); + final aRNegativeSliced1 = addOutput('a_r_neg_sliced1', width: 5); + final aRNegativeSliced2 = addOutput('a_r_neg_sliced2', width: 2); + final aRNegativeSliced3 = addOutput('a_r_neg_sliced3'); + final aRange1 = addOutput('a_range1', width: 3); + final aRange2 = addOutput('a_range2', width: 2); + final aRange3 = addOutput('a_range3'); + final aRange4 = addOutput('a_range4', width: 3); + final aNegativeRange1 = addOutput('a_neg_range1', width: 3); + final aNegativeRange2 = addOutput('a_neg_range2', width: 2); + final aNegativeRange3 = addOutput('a_neg_range3'); + final aNegativeRange4 = addOutput('a_neg_range4', width: 3); + final aOperatorIndexing1 = addOutput('a_operator_indexing1'); + final aOperatorIndexing2 = addOutput('a_operator_indexing2'); + final aOperatorIndexing3 = addOutput('a_operator_indexing3'); + final aOperatorNegIndexing1 = addOutput('a_operator_neg_indexing1'); + final aOperatorNegIndexing2 = addOutput('a_operator_neg_indexing2'); + final aOperatorNegIndexing3 = addOutput('a_operator_neg_indexing3'); + + aBar <= ~a; + aAndB <= a & b; + aBJoined <= [b, a].swizzle(); + a1 <= a[1]; + aPlusB <= a + b; + + aShrunk1 <= a.slice(2, 0); + aShrunk2 <= a.slice(1, 0); + aShrunk3 <= a.slice(0, 0); + aNegativeShrunk1 <= a.slice(-6, 0); + aNegativeShrunk2 <= a.slice(-7, 0); + aNegativeShrunk3 <= a.slice(-8, 0); + + aRSliced1 <= a.slice(3, 7); + aRSliced2 <= a.slice(6, 7); + aRSliced3 <= a.slice(7, 7); + aRNegativeSliced1 <= a.slice(-5, -1); + aRNegativeSliced2 <= a.slice(-2, -1); + aRNegativeSliced3 <= a.slice(-1, -1); + + aRange1 <= a.getRange(5, 8); + aRange2 <= a.getRange(6, 8); + aRange3 <= a.getRange(7, 8); + aRange4 <= a.getRange(5); + aNegativeRange1 <= a.getRange(-3, 8); + aNegativeRange2 <= a.getRange(-2, 8); + aNegativeRange3 <= a.getRange(-1, 8); + aNegativeRange4 <= a.getRange(-3); + + aOperatorIndexing1 <= a.elements[0]; + aOperatorIndexing2 <= a[a.width - 1]; + aOperatorIndexing3 <= a[4]; + aOperatorNegIndexing1 <= a[-a.width]; + aOperatorNegIndexing2 <= a[-1]; + aOperatorNegIndexing3 <= a[-2]; + + aReversed <= a.reversed; + + expressionBitSelect <= + [aBJoined, aShrunk1, aRange1, aRSliced1, aPlusB].swizzle().slice(3, 0); + } +} + +class ConstBusModule extends Module { + ConstBusModule(int c, {required bool subset}) { + final outWidth = subset ? 8 : 16; + addOutput('const_subset', width: outWidth) <= + Const(c, width: 16).getRange(0, outWidth); + } +} + +class SingleBitBusSubsetMod extends Module { + SingleBitBusSubsetMod(Logic oneBit) { + oneBit = addInput('oneBit', oneBit); + addOutput('result') <= BusSubset(oneBit, 0, 0).subset; + } +} + +class SelectTestModule extends Module { + SelectTestModule(Logic a1, Logic a2, Logic a3, Logic b, {Logic? defaultValue}) + : super(name: 'selecttestmodule') { + a1 = addInput('a1', a1, width: a1.width); + a2 = addInput('a2', a2, width: a2.width); + a3 = addInput('a3', a3, width: a3.width); + b = addInput('b', b, width: b.width); + + if (defaultValue != null) { + defaultValue = + addInput('defaultValue', defaultValue, width: defaultValue.width); + _selectWithDefault(a1, a2, a3, b, defaultValue); + } else { + _selectWithout(a1, a2, a3, b); + } + } + + void _selectWithout(Logic a1, Logic a2, Logic a3, Logic b) { + final selectIndexValue = addOutput('selectIndexValue', width: a1.width); + final selectFromValue = addOutput('selectFromValue', width: a1.width); + final logicList = [a1, a2, a3]; + selectIndexValue <= logicList.selectIndex(b); + selectFromValue <= b.selectFrom(logicList); + } + + void _selectWithDefault( + Logic a1, Logic a2, Logic a3, Logic b, Logic defaultValue) { + final selectFromValue = addOutput('selectFromValue', width: a1.width); + final selectIndexValue = addOutput('selectIndexValue', width: a1.width); + final logicList = [a1, a2, a3]; + selectFromValue <= b.selectFrom(logicList, defaultValue: defaultValue); + selectIndexValue <= logicList.selectIndex(b, defaultValue: defaultValue); + } +} + +// ===== Modules from conditionals_test.dart ===== + +class LoopyCombModuleSsa extends Module { + Logic get a => input('a'); + Logic get x => output('x'); + LoopyCombModuleSsa(Logic a) : super(name: 'loopycombmodule') { + a = addInput('a', a); + final x = addOutput('x'); + Combinational.ssa((s) => [ + s(x) < a, + s(x) < ~s(x), + ]); + } +} + +class CaseModule extends Module { + CaseModule(Logic a, Logic b) : super(name: 'casemodule') { + a = addInput('a', a); + b = addInput('b', b); + final c = addOutput('c'); + final d = addOutput('d'); + final e = addOutput('e'); + + Combinational([ + Case( + [b, a].swizzle(), + [ + CaseItem(Const(LogicValue.ofString('01')), [c < 1, d < 0]), + CaseItem(Const(LogicValue.ofString('10')), [c < 1, d < 0]), + ], + defaultItem: [c < 0, d < 1], + conditionalType: ConditionalType.unique), + CaseZ( + [b, a].rswizzle(), + [ + CaseItem(Const(LogicValue.ofString('1z')), [e < 1]) + ], + defaultItem: [e < 0], + conditionalType: ConditionalType.priority) + ]); + } +} + +class IfBlockModule extends Module { + IfBlockModule(Logic a, Logic b) : super(name: 'ifblockmodule') { + a = addInput('a', a); + b = addInput('b', b); + final c = addOutput('c'); + final d = addOutput('d'); + + Combinational([ + If.block([ + Iff(a & ~b, [c < 1, d < 0]), + ElseIf(b & ~a, [c < 1, d < 0]), + Else([c < 0, d < 1]) + ]) + ]); + } +} + +class SingleIfBlockModule extends Module { + SingleIfBlockModule(Logic a) : super(name: 'singleifblockmodule') { + a = addInput('a', a); + final c = addOutput('c'); + Combinational([ + If.block([Iff.s(a, c < 1)]) + ]); + } +} + +class ElseIfBlockModule extends Module { + ElseIfBlockModule(Logic a, Logic b) : super(name: 'ifblockmodule') { + a = addInput('a', a); + b = addInput('b', b); + final c = addOutput('c'); + final d = addOutput('d'); + + Combinational([ + If.block([ + ElseIf(a & ~b, [c < 1, d < 0]), + ElseIf(b & ~a, [c < 1, d < 0]), + Else([c < 0, d < 1]) + ]) + ]); + } +} + +class SingleElseIfBlockModule extends Module { + SingleElseIfBlockModule(Logic a) : super(name: 'singleifblockmodule') { + a = addInput('a', a); + final c = addOutput('c'); + final d = addOutput('d'); + Combinational([ + If.block([ + ElseIf.s(a, c < 1), + Else([c < 0, d < 1]) + ]) + ]); + } +} + +class CombModule extends Module { + CombModule(Logic a, Logic b, Logic d) : super(name: 'combmodule') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + final z = addOutput('z'); + final x = addOutput('x'); + d = addInput('d', d, width: d.width); + final q = addOutput('q', width: d.width); + + Combinational([ + If(a, then: [ + y < a, + z < b, + x < a & b, + q < d, + ], orElse: [ + If(b, then: [ + y < b, + z < a, + q < 13, + ], orElse: [ + y < 0, + z < 1, + ]) + ]) + ]); + } +} + +class SequentialModule extends Module { + SequentialModule(Logic a, Logic b, Logic d) : super(name: 'ffmodule') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + final z = addOutput('z'); + final x = addOutput('x'); + d = addInput('d', d, width: d.width); + final q = addOutput('q', width: d.width); + + Sequential(SimpleClockGenerator(10).clk, [ + If(a, then: [ + q < d, + y < a, + z < b, + x < ~x, + ], orElse: [ + x < a, + If(b, then: [ + y < b, + z < a + ], orElse: [ + y < 0, + z < 1, + ]) + ]) + ]); + } +} + +class SingleIfModule extends Module { + SingleIfModule(Logic a) : super(name: 'combmodule') { + a = addInput('a', a); + final q = addOutput('q'); + Combinational([If.s(a, q < 1)]); + } +} + +class SingleIfOrElseModule extends Module { + SingleIfOrElseModule(Logic a, Logic b) : super(name: 'combmodule') { + a = addInput('a', a); + b = addInput('b', b); + final q = addOutput('q'); + final x = addOutput('x'); + Combinational([If.s(a, q < 1, x < 1)]); + } +} + +class SingleElseModule extends Module { + SingleElseModule(Logic a, Logic b) : super(name: 'combmodule') { + a = addInput('a', a); + b = addInput('b', b); + final q = addOutput('q'); + final x = addOutput('x'); + Combinational([ + If.block([Iff.s(a, q < 1), Else.s(x < 1)]) + ]); + } +} + +class SignalRedrivenSequentialModule extends Module { + SignalRedrivenSequentialModule(Logic a, Logic b, Logic d, + {required bool allowRedrive}) + : super(name: 'ffmodule') { + a = addInput('a', a); + b = addInput('b', b); + final q = addOutput('q', width: d.width); + d = addInput('d', d, width: d.width); + final k = addOutput('k', width: 8); + Sequential( + SimpleClockGenerator(10).clk, + [ + If(a, then: [k < k, q < k, q < d]) + ], + allowMultipleAssignments: allowRedrive, + ); + } +} + +// ===== Modules from assignment_test.dart ===== + +class ConstAssignModule extends Module { + ConstAssignModule() { + final out = addOutput('out'); + final val = Logic(name: 'val'); + val <= Const(1); + Combinational([out < val]); + } + + Logic get out => output('out'); +} + +// ========================================================================= +// Tests +// ========================================================================= + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + tearDownAll(() => SimCompare.cleanupSystemCCache(keepPch: false)); + + // ===== Flop tests (from flop_test.dart) ===== + group('flop', () { + test('flop bit', () async { + final ftm = FlopTestModule(Logic()); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'a': 0}, {}), + Vector({'a': 1}, {'y': 0}), + Vector({'a': 1}, {'y': 1}), + Vector({'a': 0}, {'y': 1}), + Vector({'a': 0}, {'y': 0}), + ]); + }); + + test('flop bit with enable', () async { + final ftm = FlopTestModule(Logic(), en: Logic()); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'a': 0, 'en': 1}, {}), + Vector({'a': 1, 'en': 1}, {'y': 0}), + Vector({'a': 1, 'en': 1}, {'y': 1}), + Vector({'a': 0, 'en': 1}, {'y': 1}), + Vector({'a': 0, 'en': 1}, {'y': 0}), + Vector({'a': 1, 'en': 1}, {'y': 0}), + Vector({'a': 1, 'en': 0}, {'y': 1}), + Vector({'a': 0, 'en': 0}, {'y': 1}), + Vector({'a': 0, 'en': 1}, {'y': 1}), + Vector({'a': 1, 'en': 1}, {'y': 0}), + Vector({'a': 0, 'en': 0}, {'y': 1}), + Vector({'a': 1, 'en': 0}, {'y': 1}), + ]); + }); + + test('flop bus', () async { + final ftm = FlopTestModule(Logic(width: 8)); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'a': 0}, {}), + Vector({'a': 0xff}, {'y': 0}), + Vector({'a': 0xaa}, {'y': 0xff}), + Vector({'a': 0x55}, {'y': 0xaa}), + Vector({'a': 0x1}, {'y': 0x55}), + ]); + }); + + test('flop bus with enable', () async { + final ftm = FlopTestModule(Logic(width: 8), en: Logic()); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'a': 0, 'en': 1}, {}), + Vector({'a': 0xff, 'en': 1}, {'y': 0}), + Vector({'a': 0xaa, 'en': 1}, {'y': 0xff}), + Vector({'a': 0x55, 'en': 1}, {'y': 0xaa}), + Vector({'a': 0x1, 'en': 1}, {'y': 0x55}), + Vector({'a': 0, 'en': 1}, {'y': 0x1}), + Vector({'a': 0xff, 'en': 1}, {'y': 0}), + Vector({'a': 0xaa, 'en': 1}, {'y': 0xff}), + Vector({'a': 0x55, 'en': 0}, {'y': 0xaa}), + Vector({'a': 0x1, 'en': 0}, {'y': 0xaa}), + Vector({'a': 0x55, 'en': 1}, {'y': 0xaa}), + Vector({'a': 0x1, 'en': 1}, {'y': 0x55}), + Vector({'a': 0x55, 'en': 0}, {'y': 0x1}), + Vector({'a': 0x1, 'en': 1}, {'y': 0x1}), + ]); + }); + + test('flop bus reset, no reset value', () async { + final ftm = FlopTestModule(Logic(width: 8), reset: Logic()); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'reset': 1}, {}), + Vector({'reset': 0, 'a': 0xa5}, {'y': 0}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + + test('flop bus reset, const reset value', () async { + final ftm = + FlopTestModule(Logic(width: 8), reset: Logic(), resetValue: 3); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'reset': 1}, {}), + Vector({'reset': 0, 'a': 0xa5}, {'y': 3}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + + test('flop bus reset, logic reset value', () async { + final ftm = FlopTestModule(Logic(width: 8), + reset: Logic(), resetValue: Logic(width: 8)); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'reset': 1, 'resetValue': 5}, {}), + Vector({'reset': 0, 'a': 0xa5}, {'y': 5}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + + test('flop bus no reset, const reset value', () async { + final ftm = FlopTestModule(Logic(width: 8), resetValue: 9); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({}, {}), + Vector({'a': 0xa5}, {}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + + test('flop bus, enable, reset, const reset value', () async { + final ftm = FlopTestModule(Logic(width: 8), + en: Logic(), reset: Logic(), resetValue: 12); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'reset': 1, 'en': 0}, {}), + Vector({'reset': 0, 'a': 0xa5}, {'y': 12}), + Vector({}, {'y': 12}), + Vector({'en': 1}, {'y': 12}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + }); + + // ===== Counter tests (from counter_test.dart) ===== + group('counter', () { + test('counter', () async { + final counter = Counter(Logic(), Logic()); + await counter.build(); + SimCompare.checkSystemCVector(counter, [ + Vector({'en': 0, 'reset': 0}, {}), + Vector({'en': 0, 'reset': 1}, {'val': 0}), + Vector({'en': 1, 'reset': 1}, {'val': 0}), + Vector({'en': 1, 'reset': 0}, {'val': 0}), + Vector({'en': 1, 'reset': 0}, {'val': 1}), + Vector({'en': 1, 'reset': 0}, {'val': 2}), + Vector({'en': 1, 'reset': 0}, {'val': 3}), + Vector({'en': 0, 'reset': 0}, {'val': 4}), + Vector({'en': 0, 'reset': 0}, {'val': 4}), + Vector({'en': 1, 'reset': 0}, {'val': 4}), + Vector({'en': 0, 'reset': 0}, {'val': 5}), + ]); + }); + }); + + // ===== Comparison tests (from comparison_test.dart) ===== + group('comparison', () { + test('compares', () async { + final gtm = ComparisonTestModule(Logic(width: 8), Logic(width: 8)); + await gtm.build(); + SimCompare.checkSystemCVector(gtm, [ + Vector({ + 'a': 0, + 'b': 0 + }, { + 'a_eq_b': 1, + 'a_neq_b': 0, + 'a_lt_b': 0, + 'a_lte_b': 1, + 'a_gt_b': 0, + 'a_gte_b': 1, + 'a_gt_operator_b': 0, + 'a_gte_operator_b': 1, + 'a_eq_c': 0, + 'a_neq_c': 1, + 'a_lt_c': 1, + 'a_lte_c': 1, + 'a_gt_c': 0, + 'a_gte_c': 0, + 'a_gt_operator_c': 0, + 'a_gte_operator_c': 0, + }), + Vector({ + 'a': 5, + 'b': 6 + }, { + 'a_eq_b': 0, + 'a_neq_b': 1, + 'a_lt_b': 1, + 'a_lte_b': 1, + 'a_gt_b': 0, + 'a_gte_b': 0, + 'a_gt_operator_b': 0, + 'a_gte_operator_b': 0, + 'a_eq_c': 1, + 'a_neq_c': 0, + 'a_lt_c': 0, + 'a_lte_c': 1, + 'a_gt_c': 0, + 'a_gte_c': 1, + 'a_gt_operator_c': 0, + 'a_gte_operator_c': 1, + }), + Vector({ + 'a': 9, + 'b': 7 + }, { + 'a_eq_b': 0, + 'a_neq_b': 1, + 'a_lt_b': 0, + 'a_lte_b': 0, + 'a_gt_b': 1, + 'a_gte_b': 1, + 'a_gt_operator_b': 1, + 'a_gte_operator_b': 1, + 'a_eq_c': 0, + 'a_neq_c': 1, + 'a_lt_c': 0, + 'a_lte_c': 0, + 'a_gt_c': 1, + 'a_gte_c': 1, + 'a_gt_operator_c': 1, + 'a_gte_operator_c': 1, + }), + ]); + }); + }); + + // ===== Arithmetic shift right tests ===== + group('arithmetic shift right', () { + test('shift right and mask', () async { + final mod = + SraUnsignedTestModule(Logic(width: 32), Logic(width: 32), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'toShift': 0xe0000000, 'shiftAmount': 4, 'maskBit': 1}, + {'result': 0xfe000000}), + Vector({'toShift': 0x10000000, 'shiftAmount': 4, 'maskBit': 1}, + {'result': 0x01000000}), + Vector({'toShift': 0xe0000000, 'shiftAmount': 4, 'maskBit': 0}, + {'result': 0}), + ]); + }); + }); + + // ===== Collapse tests ===== + group('collapse', () { + test('collapse functional', () async { + final mod = CollapseTestModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1, 'b': 1}, {'c': 1, 'd': 1, 'e': 1, 'f': 1}), + Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 0, 'e': 0, 'f': 0}), + ]); + }); + }); + + // ===== Extend tests ===== + group('extend', () { + Future extendVectors( + List vectors, int newWidth, ExtendType extendType, + {int originalWidth = 8}) async { + final mod = + ExtendModule(Logic(width: originalWidth), newWidth, extendType); + await mod.build(); + SimCompare.checkSystemCVector(mod, vectors); + } + + test('zero extend same width', () async { + await extendVectors([ + Vector({'a': 0}, {'b': 0}), + Vector({'a': 0xff}, {'b': 0xff}), + Vector({'a': 0x5a}, {'b': 0x5a}), + ], 8, ExtendType.zero); + }); + + test('sign extend same width', () async { + await extendVectors([ + Vector({'a': 0}, {'b': 0}), + Vector({'a': 0xff}, {'b': 0xff}), + Vector({'a': 0x5a}, {'b': 0x5a}), + ], 8, ExtendType.sign); + }); + + test('zero extend pads 0s', () async { + await extendVectors([ + Vector({'a': 0xff}, {'b': 0xff}), + Vector({'a': 0x5a}, {'b': 0x5a}), + ], 12, ExtendType.zero); + }); + + test('sign extend positive pads 0s', () async { + await extendVectors([ + Vector({'a': 0x5a}, {'b': 0x5a}), + ], 12, ExtendType.sign); + }); + + test('sign extend negative pads 1s', () async { + await extendVectors([ + Vector({'a': 0xff}, {'b': 0xfff}), + ], 12, ExtendType.sign); + }); + + test('sign extend single bit(0) pads 0s', () async { + await extendVectors([ + Vector({'a': LogicValue.zero}, {'b': 0x000}), + ], 12, ExtendType.sign, originalWidth: 1); + }); + + test('sign extend single bit(1) pads 1s', () async { + await extendVectors([ + Vector({'a': LogicValue.one}, {'b': 0xfff}), + ], 12, ExtendType.sign, originalWidth: 1); + }); + }); + + group('withSet', () { + Future withSetVectors( + List vectors, int startIndex, int updateWidth) async { + final mod = + WithSetModule(Logic(width: 8), startIndex, Logic(width: updateWidth)); + await mod.build(); + SimCompare.checkSystemCVector(mod, vectors); + } + + test('setting same width', () async { + await withSetVectors([ + Vector({'a': 0x23, 'b': 0xff}, {'c': 0xff}), + Vector({'a': 0x45, 'b': 0x5a}, {'c': 0x5a}), + ], 0, 8); + }); + + test('setting at front', () async { + await withSetVectors([ + Vector({'a': 0x23, 'b': 0xf}, {'c': 0x2f}), + Vector({'a': 0x4a, 'b': 0x5}, {'c': 0x45}), + ], 0, 4); + }); + + test('setting at end', () async { + await withSetVectors([ + Vector({'a': 0x23, 'b': 0xf}, {'c': 0xf3}), + Vector({'a': 0x4a, 'b': 0x5}, {'c': 0x5a}), + ], 4, 4); + }); + + test('setting in the middle', () async { + await withSetVectors([ + Vector({'a': 0xff, 'b': 0x0}, {'c': bin('11000011')}), + Vector( + {'a': bin('01111110'), 'b': bin('0110')}, {'c': bin('01011010')}), + ], 2, 4); + }); + }); + + // ===== Bus tests ===== + group('bus', () { + test('single-bit bus subset', () async { + final mod = SingleBitBusSubsetMod(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'oneBit': 0}, {'result': 0}), + Vector({'oneBit': 1}, {'result': 1}), + ]); + }); + + test('const subset', () async { + final mod = ConstBusModule(0xabcd, subset: true); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({}, {'const_subset': 0xcd}), + ]); + }); + + test('const assignment', () async { + final mod = ConstBusModule(0xabcd, subset: false); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({}, {'const_subset': 0xabcd}), + ]); + }); + + // All tests below share the same BusTestModule — compile once + group('BusTestModule', () { + SystemCExecutable? exe; + + setUpAll(() async { + final gtm = BusTestModule(Logic(width: 8), Logic(width: 8)); + await gtm.build(); + exe = SimCompare.buildSystemCExecutable(gtm); + }); + + tearDownAll(() { + exe?.cleanup(); + }); + + test('NotGate bus', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0xff}, {'a_bar': 0}), + Vector({'a': 0}, {'a_bar': 0xff}), + Vector({'a': 0x55}, {'a_bar': 0xaa}), + Vector({'a': 1}, {'a_bar': 0xfe}), + ]); + }); + + test('And2Gate bus', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0, 'b': 0}, {'a_and_b': 0}), + Vector({'a': 0, 'b': 1}, {'a_and_b': 0}), + Vector({'a': 1, 'b': 0}, {'a_and_b': 0}), + Vector({'a': 1, 'b': 1}, {'a_and_b': 1}), + Vector({'a': 0xff, 'b': 0xaa}, {'a_and_b': 0xaa}), + ]); + }); + + test('Operator indexing', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': bin('11111110')}, {'a_operator_indexing1': 0}), + Vector({'a': bin('10000000')}, {'a_operator_indexing2': 1}), + Vector({'a': bin('11101111')}, {'a_operator_indexing3': 0}), + Vector({'a': bin('11111110')}, {'a_operator_neg_indexing1': 0}), + Vector({'a': bin('10000000')}, {'a_operator_neg_indexing2': 1}), + Vector({'a': bin('10111111')}, {'a_operator_neg_indexing3': 0}), + ]); + }); + + test('Bus shrink', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a_shrunk1': 0}), + Vector({'a': 0xfa}, {'a_shrunk1': bin('010')}), + Vector({'a': 0xab}, {'a_shrunk1': 3}), + Vector({'a': 0}, {'a_shrunk2': 0}), + Vector({'a': 0xec}, {'a_shrunk2': bin('00')}), + Vector({'a': 0xfa}, {'a_shrunk2': 2}), + Vector({'a': 0}, {'a_shrunk3': 0}), + Vector({'a': 0xff}, {'a_shrunk3': bin('1')}), + Vector({'a': 0xba}, {'a_shrunk3': 0}), + Vector({'a': 0}, {'a_neg_shrunk1': 0}), + Vector({'a': 0xfa}, {'a_neg_shrunk1': bin('010')}), + Vector({'a': 0xab}, {'a_neg_shrunk1': 3}), + Vector({'a': 0}, {'a_neg_shrunk2': 0}), + Vector({'a': 0xec}, {'a_neg_shrunk2': bin('00')}), + Vector({'a': 0xfa}, {'a_neg_shrunk2': 2}), + Vector({'a': 0}, {'a_neg_shrunk3': 0}), + Vector({'a': 0xff}, {'a_neg_shrunk3': bin('1')}), + Vector({'a': 0xba}, {'a_neg_shrunk3': 0}), + ]); + }); + + test('Bus reverse slice', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a_rsliced1': 0}), + Vector({'a': 0xac}, {'a_rsliced1': bin('10101')}), + Vector({'a': 0xf5}, {'a_rsliced1': 0xf}), + Vector({'a': 0}, {'a_rsliced2': 0}), + Vector({'a': 0xab}, {'a_rsliced2': bin('01')}), + Vector({'a': 0xac}, {'a_rsliced2': 1}), + Vector({'a': 0}, {'a_rsliced3': 0}), + Vector({'a': 0xaf}, {'a_rsliced3': bin('1')}), + Vector({'a': 0xaf}, {'a_rsliced3': 1}), + Vector({'a': 0}, {'a_r_neg_sliced1': 0}), + Vector({'a': 0xac}, {'a_r_neg_sliced1': bin('10101')}), + Vector({'a': 0xf5}, {'a_r_neg_sliced1': 0xf}), + Vector({'a': 0}, {'a_r_neg_sliced2': 0}), + Vector({'a': 0xab}, {'a_r_neg_sliced2': bin('01')}), + Vector({'a': 0xac}, {'a_r_neg_sliced2': 1}), + Vector({'a': 0}, {'a_r_neg_sliced3': 0}), + Vector({'a': 0xaf}, {'a_r_neg_sliced3': bin('1')}), + Vector({'a': 0xaf}, {'a_r_neg_sliced3': 1}), + ]); + }); + + test('Bus reversed', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a_reversed': 0}), + Vector({'a': 0xff}, {'a_reversed': 0xff}), + Vector({'a': 0xf5}, {'a_reversed': 0xaf}), + ]); + }); + + test('Bus range', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a_range1': 0}), + Vector({'a': 0xaf}, {'a_range1': 5}), + Vector({'a': bin('11000101')}, {'a_range1': bin('110')}), + Vector({'a': 0}, {'a_range2': 0}), + Vector({'a': 0xaf}, {'a_range2': 2}), + Vector({'a': bin('10111111')}, {'a_range2': bin('10')}), + Vector({'a': 0}, {'a_range3': 0}), + Vector({'a': 0x80}, {'a_range3': 1}), + Vector({'a': bin('10000000')}, {'a_range3': bin('1')}), + Vector({'a': 0}, {'a_range4': 0}), + Vector({'a': 0xaf}, {'a_range4': 5}), + Vector({'a': bin('11000101')}, {'a_range4': bin('110')}), + Vector({'a': 0}, {'a_neg_range1': 0}), + Vector({'a': 0xaf}, {'a_neg_range1': 5}), + Vector({'a': bin('11000101')}, {'a_neg_range1': bin('110')}), + Vector({'a': 0}, {'a_neg_range2': 0}), + Vector({'a': 0xaf}, {'a_neg_range2': 2}), + Vector({'a': bin('10111111')}, {'a_neg_range2': bin('10')}), + Vector({'a': 0}, {'a_neg_range3': 0}), + Vector({'a': 0x80}, {'a_neg_range3': 1}), + Vector({'a': bin('10000000')}, {'a_neg_range3': bin('1')}), + Vector({'a': 0}, {'a_neg_range4': 0}), + Vector({'a': 0xaf}, {'a_neg_range4': 5}), + Vector({'a': bin('11000101')}, {'a_neg_range4': bin('110')}), + ]); + }); + + test('Bus swizzle', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0, 'b': 0}, {'a_b_joined': 0}), + Vector({'a': 0xff, 'b': 0xff}, {'a_b_joined': 0xffff}), + Vector({'a': 0xff, 'b': 0}, {'a_b_joined': 0xff}), + Vector({'a': 0, 'b': 0xff}, {'a_b_joined': 0xff00}), + Vector({'a': 0xaa, 'b': 0x55}, {'a_b_joined': 0x55aa}), + ]); + }); + + test('Bus bit', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a1': 0}), + Vector({'a': 0xff}, {'a1': 1}), + Vector({'a': 0xf5}, {'a1': 0}), + ]); + }); + + test('add busses', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0, 'b': 0}, {'a_plus_b': 0}), + Vector({'a': 0, 'b': 1}, {'a_plus_b': 1}), + Vector({'a': 1, 'b': 0}, {'a_plus_b': 1}), + Vector({'a': 1, 'b': 1}, {'a_plus_b': 2}), + Vector({'a': 6, 'b': 7}, {'a_plus_b': 13}), + ]); + }); + + test('expression bit select', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 1, 'b': 1}, {'expression_bit_select': 2}), + ]); + }); + }); // end BusTestModule group + + test('selectFrom and selectIndex', () async { + final gtm = SelectTestModule(Logic(width: 8), Logic(width: 8), + Logic(width: 8), Logic(width: (log(8) / log(2)).ceil())); + await gtm.build(); + SimCompare.checkSystemCVector(gtm, [ + Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 1}, + {'selectIndexValue': 2, 'selectFromValue': 2}), + Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 0}, + {'selectIndexValue': 1, 'selectFromValue': 1}), + Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 2}, + {'selectIndexValue': 3, 'selectFromValue': 3}), + ]); + }); + + test('selectFrom with default Value', () async { + final gtm = SelectTestModule(Logic(width: 8), Logic(width: 8), + Logic(width: 8), Logic(width: (log(8) / log(2)).ceil()), + defaultValue: Logic(width: 8)); + await gtm.build(); + SimCompare.checkSystemCVector(gtm, [ + Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 4, 'defaultValue': 5}, + {'selectFromValue': 5, 'selectIndexValue': 5}), + ]); + }); + }); + + // ===== Conditionals tests ===== + group('conditionals', () { + test('conditional comb', () async { + final mod = CombModule(Logic(), Logic(), Logic(width: 10)); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0, 'b': 0, 'd': 5}, + {'y': 0, 'z': 1, 'x': LogicValue.x, 'q': LogicValue.x}), + Vector({'a': 0, 'b': 1, 'd': 6}, + {'y': 1, 'z': 0, 'x': LogicValue.x, 'q': 13}), + Vector({'a': 1, 'b': 0, 'd': 7}, {'y': 1, 'z': 0, 'x': 0, 'q': 7}), + Vector({'a': 1, 'b': 1, 'd': 8}, {'y': 1, 'z': 1, 'x': 1, 'q': 8}), + ]); + }); + + test('iffblock comb', () async { + final mod = IfBlockModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 1}), + Vector({'a': 0, 'b': 1}, {'c': 1, 'd': 0}), + Vector({'a': 1, 'b': 0}, {'c': 1, 'd': 0}), + Vector({'a': 1, 'b': 1}, {'c': 0, 'd': 1}), + ]); + }); + + test('single iffblock comb', () async { + final mod = SingleIfBlockModule(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'c': 1}), + ]); + }); + + test('elseifblock comb', () async { + final mod = ElseIfBlockModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 1}), + Vector({'a': 0, 'b': 1}, {'c': 1, 'd': 0}), + Vector({'a': 1, 'b': 0}, {'c': 1, 'd': 0}), + Vector({'a': 1, 'b': 1}, {'c': 0, 'd': 1}), + ]); + }); + + test('single elseifblock comb', () async { + final mod = SingleElseIfBlockModule(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'c': 1}), + Vector({'a': 0}, {'c': 0, 'd': 1}), + ]); + }); + + test('case comb', () async { + final mod = CaseModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 1, 'e': 0}), + Vector({'a': 0, 'b': 1}, {'c': 1, 'd': 0, 'e': 0}), + Vector({'a': 1, 'b': 0}, {'c': 1, 'd': 0, 'e': 1}), + Vector({'a': 1, 'b': 1}, {'c': 0, 'd': 1, 'e': 1}), + ]); + }); + + test('conditional ff', () async { + final mod = SequentialModule(Logic(), Logic(), Logic(width: 8)); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1, 'd': 1}, {}), + Vector({'a': 0, 'b': 0, 'd': 2}, {'q': 1}), + Vector({'a': 0, 'b': 1, 'd': 3}, {'y': 0, 'z': 1, 'x': 0, 'q': 1}), + Vector({'a': 1, 'b': 0, 'd': 4}, {'y': 1, 'z': 0, 'x': 0, 'q': 1}), + Vector({'a': 1, 'b': 1, 'd': 5}, {'y': 1, 'z': 0, 'x': 1, 'q': 4}), + Vector({}, {'y': 1, 'z': 1, 'x': 0, 'q': 5}), + ]); + }); + + test('loopy comb ssa', () async { + final mod = LoopyCombModuleSsa(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0}, {'x': 1}), + Vector({'a': 1}, {'x': 0}), + ]); + }); + + test('single if', () async { + final mod = SingleIfModule(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'q': 1}), + ]); + }); + + test('single if or else', () async { + final mod = SingleIfOrElseModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'q': 1}), + Vector({'a': 0}, {'x': 1}), + ]); + }); + + test('single else', () async { + final mod = SingleElseModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'q': 1}), + Vector({'a': 0}, {'x': 1}), + ]); + }); + + test('redrive allowed', () async { + final mod = SignalRedrivenSequentialModule( + Logic(), Logic(), Logic(width: 8), + allowRedrive: true); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1, 'd': 1}, {}), + Vector({'a': 1, 'b': 0, 'd': 2}, {'q': 1}), + Vector({'a': 1, 'b': 0, 'd': 3}, {'q': 2}), + ]); + }); + }); + + // ===== Assignment tests ===== + group('assignment', () { + test('const comb assignment', () async { + final mod = ConstAssignModule(); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({}, {'out': 1}), + ]); + }); + }); +} diff --git a/test/translations_test.dart b/test/translations_test.dart index 6d53d4f74..ed4b574d4 100644 --- a/test/translations_test.dart +++ b/test/translations_test.dart @@ -126,6 +126,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); }); } diff --git a/test/typed_port_test.dart b/test/typed_port_test.dart index ff31896d5..a1d748032 100644 --- a/test/typed_port_test.dart +++ b/test/typed_port_test.dart @@ -243,6 +243,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('typed array is an array', () async { @@ -271,6 +272,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('structure containing ports naming properly', () async { @@ -492,6 +494,9 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + if (!portType.name.contains('net')) { + SimCompare.checkSystemCVector(mod, vectors); + } }); } }); @@ -514,5 +519,6 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); } diff --git a/tool/gh_actions/install_systemc.sh b/tool/gh_actions/install_systemc.sh new file mode 100755 index 000000000..4ab6a5cb9 --- /dev/null +++ b/tool/gh_actions/install_systemc.sh @@ -0,0 +1,56 @@ +#!/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}" + +# Skip if already installed (e.g. from cache) +if [ -f "$INSTALL_PREFIX/lib/libsystemc.so" ]; then + echo "SystemC already installed at $INSTALL_PREFIX — skipping build." + exit 0 +fi + +echo "Installing Accellera SystemC $SYSTEMC_VERSION to $INSTALL_PREFIX ..." + +# Install build dependencies +sudo apt-get update -qq +sudo apt-get install --yes --no-install-recommends cmake g++ make + +# Download source +TARBALL="systemc-$SYSTEMC_VERSION.tar.gz" +DOWNLOAD_URL="https://github.com/accellera-official/systemc/archive/refs/tags/$SYSTEMC_VERSION.tar.gz" + +cd /tmp +curl -fsSL -o "$TARBALL" "$DOWNLOAD_URL" +tar xzf "$TARBALL" +cd "systemc-$SYSTEMC_VERSION" + +# Build with CMake +mkdir -p build && cd build +cmake .. \ + -DCMAKE_INSTALL_PREFIX="$INSTALL_PREFIX" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_STANDARD=17 \ + -DBUILD_SHARED_LIBS=ON \ + -DENABLE_EXAMPLES=OFF \ + -DENABLE_REGRESSION=OFF \ + -DDISABLE_COPYRIGHT_MESSAGE=ON + +make -j"$(nproc)" +sudo make install + +echo "SystemC $SYSTEMC_VERSION installed to $INSTALL_PREFIX" diff --git a/tool/gh_actions/run_tests.sh b/tool/gh_actions/run_tests.sh index 160352cd2..5e2804edc 100755 --- a/tool/gh_actions/run_tests.sh +++ b/tool/gh_actions/run_tests.sh @@ -11,8 +11,9 @@ set -euo pipefail -dart test +# Exclude FFI-dependent tests (dart:ffi unavailable on some CI platforms). +dart test $(find test -name '*_test.dart' ! -name 'systemc_ffi_cosim_test.dart' | sort) # run tests in JS (increase heap size also) export NODE_OPTIONS="--max-old-space-size=8192" -dart test --platform node \ No newline at end of file +dart test --platform node $(find test -name '*_test.dart' ! -name 'systemc_ffi_cosim_test.dart' | sort) \ No newline at end of file diff --git a/tool/gh_actions/setup_systemc_pch.sh b/tool/gh_actions/setup_systemc_pch.sh new file mode 100755 index 000000000..25d1ff93f --- /dev/null +++ b/tool/gh_actions/setup_systemc_pch.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Copyright (C) 2024-2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# setup_systemc_pch.sh +# GitHub Actions step: Pre-build SystemC precompiled header and Makefile. +# +# Run this after install_systemc.sh and before tests to avoid race +# conditions when multiple test isolates run in parallel. +# +# 2026 May +# Author: Desmond Kirkpatrick + +set -euo pipefail + +SC_HOME="${SYSTEMC_INCLUDE:-/opt/systemc/include}" +SC_LIB="${SYSTEMC_LIB:-/opt/systemc/lib}" + +if [ ! -d "$SC_HOME" ]; then + echo "SystemC not found at $SC_HOME — skipping PCH setup." + exit 0 +fi + +# Detect C++ standard from the installed library +CXX_STD="c++17" +if command -v nm &>/dev/null && [ -f "$SC_LIB/libsystemc.so" ]; then + if nm -D "$SC_LIB/libsystemc.so" 2>/dev/null | grep -q 'cxx202002L'; then + CXX_STD="c++20" + fi +fi + +echo "Setting up SystemC PCH ($CXX_STD) ..." + +# Build precompiled header +PCH_DIR="tmp_test/pch" +mkdir -p "$PCH_DIR" +cp "$SC_HOME/systemc.h" "$PCH_DIR/systemc.h" +g++ -std="$CXX_STD" -I"$SC_HOME" -x c++-header \ + -o "$PCH_DIR/systemc.h.gch" "$SC_HOME/systemc.h" + +echo "PCH built: $PCH_DIR/systemc.h.gch" diff --git a/tool/gh_codespaces/run_setup.sh b/tool/gh_codespaces/run_setup.sh index 6523e4147..ba1d14a97 100755 --- a/tool/gh_codespaces/run_setup.sh +++ b/tool/gh_codespaces/run_setup.sh @@ -20,5 +20,11 @@ tool/gh_actions/install_dependencies.sh # Install Icarus Verilog. tool/gh_actions/install_iverilog.sh +# Install Accellera SystemC. +tool/gh_actions/install_systemc.sh + +# Pre-build SystemC precompiled header and Makefile. +tool/gh_actions/setup_systemc_pch.sh + # Install Node tool/gh_actions/install_node.sh \ No newline at end of file