diff --git a/lib/src/interfaces/interface.dart b/lib/src/interfaces/interface.dart index 36d927a9e..4080bf212 100644 --- a/lib/src/interfaces/interface.dart +++ b/lib/src/interfaces/interface.dart @@ -79,43 +79,81 @@ class Interface { {Iterable? inputTags, Iterable? outputTags, Iterable? inOutTags, - String Function(String original)? uniquify}) { + String Function(String original)? uniquify, + String? groupName}) { uniquify ??= (original) => original; + // Derive the interface group name from the uniquify function. + // We probe with a sentinel to detect what the function adds. + // Supports prefix patterns (e.g. 'ace0_pcieRp_X') and suffix + // patterns (e.g. 'x_m', 'x_dti_a2f'). + if (groupName == null) { + const sentinel = '\x00'; + final probed = uniquify(sentinel); + if (probed.endsWith(sentinel)) { + // Prefix-based: uniquify('X') → 'prefix_X' + groupName = probed.substring(0, probed.length - 1); + } else if (probed.startsWith(sentinel)) { + // Suffix-based: uniquify('X') → 'X_suffix' + groupName = probed.substring(1); + } + } + + // Helper to register the port group if we successfully extracted one. + void registerGroup(String portName) { + if (groupName != null && groupName.isNotEmpty) { + // Strip leading/trailing separators for a cleaner group name. + var trimmed = groupName; + if (trimmed.startsWith('_')) { + trimmed = trimmed.substring(1); + } + if (trimmed.endsWith('_')) { + trimmed = trimmed.substring(0, trimmed.length - 1); + } + if (trimmed.isNotEmpty) { + module.registerPortGroup(portName, trimmed); + } + } + } + if (inputTags != null) { for (final port in getPorts(inputTags).values) { + final portName = uniquify(port.name); port <= (port is LogicArray ? module.addInputArray( - uniquify(port.name), + portName, srcInterface.port(port.name), dimensions: port.dimensions, elementWidth: port.elementWidth, numUnpackedDimensions: port.numUnpackedDimensions, ) : module.addInput( - uniquify(port.name), + portName, srcInterface.port(port.name), width: port.width, )); + registerGroup(portName); } } if (outputTags != null) { for (final port in getPorts(outputTags).values) { + final portName = uniquify(port.name); final output = (port is LogicArray ? module.addOutputArray( - uniquify(port.name), + portName, dimensions: port.dimensions, elementWidth: port.elementWidth, numUnpackedDimensions: port.numUnpackedDimensions, ) : module.addOutput( - uniquify(port.name), + portName, width: port.width, )); output <= port; srcInterface.port(port.name) <= output; + registerGroup(portName); } } @@ -131,20 +169,22 @@ class Interface { port, 'LogicNet must be used for inOut ports.'); } + final portName = uniquify(port.name); port <= (port is LogicArray ? module.addInOutArray( - uniquify(port.name), + portName, srcInterface.port(port.name), dimensions: port.dimensions, elementWidth: port.elementWidth, numUnpackedDimensions: port.numUnpackedDimensions, ) : module.addInOut( - uniquify(port.name), + portName, srcInterface.port(port.name), width: port.width, )); + registerGroup(portName); } } } diff --git a/lib/src/interfaces/interface_structure.dart b/lib/src/interfaces/interface_structure.dart new file mode 100644 index 000000000..35f2469ee --- /dev/null +++ b/lib/src/interfaces/interface_structure.dart @@ -0,0 +1,40 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// interface_structure.dart +// A LogicStructure that represents a group of interface ports. +// +// 2026 May +// Author: ROHD Contributors + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A [LogicStructure] created from a group of [Interface] ports. +/// +/// This enables [Interface] ports grouped by direction to be represented as +/// a single `typedef struct packed` in SystemVerilog, rather than individual +/// flat signals. +/// +/// The [interfaceTypeName] is used to derive the SV typedef name when +/// generating struct-typed output. +@internal +class InterfaceStructure extends LogicStructure { + /// The type name to use for the SV struct typedef, derived from the + /// interface class name. + final String interfaceTypeName; + + /// Creates an [InterfaceStructure] from a list of [Logic] elements. + InterfaceStructure( + super.elements, { + required this.interfaceTypeName, + super.name, + }); + + @override + InterfaceStructure clone({String? name}) => InterfaceStructure( + elements.map((e) => e.clone(name: e.name)), + interfaceTypeName: interfaceTypeName, + name: name ?? this.name, + ); +} diff --git a/lib/src/interfaces/pair_interface.dart b/lib/src/interfaces/pair_interface.dart index 5c1d8dd93..2c08f2af7 100644 --- a/lib/src/interfaces/pair_interface.dart +++ b/lib/src/interfaces/pair_interface.dart @@ -11,6 +11,7 @@ import 'dart:collection'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/interfaces/interface_structure.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; /// A direction for signals between a pair of components. @@ -164,7 +165,8 @@ class PairInterface extends Interface { {Iterable? inputTags, Iterable? outputTags, Iterable? inOutTags, - String Function(String original)? uniquify}) { + String Function(String original)? uniquify, + String? groupName}) { final nonNullUniquify = uniquify ?? (original) => original; // ignore: deprecated_member_use_from_same_package final nonNullModify = modify ?? (original) => original; @@ -175,7 +177,8 @@ class PairInterface extends Interface { inputTags: inputTags, outputTags: outputTags, inOutTags: inOutTags, - uniquify: newUniquify); + uniquify: newUniquify, + groupName: groupName); if (subInterfaces.isNotEmpty) { if (srcInterface is! PairInterface) { @@ -241,11 +244,336 @@ class PairInterface extends Interface { outputTags: subIntfOutputTags, inOutTags: subIntfInOutTags, uniquify: newSubIntfUniquify, + groupName: subInterfaceName, ); } } } + /// Like [pairConnectIO], but groups ports by direction into + /// [LogicStructure] ports for struct-typed SV generation. + void pairConnectIOAsStruct( + Module module, Interface srcInterface, PairRole role, + {String Function(String original)? uniquify, String? structName}) { + final List inputTags; + final List outputTags; + final inOutTags = [ + PairDirection.commonInOuts, + ]; + + switch (role) { + case PairRole.consumer: + inputTags = [ + PairDirection.sharedInputs, + PairDirection.fromProvider, + ]; + outputTags = [ + PairDirection.fromConsumer, + ]; + + case PairRole.provider: + inputTags = [ + PairDirection.sharedInputs, + PairDirection.fromConsumer, + ]; + outputTags = [ + PairDirection.fromProvider, + ]; + } + + connectIOAsStruct( + module, + srcInterface, + inputTags: inputTags, + outputTags: outputTags, + inOutTags: inOutTags, + uniquify: uniquify, + structName: structName, + ); + } + + /// Builds hierarchical [InterfaceStructure] ports that mirror the + /// sub-interface hierarchy, grouping all input-tagged ports into a single + /// `_in` struct and all output-tagged ports into a single `_out` struct. + void connectIOAsStruct( + Module module, + Interface srcInterface, { + Iterable? inputTags, + Iterable? outputTags, + Iterable? inOutTags, + String Function(String original)? uniquify, + String? structName, + }) { + final nonNullUniquify = uniquify ?? (original) => original; + // ignore: deprecated_member_use_from_same_package + final nonNullModify = modify ?? (original) => original; + String newUniquify(String original) => + nonNullUniquify(nonNullModify(original)); + + if (srcInterface is! PairInterface) { + throw InterfaceTypeException( + srcInterface, 'connectIOAsStruct requires a PairInterface source'); + } + + final intfTypeName = structName ?? _deriveInterfaceTypeName(srcInterface); + + // Build hierarchical input + output structs from entire sub-interface tree + final inputMappings = <_PortMapping>[]; + final outputMappings = <_PortMapping>[]; + final result = _buildHierarchicalStructPair( + srcInterface: srcInterface, + inputTags: inputTags ?? const [], + outputTags: outputTags ?? const [], + structName: intfTypeName, + inputMappings: inputMappings, + outputMappings: outputMappings, + ); + + // Wire hierarchical input struct + if (result.inputStruct != null) { + final srcStruct = result.inputStruct!; + + // Drive fresh struct leaf elements from srcInterface ports + for (var i = 0; i < inputMappings.length; i++) { + final m = inputMappings[i]; + srcStruct.leafElements[i] <= m.srcIntf.port(m.portName); + } + + // Register as a single typed input on the module + final modulePort = module.addTypedInput( + newUniquify(srcStruct.name), + srcStruct, + ); + + // Wire this interface hierarchy's ports from the module port + for (var i = 0; i < inputMappings.length; i++) { + final m = inputMappings[i]; + m.thisIntf.port(m.portName) <= modulePort.leafElements[i]; + } + } + + // Wire hierarchical output struct + if (result.outputStruct != null) { + final outTemplate = result.outputStruct!; + + // Register as a single typed output on the module + final modulePort = module.addTypedOutput( + newUniquify(outTemplate.name), + ({name = ''}) => outTemplate.clone( + name: name.isEmpty ? outTemplate.name : name, + ), + ); + + // Drive output struct elements from this interface's ports + for (var i = 0; i < outputMappings.length; i++) { + final m = outputMappings[i]; + modulePort.leafElements[i] <= m.thisIntf.port(m.portName); + } + + // Wire srcInterface ports from output struct elements + for (var i = 0; i < outputMappings.length; i++) { + final m = outputMappings[i]; + m.srcIntf.port(m.portName) <= modulePort.leafElements[i]; + } + } + + // InOut ports are still connected individually (can't be in packed structs) + if (inOutTags != null) { + _connectInOutsHierarchically( + module, srcInterface, inOutTags, newUniquify); + } + } + + /// Recursively builds hierarchical [InterfaceStructure]s for input and + /// output directions, mirroring the sub-interface hierarchy. + ({InterfaceStructure? inputStruct, InterfaceStructure? outputStruct}) + _buildHierarchicalStructPair({ + required PairInterface srcInterface, + required Iterable inputTags, + required Iterable outputTags, + required String structName, + required List<_PortMapping> inputMappings, + required List<_PortMapping> outputMappings, + }) { + final inputElements = []; + final outputElements = []; + + // 1. Collect direct ports at this level + if (inputTags.isNotEmpty) { + for (final entry in getPorts(inputTags.toSet()).entries) { + final srcPort = srcInterface.port(entry.key); + inputElements.add(Logic(name: srcPort.name, width: srcPort.width)); + inputMappings.add(_PortMapping(this, srcInterface, entry.key)); + } + } + + if (outputTags.isNotEmpty) { + for (final entry in getPorts(outputTags.toSet()).entries) { + final p = port(entry.key); + outputElements.add(Logic(name: p.name, width: p.width)); + outputMappings.add(_PortMapping(this, srcInterface, entry.key)); + } + } + + // 2. Recurse into sub-interfaces + for (final subEntry in _subInterfaces.entries) { + final subName = subEntry.key; + final subIntf = subEntry.value.interface; + + if (!srcInterface._subInterfaces.containsKey(subName)) { + throw InterfaceTypeException( + srcInterface, 'missing a sub-interface named $subName'); + } + final srcSubIntf = srcInterface._subInterfaces[subName]!.interface; + + // Compute tags for sub-interface (handle reversal) + final subTags = + _computeSubTags(inputTags, outputTags, subEntry.value.reverse); + + final subResult = subIntf._buildHierarchicalStructPair( + srcInterface: srcSubIntf, + inputTags: subTags.inputTags, + outputTags: subTags.outputTags, + structName: subName, + inputMappings: inputMappings, + outputMappings: outputMappings, + ); + + if (subResult.inputStruct != null) { + inputElements.add(subResult.inputStruct!); + } + if (subResult.outputStruct != null) { + outputElements.add(subResult.outputStruct!); + } + } + + // 3. Build structs (null if no matching ports at any depth) + final intfTypeName = _deriveInterfaceTypeName(srcInterface); + + return ( + inputStruct: inputElements.isEmpty + ? null + : InterfaceStructure( + inputElements, + interfaceTypeName: '${intfTypeName}_in', + name: '${structName}_in', + ), + outputStruct: outputElements.isEmpty + ? null + : InterfaceStructure( + outputElements, + interfaceTypeName: '${intfTypeName}_out', + name: '${structName}_out', + ), + ); + } + + /// Computes sub-interface input/output tags, applying direction reversal + /// when the sub-interface is marked as [reverse]. + static ({List inputTags, List outputTags}) + _computeSubTags( + Iterable inputTags, + Iterable outputTags, + bool reverse, + ) { + if (!reverse) { + return (inputTags: inputTags.toList(), outputTags: outputTags.toList()); + } + + final subInputTags = []; + final subOutputTags = []; + + // fromConsumer: swap between input and output + if (inputTags.contains(PairDirection.fromConsumer)) { + subOutputTags.add(PairDirection.fromConsumer); + } + if (outputTags.contains(PairDirection.fromConsumer)) { + subInputTags.add(PairDirection.fromConsumer); + } + + // fromProvider: swap between input and output + if (outputTags.contains(PairDirection.fromProvider)) { + subInputTags.add(PairDirection.fromProvider); + } + if (inputTags.contains(PairDirection.fromProvider)) { + subOutputTags.add(PairDirection.fromProvider); + } + + // sharedInputs: always stays as input + if (inputTags.contains(PairDirection.sharedInputs)) { + subInputTags.add(PairDirection.sharedInputs); + } + + return (inputTags: subInputTags, outputTags: subOutputTags); + } + + /// Recursively connects inOut ports individually across the sub-interface + /// hierarchy. + void _connectInOutsHierarchically( + Module module, + PairInterface srcInterface, + Iterable inOutTags, + String Function(String) uniquify, + ) { + // Connect this level's inOuts + for (final p in getPorts(inOutTags.toSet()).values) { + if (p is LogicArray) { + if (!p.isNet) { + throw PortTypeException( + p, 'LogicArray nets must be used for inOut array ports.'); + } + p <= + module.addInOutArray( + uniquify(p.name), + srcInterface.port(p.name), + dimensions: p.dimensions, + elementWidth: p.elementWidth, + numUnpackedDimensions: p.numUnpackedDimensions, + ); + } else if (p is LogicNet) { + p <= + module.addInOut( + uniquify(p.name), + srcInterface.port(p.name), + width: p.width, + ); + } else { + throw PortTypeException(p, 'LogicNet must be used for inOut ports.'); + } + } + + // Recurse into sub-interfaces + for (final subEntry in _subInterfaces.entries) { + final subName = subEntry.key; + final subIntf = subEntry.value.interface; + + if (!srcInterface._subInterfaces.containsKey(subName)) { + throw InterfaceTypeException( + srcInterface, 'missing a sub-interface named $subName'); + } + final srcSubIntf = srcInterface._subInterfaces[subName]!.interface; + + final subUniquify = + subEntry.value.uniquify ?? (String original) => original; + String subUq(String original) => uniquify(subUniquify(original)); + + subIntf._connectInOutsHierarchically( + module, srcSubIntf, inOutTags, subUq); + } + } + + /// Derives an interface type name from the runtime type of the interface. + static String _deriveInterfaceTypeName(Interface srcInterface) { + final runtimeName = srcInterface.runtimeType.toString(); + final baseName = runtimeName.contains('<') + ? runtimeName.substring(0, runtimeName.indexOf('<')) + : runtimeName; + if (baseName == 'Interface' || baseName == 'PairInterface') { + return 'intf'; + } + return baseName; + } + /// A mapping from sub-interface names to instances of sub-interfaces. Map get subInterfaces => UnmodifiableMapView(_subInterfaces @@ -432,3 +760,18 @@ class _SubPairInterface { /// Constructs a new sub-interface tracking object with characteristics. _SubPairInterface(this.interface, {required this.reverse, this.uniquify}); } + +/// Tracks a port mapping between the local (this) and source interfaces, +/// used for wiring hierarchical struct ports. +class _PortMapping { + /// The local interface (inside the module) owning the port. + final PairInterface thisIntf; + + /// The source interface (outside the module) owning the port. + final PairInterface srcIntf; + + /// The port name on both interfaces. + final String portName; + + _PortMapping(this.thisIntf, this.srcIntf, this.portName); +} diff --git a/lib/src/module.dart b/lib/src/module.dart index ffeff9fc8..a6d19d949 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -52,6 +52,29 @@ abstract class Module { /// An internal mapping of input names to their sources to this [Module]. late final Map _inputSources = {}; + /// Maps port names to their interface group name. + /// + /// Populated during [Interface.connectIO] to record which ports were + /// registered together as part of the same interface. The group name + /// is derived from the `uniquify` prefix applied during connection. + /// + /// This enables netlist synthesis to emit port-grouping annotations + /// without relying on naming heuristics. + final Map _portGroups = {}; + + /// Returns an unmodifiable view of the port-to-interface-group mapping. + /// + /// Keys are port names; values are the interface group name shared by + /// all ports registered in the same [Interface.connectIO] call. + Map get portGroups => UnmodifiableMapView(_portGroups); + + /// Registers [portName] as belonging to interface group [groupName]. + /// + /// Called by [Interface.connectIO] during port registration. + void registerPortGroup(String portName, String groupName) { + _portGroups[portName] = groupName; + } + // ─── Central naming (Namer) ───────────────────────────────────── /// Central namer that owns both the signal and instance namespaces. @@ -1087,26 +1110,161 @@ abstract class Module { /// Connects the [source] to this [Module] using [Interface.connectIO] and /// returns a copy of the [source] that can be used within this module. + /// + /// When [asStruct] is `true`, input and output ports are registered as + /// [LogicStructure] ports (named `_in` / `_out`) rather than + /// individual leaf ports. The prefix is derived from [uniquify]. InterfaceType addInterfacePorts, - TagType extends Enum>(InterfaceType source, - {Iterable? inputTags, - Iterable? outputTags, - Iterable? inOutTags, - String Function(String original)? uniquify}) => - (source.clone() as InterfaceType) + TagType extends Enum>(InterfaceType source, + {Iterable? inputTags, + Iterable? outputTags, + Iterable? inOutTags, + String Function(String original)? uniquify, + bool asStruct = false}) { + if (!asStruct) { + return (source.clone() as InterfaceType) ..connectIO(this, source, inputTags: inputTags, outputTags: outputTags, inOutTags: inOutTags, uniquify: uniquify); + } + + return _addInterfacePortsAsStruct( + source, + inputTags: inputTags, + outputTags: outputTags, + inOutTags: inOutTags, + uniquify: uniquify, + ); + } /// Connects the [source] to this [Module] using [PairInterface.pairConnectIO] /// and returns a copy of the [source] that can be used within this module. + /// + /// When [asStruct] is `true`, input and output ports are registered as + /// [LogicStructure] ports (named `_in` / `_out`) rather than + /// individual leaf ports. The prefix is derived from [uniquify]. InterfaceType addPairInterfacePorts( - InterfaceType source, PairRole role, - {String Function(String original)? uniquify}) => - (source.clone() as InterfaceType) + InterfaceType source, PairRole role, + {String Function(String original)? uniquify, bool asStruct = false}) { + if (!asStruct) { + return (source.clone() as InterfaceType) ..pairConnectIO(this, source, role, uniquify: uniquify); + } + + return (source.clone() as InterfaceType) + ..pairConnectIOAsStruct(this, source, role, uniquify: uniquify); + } + + /// Derives a group prefix from [uniquify], or falls back to 'intf'. + static String _derivePrefix(String Function(String original)? uniquify) { + if (uniquify == null) { + return 'intf'; + } + const sentinel = '\x00'; + final probed = uniquify(sentinel); + String raw; + if (probed.endsWith(sentinel)) { + raw = probed.substring(0, probed.length - 1); + } else if (probed.startsWith(sentinel)) { + raw = probed.substring(1); + } else { + raw = probed; + } + // Strip leading/trailing underscores. + raw = raw.replaceAll(RegExp(r'^_+|_+$'), ''); + return raw.isEmpty ? 'intf' : raw; + } + + /// Implementation of struct-based interface port registration. + /// + /// Creates [LogicStructure] ports for inputs and outputs, wires them to the + /// [source] interface, and returns the internal (cloned) interface whose + /// port signals are the struct elements. + InterfaceType _addInterfacePortsAsStruct( + InterfaceType source, { + Iterable? inputTags, + Iterable? outputTags, + Iterable? inOutTags, + String Function(String original)? uniquify, + }) { + final prefix = _derivePrefix(uniquify); + final u = uniquify ?? (original) => original; + + final cloned = source.clone() as InterfaceType; + + // --- Inputs: build LogicStructure, register via addTypedInput --- + if (inputTags != null) { + final srcInputPorts = (source as Interface) + .getPorts(inputTags) + .values + .toList(growable: false); + + if (srcInputPorts.isNotEmpty) { + // Create elements matching source port widths/names. + final elements = [ + for (final p in srcInputPorts) Logic(name: u(p.name), width: p.width), + ]; + final structIn = LogicStructure(elements, name: '${prefix}_in'); + final inPort = addTypedInput('${prefix}_in', structIn); + + // Wire: cloned interface port <= struct element. + for (var i = 0; i < srcInputPorts.length; i++) { + (cloned as Interface).port(srcInputPorts[i].name) <= + inPort.elements[i]; + } + } + } + + // --- Outputs: build LogicStructure, register via addTypedOutput --- + if (outputTags != null) { + final srcOutputPorts = (source as Interface) + .getPorts(outputTags) + .values + .toList(growable: false); + + if (srcOutputPorts.isNotEmpty) { + final outPort = addTypedOutput( + '${prefix}_out', + ({name = ''}) => LogicStructure( + [ + for (final p in srcOutputPorts) + Logic(name: u(p.name), width: p.width), + ], + name: name, + ), + ); + + // Wire: output struct element <= cloned interface port, + // and source port <= output struct element. + for (var i = 0; i < srcOutputPorts.length; i++) { + outPort.elements[i] <= + (cloned as Interface).port(srcOutputPorts[i].name); + source.port(srcOutputPorts[i].name) <= outPort.elements[i]; + } + } + } + + // --- InOuts: fall back to leaf-level registration (structs don't + // support bidirectional nets) --- + if (inOutTags != null) { + final srcInOutPorts = (source as Interface) + .getPorts(inOutTags) + .values + .toList(growable: false); + + for (final port in srcInOutPorts) { + final portName = u(port.name); + final inOutPort = + addInOut(portName, source.port(port.name), width: port.width); + (cloned as Interface).port(port.name) <= inOutPort; + } + } + + return cloned; + } @override String toString() => [