diff --git a/lib/rohd.dart b/lib/rohd.dart index 841505590..3f2dc72a3 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -6,6 +6,7 @@ export 'src/external.dart'; export 'src/finite_state_machine.dart'; export 'src/interfaces/interfaces.dart'; export 'src/module.dart'; +export 'src/module_parameter.dart'; export 'src/modules/modules.dart'; export 'src/selection.dart'; export 'src/signals/signals.dart'; diff --git a/lib/src/module.dart b/lib/src/module.dart index 92fc410e0..c1633476e 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -55,6 +55,26 @@ abstract class Module { /// An internal mapping of inOut names to their sources to this [Module]. late final Map _inOutSources = {}; + /// Registered [ModuleParameter]s for this [Module]. + /// + /// These are used during SystemVerilog generation to emit `parameter` or + /// `localparam` declarations in the module definition header. + /// + /// Use [addModuleParameter] to register parameters. + final List> _moduleParameters = []; + + /// An unmodifiable view of the registered [ModuleParameter]s. + List> get moduleParameters => + List.unmodifiable(_moduleParameters); + + /// Registers a [ModuleParameter] on this [Module]. + /// + /// The parameter will appear in generated SystemVerilog as a `parameter` or + /// `localparam` declaration in the module definition header. + void addModuleParameter(ModuleParameter param) { + _moduleParameters.add(param); + } + /// The parent [Module] of this [Module]. /// /// This only gets populated after its parent [Module], if it exists, has @@ -660,7 +680,8 @@ abstract class Module { /// The return value is the same as what is returned by [input] and should /// only be used within this [Module]. The provided [source] is accessible via /// [inputSource]. - Logic addInput(String name, Logic source, {int width = 1}) { + Logic addInput(String name, Logic source, + {int width = 1, ParameterExpression? widthExpression}) { _checkForSafePortName(name); if (source.width != width) { throw PortWidthMismatchException(source, width); @@ -670,7 +691,11 @@ abstract class Module { source = source.packed; } - final inPort = Logic(name: name, width: width, naming: Naming.reserved) + final inPort = Logic( + name: name, + width: width, + naming: Naming.reserved, + widthExpression: widthExpression) ..gets(source) ..parentModule = this; @@ -741,7 +766,8 @@ abstract class Module { /// The return value is the same as what is returned by [inOut] and should /// only be used within this [Module]. The provided [source] is accessible via /// [inOutSource]. - LogicNet addInOut(String name, Logic source, {int width = 1}) { + LogicNet addInOut(String name, Logic source, + {int width = 1, ParameterExpression? widthExpression}) { _checkForSafePortName(name); if (source.width != width) { throw PortWidthMismatchException(source, width); @@ -773,10 +799,13 @@ abstract class Module { _inOutDrivers.add(source.packed); } - final inOutPort = - LogicNet(name: name, width: width, naming: Naming.reserved) - ..parentModule = this - ..gets(source); + final inOutPort = LogicNet( + name: name, + width: width, + naming: Naming.reserved, + widthExpression: widthExpression) + ..parentModule = this + ..gets(source); _inOuts[name] = inOutPort; @@ -868,6 +897,8 @@ abstract class Module { List dimensions = const [1], int elementWidth = 1, int numUnpackedDimensions = 0, + List? dimensionExpressions, + ParameterExpression? elementWidthExpression, }) { _checkForSafePortName(name); @@ -877,6 +908,8 @@ abstract class Module { elementWidth, numUnpackedDimensions: numUnpackedDimensions, naming: Naming.reserved, + dimensionExpressions: dimensionExpressions, + elementWidthExpression: elementWidthExpression, ) ..gets(source) ..setAllParentModule(this); @@ -892,10 +925,15 @@ abstract class Module { /// can be driven by this [Module] or consumed outside of it. /// /// The return value is the same as what is returned by [output]. - Logic addOutput(String name, {int width = 1}) { + Logic addOutput(String name, + {int width = 1, ParameterExpression? widthExpression}) { _checkForSafePortName(name); - final outPort = Logic(name: name, width: width, naming: Naming.reserved) + final outPort = Logic( + name: name, + width: width, + naming: Naming.reserved, + widthExpression: widthExpression) ..parentModule = this; _outputs[name] = outPort; @@ -992,6 +1030,8 @@ abstract class Module { List dimensions = const [1], int elementWidth = 1, int numUnpackedDimensions = 0, + List? dimensionExpressions, + ParameterExpression? elementWidthExpression, }) { _checkForSafePortName(name); @@ -1001,6 +1041,8 @@ abstract class Module { elementWidth, numUnpackedDimensions: numUnpackedDimensions, naming: Naming.reserved, + dimensionExpressions: dimensionExpressions, + elementWidthExpression: elementWidthExpression, )..setAllParentModule(this); _outputs[name] = outArr; @@ -1022,6 +1064,8 @@ abstract class Module { List dimensions = const [1], int elementWidth = 1, int numUnpackedDimensions = 0, + List? dimensionExpressions, + ParameterExpression? elementWidthExpression, }) { _checkForSafePortName(name); @@ -1046,6 +1090,8 @@ abstract class Module { elementWidth, numUnpackedDimensions: numUnpackedDimensions, naming: Naming.reserved, + dimensionExpressions: dimensionExpressions, + elementWidthExpression: elementWidthExpression, ) ..gets(source) ..setAllParentModule(this); diff --git a/lib/src/module_parameter.dart b/lib/src/module_parameter.dart new file mode 100644 index 000000000..597f77b33 --- /dev/null +++ b/lib/src/module_parameter.dart @@ -0,0 +1,227 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_parameter.dart +// Definition of configurable module parameters for SV generation. +// +// 2026 June +// Author: Joel Kimmel + +import 'package:rohd/rohd.dart'; + +/// Represents a configurable parameter on a [Module] that maps to a +/// SystemVerilog `parameter` or `localparam` declaration. +/// +/// In simulation, [ModuleParameter] wraps a concrete Dart value of type [T] +/// and behaves identically to that value. In generated SystemVerilog, it +/// appears as a named parameter in the module definition. +/// +/// Example: +/// ```dart +/// final width = ModuleParameter('WIDTH', defaultValue: 8); +/// final depth = ModuleParameter('DEPTH', +/// defaultValue: 256, isLocalParam: true); +/// ``` +class ModuleParameter { + /// The SystemVerilog name for this parameter (e.g., `'WIDTH'`). + final String name; + + /// The concrete value of this parameter used during simulation. + final T defaultValue; + + /// The SystemVerilog type string for this parameter (e.g., `'int'`). + /// + /// If not provided, it is inferred from [T]: + /// - `int` → `'int'` + /// - `bool` → `'bit'` + /// - Otherwise, must be provided explicitly. + final String svType; + + /// Whether this parameter is a `localparam` (cannot be overridden at + /// instantiation) rather than a `parameter`. + final bool isLocalParam; + + /// A SystemVerilog expression for the default value. + /// + /// If not provided, it is generated from [defaultValue] using + /// [_defaultSvValue]. + final String? svDefaultValue; + + /// Creates a [ModuleParameter] with the given [name] and [defaultValue]. + /// + /// The [svType] is inferred from [T] if not provided. Set [isLocalParam] + /// to `true` for a `localparam` declaration. + ModuleParameter( + this.name, { + required this.defaultValue, + String? svType, + this.isLocalParam = false, + this.svDefaultValue, + }) : svType = svType ?? _inferSvType(); + + /// Infers a SystemVerilog type string from the Dart type [U]. + static String _inferSvType() { + if (U == int) { + return 'int'; + } + if (U == bool) { + return 'bit'; + } + throw ArgumentError('Cannot infer SV type for Dart type $U. ' + 'Provide an explicit svType.'); + } + + /// Returns the SV default value string for this parameter. + String get svDefault => svDefaultValue ?? _defaultSvValue(); + + /// Generates a default SV value string from [defaultValue]. + String _defaultSvValue() { + final v = defaultValue; + if (v is int) { + return '$v'; + } + if (v is bool) { + return v ? "1'b1" : "1'b0"; + } + return '$v'; + } + + /// Converts this parameter to a [SystemVerilogParameterDefinition] suitable + /// for inclusion in a module definition header. + SystemVerilogParameterDefinition toSvParameterDefinition() => + SystemVerilogParameterDefinition( + name, + type: svType, + defaultValue: svDefault, + isLocalParam: isLocalParam, + ); + + /// Creates a [ParameterExpression] from this parameter. + /// + /// Only valid for `int` parameters. + ParameterExpression toExpression() { + if (defaultValue is! int) { + throw StateError('toExpression() is only supported for int parameters, ' + 'but $name has type $T.'); + } + return ParameterExpression.ofParam(this as ModuleParameter); + } + + @override + String toString() => 'ModuleParameter($name=$defaultValue)'; +} + +/// Represents a value that has both a concrete Dart [int] value (for +/// simulation) and a SystemVerilog expression string (for code generation). +/// +/// This is used wherever widths or integer values flow into SV generation +/// and need to remain symbolic (e.g., `WIDTH - 1` instead of `7`). +/// +/// Example: +/// ```dart +/// final widthParam = ModuleParameter('WIDTH', defaultValue: 8); +/// final widthExpr = ParameterExpression.ofParam(widthParam); +/// final rangeExpr = widthExpr - 1; // value=7, svExpression='WIDTH - 1' +/// ``` +class ParameterExpression { + /// The concrete Dart value, used during simulation. + final int value; + + /// The SystemVerilog expression string, used during code generation. + final String svExpression; + + /// Creates a [ParameterExpression] with explicit [value] and + /// [svExpression]. + const ParameterExpression(this.value, this.svExpression); + + /// Creates a [ParameterExpression] from a [ModuleParameter]. + /// + /// The [svExpression] is set to the parameter's [ModuleParameter.name]. + ParameterExpression.ofParam(ModuleParameter param) + : value = param.defaultValue, + svExpression = param.name; + + /// Creates a [ParameterExpression] from a plain integer constant. + /// + /// The [svExpression] is the decimal string representation of [value]. + ParameterExpression.ofInt(this.value) : svExpression = '$value'; + + /// Addition. + ParameterExpression operator +(Object other) { + if (other is ParameterExpression) { + return ParameterExpression( + value + other.value, '$svExpression + ${other.svExpression}'); + } + if (other is int) { + return ParameterExpression(value + other, '$svExpression + $other'); + } + throw ArgumentError('Cannot add $runtimeType and ${other.runtimeType}'); + } + + /// Subtraction. + ParameterExpression operator -(Object other) { + if (other is ParameterExpression) { + return ParameterExpression( + value - other.value, '$svExpression - ${other.svExpression}'); + } + if (other is int) { + return ParameterExpression(value - other, '$svExpression - $other'); + } + throw ArgumentError( + 'Cannot subtract $runtimeType and ${other.runtimeType}'); + } + + /// Multiplication. + ParameterExpression operator *(Object other) { + if (other is ParameterExpression) { + return ParameterExpression( + value * other.value, '($svExpression) * (${other.svExpression})'); + } + if (other is int) { + return ParameterExpression(value * other, '($svExpression) * $other'); + } + throw ArgumentError( + 'Cannot multiply $runtimeType and ${other.runtimeType}'); + } + + /// Integer division. + ParameterExpression operator ~/(Object other) { + if (other is ParameterExpression) { + return ParameterExpression( + value ~/ other.value, '($svExpression) / (${other.svExpression})'); + } + if (other is int) { + return ParameterExpression(value ~/ other, '($svExpression) / $other'); + } + throw ArgumentError('Cannot divide $runtimeType and ${other.runtimeType}'); + } + + /// Left shift. + ParameterExpression operator <<(Object other) { + if (other is ParameterExpression) { + return ParameterExpression( + value << other.value, '($svExpression) << (${other.svExpression})'); + } + if (other is int) { + return ParameterExpression(value << other, '($svExpression) << $other'); + } + throw ArgumentError( + 'Cannot left-shift $runtimeType by ${other.runtimeType}'); + } + + /// Right shift. + ParameterExpression operator >>(Object other) { + if (other is ParameterExpression) { + return ParameterExpression( + value >> other.value, '($svExpression) >> (${other.svExpression})'); + } + if (other is int) { + return ParameterExpression(value >> other, '($svExpression) >> $other'); + } + throw ArgumentError( + 'Cannot right-shift $runtimeType by ${other.runtimeType}'); + } + + @override + String toString() => 'ParameterExpression($value, "$svExpression")'; +} diff --git a/lib/src/modules/generate.dart b/lib/src/modules/generate.dart new file mode 100644 index 000000000..7225ad907 --- /dev/null +++ b/lib/src/modules/generate.dart @@ -0,0 +1,439 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// generate.dart +// Definition for SystemVerilog generate block constructs. +// +// 2026 June +// Author: Joel Kimmel + +import 'package:rohd/rohd.dart'; + +/// Represents a SystemVerilog `generate if` block. +/// +/// In simulation, [conditionValue] is evaluated at construction time to +/// determine which branch's hardware is active. In generated SystemVerilog, +/// both branches appear wrapped in a `generate if (...) begin ... end` +/// block. +/// +/// The condition is a compile-time (elaboration-time) condition, typically +/// based on a [ModuleParameter]. For runtime conditions, use [If] inside +/// a [Combinational] or [Sequential] block instead. +/// +/// Both the `then` and optional `else` bodies must be [Module]s with +/// matching input/output port names and widths, since they drive the same +/// signals. +/// +/// The body builders receive [GenerateIf]'s internal input ports so that +/// sub-modules are properly scoped within the generate block. +/// +/// Example: +/// ```dart +/// class MyTop extends Module { +/// MyTop(Logic a, Logic b, {int width = 8}) : super(name: 'top') { +/// a = addInput('a', a, width: width); +/// b = addInput('b', b, width: width); +/// addOutput('result', width: width); +/// +/// final widthParam = ModuleParameter('WIDTH', defaultValue: width); +/// addModuleParameter(widthParam); +/// +/// final genIf = GenerateIf( +/// conditionExpression: '${widthParam.name} > 4', +/// conditionValue: widthParam.defaultValue > 4, +/// inputs: {'a': a, 'b': b}, +/// outputWidths: {'sum': width}, +/// thenBody: (inputs) => +/// WideAdder(inputs['a']!, inputs['b']!, width: width), +/// elseBody: (inputs) => +/// NarrowAdder(inputs['a']!, inputs['b']!, width: width), +/// ); +/// +/// output('result') <= genIf.output('sum'); +/// } +/// } +/// ``` +class GenerateIf extends Module with SystemVerilog { + /// The SystemVerilog expression for the generate condition. + final String conditionExpression; + + /// The concrete boolean value of the condition for simulation. + final bool conditionValue; + + /// The SV label for the `then` branch (e.g., `gen_then`). + final String thenLabel; + + /// The SV label for the `else` branch (e.g., `gen_else`). + final String elseLabel; + + /// The sub-module for the `then` branch. + late final Module _thenModule; + + /// The sub-module for the `else` branch, if provided. + late final Module? _elseModule; + + /// The module that is active during simulation (based on [conditionValue]). + Module get activeModule => + conditionValue ? _thenModule : (_elseModule ?? _thenModule); + + /// Creates a [GenerateIf] block. + /// + /// [conditionExpression] is the SV expression (e.g., `'WIDTH > 4'`). + /// + /// [conditionValue] is the concrete Dart boolean for simulation. + /// + /// [inputs] maps port names to source signals from the parent module. + /// These will become input ports on this [GenerateIf]. + /// + /// [outputWidths] declares the output port names and widths that the + /// body modules must produce. + /// + /// [thenBody] builds the [Module] for the `then` branch. It receives + /// a map of input port signals to connect to. + /// + /// [elseBody] optionally builds the [Module] for the `else` branch. + /// + /// Both body modules must have outputs matching [outputWidths]. + GenerateIf({ + required this.conditionExpression, + required this.conditionValue, + required Map inputs, + required Map outputWidths, + required Module Function(Map inputs) thenBody, + Module Function(Map inputs)? elseBody, + this.thenLabel = 'gen_then', + this.elseLabel = 'gen_else', + super.name = 'generate_if', + }) { + // Create input ports on this GenerateIf. + final internalInputs = {}; + for (final entry in inputs.entries) { + internalInputs[entry.key] = + addInput(entry.key, entry.value, width: entry.value.width); + } + + // Create output ports. + for (final entry in outputWidths.entries) { + addOutput(entry.key, width: entry.value); + } + + // Build both branches, passing our internal ports. + _thenModule = thenBody(internalInputs); + _elseModule = elseBody?.call(internalInputs); + + // Validate port matching if both branches exist. + if (_elseModule != null) { + _validatePortsMatch(_thenModule, _elseModule!); + } + + // Wire the active branch's outputs to our outputs. + for (final outputName in outputWidths.keys) { + output(outputName) <= activeModule.outputs[outputName]!; + } + } + + /// Validates that two modules have the same port names and widths. + static void _validatePortsMatch(Module a, Module b) { + for (final inputName in a.inputs.keys) { + if (!b.inputs.containsKey(inputName)) { + throw IllegalConfigurationException( + 'GenerateIf: else branch is missing input "$inputName" ' + 'that exists in then branch.'); + } + if (a.inputs[inputName]!.width != b.inputs[inputName]!.width) { + throw IllegalConfigurationException( + 'GenerateIf: input "$inputName" width mismatch between branches: ' + '${a.inputs[inputName]!.width} vs ${b.inputs[inputName]!.width}.'); + } + } + for (final inputName in b.inputs.keys) { + if (!a.inputs.containsKey(inputName)) { + throw IllegalConfigurationException( + 'GenerateIf: then branch is missing input "$inputName" ' + 'that exists in else branch.'); + } + } + + for (final outputName in a.outputs.keys) { + if (!b.outputs.containsKey(outputName)) { + throw IllegalConfigurationException( + 'GenerateIf: else branch is missing output "$outputName" ' + 'that exists in then branch.'); + } + if (a.outputs[outputName]!.width != b.outputs[outputName]!.width) { + throw IllegalConfigurationException( + 'GenerateIf: output "$outputName" width mismatch: ' + '${a.outputs[outputName]!.width} vs ' + '${b.outputs[outputName]!.width}.'); + } + } + for (final outputName in b.outputs.keys) { + if (!a.outputs.containsKey(outputName)) { + throw IllegalConfigurationException( + 'GenerateIf: then branch is missing output "$outputName" ' + 'that exists in else branch.'); + } + } + } + + @override + String instantiationVerilog( + String instanceType, + String instanceName, + Map ports, + ) { + final buf = StringBuffer() + ..writeln('// $instanceName') + ..writeln('generate') + ..writeln(' if ($conditionExpression) begin : $thenLabel') + ..writeln(' ${_instantiationForBranch(_thenModule, ports)}'); + + if (_elseModule != null) { + buf + ..writeln(' end else begin : $elseLabel') + ..writeln(' ${_instantiationForBranch(_elseModule!, ports)}'); + } + + buf + ..writeln(' end') + ..writeln('endgenerate'); + + return buf.toString(); + } + + /// Generates a sub-module instantiation line for a branch module, + /// mapping our ports to the branch module's ports. + String _instantiationForBranch( + Module branchModule, Map outerPorts) { + final branchPorts = {}; + + for (final inputName in branchModule.inputs.keys) { + if (outerPorts.containsKey(inputName)) { + branchPorts[inputName] = outerPorts[inputName]!; + } + } + + for (final outputName in branchModule.outputs.keys) { + if (outerPorts.containsKey(outputName)) { + branchPorts[outputName] = outerPorts[outputName]!; + } + } + + return SystemVerilogSynthesizer.instantiationVerilogFor( + module: branchModule, + instanceType: branchModule.definitionName, + instanceName: '${branchModule.name}_inst', + ports: branchPorts, + forceStandardInstantiation: true, + ); + } +} + +/// Represents a SystemVerilog `generate for` block. +/// +/// In simulation, the loop is unrolled at construction time — each iteration +/// calls `bodyBuilder` with the concrete index value to create real hardware. +/// In generated SystemVerilog, the iterations are wrapped in a +/// `generate for (genvar ...) begin ... end endgenerate` block. +/// +/// This is useful for creating parameterized, repetitive hardware structures +/// where the iteration count depends on a [ModuleParameter]. +/// +/// The `bodyBuilder` must produce modules with the same port names and widths +/// for every iteration. +/// +/// Each output declared in `outputWidths` is exposed as a single bus of width +/// `count * perIterationWidth`. In the generated SV, the sub-module's output +/// port is connected using genvar indexing (e.g., `.out(out[i])` for 1-bit +/// outputs or `.out(out[i*W +: W])` for multi-bit outputs). +/// +/// Example: +/// ```dart +/// class MyTop extends Module { +/// MyTop(Logic inp, {int count = 4}) : super(name: 'top') { +/// inp = addInput('inp', inp); +/// final countParam = ModuleParameter('N', defaultValue: count); +/// addModuleParameter(countParam); +/// +/// final genFor = GenerateFor( +/// count: count, +/// countExpression: countParam.name, +/// inputs: {'inp': inp}, +/// outputWidths: {'out': 1}, +/// bodyBuilder: (i, inputs) => Inverter(inputs['inp']!), +/// ); +/// +/// addOutput('out', width: count, +/// widthExpression: countParam.toExpression()); +/// output('out') <= genFor.output('out'); +/// } +/// } +/// ``` +class GenerateFor extends Module with SystemVerilog { + /// The concrete number of iterations for simulation. + final int count; + + /// The SV expression for the upper bound of the loop + /// (e.g., `'N'` or `'WIDTH'`). + final String countExpression; + + /// The name of the genvar variable (e.g., `'i'`). + final String genvarName; + + /// The SV label for the generate block. + final String blockLabel; + + /// The per-iteration output widths, used for genvar index expressions. + final Map _outputWidths; + + /// The sub-modules for each iteration, built at construction time. + late final List _iterationModules; + + /// Creates a [GenerateFor] block. + /// + /// [count] is the concrete iteration count for simulation. + /// + /// [countExpression] is the SV expression for the loop upper bound + /// (e.g., a parameter name like `'N'`). + /// + /// [inputs] maps port names to source signals from the parent module. + /// + /// [outputWidths] declares the per-iteration output port widths. + /// Each output is exposed as a single bus of width + /// `count * perIterationWidth`. + /// + /// [genvarName] is the name of the genvar variable (default: `'i'`). + /// + /// [bodyBuilder] creates a [Module] for each iteration, receiving the + /// loop index and a map of input ports. All iterations must produce + /// modules with outputs matching [outputWidths]. + GenerateFor({ + required this.count, + required this.countExpression, + required Map inputs, + required Map outputWidths, + required Module Function(int index, Map inputs) bodyBuilder, + this.genvarName = 'i', + String? blockLabel, + super.name = 'generate_for', + }) : _outputWidths = outputWidths, + blockLabel = blockLabel ?? 'gen_for_block' { + if (count < 1) { + throw IllegalConfigurationException( + 'GenerateFor: count must be >= 1, but got $count.'); + } + + // Create input ports on this GenerateFor. + final internalInputs = {}; + for (final entry in inputs.entries) { + internalInputs[entry.key] = + addInput(entry.key, entry.value, width: entry.value.width); + } + + // Create bus output ports (width = count * perIterationWidth). + for (final entry in outputWidths.entries) { + addOutput(entry.key, width: count * entry.value); + } + + // Build all iterations for simulation, passing our internal inputs. + _iterationModules = List.generate( + count, (i) => bodyBuilder(i, internalInputs), + growable: false); + + // Validate all iterations match. + for (var i = 1; i < _iterationModules.length; i++) { + _validatePortsMatch(_iterationModules.first, _iterationModules[i], i); + } + + // Wire each iteration's outputs into the bus output via rswizzle. + for (final outputName in outputWidths.keys) { + final bits = [ + for (var i = 0; i < count; i++) + _iterationModules[i].outputs[outputName]!, + ]; + output(outputName) <= bits.rswizzle(); + } + } + + /// Validates that iteration [index] has matching ports to the first. + static void _validatePortsMatch(Module first, Module other, int index) { + for (final inputName in first.inputs.keys) { + if (!other.inputs.containsKey(inputName)) { + throw IllegalConfigurationException( + 'GenerateFor: iteration $index is missing input "$inputName" ' + 'that exists in iteration 0.'); + } + if (first.inputs[inputName]!.width != other.inputs[inputName]!.width) { + throw IllegalConfigurationException( + 'GenerateFor: input "$inputName" width mismatch between ' + 'iteration 0 and $index.'); + } + } + + for (final outputName in first.outputs.keys) { + if (!other.outputs.containsKey(outputName)) { + throw IllegalConfigurationException( + 'GenerateFor: iteration $index is missing output "$outputName" ' + 'that exists in iteration 0.'); + } + if (first.outputs[outputName]!.width != + other.outputs[outputName]!.width) { + throw IllegalConfigurationException( + 'GenerateFor: output "$outputName" width mismatch between ' + 'iteration 0 and $index.'); + } + } + } + + @override + String instantiationVerilog( + String instanceType, + String instanceName, + Map ports, + ) { + // Use the first iteration module as the template for the generate body. + final templateModule = _iterationModules.first; + + final buf = StringBuffer() + ..writeln('// $instanceName') + ..writeln('genvar $genvarName;') + ..writeln('generate') + ..writeln(' for ($genvarName = 0; ' + '$genvarName < $countExpression; ' + '$genvarName = $genvarName + 1) begin : $blockLabel'); + + // Build the port mapping for the template module. + final branchPorts = {}; + for (final inputName in templateModule.inputs.keys) { + if (ports.containsKey(inputName)) { + branchPorts[inputName] = ports[inputName]!; + } + } + + // For outputs, use genvar-indexed expressions. + for (final outputName in templateModule.outputs.keys) { + final outerName = ports[outputName]; + if (outerName != null) { + final w = _outputWidths[outputName] ?? 1; + if (w == 1) { + branchPorts[outputName] = '$outerName[$genvarName]'; + } else { + branchPorts[outputName] = '$outerName[$genvarName * $w +: $w]'; + } + } + } + + buf + ..writeln(' ${SystemVerilogSynthesizer.instantiationVerilogFor( + module: templateModule, + instanceType: templateModule.definitionName, + instanceName: '${templateModule.name}_inst', + ports: branchPorts, + forceStandardInstantiation: true, + )}') + ..writeln(' end') + ..writeln('endgenerate'); + + return buf.toString(); + } +} diff --git a/lib/src/modules/modules.dart b/lib/src/modules/modules.dart index 7a4afaca5..a60ed6da2 100644 --- a/lib/src/modules/modules.dart +++ b/lib/src/modules/modules.dart @@ -5,5 +5,6 @@ export 'bus.dart'; export 'clkgen.dart'; export 'conditionals/conditionals.dart'; export 'gates.dart'; +export 'generate.dart'; export 'pipeline.dart'; export 'tristate.dart'; diff --git a/lib/src/signals/logic.dart b/lib/src/signals/logic.dart index 4c5f99e5e..5a0aea336 100644 --- a/lib/src/signals/logic.dart +++ b/lib/src/signals/logic.dart @@ -238,6 +238,13 @@ class Logic { /// outputs. final Naming naming; + /// An optional symbolic width expression for SystemVerilog generation. + /// + /// When set, the generated SV will use this expression (e.g., `WIDTH - 1`) + /// instead of the concrete integer [width] for range declarations. + /// The concrete [width] is still used for simulation. + final ParameterExpression? widthExpression; + /// Constructs a new [Logic] named [name] with [width] bits. /// /// The default value for [width] is 1. The [name] should be sanitary @@ -245,14 +252,20 @@ class Logic { /// /// The [naming] and [name], if unspecified, are chosen based on the rules in /// [Naming.chooseNaming] and [Naming.chooseName], respectively. + /// + /// If [widthExpression] is provided, the generated SystemVerilog will use + /// that expression for the signal's range declaration instead of the + /// concrete [width]. Logic({ String? name, int width = 1, Naming? naming, + ParameterExpression? widthExpression, }) : this._( name: name, width: width, naming: naming, + widthExpression: widthExpression, ); /// A cloning utility for [clone] and [named]. @@ -264,7 +277,8 @@ class Logic { newName: name, originalNaming: this.naming, newNaming: naming), - width: width); + width: width, + widthExpression: widthExpression); /// Makes a copy of `this`, optionally with the specified [name], but the same /// [width]. @@ -295,6 +309,7 @@ class Logic { int width = 1, Naming? naming, _Wire? wire, + this.widthExpression, }) : naming = Naming.chooseNaming(name, naming), name = Naming.chooseName(name, naming), _wire = wire ?? _Wire(width: width) { diff --git a/lib/src/signals/logic_array.dart b/lib/src/signals/logic_array.dart index 6e7c9bd34..3734e3e5a 100644 --- a/lib/src/signals/logic_array.dart +++ b/lib/src/signals/logic_array.dart @@ -52,7 +52,11 @@ class LogicArray extends LogicStructure { /// impact on simulation functionality or behavior. In SystemVerilog, there /// are some differences in access patterns for packed vs. unpacked arrays. factory LogicArray(List dimensions, int elementWidth, - {String? name, int numUnpackedDimensions = 0, Naming? naming}) => + {String? name, + int numUnpackedDimensions = 0, + Naming? naming, + List? dimensionExpressions, + ParameterExpression? elementWidthExpression}) => LogicArray._factory( dimensions, elementWidth, @@ -62,6 +66,8 @@ class LogicArray extends LogicStructure { logicBuilder: Logic.new, logicArrayBuilder: LogicArray.new, isNet: false, + dimensionExpressions: dimensionExpressions, + elementWidthExpression: elementWidthExpression, ); @override @@ -78,7 +84,11 @@ class LogicArray extends LogicStructure { /// impact on simulation functionality or behavior. In SystemVerilog, there /// are some differences in access patterns for packed vs. unpacked arrays. factory LogicArray.net(List dimensions, int elementWidth, - {String? name, int numUnpackedDimensions = 0, Naming? naming}) => + {String? name, + int numUnpackedDimensions = 0, + Naming? naming, + List? dimensionExpressions, + ParameterExpression? elementWidthExpression}) => LogicArray._factory( dimensions, elementWidth, @@ -88,6 +98,8 @@ class LogicArray extends LogicStructure { logicBuilder: LogicNet.new, logicArrayBuilder: LogicArray.net, isNet: true, + dimensionExpressions: dimensionExpressions, + elementWidthExpression: elementWidthExpression, ); /// Internal factory constructor. @@ -123,6 +135,8 @@ class LogicArray extends LogicStructure { int numUnpackedDimensions, String name, }) logicArrayBuilder, + List? dimensionExpressions, + ParameterExpression? elementWidthExpression, }) { if (dimensions.isEmpty) { throw LogicConstructionException( @@ -173,6 +187,8 @@ class LogicArray extends LogicStructure { name: name, naming: naming, isNet: isNet, + dimensionExpressions: dimensionExpressions, + elementWidthExpression: elementWidthExpression, ); } @@ -213,6 +229,18 @@ class LogicArray extends LogicStructure { LogicArray named(String name, {Naming? naming}) => _clone(name: name, naming: naming)..gets(this); + /// Optional symbolic expressions for each dimension, for parameterized + /// SV generation. + /// + /// When set, the generated SV will use these expressions for the dimension + /// ranges (e.g., `[N-1:0]`) instead of concrete integer bounds. + /// The list length must match [dimensions] if provided. + final List? dimensionExpressions; + + /// Optional symbolic expression for the leaf element width, for + /// parameterized SV generation. + final ParameterExpression? elementWidthExpression; + /// Private constructor for the factory [LogicArray] constructor. /// /// The [name] and [naming] should have been identified before calling this. @@ -224,6 +252,8 @@ class LogicArray extends LogicStructure { required String super.name, required this.naming, required this.isNet, + this.dimensionExpressions, + this.elementWidthExpression, }); /// Constructs a new [LogicArray] with a more convenient constructor signature diff --git a/lib/src/signals/logic_net.dart b/lib/src/signals/logic_net.dart index 1b945d1e6..9843599ce 100644 --- a/lib/src/signals/logic_net.dart +++ b/lib/src/signals/logic_net.dart @@ -26,7 +26,7 @@ class LogicNet extends Logic { /// /// The [naming] and [name], if unspecified, are chosen based on the rules in /// [Naming.chooseNaming] and [Naming.chooseName], respectively. - LogicNet({super.name, super.width, super.naming}) + LogicNet({super.name, super.width, super.naming, super.widthExpression}) : super._(wire: _WireNet(width: width)); /// Constructs a new [LogicNet] with some additional validation for ports of diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index ef463b9bb..19e6ccda3 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -45,6 +45,9 @@ class LogicStructure implements Logic { @override Naming get naming => Naming.unnamed; + @override + ParameterExpression? get widthExpression => null; + /// Creates a new [LogicStructure] with [elements] as elements. /// /// None of the [elements] can already be members of another [LogicStructure]. diff --git a/lib/src/signals/parameter_const.dart b/lib/src/signals/parameter_const.dart new file mode 100644 index 000000000..4740ccd73 --- /dev/null +++ b/lib/src/signals/parameter_const.dart @@ -0,0 +1,71 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// parameter_const.dart +// Definition of a constant signal that references a module parameter in SV. +// +// 2026 June +// Author: Joel Kimmel + +part of 'signals.dart'; + +/// A [Const]-like signal that emits a [ModuleParameter] name instead of a +/// literal value in generated SystemVerilog. +/// +/// In simulation, [ParameterConst] behaves identically to a [Const] — it holds +/// the concrete value of the parameter. In generated SV, wherever this signal +/// would normally appear as a literal (e.g., `8'h8`), it instead appears as +/// the parameter name (e.g., `WIDTH`). +/// +/// Example: +/// ```dart +/// final widthParam = ModuleParameter('WIDTH', defaultValue: 8); +/// final widthConst = ParameterConst(widthParam); +/// // In SV: uses 'WIDTH' instead of '8' +/// ``` +class ParameterConst extends Const { + /// The [ModuleParameter] that this constant references. + final ModuleParameter parameter; + + /// The SystemVerilog expression to emit for this constant. + /// + /// Defaults to [ModuleParameter.name], but can be overridden for derived + /// expressions (e.g., `WIDTH - 1`). + final String svExpression; + + /// Creates a [ParameterConst] that references [parameter]. + /// + /// The concrete value is [ModuleParameter.defaultValue], and the width + /// is determined by [width] (defaulting to the bit-length of the value, + /// minimum 1). + /// + /// An optional [svExpression] can override the SV name (useful for + /// derived values like `WIDTH - 1`). + ParameterConst( + this.parameter, { + int? width, + String? svExpression, + }) : svExpression = svExpression ?? parameter.name, + super( + parameter.defaultValue, + width: width ?? max(parameter.defaultValue.bitLength, 1), + ); + + /// Creates a [ParameterConst] from a [ParameterExpression]. + /// + /// The concrete value is [ParameterExpression.value], and the SV expression + /// is [ParameterExpression.svExpression]. + ParameterConst.fromExpression( + ParameterExpression expr, { + required this.parameter, + int? width, + }) : svExpression = expr.svExpression, + super( + expr.value, + width: width ?? max(expr.value.bitLength, 1), + ); + + @override + ParameterConst clone({String? name}) => + ParameterConst(parameter, width: width, svExpression: svExpression); +} diff --git a/lib/src/signals/signals.dart b/lib/src/signals/signals.dart index 348487a72..ec69d3bbf 100644 --- a/lib/src/signals/signals.dart +++ b/lib/src/signals/signals.dart @@ -18,6 +18,7 @@ export 'port.dart'; part 'const.dart'; part 'logic.dart'; +part 'parameter_const.dart'; part 'wire.dart'; part 'wire_net.dart'; part 'logic_structure.dart'; diff --git a/lib/src/synthesizers/systemverilog/systemverilog_mixins.dart b/lib/src/synthesizers/systemverilog/systemverilog_mixins.dart index 20de51535..a685b5df2 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_mixins.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_mixins.dart @@ -22,10 +22,15 @@ class SystemVerilogParameterDefinition { /// The name of the parameter. final String name; + /// Whether this is a `localparam` rather than a `parameter`. + final bool isLocalParam; + /// Creates a new SystemVerilog parameter definition with [name] of the /// provided [type] with the [defaultValue]. const SystemVerilogParameterDefinition(this.name, - {required this.type, required this.defaultValue}); + {required this.type, + required this.defaultValue, + this.isLocalParam = false}); } /// Allows a [Module] to control the instantiation and/or definition of diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart index eb1a0cd45..51e3c7eed 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart @@ -68,10 +68,24 @@ class SystemVerilogSynthSubModuleInstantiation if (!needsInstantiation) { return null; } + + // Collect instantiation parameters from registered ModuleParameters. + Map? parameters; + if (module.moduleParameters.isNotEmpty) { + parameters = { + for (final mp in module.moduleParameters) + if (!mp.isLocalParam) mp.name: mp.svDefault, + }; + if (parameters.isEmpty) { + parameters = null; + } + } + return SystemVerilogSynthesizer.instantiationVerilogFor( module: module, instanceType: instanceType, instanceName: name, + parameters: parameters, ports: _modulePortsMapWithInline({ ...inputMapping, ...outputMapping, diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart index 72471eef1..1409da704 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart @@ -200,22 +200,34 @@ class SystemVerilogSynthesisResult extends SynthesisResult { ].join(',\n'); String? _verilogParameters(Module module) { + // Collect parameters from multiple sources. + final allParams = []; + + // 1. From the SystemVerilog mixin's definitionParameters (if applicable). if (module is SystemVerilog) { final defParams = module.definitionParameters; - if (defParams == null || defParams.isEmpty) { - return null; + if (defParams != null) { + allParams.addAll(defParams); } + } + + // 2. From registered ModuleParameters on the Module. + for (final mp in module.moduleParameters) { + allParams.add(mp.toSvParameterDefinition()); + } - return [ - '#(', - defParams - .map((p) => 'parameter ${p.type} ${p.name} = ${p.defaultValue}') - .join(',\n'), - ')', - ].join('\n'); + if (allParams.isEmpty) { + return null; } - return null; + return [ + '#(', + allParams + .map((p) => '${p.isLocalParam ? 'localparam' : 'parameter'}' + ' ${p.type} ${p.name} = ${p.defaultValue}') + .join(',\n'), + ')', + ].join('\n'); } /// The full SV representation of this module. diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index c3026a0d5..23952fcd4 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -192,6 +192,12 @@ class SynthLogic { /// True only if this represents a [LogicArray]. final bool isArray; + /// An optional symbolic width expression for SV generation. + /// + /// When set, the generated SV will use this expression for the range + /// declaration (e.g., `[WIDTH-1:0]`) instead of concrete integer bounds. + final ParameterExpression? _widthExpression; + /// The chosen name of this. /// /// Must call [pickName] before this is accessible. @@ -226,6 +232,11 @@ class SynthLogic { // check for const if (_constLogic != null) { if (!_constNameDisallowed) { + // If this is a ParameterConst, use the SV expression (parameter name) + // instead of the literal value. + if (_constLogic is ParameterConst) { + return (_constLogic! as ParameterConst).svExpression; + } return _constLogic!.value.toString(); } else { assert( @@ -307,6 +318,7 @@ class SynthLogic { Naming? namingOverride, bool constNameDisallowed = false, }) : isArray = initialLogic is LogicArray, + _widthExpression = initialLogic.widthExpression, _constNameDisallowed = constNameDisallowed { _addLogic(initialLogic, namingOverride: namingOverride); } @@ -407,7 +419,14 @@ class SynthLogic { 'logics contained: ${logics.map((e) => e.preferredSynthName).toList()}'; /// Provides a definition for a range in SV from a width. - static String _widthToRangeDef(int width, {bool forceRange = false}) { + /// + /// If [widthExpr] is provided, the expression is used instead of + /// concrete integer values (e.g., `[WIDTH-1:0]` instead of `[7:0]`). + static String _widthToRangeDef(int width, + {bool forceRange = false, ParameterExpression? widthExpr}) { + if (widthExpr != null) { + return '[${widthExpr.svExpression} - 1:0]'; + } if (width > 1 || forceRange) { return '[${width - 1}:0]'; } else { @@ -431,9 +450,13 @@ class SynthLogic { final unpackedDimsBuf = StringBuffer(); final dims = logicArr.dimensions; + final dimExprs = logicArr.dimensionExpressions; for (var i = 0; i < dims.length; i++) { final dim = dims[i]; - final dimStr = _widthToRangeDef(dim, forceRange: true); + final dimExpr = + (dimExprs != null && i < dimExprs.length) ? dimExprs[i] : null; + final dimStr = + _widthToRangeDef(dim, forceRange: true, widthExpr: dimExpr); if (i < logicArr.numUnpackedDimensions) { unpackedDimsBuf.write(dimStr); } else { @@ -441,12 +464,13 @@ class SynthLogic { } } - packedDimsBuf.write(_widthToRangeDef(logicArr.elementWidth)); + packedDimsBuf.write(_widthToRangeDef(logicArr.elementWidth, + widthExpr: logicArr.elementWidthExpression)); packedDims = packedDimsBuf.toString(); unpackedDims = unpackedDimsBuf.toString(); } else { - packedDims = _widthToRangeDef(logic.width); + packedDims = _widthToRangeDef(logic.width, widthExpr: _widthExpression); unpackedDims = ''; } diff --git a/test/generate_test.dart b/test/generate_test.dart new file mode 100644 index 000000000..41da77cd1 --- /dev/null +++ b/test/generate_test.dart @@ -0,0 +1,391 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// generate_test.dart +// Tests for GenerateIf and GenerateFor constructs. +// +// 2026 June +// Author: Joel Kimmel + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +/// A simple adder module for use as a generate body. +class SimpleAdder extends Module { + Logic get sum => output('sum'); + + SimpleAdder(Logic a, Logic b, {int width = 8, super.name = 'simple_adder'}) { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('sum', width: width); + + sum <= a + b; + } +} + +/// An adder that also outputs a carry bit. +class WideAdder extends Module { + Logic get sum => output('sum'); + + WideAdder(Logic a, Logic b, {int width = 8, super.name = 'wide_adder'}) { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('sum', width: width); + + // Compute sum with wrapping + sum <= a + b; + } +} + +/// An adder that masks the result (different implementation path). +class NarrowAdder extends Module { + Logic get sum => output('sum'); + + NarrowAdder(Logic a, Logic b, {int width = 8, super.name = 'narrow_adder'}) { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('sum', width: width); + + sum <= a & b; + } +} + +/// A simple inverter module for use in GenerateFor. +class SimpleInverter extends Module { + Logic get out => output('out'); + + SimpleInverter(Logic inp, {super.name = 'simple_inverter'}) { + inp = addInput('inp', inp); + addOutput('out'); + + out <= ~inp; + } +} + +/// Top module using GenerateIf with then-only (no else). +class TopWithGenerateIfThenOnly extends Module { + Logic get result => output('result'); + + TopWithGenerateIfThenOnly(Logic a, Logic b, + {int width = 8, bool condition = true}) + : super(name: 'top_gen_if_then_only') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('result', width: width); + + final genIf = GenerateIf( + conditionExpression: 'WIDTH > 4', + conditionValue: condition, + inputs: {'a': a, 'b': b}, + outputWidths: {'sum': width}, + thenBody: (inputs) => + SimpleAdder(inputs['a']!, inputs['b']!, width: width), + ); + + result <= genIf.output('sum'); + } +} + +/// Top module using GenerateIf with both then and else branches. +class TopWithGenerateIfElse extends Module { + Logic get result => output('result'); + + TopWithGenerateIfElse(Logic a, Logic b, + {int width = 8, bool condition = true}) + : super(name: 'top_gen_if_else') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('result', width: width); + + final genIf = GenerateIf( + conditionExpression: 'WIDTH > 4', + conditionValue: condition, + inputs: {'a': a, 'b': b}, + outputWidths: {'sum': width}, + thenBody: (inputs) => WideAdder(inputs['a']!, inputs['b']!, width: width), + elseBody: (inputs) => + NarrowAdder(inputs['a']!, inputs['b']!, width: width), + ); + + result <= genIf.output('sum'); + } +} + +/// Top module using GenerateFor with inverters. +class TopWithGenerateFor extends Module { + Logic get out => output('out'); + + final ModuleParameter countParam; + + TopWithGenerateFor(Logic inp, {int count = 4}) + : countParam = ModuleParameter('N', defaultValue: count), + super(name: 'top_gen_for') { + addModuleParameter(countParam); + + inp = addInput('inp', inp); + addOutput('out', width: count, widthExpression: countParam.toExpression()); + + final genFor = GenerateFor( + count: count, + countExpression: countParam.name, + inputs: {'inp': inp}, + outputWidths: {'out': 1}, + bodyBuilder: (i, inputs) => SimpleInverter(inputs['inp']!), + ); + + out <= genFor.output('out'); + } +} + +/// A module with a deliberately different output name for testing mismatches. +class MismatchedOutputModule extends Module { + MismatchedOutputModule(Logic a, Logic b, {int width = 8}) + : super(name: 'mismatched') { + a = addInput('a', a, width: width); + addInput('b', b, width: width); + addOutput('different_name', width: width); + output('different_name') <= a; + } +} + +/// A parameterized buffer submodule (identity with parameterized width). +class ParameterizedBuffer extends Module { + Logic get out => output('out'); + + final ModuleParameter widthParam; + + ParameterizedBuffer(Logic inp, + {int width = 8, + String? svDefaultValue, + super.name = 'parameterized_buffer'}) + : widthParam = ModuleParameter('WIDTH', + defaultValue: width, svDefaultValue: svDefaultValue), + super() { + addModuleParameter(widthParam); + + final widthExpr = widthParam.toExpression(); + + inp = addInput('inp', inp, width: width, widthExpression: widthExpr); + addOutput('out', width: width, widthExpression: widthExpr); + + out <= inp; + } +} + +/// Top module that has its own WIDTH parameter and passes it to a submodule. +class TopWithParameterPassthrough extends Module { + Logic get result => output('result'); + + final ModuleParameter widthParam; + + TopWithParameterPassthrough(Logic a, {int width = 8}) + : widthParam = ModuleParameter('WIDTH', defaultValue: width), + super(name: 'top_param_passthrough') { + addModuleParameter(widthParam); + + final widthExpr = widthParam.toExpression(); + + a = addInput('a', a, width: width, widthExpression: widthExpr); + addOutput('result', width: width, widthExpression: widthExpr); + + // Instantiate submodule, passing our parameter name as its svDefaultValue + result <= ParameterizedBuffer(a, width: width, svDefaultValue: 'WIDTH').out; + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('GenerateIf', () { + test('then-only: simulation uses then branch', () async { + final a = Logic(name: 'a', width: 8); + final b = Logic(name: 'b', width: 8); + final mod = TopWithGenerateIfThenOnly(a, b); + await mod.build(); + + a.put(5); + b.put(3); + expect(mod.result.value.toInt(), equals(8)); + }); + + test('if-else: condition true uses then branch (adder)', () async { + final a = Logic(name: 'a', width: 8); + final b = Logic(name: 'b', width: 8); + final mod = TopWithGenerateIfElse(a, b); + await mod.build(); + + a.put(10); + b.put(6); + // WideAdder: sum = a + b = 16 + expect(mod.result.value.toInt(), equals(16)); + }); + + test('if-else: condition false uses else branch (AND)', () async { + final a = Logic(name: 'a', width: 8); + final b = Logic(name: 'b', width: 8); + final mod = TopWithGenerateIfElse(a, b, condition: false); + await mod.build(); + + a.put(0x0F); + b.put(0x33); + // NarrowAdder: sum = a & b = 0x03 + expect(mod.result.value.toInt(), equals(0x03)); + }); + + test('generates SV with generate if block (then-only)', () async { + final a = Logic(name: 'a', width: 8); + final b = Logic(name: 'b', width: 8); + final mod = TopWithGenerateIfThenOnly(a, b); + await mod.build(); + + final sv = mod.generateSynth(); + expect(sv, contains('generate')); + expect(sv, contains('if (WIDTH > 4) begin : gen_then')); + expect(sv, contains('endgenerate')); + }); + + test('generates SV with generate if-else block', () async { + final a = Logic(name: 'a', width: 8); + final b = Logic(name: 'b', width: 8); + final mod = TopWithGenerateIfElse(a, b); + await mod.build(); + + final sv = mod.generateSynth(); + expect(sv, contains('generate')); + expect(sv, contains('if (WIDTH > 4) begin : gen_then')); + expect(sv, contains('end else begin : gen_else')); + expect(sv, contains('endgenerate')); + }); + + test('throws on port name mismatch between branches', () { + final a = Logic(name: 'a', width: 8); + final b = Logic(name: 'b', width: 8); + + expect( + () => GenerateIf( + conditionExpression: 'X', + conditionValue: true, + inputs: {'a': a, 'b': b}, + outputWidths: {'sum': 8}, + thenBody: (inputs) => SimpleAdder(inputs['a']!, inputs['b']!), + elseBody: (inputs) => + MismatchedOutputModule(inputs['a']!, inputs['b']!), + ), + throwsA(isA()), + ); + }); + + test('throws on port width mismatch between branches', () { + final a = Logic(name: 'a', width: 8); + final b = Logic(name: 'b', width: 8); + + // When the else branch expects different widths, an error is thrown + // because the shared internal inputs have the width of the declared + // inputs (8), but the else branch module expects width 4. + expect( + () => GenerateIf( + conditionExpression: 'X', + conditionValue: true, + inputs: {'a': a, 'b': b}, + outputWidths: {'sum': 8}, + thenBody: (inputs) => SimpleAdder(inputs['a']!, inputs['b']!), + elseBody: (inputs) => + SimpleAdder(inputs['a']!, inputs['b']!, width: 4), + ), + throwsA(isA()), + ); + }); + }); + + group('GenerateFor', () { + test('simulation: all iterations produce correct output', () async { + final inp = Logic(name: 'inp'); + final mod = TopWithGenerateFor(inp); + await mod.build(); + + inp.put(1); + for (var i = 0; i < 4; i++) { + expect(mod.out[i].value.toInt(), equals(0)); + } + + inp.put(0); + for (var i = 0; i < 4; i++) { + expect(mod.out[i].value.toInt(), equals(1)); + } + }); + + test('generates SV with generate for block', () async { + final inp = Logic(name: 'inp'); + final mod = TopWithGenerateFor(inp); + await mod.build(); + + final sv = mod.generateSynth(); + expect(sv, contains('genvar i;')); + expect(sv, contains('generate')); + expect(sv, contains('for (i = 0; i < N; i = i + 1)')); + expect(sv, contains('begin : gen_for_block')); + expect(sv, contains('endgenerate')); + }); + + test('throws on count < 1', () { + final inp = Logic(name: 'inp'); + + expect( + () => GenerateFor( + count: 0, + countExpression: 'N', + inputs: {'inp': inp}, + outputWidths: {'out': 1}, + bodyBuilder: (i, inputs) => SimpleInverter(inputs['inp']!), + ), + throwsA(isA()), + ); + }); + + test('single iteration works', () async { + final inp = Logic(name: 'inp'); + final mod = TopWithGenerateFor(inp, count: 1); + await mod.build(); + + inp.put(1); + expect(mod.out.value.toInt(), equals(0)); + }); + }); + + group('Parameter passthrough', () { + test('top module passes parameter to submodule in SV', () async { + final a = Logic(name: 'a', width: 16); + final mod = TopWithParameterPassthrough(a, width: 16); + await mod.build(); + + final sv = mod.generateSynth(); + + // Print so the user can inspect + // ignore: avoid_print + print(sv); + + // Top module should declare its own WIDTH parameter + expect(sv, contains('parameter int WIDTH = 16')); + + // Top module ports should use WIDTH expression + expect(sv, contains('[WIDTH - 1:0]')); + + // Submodule instantiation should pass WIDTH through + expect(sv, contains('#(.WIDTH(WIDTH))')); + + // Submodule definition should also have its own WIDTH parameter + expect(sv, contains('module ParameterizedBuffer')); + }); + + test('simulation still works with parameter passthrough', () async { + final a = Logic(name: 'a', width: 16); + final mod = TopWithParameterPassthrough(a, width: 16); + await mod.build(); + + a.put(0xABCD); + expect(mod.result.value.toInt(), equals(0xABCD)); + }); + }); +} diff --git a/test/module_parameter_test.dart b/test/module_parameter_test.dart new file mode 100644 index 000000000..aa5042e62 --- /dev/null +++ b/test/module_parameter_test.dart @@ -0,0 +1,425 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_parameter_test.dart +// Tests for ModuleParameter, ParameterExpression, ParameterConst, and +// parameterized SV generation. +// +// 2026 June +// Author: Joel Kimmel + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +/// A simple parameterized adder module for testing. +/// +/// Generated SV should have parameterized port widths using WIDTH. +class ParameterizedAdder extends Module { + Logic get sum => output('sum'); + + final ModuleParameter widthParam; + + ParameterizedAdder(Logic a, Logic b, {int width = 8}) + : widthParam = ModuleParameter('WIDTH', defaultValue: width), + super(name: 'parameterized_adder') { + addModuleParameter(widthParam); + + final widthExpr = widthParam.toExpression(); + + a = addInput('a', a, width: width, widthExpression: widthExpr); + b = addInput('b', b, width: width, widthExpression: widthExpr); + addOutput('sum', width: width, widthExpression: widthExpr); + + sum <= a + b; + } +} + +/// A parameterized wrapper that instantiates ParameterizedAdder as a submodule. +class TopWithParameterizedAdder extends Module { + Logic get result => output('result'); + + TopWithParameterizedAdder(Logic x, Logic y, {int width = 8}) + : super(name: 'top_wrapper') { + x = addInput('x', x, width: width); + y = addInput('y', y, width: width); + addOutput('result', width: width); + + result <= ParameterizedAdder(x, y, width: width).sum; + } +} + +/// Module that uses ParameterConst in expressions. +class ModuleWithParameterConst extends Module { + Logic get out => output('out'); + + final ModuleParameter widthParam; + + ModuleWithParameterConst(Logic inp, {int width = 8}) + : widthParam = ModuleParameter('WIDTH', defaultValue: width), + super(name: 'param_const_module') { + addModuleParameter(widthParam); + + final widthExpr = widthParam.toExpression(); + + inp = addInput('inp', inp, width: width, widthExpression: widthExpr); + addOutput('out', width: width, widthExpression: widthExpr); + + // Use ParameterConst to reference the parameter in SV + out <= inp & ParameterConst(widthParam, width: width); + } +} + +/// Module with localparam. +class ModuleWithLocalParam extends Module { + Logic get out => output('out'); + + ModuleWithLocalParam(Logic inp) : super(name: 'localparam_module') { + final addrWidth = + ModuleParameter('ADDR_WIDTH', defaultValue: 4, isLocalParam: true); + addModuleParameter(addrWidth); + + inp = addInput('inp', inp, width: 4); + addOutput('out', width: 4); + + out <= inp; + } +} + +/// Module with both parameter and localparam. +class ModuleWithMixedParams extends Module { + Logic get out => output('out'); + + ModuleWithMixedParams(Logic inp, {int width = 8}) + : super(name: 'mixed_params_module') { + final widthParam = ModuleParameter('WIDTH', defaultValue: width); + final depth = + ModuleParameter('DEPTH', defaultValue: 16, isLocalParam: true); + addModuleParameter(widthParam); + addModuleParameter(depth); + + final widthExpr = widthParam.toExpression(); + inp = addInput('inp', inp, width: width, widthExpression: widthExpr); + addOutput('out', width: width, widthExpression: widthExpr); + + out <= inp; + } +} + +/// Module with parameterized LogicArray dimensions. +class ModuleWithParameterizedArray extends Module { + LogicArray get out => output('out') as LogicArray; + + final ModuleParameter nParam; + final ModuleParameter wParam; + + ModuleWithParameterizedArray(LogicArray inp, LogicArray src, + {int n = 4, int w = 8}) + : nParam = ModuleParameter('N', defaultValue: n), + wParam = ModuleParameter('W', defaultValue: w), + super(name: 'param_array_module') { + addModuleParameter(nParam); + addModuleParameter(wParam); + + final nExpr = nParam.toExpression(); + final wExpr = wParam.toExpression(); + + inp = addInputArray('inp', inp, + dimensions: [n], + elementWidth: w, + dimensionExpressions: [nExpr], + elementWidthExpression: wExpr); + addOutputArray('out', + dimensions: [n], + elementWidth: w, + dimensionExpressions: [nExpr], + elementWidthExpression: wExpr); + + out <= inp; + } +} + +/// Wrapper that instantiates ModuleWithMixedParams as a sub-module. +class WrapperOfMixedParams extends Module { + Logic get result => output('result'); + + WrapperOfMixedParams(Logic x, {int width = 8}) + : super(name: 'wrapper_mixed') { + x = addInput('x', x, width: width); + addOutput('result', width: width); + + result <= ModuleWithMixedParams(x, width: width).out; + } +} + +void main() { + group('ModuleParameter', () { + test('creates with correct name and value', () { + final param = ModuleParameter('WIDTH', defaultValue: 8); + expect(param.name, 'WIDTH'); + expect(param.defaultValue, 8); + expect(param.svType, 'int'); + expect(param.isLocalParam, false); + }); + + test('creates localparam', () { + final param = + ModuleParameter('DEPTH', defaultValue: 256, isLocalParam: true); + expect(param.isLocalParam, true); + expect(param.name, 'DEPTH'); + }); + + test('infers svType for int', () { + final param = ModuleParameter('W', defaultValue: 8); + expect(param.svType, 'int'); + }); + + test('infers svType for bool', () { + final param = ModuleParameter('EN', defaultValue: true); + expect(param.svType, 'bit'); + }); + + test('uses explicit svType when provided', () { + final param = + ModuleParameter('W', defaultValue: 8, svType: 'logic [3:0]'); + expect(param.svType, 'logic [3:0]'); + }); + + test('generates correct SV default value for int', () { + final param = ModuleParameter('W', defaultValue: 42); + expect(param.svDefault, '42'); + }); + + test('generates correct SV default value for bool', () { + final paramT = ModuleParameter('EN', defaultValue: true); + expect(paramT.svDefault, "1'b1"); + final paramF = ModuleParameter('EN', defaultValue: false); + expect(paramF.svDefault, "1'b0"); + }); + + test('toSvParameterDefinition produces correct output', () { + final param = ModuleParameter('WIDTH', defaultValue: 8); + final def = param.toSvParameterDefinition(); + expect(def.name, 'WIDTH'); + expect(def.type, 'int'); + expect(def.defaultValue, '8'); + }); + + test('toExpression creates ParameterExpression for int', () { + final param = ModuleParameter('W', defaultValue: 16); + final expr = param.toExpression(); + expect(expr.value, 16); + expect(expr.svExpression, 'W'); + }); + + test('toExpression throws for non-int', () { + final param = ModuleParameter('EN', defaultValue: true); + expect(param.toExpression, throwsStateError); + }); + }); + + group('ParameterExpression', () { + test('ofParam creates from ModuleParameter', () { + final param = ModuleParameter('WIDTH', defaultValue: 8); + final expr = ParameterExpression.ofParam(param); + expect(expr.value, 8); + expect(expr.svExpression, 'WIDTH'); + }); + + test('ofInt creates from plain int', () { + final expr = ParameterExpression.ofInt(42); + expect(expr.value, 42); + expect(expr.svExpression, '42'); + }); + + test('addition with int', () { + final expr = ParameterExpression.ofParam( + ModuleParameter('W', defaultValue: 8)); + final result = expr + 1; + expect(result.value, 9); + expect(result.svExpression, 'W + 1'); + }); + + test('subtraction with int', () { + final expr = ParameterExpression.ofParam( + ModuleParameter('W', defaultValue: 8)); + final result = expr - 1; + expect(result.value, 7); + expect(result.svExpression, 'W - 1'); + }); + + test('multiplication with int', () { + final expr = ParameterExpression.ofParam( + ModuleParameter('W', defaultValue: 4)); + final result = expr * 2; + expect(result.value, 8); + expect(result.svExpression, '(W) * 2'); + }); + + test('integer division with int', () { + final expr = ParameterExpression.ofParam( + ModuleParameter('W', defaultValue: 16)); + final result = expr ~/ 2; + expect(result.value, 8); + expect(result.svExpression, '(W) / 2'); + }); + + test('left shift with int', () { + final expr = ParameterExpression.ofParam( + ModuleParameter('W', defaultValue: 1)); + final result = expr << 3; + expect(result.value, 8); + expect(result.svExpression, '(W) << 3'); + }); + + test('right shift with int', () { + final expr = ParameterExpression.ofParam( + ModuleParameter('W', defaultValue: 16)); + final result = expr >> 2; + expect(result.value, 4); + expect(result.svExpression, '(W) >> 2'); + }); + + test('chained operations', () { + final expr = ParameterExpression.ofParam( + ModuleParameter('W', defaultValue: 8)); + final result = expr * 2 + 1; + expect(result.value, 17); + expect(result.svExpression, '(W) * 2 + 1'); + }); + + test('addition with another ParameterExpression', () { + final a = ParameterExpression.ofParam( + ModuleParameter('A', defaultValue: 3)); + final b = ParameterExpression.ofParam( + ModuleParameter('B', defaultValue: 5)); + final result = a + b; + expect(result.value, 8); + expect(result.svExpression, 'A + B'); + }); + }); + + group('ParameterConst', () { + test('holds correct value', () { + final param = ModuleParameter('WIDTH', defaultValue: 8); + final pc = ParameterConst(param, width: 32); + expect(pc.value, LogicValue.ofInt(8, 32)); + expect(pc.svExpression, 'WIDTH'); + }); + + test('uses custom svExpression', () { + final param = ModuleParameter('WIDTH', defaultValue: 8); + final pc = ParameterConst(param, width: 32, svExpression: 'WIDTH - 1'); + expect(pc.svExpression, 'WIDTH - 1'); + }); + + test('fromExpression constructor', () { + final param = ModuleParameter('W', defaultValue: 8); + final expr = param.toExpression() - 1; + final pc = + ParameterConst.fromExpression(expr, parameter: param, width: 32); + expect(pc.value, LogicValue.ofInt(7, 32)); + expect(pc.svExpression, 'W - 1'); + }); + }); + + group('Parameterized SV generation', () { + test('module with parameters emits parameter declarations', () async { + final mod = ParameterizedAdder(Logic(width: 8), Logic(width: 8)); + await mod.build(); + final sv = mod.generateSynth(); + + // Should contain parameter declaration + expect(sv, contains('parameter int WIDTH = 8')); + }); + + test('parameterized ports use expression-based widths', () async { + final mod = ParameterizedAdder(Logic(width: 8), Logic(width: 8)); + await mod.build(); + final sv = mod.generateSynth(); + + // Ports should use WIDTH-based ranges instead of [7:0] + expect(sv, contains('WIDTH - 1:0')); + }); + + test('submodule instantiation includes parameter values', () async { + final mod = TopWithParameterizedAdder(Logic(width: 8), Logic(width: 8)); + await mod.build(); + final sv = mod.generateSynth(); + + // The instantiation of ParameterizedAdder should include #(.WIDTH(8)) + expect(sv, contains('.WIDTH(')); + }); + + test('two instances with same params share definition', () async { + final mod = TopWithParameterizedAdder(Logic(width: 8), Logic(width: 8)); + await mod.build(); + final sv = mod.generateSynth(); + + // Should have exactly one 'module ParameterizedAdder' definition + final moduleDefCount = 'module ParameterizedAdder'.allMatches(sv).length; + expect(moduleDefCount, 1); + }); + + test('ParameterConst emits parameter name in SV', () async { + final mod = ModuleWithParameterConst(Logic(width: 8)); + await mod.build(); + final sv = mod.generateSynth(); + + // Should reference WIDTH in expressions instead of literal 8 + expect(sv, contains('WIDTH')); + expect(sv, contains('parameter int WIDTH = 8')); + }); + + test('localparam module generates localparam in definition', () async { + final mod = ModuleWithLocalParam(Logic(width: 4)); + await mod.build(); + final sv = mod.generateSynth(); + + expect(sv, contains('localparam int ADDR_WIDTH = 4')); + // Should NOT contain 'parameter int ADDR_WIDTH' + expect(sv, isNot(contains('parameter int ADDR_WIDTH'))); + }); + + test('mixed params: definition has both parameter and localparam', + () async { + final mod = ModuleWithMixedParams(Logic(width: 8)); + await mod.build(); + final sv = mod.generateSynth(); + + expect(sv, contains('parameter int WIDTH = 8')); + expect(sv, contains('localparam int DEPTH = 16')); + }); + + test('mixed params: instantiation only passes non-localparams', () async { + final mod = WrapperOfMixedParams(Logic(width: 8)); + await mod.build(); + final sv = mod.generateSynth(); + + // Instantiation should include WIDTH but not DEPTH + expect(sv, contains('#(.WIDTH(8))')); + // The instantiation line should not reference DEPTH + expect(sv, isNot(contains('#(.WIDTH(8), .DEPTH'))); + expect(sv, isNot(contains('.DEPTH('))); + }); + + test('module registers parameters correctly', () { + final mod = ParameterizedAdder(Logic(width: 8), Logic(width: 8)); + expect(mod.moduleParameters.length, 1); + expect(mod.moduleParameters.first.name, 'WIDTH'); + expect(mod.moduleParameters.first.defaultValue, 8); + }); + + test('parameterized LogicArray dimensions in SV', () async { + final mod = + ModuleWithParameterizedArray(LogicArray([4], 8), LogicArray([4], 8)); + await mod.build(); + final sv = mod.generateSynth(); + + // Should have parameterized dimension and element width + expect(sv, contains('parameter int N = 4')); + expect(sv, contains('parameter int W = 8')); + expect(sv, contains('[N - 1:0]')); + expect(sv, contains('[W - 1:0]')); + }); + }); +}