From ad2e656907deb5318512de45a042814d6c7c352b Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 18 Dec 2025 09:23:48 -0800 Subject: [PATCH 1/9] SchematicSynthesizer --- doc/architecture.md | 12 +- .../schematic/constant_handler.dart | 415 +++++++ .../synthesizers/schematic/module_map.dart | 335 ++++++ .../synthesizers/schematic/module_utils.dart | 19 + .../schematic/passthrough_handler.dart | 109 ++ lib/src/synthesizers/schematic/schematic.dart | 14 + .../schematic/schematic_dumper.dart | 1040 +++++++++++++++++ .../schematic/schematic_mixins.dart | 236 ++++ .../schematic/schematic_primitives.dart | 838 +++++++++++++ .../schematic/schematic_synthesis_result.dart | 899 ++++++++++++++ .../schematic/schematic_synthesizer.dart | 163 +++ .../schematic/sequential_handler.dart | 199 ++++ .../schematic/yosys/_yosys_loader_runner.mjs | 44 + .../schematic/yosys/d3-yosys/LICENSE | 277 +++++ .../schematic/yosys/d3-yosys/README.md | 3 + .../schematic/yosys/d3-yosys/src/yosys.js | 573 +++++++++ .../yosys/d3-yosys/src/yosysIcons.js | 85 ++ .../yosys/d3-yosys/src/yosysUtills.js | 167 +++ .../schematic/yosys/yosys_loader_helper.dart | 97 ++ lib/src/synthesizers/synth_builder.dart | 3 + lib/src/synthesizers/synthesizer.dart | 12 +- test/schematic_example_test.dart | 263 +++++ test/synth_builder_test.dart | 86 ++ 23 files changed, 5884 insertions(+), 5 deletions(-) create mode 100644 lib/src/synthesizers/schematic/constant_handler.dart create mode 100644 lib/src/synthesizers/schematic/module_map.dart create mode 100644 lib/src/synthesizers/schematic/module_utils.dart create mode 100644 lib/src/synthesizers/schematic/passthrough_handler.dart create mode 100644 lib/src/synthesizers/schematic/schematic.dart create mode 100644 lib/src/synthesizers/schematic/schematic_dumper.dart create mode 100644 lib/src/synthesizers/schematic/schematic_mixins.dart create mode 100644 lib/src/synthesizers/schematic/schematic_primitives.dart create mode 100644 lib/src/synthesizers/schematic/schematic_synthesis_result.dart create mode 100644 lib/src/synthesizers/schematic/schematic_synthesizer.dart create mode 100644 lib/src/synthesizers/schematic/sequential_handler.dart create mode 100644 lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs create mode 100644 lib/src/synthesizers/schematic/yosys/d3-yosys/LICENSE create mode 100644 lib/src/synthesizers/schematic/yosys/d3-yosys/README.md create mode 100644 lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosys.js create mode 100644 lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosysIcons.js create mode 100644 lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosysUtills.js create mode 100644 lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart create mode 100644 test/schematic_example_test.dart diff --git a/doc/architecture.md b/doc/architecture.md index cc1e775ae..e7f92aa47 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -22,10 +22,6 @@ Every `Module` defines its own functionality. This could be through composition The `Simulator` acts as a statically accessible driver of the overall simulation. Anything can register arbitrary `Function`s to be executed at any timestamp that has not already occurred. The `Simulator` does not need to understand much about the functionality of a design; rather, the `Module`s and `Logic`s are responsible for propagating changes throughout. -### Synthesizer - -A separate type of object responsible for taking a `Module` and converting it to some output, such as SystemVerilog. - ## Organization All the code for the ROHD framework library is in `lib/src/`, with `lib/rohd.dart` exporting the main stuff for usage. @@ -46,6 +42,14 @@ Contains a collection of `Module` implementations that can be used as primitive Contains logic for synthesizing `Module`s into some output. It is structured to maximize reusability across different output types (including those not yet supported). +#### SystemVerilogSynthesizer + +A `Synthesizer` that produces synthesizable SystemVerilog for a `Module` and its dependencies. + +#### SchematicSynthesizer + +A `Synthesizer` that produces [ELK JSON] () for use with a schematic visualizer like that in [d3-hwschematic](). + ### Utilities Various generic objects and classes that may be useful in different areas of ROHD. diff --git a/lib/src/synthesizers/schematic/constant_handler.dart b/lib/src/synthesizers/schematic/constant_handler.dart new file mode 100644 index 000000000..043113881 --- /dev/null +++ b/lib/src/synthesizers/schematic/constant_handler.dart @@ -0,0 +1,415 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// constant_handler.dart +// Encapsulates constant collection and emission logic used by the schematic +// dumper. This file contains `PerInputConstInfo`, `ConstantCollectionResult`, +// and `ConstantHandler`. +// +// 2025 December 16 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +import 'package:rohd/src/synthesizers/schematic/module_map.dart'; +import 'package:rohd/src/synthesizers/schematic/schematic_primitives.dart'; + +bool _listEquals(List a, List b) { + if (a.length != b.length) { + return false; + } + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +/// Information about a per-input constant driver. +class PerInputConstInfo { + /// The list of internal net IDs driving the input. + final List _ids; + + /// The constant value being driven. + final BigInt _value; + + /// Creates a [PerInputConstInfo]. + PerInputConstInfo({ + required List ids, + required BigInt value, + }) : _value = value, + _ids = ids; + + /// The bit-width of the constant driver. + int get width => _ids.length; + + /// The stable net name for this constant driver. + /// Use a simple name `const_` as the base; replication suffixes + /// will be appended by the emitter if necessary to avoid collisions. + String get netName => 'const_$_value'; +} + +/// Result of constant collection. +class ConstantCollectionResult { + final Map> _patternToIds; + final Map _perInputConsts; + + /// The set of IDs that should not have shared $const cells emitted for them. + final Set blockedIds; + + /// Creates a [ConstantCollectionResult]. + ConstantCollectionResult({ + required Map> patternToIds, + required Map perInputConsts, + required this.blockedIds, + }) : _perInputConsts = perInputConsts, + _patternToIds = patternToIds; + + /// Generates a stable name for a constant pattern given its [pattern] + /// string and associated [ids]. + static String nameForPattern(String pattern, List ids) => + 'const_${ids.length}_${pattern.hashCode}'; +} + +/// Handler for collecting and emitting constants in a module tree. +class ConstantHandler { + /// Collect constants from a module and its children. + /// + /// Returns a [ConstantCollectionResult] containing all constant state, + /// and populates [internalNetIds] with ID mappings for constant Logics. + /// + /// Parameters: + /// - [module]: The module being processed + /// - [map]: The ModuleMap for this module (kept `dynamic` to avoid + /// circular type dependencies with schematic_dumper) + /// - [internalNetIds]: Map to populate with constant Logic → IDs + /// - [ports]: The ports map (for name collision avoidance) + /// - [nextIdRef]: Reference to the next available ID (will be mutated) + /// - [isTop]: Whether this is the top-level module + ConstantCollectionResult collectConstants({ + required Module module, + required ModuleMap map, + required Map> internalNetIds, + required Map> ports, + required List nextIdRef, // Use list as mutable reference + required bool isTop, + bool filterConstInputsToCombinational = false, + }) { + var nextId = nextIdRef[0]; + + final constPatternToIds = >{}; + final perInputConsts = {}; + final blockedIds = {}; + final blockedPatternKeys = {}; + + /// Helper to register a constant pattern and return its IDs. + List registerConstPattern(Const sig) { + final patternKey = sig.value.toRadixString().split('').reversed.join(); + if (constPatternToIds.containsKey(patternKey)) { + return constPatternToIds[patternKey]!; + } + final ids = List.generate(sig.width, (_) => nextId++); + constPatternToIds[patternKey] = ids; + return ids; + } + + // Process each child module + for (final childMap in map.submodules.values) { + final childModule = childMap.module; + + // Scan primitive child's internal signals for `Const` values. + // For combinational-like primitives we allocate fresh internal IDs + // so they do not become shared pattern-level $const cells; for + // other primitives we register const patterns as before. + final childPrimDesc = Primitives.instance.lookupForModule(childModule); + final childIsPrimitive = childPrimDesc != null; + // Only treat explicit ROHD helper modules with definitionName + // 'Combinational' or 'Sequential' as candidates for the const-input + // filtering behavior. We intentionally do NOT apply this logic to + // "combinational-like" primitives or other helper modules. + final defLower = childModule.definitionName.toLowerCase(); + final childIsCombOrSeq = + defLower == 'combinational' || defLower == 'sequential'; + + if (childIsPrimitive) { + childModule.signals + .whereType() + .where((sig) => !internalNetIds.containsKey(sig)) + .forEach((sig) { + if (filterConstInputsToCombinational && childIsCombOrSeq) { + final ids = List.generate(sig.width, (_) => nextId++); + internalNetIds[sig] = ids; + final patKey = sig.value.toRadixString().split('').reversed.join(); + blockedPatternKeys.add(patKey); + if (constPatternToIds.containsKey(patKey)) { + blockedIds.addAll(constPatternToIds[patKey]!.whereType()); + } + } else { + internalNetIds[sig] = registerConstPattern(sig); + } + }); + } + + // Collect constants used by child inputs. + final childInputs = []; + for (final l in childMap.portLogics.keys) { + if (l.isInput) { + childInputs.add(l); + } + } + // Collect per-input `Const`s: process each input's srcConnections and + // handle combinational-internalization vs per-input driver creation. + for (final input in childInputs) { + input.srcConnections.whereType().forEach((src) { + final defLower2 = childModule.definitionName.toLowerCase(); + final isCombOrSeq = + defLower2 == 'combinational' || defLower2 == 'sequential'; + if (filterConstInputsToCombinational && isCombOrSeq) { + internalNetIds.putIfAbsent( + src, () => List.generate(src.width, (_) => nextId++)); + blockedIds.addAll(internalNetIds[src]!.whereType()); + final patKey = src.value.toRadixString().split('').reversed.join(); + blockedPatternKeys.add(patKey); + if (constPatternToIds.containsKey(patKey)) { + blockedIds.addAll(constPatternToIds[patKey]!.whereType()); + } + return; // continue to next src + } + + perInputConsts.putIfAbsent(input, () { + final ids = List.generate(src.width, (_) => nextId++); + var constValue = BigInt.zero; + for (var i = 0; i < src.width; i++) { + if (src.value[i] == LogicValue.one) { + constValue |= BigInt.one << i; + } + } + return PerInputConstInfo(ids: ids, value: constValue); + }); + internalNetIds[src] = perInputConsts[input]!._ids; + }); + } + + // Scan module's own signals for `Const` values. + module.signals + .whereType() + .where((sig) => !internalNetIds.containsKey(sig)) + .forEach((sig) { + final hasScopeConsumer = sig.dstConnections.any((dst) { + final pm = dst.parentModule; + return pm != null && (pm == module || map.submodules.containsKey(pm)); + }); + if (isTop && !hasScopeConsumer) { + return; + } + + final dsts = sig.dstConnections; + final anyToCombOrSeq = dsts.any((d) { + final pm = d.parentModule; + if (pm == null || !map.submodules.containsKey(pm)) { + return false; + } + final pmDefLower = pm.definitionName.toLowerCase(); + return pmDefLower == 'combinational' || pmDefLower == 'sequential'; + }); + + if (filterConstInputsToCombinational && anyToCombOrSeq) { + internalNetIds[sig] = List.generate(sig.width, (_) => nextId++); + blockedIds.addAll(internalNetIds[sig]!.whereType()); + final patKey = sig.value.toRadixString().split('').reversed.join(); + blockedPatternKeys.add(patKey); + if (constPatternToIds.containsKey(patKey)) { + blockedIds.addAll(constPatternToIds[patKey]!.whereType()); + } + } else { + internalNetIds[sig] = registerConstPattern(sig); + } + }); + } + + nextIdRef[0] = nextId; + // If any pattern-level IDs overlap with blocked IDs (from + // internalization or per-input blocking), then mark the entire + // pattern as blocked so we don't emit a shared $const cell. + for (final ids in constPatternToIds.values) { + if (ids.any(blockedIds.contains)) { + blockedIds.addAll(ids); + } + } + // Also block any patterns that were previously marked as blocked by + // key (internalized earlier before pattern registration). + for (final key in blockedPatternKeys) { + final ids = constPatternToIds[key]; + if (ids != null) { + blockedIds.addAll(ids); + } + } + + return ConstantCollectionResult( + patternToIds: constPatternToIds, + perInputConsts: perInputConsts, + blockedIds: blockedIds, + ); + } + + /// Emit $const cells into the cells map. + /// + /// Parameters: + /// - [constResult]: The result from [collectConstants] + /// - [cells]: The cells map to add $const cells to + /// - [referencedIds]: Set of IDs referenced by ports and other cells + void emitConstCells({ + required ConstantCollectionResult constResult, + required Map> cells, + required Set referencedIds, + }) { + // Emit per-input $const driver cells + for (final entry in constResult._perInputConsts.entries) { + final info = entry.value; + if (info._ids.isEmpty || !info._ids.any(referencedIds.contains)) { + continue; + } + + final verilogName = "${info.width}'h${info._value.toRadixString(16)}"; + final baseName = 'const_${info._value}'; + + var cellKey = baseName; + var suffix = 0; + while (cells.containsKey(cellKey)) { + suffix++; + cellKey = '${baseName}_$suffix'; + } + cells[cellKey] = { + 'hide_name': 1, + 'type': r'$const', + 'parameters': { + 'WIDTH': info.width, + 'VALUE': info._value.toInt() + }, + 'attributes': {'hide_instance_name': true}, + 'port_directions': {verilogName: 'output'}, + 'connections': {verilogName: info._ids} + }; + } + + // Emit pattern-level $const cells not already materialized per-input + for (final patternEntry in constResult._patternToIds.entries) { + final pattern = patternEntry.key; + final ids = patternEntry.value; + if (ids.isEmpty || !ids.any(referencedIds.contains)) { + continue; + } + + // Skip if any of these ids are blocked (don't materialize $const) + if (constResult.blockedIds.any(ids.contains)) { + continue; + } + + // Skip if already materialized as per-input + final alreadyEmitted = constResult._perInputConsts.values + .any((info) => _listEquals(info._ids, ids)); + if (alreadyEmitted) { + continue; + } + + final width = ids.length; + var constValue = BigInt.zero; + for (var i = 0; i < pattern.length; i++) { + if (pattern[i] == '1') { + constValue |= BigInt.one << i; + } + } + final verilogValue = constValue.toRadixString(16); + final verilogName = "$width'h$verilogValue"; + final baseName = ConstantCollectionResult.nameForPattern(pattern, ids); + + var cellKey = baseName; + var suffix = 0; + while (cells.containsKey(cellKey)) { + suffix++; + cellKey = '${baseName}_$suffix'; + } + cells[cellKey] = { + 'hide_name': 1, + 'type': r'$const', + 'parameters': { + 'WIDTH': width, + 'VALUE': constValue.toInt() + }, + 'attributes': {'hide_instance_name': true}, + 'port_directions': {verilogName: 'output'}, + 'connections': {verilogName: ids} + }; + } + } + + /// Populate `netnames` with entries for constants collected in + /// [constResult]. This adds per-input const netnames and pattern-level + /// const netnames into the provided [netnames] map. Returns nothing; the + /// map is mutated in-place. + void emitConstNetnames({ + required ConstantCollectionResult constResult, + required Map netnames, + }) { + // First, add per-input const netnames derived from the ports they + // drive in submodules. + for (final entry in constResult._perInputConsts.entries) { + final info = entry.value; + if (info._ids.isEmpty) { + continue; + } + // Use 'const_' base name for netname; emitter can append a + // suffix if collisions occur elsewhere. + var netName = 'const_${info._value}'; + var suffix = 0; + while (netnames.containsKey(netName)) { + suffix++; + netName = 'const_${info._value}_$suffix'; + } + // Compute a pattern string for clarity (LSB index 0) + final bits = List.generate(info.width, (i) { + final bit = ((info._value >> i) & BigInt.one) == BigInt.one ? '1' : '0'; + return bit; + }); + final patternKey = bits.join(); + netnames[netName] = { + 'bits': info._ids, + 'attributes': {'const_pattern': patternKey} + }; + } + + // Add const pattern netnames for any remaining allocated const + // patterns that were not materialized per-input. + for (final patternEntry in constResult._patternToIds.entries) { + final pattern = patternEntry.key; + final ids = patternEntry.value; + if (ids.isEmpty) { + continue; + } + // Skip if any of these ids are blocked (don't materialize netname) + if (constResult.blockedIds.any(ids.contains)) { + continue; + } + // Skip if already materialized as per-input + final alreadyEmitted = constResult._perInputConsts.values + .any((info) => _listEquals(info._ids, ids)); + if (alreadyEmitted) { + continue; + } + final baseName = ConstantCollectionResult.nameForPattern(pattern, ids); + var netName = baseName; + var suffix = 0; + while (netnames.containsKey(netName)) { + suffix++; + netName = '${baseName}_$suffix'; + } + if (!netnames.containsKey(netName)) { + netnames[netName] = { + 'bits': ids, + 'attributes': {'const_pattern': pattern} + }; + } + } + } +} diff --git a/lib/src/synthesizers/schematic/module_map.dart b/lib/src/synthesizers/schematic/module_map.dart new file mode 100644 index 000000000..7e1d63303 --- /dev/null +++ b/lib/src/synthesizers/schematic/module_map.dart @@ -0,0 +1,335 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_map.dart +// Extracted ModuleMap, computeComponents, and listEquals helpers so +// other handlers can reference them without circular imports. +// +// 2025 December 14 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/schematic/schematic_primitives.dart'; + +/// Compute connected-component roots for `n` items given a list of union +/// operations as index pairs. Returns a list `roots` where `roots[i]` is the +/// canonical root index for element `i`. +List computeComponents(int n, Iterable> unions, + {List? priority}) { + final parent = List.generate(n, (i) => i); + // If a priority list is provided, ensure it has length n by + // padding with zeros if necessary. + var pri = priority ?? List.filled(n, 0); + if (pri.length < n) { + pri = [...pri, ...List.filled(n - pri.length, 0)]; + } + int find(int x) { + var r = x; + while (parent[r] != r) { + parent[r] = parent[parent[r]]; + r = parent[r]; + } + return r; + } + + void unite(int a, int b) { + final ra = find(a); + final rb = find(b); + if (ra == rb) { + return; + } + // Prefer the root with higher priority; on tie, pick the smaller index. + final pra = pri[ra]; + final prb = pri[rb]; + final winner = (pra > prb) ? ra : (prb > pra ? rb : (ra < rb ? ra : rb)); + final loser = (winner == ra) ? rb : ra; + parent[loser] = winner; + } + + for (final u in unions) { + if (u.length >= 2) { + unite(u[0], u[1]); + } + } + + return List.generate(n, find); +} + +/// Deep list equality (compare contents, not identity). +bool listEquals(List a, List b) { + if (a.length != b.length) { + return false; + } + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +/// Minimal recursive representation of a Module hierarchy. +class ModuleMap { + /// The Module this map was constructed from. + final Module _module; + + /// Public accessor for the underlying Module. + Module get module => _module; + + /// The unique name of the module this map represents. + final String uniqueName; + + /// Maps of port logics to a list of schematic bit-ids. + final Map> portLogics = {}; + + /// Maps of internal logics to a list of schematic bit-ids. + final Map> internalLogics = {}; + + /// Maps of submodule unique names to their ModuleMaps. + final Map submodules = {}; + + /// Set of Logic objects in this module that are considered "global". + /// Global Logics will not have connectivity generated for them and any + /// signals reachable from these Logics will be excluded from the + /// ModuleMap's port/internal id assignments. + final Set globalLogics = {}; + + /// Creates a ModuleMap for [module]. + ModuleMap(Module module, + {bool includeInternals = false, + bool includeChildPorts = true, + Set? globalLogics}) + : _module = module, + uniqueName = module.hasBuilt ? module.uniqueInstanceName : module.name { + var nextId = 0; + // Collect declared ports (inputs/outputs/inouts) as the logical + // port set for the module. + // Initialize globalLogics set if provided. + if (globalLogics != null) { + this.globalLogics.addAll(globalLogics); + } + + final portLogicsCandidates = [ + ...module.inputs.values, + ...module.outputs.values, + ...module.inOuts.values + ]; + + // If global logics were provided, we want to compute the transitive set + // of signals reachable from them (following dstConnections) so we can + // exclude those from connectivity/id assignment. + final reachableFromGlobals = {}; + if (this.globalLogics.isNotEmpty) { + final visitQueue = [...this.globalLogics]; + while (visitQueue.isNotEmpty) { + final cur = visitQueue.removeLast(); + if (reachableFromGlobals.contains(cur)) { + continue; + } + reachableFromGlobals.add(cur); + for (final dst in cur.dstConnections) { + if (!reachableFromGlobals.contains(dst)) { + visitQueue.add(dst); + } + } + } + } + for (final logic in portLogicsCandidates) { + // Skip any ports that are reachable from the global set; these are + // intentionally hidden and should not have connectivity generated. + if (reachableFromGlobals.contains(logic)) { + continue; + } + final ids = List.generate(logic.width, (_) => nextId++); + portLogics[logic] = ids; + } + + if (includeInternals) { + final internalSignals = [ + for (final s in module.signals) + if (!portLogics.containsKey(s)) s + ]; + for (final sig in internalSignals) { + // Skip any internal signals reachable from globals. + if (reachableFromGlobals.contains(sig)) { + continue; + } + final ids = List.generate(sig.width, (_) => nextId++); + internalLogics[sig] = ids; + } + + for (final sub in module.subModules) { + // Determine child input ports that should be considered global + // within the child. For each input port on the child, check its + // srcConnections and if any source is within our reachableFromGlobals + // set (i.e., parent-side global), mark that child input as global. + final childGlobals = {}; + if (reachableFromGlobals.isNotEmpty) { + for (final input in sub.inputs.values) { + for (final src in input.srcConnections) { + if (reachableFromGlobals.contains(src)) { + childGlobals.add(input); + break; + } + } + } + } + + submodules[sub] = ModuleMap(sub, + includeInternals: includeInternals, + includeChildPorts: includeChildPorts, + globalLogics: childGlobals.isEmpty ? null : childGlobals); + } + } + } + + /// Validates that the ModuleMap is internally consistent. + void validate() { + final logicToIds = >{} + ..addAll(portLogics) + ..addAll(internalLogics); + final allLogics = [...portLogics.keys, ...internalLogics.keys]; + for (final l in allLogics) { + if (!logicToIds.containsKey(l)) { + throw StateError('Logic $l missing ids in module $uniqueName'); + } + } + + final bitIdToMembers = >{}; + for (final e in logicToIds.entries) { + for (final bitId in e.value) { + bitIdToMembers.putIfAbsent(bitId, () => []).add(e.key); + } + } + + final signals = [...portLogics.keys, ...internalLogics.keys]; + final indexOf = {for (var i = 0; i < signals.length; i++) signals[i]: i}; + final unions = >[ + for (var i = 0; i < signals.length; i++) + for (final conn in [ + ...signals[i].srcConnections, + ...signals[i].dstConnections + ]) + if (indexOf[conn] != null) [i, indexOf[conn]!] + ]; + final roots = computeComponents(signals.length, unions); + + for (final members in bitIdToMembers.values) { + if (members.length <= 1) { + continue; + } + final root0 = roots[indexOf[members.first]!]; + for (final other in members.skip(1)) { + final rootN = roots[indexOf[other]!]; + if (root0 != rootN) { + final buf = StringBuffer() + ..writeln( + 'Members ${members.first} and $other share bit-id but are ' + 'not in same component in $uniqueName') + ..writeln('Member info:'); + for (final m in members) { + buf.writeln( + ' - $m (ids=${logicToIds[m]}, root=${roots[indexOf[m]!]}'); + } + throw StateError(buf.toString()); + } + } + } + + for (final sub in submodules.values) { + sub.validate(); + } + } + + /// Validates that the ModuleMap hierarchy is acyclic and unique. + /// + /// This implementation detects cycles by tracking `Module` instances in the + /// current ancestor chain. Using `ModuleMap` identity alone is insufficient + /// because different `ModuleMap` objects may be created for the same + /// underlying `Module` when the same module is instantiated in multiple + /// places; we want to detect the case where a module becomes a submodule of + /// itself (directly or transitively). + void validateHierarchy( + {required Map> visited, + List hierarchy = const []}) { + final newHierarchy = [...hierarchy, this]; + + // Detect cycles by module identity: if any ancestor in the hierarchy + // refers to the same Module object as `this.module`, we've created a + // recursive instantiation and must error. + if (hierarchy.any((m) => m.module == module)) { + final loop = newHierarchy.map((m) => m.uniqueName).join('.'); + throw StateError('Module $uniqueName is a submodule of itself: $loop'); + } + + // Detect if the same Module appears in more than one place in the + // hierarchy (different paths). Record the current path for this module + // so that subsequent occurrences can report both locations. + if (visited.containsKey(module)) { + final other = visited[module]!; + final otherStr = other.map((m) => m.uniqueName).join('.'); + final thisStr = hierarchy.map((m) => m.uniqueName).join('.'); + throw StateError( + 'Module $uniqueName exists at more than one hierarchy: $otherStr ' + 'and $thisStr'); + } + visited[module] = newHierarchy; + + for (final sub in submodules.values) { + sub.validateHierarchy(visited: visited, hierarchy: newHierarchy); + } + } + + /// Validates that the ModuleMap's schematic IDs are connected properly. + + List validateIdConnectivity() { + final errors = []; + final allIds = {}; + void checkIds(Logic logic, List ids, String context) { + for (final id in ids) { + if (id < 0) { + errors.add('$context: Logic "${logic.name}" has negative id $id'); + } + final existing = allIds[id]; + if (existing != null && existing != logic) { + final connected = logic.srcConnections.contains(existing) || + logic.dstConnections.contains(existing) || + existing.srcConnections.contains(logic) || + existing.dstConnections.contains(logic); + if (!connected) { + errors.add('$context: ID $id assigned to both "${logic.name}" and ' + '"${existing.name}" but they are not connected'); + } + } + allIds[id] = logic; + } + } + + for (final entry in portLogics.entries) { + checkIds(entry.key, entry.value, uniqueName); + } + for (final entry in internalLogics.entries) { + checkIds(entry.key, entry.value, '$uniqueName (internal)'); + } + for (final sub in submodules.values) { + errors.addAll(sub.validateIdConnectivity()); + } + for (final sub in submodules.values) { + var prim = + Primitives.instance.lookupByDefinitionName(sub.module.definitionName); + if (prim == null && sub.submodules.isEmpty) { + prim = Primitives.instance.lookupForModule(sub.module); + } + if (prim == null) { + continue; + } + for (final inLogic in sub.module.inputs.values) { + if (inLogic.srcConnections.isEmpty) { + errors.add('$uniqueName: Primitive ${sub.uniqueName} input ' + '"${inLogic.name}" has no driver'); + } + } + } + return errors; + } +} diff --git a/lib/src/synthesizers/schematic/module_utils.dart b/lib/src/synthesizers/schematic/module_utils.dart new file mode 100644 index 000000000..cb4000707 --- /dev/null +++ b/lib/src/synthesizers/schematic/module_utils.dart @@ -0,0 +1,19 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_utils.dart +// Small helpers for module port lookup used by the schematic tools. +// +// 2025 December 14 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Convenience extension on `Module` to return a combined view of all +/// declared ports (inputs, outputs, inOuts) keyed by port name, and +/// helpers to lookup port names and port `Logic` objects. +extension ModuleUtils on Module { + /// Returns a map of all declared ports (inputs, outputs, inOuts) + /// keyed by port name. + Map get ports => {...inputs, ...outputs, ...inOuts}; +} diff --git a/lib/src/synthesizers/schematic/passthrough_handler.dart b/lib/src/synthesizers/schematic/passthrough_handler.dart new file mode 100644 index 000000000..1415d9970 --- /dev/null +++ b/lib/src/synthesizers/schematic/passthrough_handler.dart @@ -0,0 +1,109 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// passthrough_handler.dart +// Detect pass-through connections and allocate synthetic nets for the +// schematic dumper. API follows ConstantHandler.collectConstants style so +// it can be invoked in the same phase where new net IDs are allocated. +// +// 2025 December 16 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Result of pass-through collection. +class PassThroughResult { + /// Map of output Logic to input Logic for pass-through connections. + final Map passThroughConnections; + + /// Map of synthetic net names to their allocated IDs. + final Map> syntheticNets; + + /// Map of output port names to input port names for pass-throughs. + final Map passThroughNames; + + /// Creates a [PassThroughResult]. + PassThroughResult( + this.passThroughConnections, this.syntheticNets, this.passThroughNames); +} + +/// Handler to detect pass-through connections in a Module and allocate +/// synthetic nets for them. +class PassThroughHandler { + /// Collects pass-through connections in [module] and allocates synthetic + /// nets. + PassThroughResult collectPassThroughs({ + required Module module, + required dynamic map, + required Map> internalNetIds, + required Map ports, + required List nextIdRef, + }) { + final passThroughConnections = {}; + final syntheticNets = >{}; + + // Precompute maps for fast lookup: + // - driverToOutput: direct driver Logic -> output Logic + // - outputsByLogic: map an output Logic instance to itself for identity + // detection when BFS visits the output node directly. + final driverToOutput = { + for (final outLogic in module.outputs.values) + for (final driver in outLogic.srcConnections) driver: outLogic, + }; + + final outputsByLogic = { + for (final outLogic in module.outputs.values) outLogic: outLogic, + }; + + // Detect pass-through connections by BFS from each input's dsts and + // checking whether any visited node is a driver of a module output. + for (final inputEntry in module.inputs.entries) { + final inputLogic = inputEntry.value; + final visited = {}; + final toVisit = [...inputLogic.dstConnections]; + while (toVisit.isNotEmpty) { + final current = toVisit.removeLast(); + if (visited.add(current)) { + final out = outputsByLogic[current] ?? driverToOutput[current]; + if (out != null) { + passThroughConnections[out] = inputLogic; + } + + // Continue tracing if within module scope + toVisit.addAll(current.dstConnections.where((dst) => + module.signals.contains(dst) || + module.outputs.values.contains(dst))); + } + } + } + + // Allocate synthetic net IDs for each pass-through output (a separate + // net for the module output). Use nextIdRef as mutable reference. + var nextId = nextIdRef[0]; + for (final outLogic in passThroughConnections.keys) { + final ids = List.generate(outLogic.width, (_) => nextId++); + final name = 'passthrough_' + '${outLogic.parentModule?.uniqueInstanceName ?? 'unknown'}_' + '${outLogic.name}'; + syntheticNets[name] = ids; + internalNetIds[outLogic] = ids; + } + + // Build a map of output-name -> input-name for callers that want the + // original port name pairs instead of Logic objects. This avoids the + // caller having to reverse-lookup port names by scanning maps. + // Build a reverse lookup of input Logic -> input name for quick mapping. + final inputsByLogic = { + for (final inp in module.inputs.entries) inp.value: inp.key, + }; + + final passThroughNames = { + for (final e in passThroughConnections.entries) + e.key.name: (inputsByLogic[e.value] ?? e.value.name), + }; + + nextIdRef[0] = nextId; + return PassThroughResult( + passThroughConnections, syntheticNets, passThroughNames); + } +} diff --git a/lib/src/synthesizers/schematic/schematic.dart b/lib/src/synthesizers/schematic/schematic.dart new file mode 100644 index 000000000..88920e3e6 --- /dev/null +++ b/lib/src/synthesizers/schematic/schematic.dart @@ -0,0 +1,14 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +export 'constant_handler.dart'; +export 'module_map.dart'; +export 'module_utils.dart'; +export 'passthrough_handler.dart'; +export 'schematic_dumper.dart'; +export 'schematic_mixins.dart'; +export 'schematic_primitives.dart'; +export 'schematic_synthesis_result.dart'; +export 'schematic_synthesizer.dart'; +export 'sequential_handler.dart'; +export 'yosys/yosys_loader_helper.dart'; diff --git a/lib/src/synthesizers/schematic/schematic_dumper.dart b/lib/src/synthesizers/schematic/schematic_dumper.dart new file mode 100644 index 000000000..08e11a918 --- /dev/null +++ b/lib/src/synthesizers/schematic/schematic_dumper.dart @@ -0,0 +1,1040 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// schematic_dumper.dart +// Schematic dumping into ELK-JSON Yosys format. + +// 2025 December 12 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; + +import 'package:rohd/src/synthesizers/schematic/schematic.dart'; + +/// Helper: follow srcConnection chain to return canonical driver Logic. +Logic getCanonicalLogic(Logic logic) { + var cur = logic; + final visited = {}; + while (cur.srcConnection != null && !visited.contains(cur.hashCode)) { + visited.add(cur.hashCode); + cur = cur.srcConnection!; + } + return cur; +} + +/// Lightweight schematic dumper similar in intent to WaveDumper. +class SchematicDumper { + /// The top-level module provided to the dumper. + final Module topModule; + + /// The constructed module map for the top module. + final ModuleMap topMap; + + /// Optional output path (mirrors WaveDumper's `outputPath` argument). + final String? outputPath; + + /// Whether to filter out input ports driven only by constants. + + final bool filterConstInputsToCombinational; + + /// Construct a `SchematicDumper` directly from [module]. The + /// [includeInternals] flag controls whether internal signals and submodules + /// are mapped. + SchematicDumper(Module module, + {bool includeInternals = true, + this.outputPath, + this.filterConstInputsToCombinational = false, + List? globalPortNames}) + : topModule = module, + topMap = (() { + // Build an initial set of global Logics based on explicit top-level + // port names. + final gnames = globalPortNames ?? []; + final globals = {}; + if (gnames.isNotEmpty) { + globals + .addAll(gnames.map((n) => module.ports[n]).whereType()); + if (globals.isEmpty) { + throw StateError( + 'No top-level ports found matching globalPortNames $gnames. ' + 'Ensure the top module declares ports with these names or ' + 'pass appropriate Logic ports to the dumper.'); + } + } + + return ModuleMap(module, + includeInternals: includeInternals, + globalLogics: globals.isEmpty ? null : globals); + })() { + if (outputPath != null) { + // Synchronous export: require the module to already be built. + if (!module.hasBuilt) { + throw StateError('Top module must be built before constructor export'); + } + final out = outputPath!; + _exportYosysJson(out); + } + } + + /// Private implementation that writes a Yosys-style JSON file to [outPath]. + /// Synchronous version: callers must ensure `topModule.hasBuilt` is true + /// before calling this method. Throws if validation fails. + void _exportYosysJson(String outPath) { + // Validate the ModuleMap hierarchy before exporting to catch issues + // early and provide a clear error message rather than producing + // malformed JSON that can crash downstream tools. + try { + topMap + ..validateHierarchy(visited: >{}) + ..validate(); + final idErrors = topMap.validateIdConnectivity(); + if (idErrors.isNotEmpty) { + final buf = StringBuffer()..writeln('ID connectivity errors:'); + for (final e in idErrors) { + buf.writeln(' - $e'); + } + throw Exception(buf.toString()); + } + } catch (e) { + throw StateError('ModuleMap validation failed before export: $e'); + } + + final modulesOut = >{}; + + // Use the module's `definitionName` as the stable type key for module + // definitions and for cell `type` fields so the output matches Yosys + // semantics where `type` is the module's definition name. + + Map buildModuleEntryHierarchy(ModuleMap map, + {bool isTop = false}) { + final module = map.module; + + // Emit ports (names + directions) but do not attempt to compute bits + // or connections yet. Combine input/output/inout emission. + final ports = >{}; + void addPorts(Map map, String dir) { + for (final p in map.entries) { + ports[p.key] = {'direction': dir, 'bits': []}; + } + } + + addPorts(module.inputs, 'input'); + addPorts(module.outputs, 'output'); + addPorts(module.inOuts, 'inout'); + + // Optionally remove input ports for combinational-like module + // definitions when those inputs are driven only by Const sources. + if (filterConstInputsToCombinational && + module.definitionName == 'Combinational') { + module.inputs.forEach((pname, logic) { + if (logic.srcConnections.isNotEmpty && + logic.srcConnections.every((s) => s is Const)) { + ports.remove(pname); + } + }); + } + + // --- STEP 1: Assign IDs to internal nets (child outputs + constants) --- + // + // Symmetry principle for understanding the data flow: + // Module inputs ↔ Child outputs (both are PRODUCERS in module scope) + // Module outputs ↔ Child inputs (both are CONSUMERS in module scope) + // + // Producers get fresh IDs allocated here. Consumers look up IDs from + // their sources via union-find. + // + // This map covers: + // - Child outputs (producers of internal nets) - get fresh IDs + // - Constants - get fresh IDs + // - (Module ports are in map.portLogics, also producers) + final internalNetIds = >{}; + + // Find the next available ID (after all port IDs) + final maxPortId = map.portLogics.values + .expand((ids) => ids) + .whereType() + .fold(-1, (m, id) => id > m ? id : m); + var nextId = maxPortId + 1; + + // Assign IDs to each child's output ports + for (final childMap in map.submodules.values) { + final childModule = childMap.module; + for (final output in childModule.outputs.values) { + // If this child port is marked global (the child's ModuleMap + // indicates it), skip allocating internal net IDs so no + // connectivity/netnames are generated for it. + if (childMap.globalLogics.contains(output)) { + continue; + } + final ids = List.generate(output.width, (_) => nextId++); + internalNetIds[output] = ids; + } + } + + // Collect constants using the ConstantHandler + final nextIdRef = [nextId]; + final constHandler = ConstantHandler(); + final constResult = constHandler.collectConstants( + module: module, + map: map, + internalNetIds: internalNetIds, + ports: ports, + nextIdRef: nextIdRef, + isTop: isTop, + filterConstInputsToCombinational: filterConstInputsToCombinational, + ); + nextId = nextIdRef[0]; + + // Collect pass-through connections and allocate synthetic nets + final passHandler = PassThroughHandler(); + final passResult = passHandler.collectPassThroughs( + module: module, + map: map, + internalNetIds: internalNetIds, + ports: ports, + nextIdRef: nextIdRef, + ); + final passThroughConnections = passResult.passThroughConnections; + final syntheticNetsFromPass = passResult.syntheticNets; + nextId = nextIdRef[0]; + + // Merge synthetic nets from pass-through handler into local map used + // later when emitting netnames and connections. + final syntheticNets = >{}; + for (final e in syntheticNetsFromPass.entries) { + syntheticNets[e.key] = e.value; + } + + // --- STEP 2: Collect transitive closure of intermediate Logics --- + // Starting from child inputs, trace srcConnections to find all + // intermediate Logics that connect to sources (module ports, child + // outputs, or constants). This keeps the scope per-module. + final intermediateLogics = {}; + void collectIntermediates(Logic logic, Set visited) { + if (!visited.add(logic)) { + return; + } + // Skip if already in our ID maps (ports or child outputs) + if (map.portLogics.containsKey(logic) || + internalNetIds.containsKey(logic)) { + return; + } + // Add as intermediate + intermediateLogics.add(logic); + // Continue tracing + for (final src in logic.srcConnections) { + collectIntermediates(src, visited); + } + } + + // Trace from each child input's srcConnections + for (final childMap in map.submodules.values) { + for (final input in childMap.module.inputs.values) { + final visited = {}; + for (final src in input.srcConnections) { + collectIntermediates(src, visited); + } + } + } + + // Also trace from non-LogicStructure module output srcConnections. + // This finds intermediate Logics between child outputs and module + // outputs. We skip LogicStructure outputs because their IDs get + // replaced later by element-based resolution. + for (final portLogic in map.portLogics.keys) { + if (module.outputs.values.contains(portLogic) && + portLogic is! LogicStructure) { + final visited = {}; + for (final src in portLogic.srcConnections) { + collectIntermediates(src, visited); + } + } + } + + // --- STEP 3: Build union-find on all Logics --- + // Collect all Logics: module ports + child outputs + intermediates + // Note: We intentionally exclude map.internalLogics to avoid using + // internal signal IDs that may be replaced later (e.g., LogicStructure + // port IDs get replaced by element-based IDs). + final allLogics = [ + ...map.portLogics.keys, + ...internalNetIds.keys, + ...intermediateLogics, + ]; + final logicIndex = { + for (var i = 0; i < allLogics.length; i++) allLogics[i]: i + }; + + // Build union pairs from srcConnections/dstConnections + final cellUnions = >[]; + for (var i = 0; i < allLogics.length; i++) { + final logic = allLogics[i]; + for (final conn in [...logic.srcConnections, ...logic.dstConnections]) { + final j = logicIndex[conn]; + if (j != null) { + cellUnions.add([i, j]); + } + } + } + + // Compute connected components + final cellRoots = computeComponents(allLogics.length, cellUnions); + + // Build root -> canonical IDs mapping. We explicitly prioritize: + // 1. Module ports (top-level declared Logics with names) + // 2. Child outputs (internal nets between cells) + // This ensures module-level signals are used as canonical IDs for their + // connected components, not intermediate or internal logics. + final rootToIds = >{}; + + // First pass: set canonical IDs from module ports only + for (final portLogic in map.portLogics.keys) { + final idx = logicIndex[portLogic]; + if (idx == null) { + continue; + } + final root = cellRoots[idx]; + final ids = map.portLogics[portLogic]; + if (ids != null && ids.isNotEmpty) { + rootToIds.putIfAbsent(root, () => ids); + } + } + + // Second pass: fill in remaining roots from child outputs + for (final childOutput in internalNetIds.keys) { + final idx = logicIndex[childOutput]; + if (idx == null) { + continue; + } + final root = cellRoots[idx]; + if (!rootToIds.containsKey(root)) { + final ids = internalNetIds[childOutput]; + if (ids != null && ids.isNotEmpty) { + rootToIds[root] = ids; + } + } + } + + // If the PassThroughHandler allocated synthetic internal IDs for a + // module output that is part of a connected component, prefer those + // synthetic IDs as the canonical IDs for the component so that the + // pass-through buffer's Y IDs are used by netnames and consumers. + for (final e in passThroughConnections.entries) { + final outLogic = e.key; + final idx = logicIndex[outLogic]; + if (idx == null) { + continue; + } + final root = cellRoots[idx]; + final synth = internalNetIds[outLogic]; + if (synth != null && synth.isNotEmpty) { + rootToIds[root] = synth; + } + } + + // --- STEP 4: Helper to get IDs for any child port (now a simple lookup) + // --- + List idsForChildLogic(Logic childLogic) { + List tryFromRootOrMaps(Logic l) { + final idx = logicIndex[l]; + if (idx != null) { + return rootToIds[cellRoots[idx]] ?? + map.portLogics[l] ?? + internalNetIds[l] ?? + []; + } + return map.portLogics[l] ?? internalNetIds[l] ?? []; + } + + // Directly assigned internal net IDs (child outputs / constants) + if (internalNetIds.containsKey(childLogic)) { + final idx = logicIndex[childLogic]; + if (idx != null) { + return rootToIds[cellRoots[idx]] ?? internalNetIds[childLogic]!; + } + return internalNetIds[childLogic]!; + } + + // Check immediate parent sources first (for child inputs) + for (final src in childLogic.srcConnections) { + final ids = tryFromRootOrMaps(src); + if (ids.isNotEmpty) { + return ids; + } + } + + // Then check downstream destinations (for child outputs) + for (final dst in childLogic.dstConnections) { + final ids = tryFromRootOrMaps(dst); + if (ids.isNotEmpty) { + return ids; + } + } + + return []; + } + + // Emit cells with type and connections + final cells = >{}; + + // Track next available internal net ID for synthetic wires + // Compute max ID from all assigned IDs (port IDs and internal net IDs) + var nextInternalNetId = 0; + for (final ids in map.portLogics.values) { + for (final id in ids) { + if (id >= nextInternalNetId) { + nextInternalNetId = id + 1; + } + } + } + for (final ids in internalNetIds.values) { + for (final id in ids) { + if (id is int && id >= nextInternalNetId) { + nextInternalNetId = id + 1; + } + } + } + + for (final childMap in map.submodules.values) { + final childModule = childMap.module; + final cellKey = childModule.hasBuilt + ? childModule.uniqueInstanceName + : childModule.name; // instance name (cell key) — keep as-is + // Default cell type is the child module's definition name. + final cellType = childModule.definitionName; + final parameters = {}; + + // Delegate Sequential handling to the refactored SequentialHandler + final seqHandler = SequentialHandler(); + final handled = seqHandler.handleSequential( + childModule: childModule, + ports: childModule.ports, + internalNetIds: internalNetIds, + idsForChildLogic: idsForChildLogic, + cells: cells, + syntheticNets: syntheticNets, + nextInternalNetIdGetter: () => nextInternalNetId, + nextInternalNetIdSetter: (v) => nextInternalNetId = v, + ); + if (handled) { + continue; + } + + // Try exact definitionName mapping first (map true helper modules + // like 'Swizzle'/'BusSubset' even if they contain small internals). + // For other cases, only apply the flexible lookup for leaf modules + // to avoid accidentally matching composite modules like Adders. + var prim = Primitives.instance + .lookupByDefinitionName(childModule.definitionName); + if (prim == null && childMap.submodules.isEmpty) { + prim = Primitives.instance.lookupForModule(childModule); + } + + if (prim != null) { + // For primitives with useRawPortNames (like Add/Combinational), use the + // actual ROHD port names directly instead of generic A/B/Y names + if (prim.useRawPortNames) { + final connMap = >{}; + final portDirs = { + for (final e in childModule.ports.entries) + e.value.name: e.value.isInput + ? 'input' + : e.value.isOutput + ? 'output' + : 'inout' + }; + childModule.ports.forEach((_, logic) { + final ids = idsForChildLogic(logic); + if (ids.isNotEmpty) { + connMap[logic.name] = ids; + } + }); + + // Connectivity check: ensure not all outputs are floating. + if (childModule.outputs.isNotEmpty) { + final anyOutputDrives = childModule.outputs.values + .any((logic) => idsForChildLogic(logic).isNotEmpty); + if (!anyOutputDrives) { + throw StateError( + 'Submodule ${childModule.uniqueInstanceName} has outputs ' + 'but none drive any nets'); + } + } + + // Optionally remove ports that are driven only by constants for + // combinational-like primitives when the dumper option requests + // const-input filtering. + if (filterConstInputsToCombinational) { + if (childModule.definitionName == 'Combinational') { + connMap.removeWhere((pname, ids) => + ids.isNotEmpty && + ids.whereType().isNotEmpty && + ids + .whereType() + .every(constResult.blockedIds.contains)); + portDirs.removeWhere((k, _) => !connMap.containsKey(k)); + } + } + + cells[cellKey] = { + 'hide_name': 0, + 'type': prim.primitiveName, + 'parameters': {'CLK_POLARITY': 1}, + 'attributes': {}, + 'port_directions': portDirs, + 'connections': connMap, + }; + continue; + } + + final primCell = + Primitives.instance.computePrimitiveCell(childModule, prim); + + final portDirs = Map.from( + (primCell['port_directions']! as Map).cast()); + + // Build the primitive connection map using the centralized helper + // in the Primitives registry. Provide a small adapter to lookup + // ROHD-port ids from this dumper's `idsForChildLogic` helper. + final connMap = Primitives.instance + .buildPrimitiveConnectionsWithChildLogicLookup( + childModule, + prim, + (primCell['parameters']! as Map).cast(), + portDirs, + (m) => map.submodules[m], + idsForChildLogic); + + // Connectivity check: ensure not all outputs are floating. + if (childModule.outputs.isNotEmpty) { + final anyOutputDrives = childModule.outputs.values + .any((logic) => idsForChildLogic(logic).isNotEmpty); + if (!anyOutputDrives) { + throw StateError( + 'Submodule ${childModule.uniqueInstanceName} has outputs ' + 'but none drive any nets'); + } + } + + // Optionally remove const-only ports for combinational primitives + // when requested. Only apply this filtering for modules whose + // definitionName is exactly 'Combinational' to avoid affecting + // comparators and other primitives. + if (filterConstInputsToCombinational) { + if (childModule.definitionName == 'Combinational') { + connMap.removeWhere((pname, ids) => + ids.isNotEmpty && + ids.whereType().isNotEmpty && + ids.whereType().every(constResult.blockedIds.contains)); + portDirs.removeWhere((k, _) => !connMap.containsKey(k)); + } + } + + cells[cellKey] = { + 'hide_name': 0, + 'type': primCell['type'], + 'parameters': primCell['parameters'], + 'attributes': {}, + 'port_directions': portDirs, + 'connections': connMap, + }; + continue; + } + + final connMap = >{}; + final portDirs = { + for (final e in childModule.ports.entries) + e.key: e.value.isInput + ? 'input' + : e.value.isOutput + ? 'output' + : 'inout' + }; + childModule.ports.forEach((pname, logic) { + final ids = idsForChildLogic(logic); + if (ids.isNotEmpty) { + connMap[pname] = ids.cast(); + } + }); + + // Connectivity check: ensure not all outputs are floating. + if (childModule.outputs.isNotEmpty) { + final anyOutputDrives = childModule.outputs.values + .any((logic) => idsForChildLogic(logic).isNotEmpty); + if (!anyOutputDrives) { + throw StateError( + 'Submodule ${childModule.uniqueInstanceName} has outputs but ' + 'none drive any nets'); + } + } + + cells[cellKey] = { + 'hide_name': 0, + 'type': cellType, + 'parameters': parameters, + 'attributes': {}, + 'port_directions': portDirs, + 'connections': connMap, + }; + } + + final attr = {'src': 'generated'}; + if (isTop) { + attr['top'] = 1; + } + + // Compute bit-id -> Logic map for this module (for netnames and port + // bits) Since each Logic now has multiple bit-ids, we map each bit-id to + // its Logic. + final bitIdToLogic = {}; + for (final e in map.portLogics.entries) { + for (final bitId in e.value) { + bitIdToLogic[bitId] = e.key; + } + } + for (final e in map.internalLogics.entries) { + for (final bitId in e.value) { + bitIdToLogic[bitId] = e.key; + } + } + // Also add child output IDs to the bit->Logic mapping so they appear in + // netnames. Only numeric bit-ids can be keys in `bitIdToLogic`; string + // tokens representing constant bit values are not added here and will + // instead be included directly in netname bit lists when appropriate. + for (final e in internalNetIds.entries) { + for (final bitId in e.value) { + if (bitId is int) { + bitIdToLogic[bitId] = e.key; + } + } + } + + // Build connectivity across module.signals and compute components + final signals = List.from(module.signals); + final indexOf = {for (var i = 0; i < signals.length; i++) signals[i]: i}; + final unions = >[]; + for (var i = 0; i < signals.length; i++) { + final s = signals[i]; + for (final conn in [...s.srcConnections, ...s.dstConnections]) { + final j = indexOf[conn]; + if (j != null) { + unions.add([i, j]); + } + } + } + final roots = computeComponents(signals.length, unions); + + // Group bit-ids by component root (using Logic -> root mapping) + final compToIds = >{}; + for (final entry in bitIdToLogic.entries) { + final bitId = entry.key; + final logic = entry.value; + final idx = indexOf[logic]; + if (idx == null) { + continue; + } + final root = roots[idx]; + compToIds.putIfAbsent(root, () => []).add(bitId); + } + + // Fill ports.bits and build a mapping from component root -> preferred + // net name (prefer port names). We'll then ensure every component has + // a netname entry so cell `connections` ids always reference a net. + // + // Each port keeps its own unique IDs. For pass-through connections + // (output directly connected to input), we'll add explicit buffer cells + // in the cells section to show the internal wiring. + final rootToPreferred = {}; + final rootToCanonicalIds = >{}; + + // passThroughConnections provided by PassThroughHandler + + // First pass: process INPUT ports to establish canonical IDs + for (final entry in ports.entries) { + final pname = entry.key; + final pdata = entry.value; + final direction = pdata['direction'] as String?; + if (direction != 'input') { + continue; + } + + final logic = map.module.ports[pname]; + if (logic == null) { + continue; + } + + // Input port bit lists are guaranteed to be List from + // ModuleMap.portLogics. Use them directly. + final portBitIds = List.from(map.portLogics[logic] ?? []); + entry.value['bits'] = portBitIds; + + final idx = indexOf[logic]; + if (idx != null && portBitIds.isNotEmpty) { + final root = roots[idx]; + rootToPreferred.putIfAbsent(root, () => pname); + rootToCanonicalIds.putIfAbsent(root, () => portBitIds); + } + } + + // Second pass: process OUTPUT ports with their own IDs. Prefer + // synthetic/internal IDs allocated for pass-throughs when present. + for (final entry in ports.entries) { + final pname = entry.key; + final pdata = entry.value; + final direction = pdata['direction'] as String?; + if (direction != 'output') { + continue; + } + + final logic = map.module.ports[pname]; + if (logic == null) { + continue; + } + + // Normalize to List (port bits must be integers). Prefer + // synthetic IDs from PassThroughHandler when available; otherwise + // use ModuleMap-assigned port bits. + final portBitIds = passThroughConnections.containsKey(logic) + ? (internalNetIds[logic]?.whereType().toList() ?? + (map.portLogics[logic] ?? [])) + : (map.portLogics[logic] ?? []); + + entry.value['bits'] = portBitIds; + + final idx = indexOf[logic]; + if (idx != null && portBitIds.isNotEmpty) { + final root = roots[idx]; + rootToPreferred.putIfAbsent(root, () => pname); + rootToCanonicalIds.putIfAbsent(root, () => portBitIds); + } + } + + // pass-through detection/allocation handled by PassThroughHandler + + // Third pass: handle inout ports + for (final entry in ports.entries) { + final pname = entry.key; + final pdata = entry.value; + final direction = pdata['direction'] as String?; + if (direction == 'input' || direction == 'output') { + continue; + } + + final logic = map.module.ports[pname]; + if (logic == null) { + continue; + } + + final portBitIds = map.portLogics[logic] ?? []; + entry.value['bits'] = portBitIds; + + final idx = indexOf[logic]; + if (idx != null && portBitIds.isNotEmpty) { + final root = roots[idx]; + rootToPreferred.putIfAbsent(root, () => pname); + rootToCanonicalIds.putIfAbsent(root, () => portBitIds); + } + } + + // Add named internal Logic signals to rootToPreferred. Priority is lower + // than parent ports but higher than child port names. Iterate over all + // signals from module.signals (already in `signals` list) and add named + // ones that aren't ports. + final portLogicsSet = map.portLogics.keys.toSet(); + signals.asMap().entries.where((e) { + final logic = e.value; + return !portLogicsSet.contains(logic) && + logic.naming != Naming.unnamed && + !Naming.isUnpreferred(logic.name); + }).forEach( + (e) => rootToPreferred.putIfAbsent(roots[e.key], () => e.value.name)); + + // Add buffer cells for pass-through connections (input → output) + // This makes the internal wiring visible in the schematic. Use the + // `passThroughNames` map returned by the handler to avoid scanning + // the module's input/output maps here. + for (final e in passThroughConnections.entries) { + final out = e.key; + final inn = e.value; + final outName = out.name; + final inName = passResult.passThroughNames[outName] ?? inn.name; + final inIds = map.portLogics[inn] ?? []; + final outIds = internalNetIds[out] ?? map.portLogics[out] ?? []; + if (inIds.isEmpty || outIds.isEmpty) { + continue; + } + cells['passthrough_${inName}_to_$outName'] = { + 'hide_name': 0, + 'type': r'$buf', + 'parameters': {'WIDTH': inn.width}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': >{'A': inIds, 'Y': outIds}, + }; + } + + final netnames = {}; + + List uniquePreserve(Iterable items) { + final seen = {}; + return items.where(seen.add).toList(); + } + + compToIds.forEach((root, ids) { + final name = rootToPreferred[root] ?? 'net_$root'; + final existing = netnames.putIfAbsent( + name, () => {'bits': ids, 'attributes': {}})! + as Map; + final existingBits = (existing['bits']! as List).cast(); + existing['bits'] = uniquePreserve([...existingBits, ...ids]); + }); + + // Add child output IDs as netnames (internal wires between cells) + // These IDs connect child outputs to child inputs. + // Priority: parent port names > named() Logic names > child port names + // Only add child-derived netnames if the IDs are not already covered + // by a higher-priority netname. + // + // Build a set of IDs already covered by existing netnames (from + // union-find preferred names which include parent ports and named + // Logics). + final coveredIds = {}; + for (final nn in netnames.values) { + final bits = (nn! as Map)['bits'] as List?; + if (bits != null) { + for (final b in bits) { + if (b is int) { + coveredIds.add(b); + } + } + } + } + + // Add child output IDs as netnames unless already covered by higher- + // priority netnames. Prefer a connected parent signal name when one + // exists and is meaningful; otherwise fall back to "_". + for (final entry in internalNetIds.entries) { + final outputLogic = entry.key; + final ids = entry.value; + final intIds = ids.whereType().toList(); + if (intIds.isNotEmpty && intIds.every(coveredIds.contains)) { + continue; + } + + final preferredName = + outputLogic.dstConnections.cast().firstWhere((l) { + if (l == null) { + return false; + } + final idx = indexOf[l]; + return idx != null && + l.naming != Naming.unnamed && + !Naming.isUnpreferred(l.name); + }, orElse: () => null)?.name; + + final netName = (preferredName != null && + !netnames.containsKey(preferredName)) + ? preferredName + : '${outputLogic.parentModule?.uniqueInstanceName ?? 'unknown'}_' + '${outputLogic.name}'; + + if (!netnames.containsKey(netName)) { + netnames[netName] = { + 'bits': ids, + 'attributes': {} + }; + intIds.forEach(coveredIds.add); + } + } + + // Merge synthetic nets created during Sequential expansion when absent + syntheticNets.forEach((name, ids) { + netnames.putIfAbsent( + name, () => {'bits': ids, 'attributes': {}}); + }); + + // Attempt element-based resolution for LogicStructure module outputs. + // If a module output is a LogicStructure, try to build its bitlist from + // child outputs that are structures or from direct element producers. + for (final outEntry in module.outputs.entries) { + final outName = outEntry.key; + final outLogic = outEntry.value; + if (outLogic is! LogicStructure) { + continue; + } + + final struct = outLogic; + final combined = []; + var allFound = true; + + List? findElemIds(Logic elem) { + // Direct child output mapping + if (internalNetIds.containsKey(elem)) { + return internalNetIds[elem]; + } + + // Direct canonical match (same canonical logic) + for (final e in internalNetIds.entries) { + if (identical(getCanonicalLogic(e.key), getCanonicalLogic(elem))) { + return e.value; + } + } + + // If a child exported a full LogicStructure, slice its ids + for (final e in internalNetIds.entries) { + if (e.key is! LogicStructure) { + continue; + } + final childStruct = e.key as LogicStructure; + // Find the index of the matching child element and compute its + // bit offset by summing widths of preceding elements. + final idx = childStruct.elements.indexWhere((childElem) => + identical(childElem, elem) || + identical( + getCanonicalLogic(childElem), getCanonicalLogic(elem))); + if (idx == -1) { + continue; + } + final bitOffset = childStruct.elements + .take(idx) + .fold(0, (s, el) => s + el.width); + final childElemWidth = childStruct.elements[idx].width; + final ids = e.value; + if (ids.length >= bitOffset + childElemWidth) { + final elemIds = + ids.sublist(bitOffset, bitOffset + childElemWidth); + internalNetIds[elem] = List.from(elemIds); + return elemIds; + } + } + + // Fall back: check module-level port or internal mapping + return map.portLogics[elem] ?? map.internalLogics[elem]; + } + + final elemLists = struct.elements.map(findElemIds).toList(); + if (elemLists.any((l) => l == null)) { + allFound = false; + } else { + combined.addAll(elemLists.expand((l) => l!)); + } + + if (allFound && combined.length == struct.width) { + // Replace the port bits and create a netname so viewers show + // the module output connected to upstream producers. + ports[outName] = { + 'direction': 'output', + 'bits': List.from(combined), + }; + netnames[outName] = { + 'bits': List.from(combined), + 'attributes': {}, + }; + } + } + + // Add const netnames (per-input and pattern-level) using ConstantHandler + // so constant handling logic is consolidated in one place. + constHandler.emitConstNetnames( + constResult: constResult, + netnames: netnames, + ); + // Create $const driver cells using the ConstantHandler + final referencedIds = { + ...ports.values + .expand((p) => (p['bits']! as List).cast()), + ...cells.values + .where((c) => c['type'] != r'$const') + .map((c) => c['connections'] as Map?) + .where((conns) => conns != null) + .expand((conns) => conns!.values + .whereType>() + .expand((l) => l) + .whereType()), + }; + + constHandler.emitConstCells( + constResult: constResult, + cells: cells, + referencedIds: referencedIds, + ); + + return { + 'attributes': attr, + 'ports': ports, + 'cells': cells, + 'netnames': netnames, + }; + } + + // Walk module tree and emit hierarchy entries. Use the module's type + // name (`module.name`) as the key so all instances of the same type + // share the same module definition (Yosys-style). + // Skip modules that are primitives - they don't need module definitions. + void walkHierarchy(ModuleMap map, {bool isTop = false}) { + final typeName = map.module.definitionName; + + // Check if this module is a primitive - if so, don't emit a module + // definition. + final prim = Primitives.instance.lookupForModule(map.module); + if (prim != null) { + // Primitives don't get module definitions - they're handled by + // port_directions in cells. + return; + } + + if (!modulesOut.containsKey(typeName)) { + modulesOut[typeName] = buildModuleEntryHierarchy(map, isTop: isTop); + } else { + // Merge ports from this instance into the existing module definition + // This handles cases where different instances have different optional + // ports. + final existing = modulesOut[typeName]!; + final existingPorts = existing['ports'] as Map? ?? {}; + final module = map.module; + + for (final name in module.inputs.keys) { + existingPorts.putIfAbsent( + name, () => {'direction': 'input', 'bits': []}); + } + for (final name in module.outputs.keys) { + existingPorts.putIfAbsent( + name, () => {'direction': 'output', 'bits': []}); + } + for (final name in module.inOuts.keys) { + existingPorts.putIfAbsent( + name, () => {'direction': 'inout', 'bits': []}); + } + } + map.submodules.values.forEach(walkHierarchy); + } + + walkHierarchy(topMap, isTop: true); + + final out = { + 'creator': 'SchematicDumper (rohd_hcl)', + 'modules': modulesOut + }; + + // (Diagnostics removed.) + File(outPath) + ..createSync(recursive: true) + ..writeAsStringSync(const JsonEncoder.withIndent(' ').convert(out)); + } + + /// Synchronous accessor for the top module map. + ModuleMap get moduleMap => topMap; + + /// Public export helper. Call this to write a Yosys-style JSON file to + /// [outPath]. The constructor no longer triggers automatic exports. + /// Synchronous export. Throws if `topModule.hasBuilt` is false. + void exportYosysJson(String outPath) { + if (!topModule.hasBuilt) { + throw StateError('Top module must be built before exporting JSON'); + } + _exportYosysJson(outPath); + } +} diff --git a/lib/src/synthesizers/schematic/schematic_mixins.dart b/lib/src/synthesizers/schematic/schematic_mixins.dart new file mode 100644 index 000000000..c253cbb6c --- /dev/null +++ b/lib/src/synthesizers/schematic/schematic_mixins.dart @@ -0,0 +1,236 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// schematic_mixins.dart +// Definition for Schematic Mixins for controlling schematic synthesis. +// +// 2025 December +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// Represents a primitive cell in the schematic JSON output. +/// +/// Used by [Schematic.schematicCell] to return custom cell representations. +class SchematicCellDefinition { + /// The Yosys-style type name (e.g., `$and`, `$mux`, `$dff`). + final String type; + + /// Parameters for the cell (e.g., `{'WIDTH': 8}`). + final Map parameters; + + /// Attributes for the cell. + final Map attributes; + + /// Port directions: port name → `'input'` | `'output'` | `'inout'`. + final Map portDirections; + + /// Creates a [SchematicCellDefinition]. + const SchematicCellDefinition({ + required this.type, + this.parameters = const {}, + this.attributes = const {}, + this.portDirections = const {}, + }); +} + +/// What kind of schematic definition this [Module] generates, or whether it +/// does at all. +enum SchematicDefinitionGenerationType { + /// No definition will be generated; the module is a primitive/leaf. + none, + + /// A standard definition will be generated via the normal synthesis flow. + standard, + + /// A custom definition will be generated via [Schematic.schematicDefinition]. + custom, +} + +/// Allows a [Module] to control the instantiation and/or definition of +/// generated schematic JSON for that module. +/// +/// Similar to [SystemVerilog] mixin for SystemVerilog synthesis, this mixin +/// provides hooks for modules to customize their schematic representation. +/// +/// ## Example +/// +/// ```dart +/// class MyCustomPrimitive extends Module with Schematic { +/// MyCustomPrimitive(Logic a, Logic b) { +/// a = addInput('a', a); +/// b = addInput('b', b); +/// addOutput('y') <= a & b; +/// } +/// +/// @override +/// SchematicDefinitionGenerationType get schematicDefinitionType => +/// SchematicDefinitionGenerationType.none; +/// +/// @override +/// SchematicCellDefinition? schematicCell( +/// String instanceType, +/// String instanceName, +/// Map ports, +/// ) { +/// return SchematicCellDefinition( +/// type: r'$and', +/// parameters: { +/// 'A_WIDTH': ports['a']!.width, +/// 'B_WIDTH': ports['b']!.width, +/// }, +/// portDirections: {'A': 'input', 'B': 'input', 'Y': 'output'}, +/// ); +/// } +/// } +/// ``` +mixin Schematic on Module { + /// Generates a custom schematic cell definition to be used when this module + /// is instantiated as a child in another module's schematic. + /// + /// The [instanceType] and [instanceName] represent the type and name, + /// respectively, of the module that would have been instantiated. + /// [ports] provides access to the actual port [Logic] objects. + /// + /// Return a [SchematicCellDefinition] to provide custom cell data. + /// Return `null` to use standard cell generation. + /// + /// By default, returns `null` (use standard generation). + SchematicCellDefinition? schematicCell( + String instanceType, + String instanceName, + Map ports, + ) => + null; + + /// A custom schematic module definition to be produced for this [Module]. + /// + /// Returns a map representing the module's JSON structure with keys: + /// - `'ports'`: `Map>` + /// - `'cells'`: `Map>` + /// - `'netnames'`: `Map` + /// - `'attributes'`: `Map` + /// + /// If `null` is returned, a standard definition will be generated. + /// If an empty map is returned, no definition will be generated. + /// + /// This function should have no side effects and always return the same thing + /// for the same inputs. + /// + /// By default, returns `null` (use standard generation). + Map? schematicDefinition(String definitionType) => null; + + /// What kind of schematic definition this [Module] generates, or whether it + /// does at all. + /// + /// By default, this is automatically calculated based on the return value of + /// [schematicDefinition] and [schematicCell]. + SchematicDefinitionGenerationType get schematicDefinitionType { + // If schematicCell returns non-null, treat as primitive (no definition) + // We use an empty ports map for the check since we just need to see if + // the module provides a custom implementation. + final cell = schematicCell('*PLACEHOLDER*', '*PLACEHOLDER*', {}); + if (cell != null) { + return SchematicDefinitionGenerationType.none; + } + + // Check schematicDefinition + final def = schematicDefinition('*PLACEHOLDER*'); + if (def == null) { + return SchematicDefinitionGenerationType.standard; + } else if (def.isNotEmpty) { + return SchematicDefinitionGenerationType.custom; + } else { + return SchematicDefinitionGenerationType.none; + } + } + + /// Whether this module should be treated as a primitive in schematic output. + /// + /// When `true`, no separate module definition is generated; instead, the + /// module is represented directly as a cell in the parent module. + /// + /// Override this to `true` for leaf primitives that should not have their + /// own definition. + /// + /// By default, returns `true` if [schematicDefinitionType] is + /// [SchematicDefinitionGenerationType.none]. + bool get isSchematicPrimitive => + schematicDefinitionType == SchematicDefinitionGenerationType.none; + + /// The Yosys primitive type name to use when this module is emitted as a + /// cell (e.g., `$and`, `$mux`, `$dff`). + /// + /// Only used when [isSchematicPrimitive] is `true` or [schematicCell] + /// returns `null` but the synthesizer determines this is a primitive. + /// + /// By default, returns `null`, meaning the module's definition name is used. + String? get schematicPrimitiveName => null; + + /// Indicates that this module is only wires, no logic inside, which can be + /// leveraged for pruning in schematic generation. + @internal + bool get isSchematicWiresOnly => false; +} + +/// Allows a [Module] to define a type of [Schematic] which can be represented +/// as an inline primitive cell without generating a separate definition. +/// +/// This is the schematic equivalent of [InlineSystemVerilog]. +mixin InlineSchematic on Module implements Schematic { + /// The Yosys primitive type to use for this inline cell. + /// + /// Override this to specify the primitive type (e.g., `$and`, `$or`). + @override + String get schematicPrimitiveName; + + /// Parameters to include in the primitive cell. + /// + /// Override to provide cell parameters like `{'WIDTH': 8}`. + Map get schematicParameters => const {}; + + /// Port name mapping from ROHD port names to primitive port names. + /// + /// Override if the primitive uses different port names than the ROHD module. + /// For example: `{'a': 'A', 'b': 'B', 'y': 'Y'}`. + Map get schematicPortMap => const {}; + + @override + bool get isSchematicPrimitive => true; + + @override + SchematicCellDefinition? schematicCell( + String instanceType, + String instanceName, + Map ports, + ) { + final portDirs = {}; + for (final entry in ports.entries) { + final primPortName = schematicPortMap[entry.key] ?? entry.key; + final logic = entry.value; + portDirs[primPortName] = logic.isInput + ? 'input' + : logic.isOutput + ? 'output' + : 'inout'; + } + + return SchematicCellDefinition( + type: schematicPrimitiveName, + parameters: schematicParameters, + portDirections: portDirs, + ); + } + + @override + SchematicDefinitionGenerationType get schematicDefinitionType => + SchematicDefinitionGenerationType.none; + + @override + Map? schematicDefinition(String definitionType) => {}; + + @internal + @override + bool get isSchematicWiresOnly => false; +} diff --git a/lib/src/synthesizers/schematic/schematic_primitives.dart b/lib/src/synthesizers/schematic/schematic_primitives.dart new file mode 100644 index 000000000..27cce754f --- /dev/null +++ b/lib/src/synthesizers/schematic/schematic_primitives.dart @@ -0,0 +1,838 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// schematic_primitives.dart +// Primitive mapping helpers extracted from schematic_dumper for reuse. + +// 2025 December 14 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/schematic/module_map.dart'; +import 'package:rohd/src/synthesizers/schematic/module_utils.dart'; + +/// Descriptor describing how a ROHD helper module maps to a Yosys +/// primitive type. +class PrimitiveDescriptor { + /// The Yosys primitive type name (e.g. "\$concat", "\$dff", "\$mux"). + final String primitiveName; + + /// Map from the ROHD module's port name to the primitive port name. + final Map portMap; + + /// Map of primitive parameter name -> ROHD port name or expression key. + final Map paramFromPort; + + /// Optional primitive port directions (primitive port -> + /// 'input'|'output'|'inout'). When provided in a descriptor, these directions + /// are used directly and the automatic direction inference is skipped for + /// those ports. + final Map portDirs; + + /// Default parameter values supplied by the descriptor (applied before + /// inference). Use this to move always-1 defaults into registration. + final Map defaultParams; + + /// When true, use the ROHD module's actual port names directly instead of + /// generating generic A/B/Y names. Useful for modules like Sequential that + /// have dynamic port names per instance. + final bool useRawPortNames; + + /// Creates a [PrimitiveDescriptor] for leaf schematic primitive mapping. + const PrimitiveDescriptor( + {required this.primitiveName, + this.portMap = const {}, + this.paramFromPort = const {}, + this.portDirs = const {}, + this.defaultParams = const {}, + this.useRawPortNames = false}); +} + +/// Singleton registry for primitive mappings used by the schematic dumper. +class Primitives { + Primitives._() { + _populateDefaults(); + } + + /// The singleton instance. + static final Primitives instance = Primitives._(); + + final Map _byDefinitionName = {}; + + /// Registers a [PrimitiveDescriptor] for a given ROHD module. + void register(String definitionName, PrimitiveDescriptor desc) { + _byDefinitionName[definitionName] = desc; + } + + /// Find a registered [PrimitiveDescriptor] by ROHD module definition name. + PrimitiveDescriptor? lookupByDefinitionName(String defName) => + _byDefinitionName[defName]; + + /// Lookup a [PrimitiveDescriptor] for a given [Module] by trying exact + /// definition name match first, then case-insensitive match, then + /// pattern match. + PrimitiveDescriptor? lookupForModule(Module m) { + final def = m.definitionName; + final nm = m.name; + + final p = lookupByDefinitionName(def); + if (p != null) { + return p; + } + + final defLower = def.toLowerCase(); + final nmLower = nm.toLowerCase(); + for (final entry in _byDefinitionName.entries) { + final keyLower = entry.key.toLowerCase(); + if (keyLower == defLower || keyLower == nmLower) { + return entry.value; + } + } + + for (final entry in _byDefinitionName.entries) { + final key = entry.key; + final pattern = RegExp( + '(^|[^A-Za-z0-9])${RegExp.escape(key)}(\$|[^A-Za-z0-9])', + caseSensitive: false); + if (pattern.hasMatch(def) || pattern.hasMatch(nm)) { + return entry.value; + } + } + + return null; + } + + /// Compute the primitive cell representation for a `Module` that maps to + /// a known primitive descriptor. Returns a map containing keys: + /// - 'type' -> String primitive type (e.g. r'$concat') + /// - 'parameters' -> `Map `of parameter values + /// - 'port_directions' -> `Map` mapping primitive port names + /// to directions expected by the loader ('input'/'output'/'inout') + Map computePrimitiveCell( + Module childModule, PrimitiveDescriptor prim) { + final cellType = prim.primitiveName; + final parameters = {}; + // Apply any descriptor-provided default parameters before inference. + // This lets registrations move always-1 (or other) defaults into the + // descriptor so they don't need to be inferred here. + if (prim.defaultParams.isNotEmpty) { + parameters.addAll(prim.defaultParams); + } + + void ensureIntParam(String k, int defaultVal) { + final v = parameters[k]; + if (v is int) { + if (v <= 0) { + parameters[k] = defaultVal; + } + } else { + parameters[k] = defaultVal; + } + } + + ensureIntParam('A_WIDTH', 1); + ensureIntParam('B_WIDTH', 1); + ensureIntParam('Y_WIDTH', 1); + if (parameters['OFFSET'] == null) { + parameters['OFFSET'] = 0; + } + + final ywVal = parameters['Y_WIDTH']; + if ((parameters['HIGH'] == null || parameters['LOW'] == null) && + ywVal is int) { + parameters['LOW'] = 0; + parameters['HIGH'] = (ywVal - 1) >= 0 ? (ywVal - 1) : 0; + } + + // Initialize `portDirs` from the descriptor. + final portDirs = {}..addAll(prim.portDirs); + + return { + 'type': cellType, + 'parameters': parameters, + 'port_directions': portDirs, + }; + } + + /// Finalize/adjust primitive parameters using the connection map built by + /// the dumper. This allows inference that depends on actual bit-id + /// connections (for example, determining slice offsets/high/low and + /// input widths) rather than only on port names or descriptor defaults. + /// + /// - [childModule] is the module instance for the primitive. + /// - [prim] is the primitive descriptor. + /// - [parameters] is the mutable parameters map produced by + /// `computePrimitiveCell` (will be modified in-place). + /// - [connMap] maps primitive port names (A/B/Y/etc) to lists of bit ids + /// (as produced by the dumper's connection resolution). Bit ids may be + /// integers (net ids) or string tokens for constants. + void finalizePrimitiveCell(Module childModule, PrimitiveDescriptor prim, + Map parameters, Map> connMap) { + // Apply simple paramFromPort mappings (e.g., A_WIDTH -> A) + prim.paramFromPort.entries + .where((e) => e.key.endsWith('_WIDTH')) + .forEach((e) { + final bits = connMap[e.value]; + if (bits != null) { + parameters[e.key] = bits.length; + } + }); + + // Specialized handling for $slice (BusSubset) primitives. Compute + // OFFSET/HIGH/LOW/Y_WIDTH/A_WIDTH when we have concrete connection ids + // for the source ('A') and the result ('Y'). + if (prim.primitiveName == r'$slice') { + final aBits = connMap['A'] ?? []; + final yBits = connMap['Y'] ?? []; + + // Populate widths if available + if (aBits.isNotEmpty) { + parameters['A_WIDTH'] = aBits.length; + } + if (yBits.isNotEmpty) { + parameters['Y_WIDTH'] = yBits.length; + } + + // For offset/high/low we need integer net ids to compute positions + final aInts = aBits.whereType().toList()..sort(); + final yInts = yBits.whereType().toList()..sort(); + + if (aInts.isNotEmpty && yInts.isNotEmpty) { + // Build index map from A net id -> position within A (0-based) + final aIndex = {}; + for (var i = 0; i < aInts.length; i++) { + aIndex[aInts[i]] = i; + } + + // Map each Y net id to its index in A; require that all Y ids exist + // within A to compute a contiguous OFFSET/HIGH/LOW. If not present, + // fall back to conservative defaults. + final mappedCandidates = yInts.map((yId) => aIndex[yId]).toList(); + final allMapped = mappedCandidates.every((e) => e != null); + final mappedIndices = + allMapped ? mappedCandidates.cast().toList() : []; + + if (allMapped && mappedIndices.isNotEmpty) { + mappedIndices.sort(); + final low = mappedIndices.first; + final high = mappedIndices.last; + parameters['OFFSET'] = low; + parameters['LOW'] = low; + parameters['HIGH'] = high; + parameters['Y_WIDTH'] = mappedIndices.length; + parameters['A_WIDTH'] = aInts.length; + } + // If connection-based mapping failed to determine offsets, try a + // fallback: parse output names for `_subset_HIGH_LOW` patterns which + // ROHD may emit for BusSubset outputs. This preserves previous + // behavior that relied on naming heuristics when structural mapping + // is not straightforward. + if ((parameters['LOW'] == null || + parameters['HIGH'] == null || + (parameters['LOW'] is int && + parameters['LOW'] == 0 && + parameters['HIGH'] is int && + parameters['HIGH'] == 0)) && + childModule.outputs.isNotEmpty) { + final re = RegExp(r'_subset_(\d+)_(\d+)'); + final match = childModule.outputs.keys + .map(re.firstMatch) + .firstWhere((m) => m != null, orElse: () => null); + if (match != null) { + final hi = int.parse(match.group(1)!); + final lo = int.parse(match.group(2)!); + final low = hi < lo ? hi : lo; + final high = hi < lo ? lo : hi; + parameters['LOW'] = low; + parameters['HIGH'] = high; + parameters['OFFSET'] = low; + parameters['Y_WIDTH'] = (high - low) + 1; + } + } + } + } + + // For $concat (concat/swizzle), derive input widths from mapped ports + if (prim.primitiveName == r'$concat') { + // Common placeholders A/B may represent inputs; if present, set widths + if (connMap.containsKey('A')) { + parameters['A_WIDTH'] = connMap['A']!.length; + } + if (connMap.containsKey('B')) { + parameters['B_WIDTH'] = connMap['B']!.length; + } + // Update Y width as sum if A/B provided + final aW = parameters['A_WIDTH']; + final bW = parameters['B_WIDTH']; + if (aW is int && bW is int) { + parameters['Y_WIDTH'] = aW + bW; + } + } + } + + /// Deterministically map ROHD port names to primitive port names. + /// + /// Returns a map where the key is the ROHD port name and the value is the + /// corresponding primitive port name. The mapping rules are: + /// 1. If the descriptor's `portMap` provides a literal ROHD name for a + /// primitive port and that ROHD port exists, use it. + /// 2. Group placeholder mappings (single-letter placeholders like 'A', 'B') + /// and map remaining ROHD ports in deterministic sorted order to the + /// placeholder-named primitive ports (sorted). + /// 3. Any remaining primitive ports are assigned positionally to remaining + /// ROHD ports in sorted order. + Map mapRohdToPrimitivePorts(PrimitiveDescriptor prim, + Module childModule, Map portDirs) { + final rohdInputs = childModule.inputs.keys.toList()..sort(); + final rohdOutputs = childModule.outputs.keys.toList()..sort(); + final rohdInouts = childModule.inOuts.keys.toList()..sort(); + + // Normalize prim.portMap: it may be registered in either direction + // (primPort -> rohdName) or (rohdName -> primPort). Detect which form + // is used and build a `primToRohd` map. Build the set of primitive port + // names from both the explicit `portDirs` and any keys present in the + // descriptor's `portMap` so registrations may omit input entries and + // only declare outputs/inouts if desired. Missing directions default to + // 'input' during mapping below. + final primPortNames = {} + ..addAll(portDirs.keys) + ..addAll(prim.portMap.keys); + final rohdPortNames = childModule.ports.keys.toSet(); + + // Build mapping candidates: for each primitive port, collect either a + // literal mapping or a deterministic list of ROHD names matching a + // regex. We will consume these lists deterministically when assigning + // ROHD ports so regex matches are not accidentally reused or picked + // nondeterministically by different prim ports. + final primToRohdLists = >{}; + // Detect inverted maps (rohd->prim) where keys are ROHD names. + final anyKeyIsRohd = prim.portMap.keys.any(rohdPortNames.contains); + final anyKeyIsPrim = prim.portMap.keys.any(primPortNames.contains); + if (anyKeyIsRohd && !anyKeyIsPrim) { + // Invert rohd->prim into prim->rohd lists + for (final e in prim.portMap.entries) { + primToRohdLists[e.value] = [e.key]; + } + } else { + for (final e in prim.portMap.entries) { + final primPort = e.key; + final mapping = e.value; + if (mapping.startsWith('re:')) { + final pattern = RegExp(mapping.substring(3)); + // Collect all ROHD ports matching the regex and sort + // deterministically + final matches = rohdPortNames.where(pattern.hasMatch).toList() + ..sort(); + if (matches.isNotEmpty) { + primToRohdLists[primPort] = matches; + } + } else { + // Literal mapping or placeholder (like 'A'/'B'). Store the literal + // string so calling code can detect placeholders vs literal names. + primToRohdLists[primPort] = [mapping]; + } + } + } + + // Helper to map for a given direction. Treat prim ports missing from + // `portDirs` as inputs by default so registrations can declare only + // outputs/inouts when convenient. + Map doDirection(String direction, List rohdPorts) { + String getDir(String p) => portDirs[p] ?? 'input'; + // Primitive ports of this direction, sorted + final primPorts = + primPortNames.where((p) => getDir(p) == direction).toList()..sort(); + + final mapping = {}; // rohd -> prim + + // 1) Literal mappings from portMap. For regex mappings we collected + // candidate lists; pick the first unassigned ROHD match for each + // prim-port and mark it assigned so matches are not reused. + final assignedPrim = {}; + final assignedRohd = {}; + for (final primPort in primPorts) { + final candidates = primToRohdLists[primPort]; + if (candidates != null && candidates.isNotEmpty) { + // If any candidate is an actual ROHD port name, assign the first + // one that is not already assigned. + String? chosen; + for (final cand in candidates) { + if (rohdPorts.contains(cand) && !assignedRohd.contains(cand)) { + chosen = cand; + break; + } + } + if (chosen != null) { + mapping[chosen] = primPort; + assignedPrim.add(primPort); + assignedRohd.add(chosen); + continue; + } + // If candidates exist but none are actual ROHD names, fall + // through to placeholder handling below (mapping may be a + // placeholder like 'A' or 'B'). + } + // Also support the legacy case where primToRohdLists may be empty + // and prim.portMap contains a literal ROHD name. + final mappedLiteral = prim.portMap[primPort]; + if (mappedLiteral != null && rohdPorts.contains(mappedLiteral)) { + mapping[mappedLiteral] = primPort; + assignedPrim.add(primPort); + assignedRohd.add(mappedLiteral); + continue; + } + } + + // 2) Placeholder groups (e.g., 'A', 'B') + // Group prim ports by their placeholder value + final placeholderGroups = >{}; + for (final primPort in primPorts) { + if (assignedPrim.contains(primPort)) { + continue; + } + final mapped = prim.portMap[primPort]; + if (mapped != null && RegExp(r'^[A-Z][0-9]*$').hasMatch(mapped)) { + final key = mapped.replaceAll(RegExp('[0-9]+'), ''); + placeholderGroups.putIfAbsent(key, () => []).add(primPort); + assignedPrim.add(primPort); + } + } + // Sort each group's prim ports for deterministic assignment + for (final g in placeholderGroups.values) { + g.sort(); + } + + // Assign ROHD ports to placeholder prim ports in sorted order + var rohdIdx = 0; + for (final primList in placeholderGroups.values) { + for (final primPort in primList) { + while (rohdIdx < rohdPorts.length && + assignedRohd.contains(rohdPorts[rohdIdx])) { + rohdIdx++; + } + if (rohdIdx >= rohdPorts.length) { + break; + } + final rohdName = rohdPorts[rohdIdx++]; + mapping[rohdName] = primPort; + assignedRohd.add(rohdName); + } + } + + // 3) Positional mapping for any remaining prim ports + // Collect remaining prim ports not assigned + final remainingPrim = + primPorts.where((p) => !mapping.values.contains(p)).toList()..sort(); + // Collect remaining rohd ports + final remainingRohd = + rohdPorts.where((r) => !assignedRohd.contains(r)).toList()..sort(); + + // No primitive-specific positional heuristics; rely on descriptor + // mappings and deterministic regex consumption above. + + for (var i = 0; + i < remainingPrim.length && i < remainingRohd.length; + i++) { + mapping[remainingRohd[i]] = remainingPrim[i]; + } + + return mapping; + } + + final result = {} + ..addAll(doDirection('input', rohdInputs)) + ..addAll(doDirection('output', rohdOutputs)) + ..addAll(doDirection('inout', rohdInouts)); + // If the descriptor did not supply explicit `portDirs`, infer primitive + // port directions from the instantiation point (ROHD ports) so the + // caller can use instance-derived directions rather than requiring the + // descriptor to provide them. This mirrors how combinational/raw-port + // instances display correctly using their instantiation context. + if (prim.portDirs.isEmpty) { + // Build reverse map: primPort -> list of rohd ports mapped to it + final primToRohd = >{}; + for (final e in result.entries) { + primToRohd.putIfAbsent(e.value, () => []).add(e.key); + } + + String decideForPrim(String primPort) { + final rohdList = primToRohd[primPort] ?? const []; + return rohdList.any((r) => childModule.ports[r]?.isInOut ?? false) + ? 'inout' + : (rohdList.any((r) => childModule.ports[r]?.isOutput ?? false) + ? 'output' + : 'input'); + } + + // Populate missing entries in the provided portDirs map. + for (final primPort in primPortNames) { + portDirs.putIfAbsent( + primPort, + () => primPort == 'Y' + ? (childModule.outputs.isNotEmpty ? 'output' : 'input') + : decideForPrim(primPort)); + } + } + + return result; + } + + /// Build a primitive connection map (`primPort` -> bit-id list) using the + /// deterministic ROHD->primitive port mapping and the provided + /// `idsForRohd` lookup function which returns the bit ids for a ROHD + /// port name. This function also calls the safe finalizer to allow + /// parameter inference that depends on concrete connections. + Map> buildPrimitiveConnections( + Module childModule, + PrimitiveDescriptor prim, + Map parameters, + Map portDirs, + List Function(String rohdName) idsForRohd) { + final connMap = >{}; + final rohdToPrim = mapRohdToPrimitivePorts(prim, childModule, portDirs); + + for (final entry in rohdToPrim.entries) { + final rohdName = entry.key; + final primPortName = entry.value; + final ids = idsForRohd(rohdName); + if (ids.isNotEmpty) { + connMap[primPortName] = ids; + } + } + + // Allow primitive logic to finalize parameters using the concrete + // connection ids we built. + finalizePrimitiveCell(childModule, prim, parameters, connMap); + + return connMap; + } + + /// Convenience wrapper used by the dumper when the lookup for ROHD port + /// ids needs to resolve ports via a child ModuleMap (or fallback to the + /// child module's own ports). The [idsForChildLogic] callback should + /// accept a `Logic` and return the corresponding bit id list. The + /// [childMapLookup] callback, when provided, should return the ModuleMap + /// for a given child module or null if not present. + Map> buildPrimitiveConnectionsWithChildLogicLookup( + Module childModule, + PrimitiveDescriptor prim, + Map parameters, + Map portDirs, + ModuleMap? Function(Module) childMapLookup, + List Function(Logic) idsForChildLogic) { + // Adapter: convert rohdName -> idsForRohd by resolving the Logic + // either from the child ModuleMap (if available) or directly from + // the child module. + List idsForRohd(String rohdName) { + final childMap = childMapLookup(childModule); + final logic = + childMap?.module.ports[rohdName] ?? childModule.ports[rohdName]; + if (logic == null) { + return []; + } + return idsForChildLogic(logic); + } + + return buildPrimitiveConnections( + childModule, prim, parameters, portDirs, idsForRohd); + } + + void _populateDefaults() { + register( + 'Swizzle', + const PrimitiveDescriptor( + primitiveName: r'$concat', + // ROHD swizzles commonly name inputs like `in0_`, + // `in1_` and the output `swizzled` or `out`. Use regex + // mappings so the dumper can deterministically pick inputs and + // output without relying on positional heuristics. + portMap: { + 'A': r're:^in\d+_.+', + 'B': r're:^in\d+_.+', + 'Y': r're:^(?:swizzled$|out$)' + }, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + 'BusSubset', + const PrimitiveDescriptor( + primitiveName: r'$slice', + // BusSubset (slice) instances often expose outputs with names + // containing `_subset_HIGH_LOW`. Inputs are typically `in...` or + // `A`. Prefer regex matches for both A (source bus) and Y + // (sliced output) so parameters can be extracted from names. + portMap: { + 'A': r're:^in\d*_.+|^in_.+|^A$', + 'Y': r're:.*_subset_\d+_\d+|^out$' + }, + paramFromPort: {'HIGH': 'A', 'LOW': 'A'}, + portDirs: {'A': 'input', 'Y': 'output'}, + )); + + // Comparison gates: ROHD uses in0_, in1_ for inputs. + register( + 'Equals', + const PrimitiveDescriptor( + primitiveName: r'$eq', + portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, + paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + 'NotEquals', + const PrimitiveDescriptor( + primitiveName: r'$ne', + portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, + paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + 'LessThan', + const PrimitiveDescriptor( + primitiveName: r'$lt', + portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, + paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + 'LessThanOrEqual', + const PrimitiveDescriptor( + primitiveName: r'$le', + portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, + paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + 'GreaterThan', + const PrimitiveDescriptor( + primitiveName: r'$gt', + portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, + paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + 'GreaterThanOrEqual', + const PrimitiveDescriptor( + primitiveName: r'$ge', + portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, + paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + + register( + 'lshift', + const PrimitiveDescriptor( + primitiveName: r'$shl', + portMap: {'A': 're:^in_.+', 'B': 're:^shiftAmount_.+'}, + paramFromPort: {'A_WIDTH': 'A'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); + register( + 'rshift', + const PrimitiveDescriptor( + primitiveName: r'$shr', + portMap: {'A': 're:^in_.+', 'B': 're:^shiftAmount_.+'}, + paramFromPort: {'A_WIDTH': 'A'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); + register( + 'ARShift', + const PrimitiveDescriptor( + primitiveName: r'$shiftx', + portMap: {'A': 'A', 'B': 'B', 'Y': 'Y'}, + paramFromPort: {'A_WIDTH': 'A'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + + register( + 'And2Gate', + const PrimitiveDescriptor( + primitiveName: r'$and', + portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); + register( + 'Or2Gate', + const PrimitiveDescriptor( + primitiveName: r'$or', + portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); + register( + 'Xor2Gate', + const PrimitiveDescriptor( + primitiveName: r'$xor', + portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); + register( + 'NotGate', + const PrimitiveDescriptor( + primitiveName: r'$not', + portMap: {'A': 're:^in_.+'}, + portDirs: {'A': 'input', 'Y': 'output'})); + + register( + 'AndUnary', + const PrimitiveDescriptor( + primitiveName: r'$logic_and', + portDirs: {'A': 'input', 'Y': 'output'})); + register( + 'OrUnary', + const PrimitiveDescriptor( + primitiveName: r'$logic_or', + portDirs: {'A': 'input', 'Y': 'output'})); + register( + 'XorUnary', + const PrimitiveDescriptor( + primitiveName: r'$xor', portDirs: {'A': 'input', 'Y': 'output'})); + + // Note: bitwise/logical gate descriptors are updated below with + // explicit `portDirs`; the earlier implicit registrations were + // redundant and have been removed to avoid confusion. + + // Update bitwise/logical gate descriptors to include directions. + register( + 'BitwiseAnd', + const PrimitiveDescriptor( + primitiveName: r'$and', + portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); + register( + 'BitwiseOr', + const PrimitiveDescriptor( + primitiveName: r'$or', + portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); + register( + 'BitwiseXor', + const PrimitiveDescriptor( + primitiveName: r'$xor', + portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); + register( + 'LogicNot', + const PrimitiveDescriptor( + primitiveName: r'$not', + portMap: {'A': 're:^in_.+'}, + portDirs: {'A': 'input', 'Y': 'output'})); + + register( + 'mux', + const PrimitiveDescriptor( + primitiveName: r'$mux', + // Use regex-based mappings to detect ROHD dynamic port names like + // control_, d0_, d1_, and out. Values prefixed + // with 're:' are treated as regular expressions against ROHD port + // names. + portMap: { + // Match common selector names: control_, sel_, + // s_, in0_/in1_ (sometimes select is named as in0/in1), or + // literal 'A'. + 'S': + r're:^(?:_?control_.+|_?sel_.+|_?s_.+|in0_.+|in1_.+|A$|.*_subset_\d+_\d+)', + // Data inputs: match d1_/d0_, or literal B/C/A depending on ROHD + 'A': r're:^(?:d1_.+|B$|d1$|d1_.+)', + 'B': r're:^(?:d0_.+|C$|d0$|d0_.+)', + 'Y': r're:^(?:out$|Y$)' + }, + // The WIDTH parameter should come from the data input (d1/d0), but + // we leave this as 'B' for compatibility — callers interpret this + // as the ROHD port name after mapping resolution. + paramFromPort: { + 'WIDTH': 'B' + })); + // Merged mux descriptor: provide port mappings, parameter source, + // and explicit port directions so fallback inference isn't required. + register( + 'mux', + const PrimitiveDescriptor(primitiveName: r'$mux', portMap: { + 'S': + r're:^(?:_?control_.+|_?sel_.+|_?s_.+|in0_.+|in1_.+|A$|.*_subset_\d+_\d+)', + 'A': r're:^(?:d1_.+|B$|d1$|d1_.+)', + 'B': r're:^(?:d0_.+|C$|d0$|d0_.+)', + 'Y': r're:^(?:out$|Y$)' + }, paramFromPort: { + 'WIDTH': 'B' + }, portDirs: { + 'S': 'input', + 'A': 'input', + 'B': 'input', + 'Y': 'output' + })); + + register( + 'mul', + const PrimitiveDescriptor( + primitiveName: r'$mul', + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); + + register( + 'AddSigned', + const PrimitiveDescriptor( + primitiveName: r'$add', + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); + register( + 'SubSigned', + const PrimitiveDescriptor( + primitiveName: r'$sub', + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); + register( + 'AddUnsigned', + const PrimitiveDescriptor( + primitiveName: r'$add', + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); + register( + 'SubUnsigned', + const PrimitiveDescriptor( + primitiveName: r'$sub', + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); + + register( + 'FlipFlop', + const PrimitiveDescriptor(primitiveName: r'$dff', portMap: { + 'd': 'D', + 'q': 'Q', + 'clk': 'CLK', + 'en': 'EN', + 'reset': 'SRST' + }, portDirs: { + 'd': 'input', + 'q': 'output', + 'clk': 'input', + 'en': 'input', + 'reset': 'input' + })); + + // Sequential is handled specially by SequentialHandler: + // - Simple Sequential (1 data input/output) → $dff primitive + // - Complex Sequential.multi → generates internal mux + dff structure + // Register it so that generatesDefinition() returns false (it's a leaf). + // The actual cell emission is done by SequentialHandler, not this + // descriptor. + register( + 'Sequential', + const PrimitiveDescriptor( + primitiveName: r'$sequential', useRawPortNames: true)); + + // Add and Combinational have dynamic port names per instance + register( + 'Add', + const PrimitiveDescriptor( + primitiveName: r'$add', useRawPortNames: true)); + register( + 'Combinational', + const PrimitiveDescriptor( + primitiveName: r'$combinational', useRawPortNames: true)); + + register( + 'AndUnary', + const PrimitiveDescriptor( + primitiveName: r'$logic_and', + portDirs: {'A': 'input', 'Y': 'output'})); + register( + 'OrUnary', + const PrimitiveDescriptor( + primitiveName: r'$logic_or', + portDirs: {'A': 'input', 'Y': 'output'})); + register( + 'XorUnary', + const PrimitiveDescriptor( + primitiveName: r'$xor', portDirs: {'A': 'input', 'Y': 'output'})); + } +} diff --git a/lib/src/synthesizers/schematic/schematic_synthesis_result.dart b/lib/src/synthesizers/schematic/schematic_synthesis_result.dart new file mode 100644 index 000000000..b02d87ebf --- /dev/null +++ b/lib/src/synthesizers/schematic/schematic_synthesis_result.dart @@ -0,0 +1,899 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// schematic_synthesis_result.dart +// Synthesis result for schematic generation. +// +// 2025 December 18 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/schematic/schematic.dart'; + +/// A [SynthesisResult] representing schematic output for a single [Module]. +/// +/// Contains ports, cells (child instances), and netnames for one level of +/// module hierarchy. The [SynthBuilder] handles recursion across submodules. +class SchematicSynthesisResult extends SynthesisResult { + /// The ports map: name → {direction, bits}. + final Map> ports; + + /// The cells map: instance name → cell data. + final Map> cells; + + /// The netnames map: net name → {bits, attributes}. + final Map netnames; + + /// Attributes for this module (e.g., top marker). + final Map attributes; + + /// Cached JSON string for comparison and output. + late final String _cachedJson = _buildJson(); + + /// Creates a [SchematicSynthesisResult] for [module]. + SchematicSynthesisResult( + super.module, + super.getInstanceTypeOfModule, { + required this.ports, + required this.cells, + required this.netnames, + this.attributes = const {}, + }); + + String _buildJson() { + final moduleEntry = { + 'attributes': attributes, + 'ports': ports, + 'cells': cells, + 'netnames': netnames, + }; + return const JsonEncoder().convert(moduleEntry); + } + + @override + bool matchesImplementation(SynthesisResult other) => + other is SchematicSynthesisResult && _cachedJson == other._cachedJson; + + @override + int get matchHashCode => _cachedJson.hashCode; + + @override + @Deprecated('Use `toSynthFileContents()` instead.') + String toFileContents() => toSynthFileContents().first.contents; + + @override + List toSynthFileContents() { + // Use instanceTypeName (uniquified by SynthBuilder) instead of + // module.definitionName to match SystemVerilog synthesizer behavior + final typeName = instanceTypeName; + // Produce a JSON file for this module definition + final moduleEntry = { + 'attributes': attributes, + 'ports': ports, + 'cells': cells, + 'netnames': netnames, + }; + final contents = const JsonEncoder.withIndent(' ').convert({ + 'creator': 'SchematicSynthesizer (rohd)', + 'modules': {typeName: moduleEntry}, + }); + return [ + SynthFileContents( + name: '$typeName.rohd.json', + description: 'Schematic for $typeName', + contents: contents, + ), + ]; + } +} + +/// Factory helper to build [SchematicSynthesisResult] from a [ModuleMap]. +/// +/// This extracts the logic from SchematicDumper.buildModuleEntryHierarchy to +/// handle one module level without recursion. +class SchematicSynthesisResultBuilder { + /// The module to synthesize. + final Module module; + + /// The ModuleMap for this module. + final ModuleMap map; + + /// Whether to filter const-only inputs to combinational primitives. + final bool filterConstInputsToCombinational; + + /// Function to get instance type names for submodules. + final String Function(Module) getInstanceTypeOfModule; + + /// Creates a builder for [module]. + SchematicSynthesisResultBuilder({ + required this.module, + required this.map, + required this.getInstanceTypeOfModule, + this.filterConstInputsToCombinational = false, + }); + + /// Builds the [SchematicSynthesisResult]. + SchematicSynthesisResult build({bool isTop = false}) { + // Build ports, cells, netnames for this one module level. + final ports = >{}; + final cells = >{}; + final netnames = {}; + final attr = {'src': 'generated'}; + if (isTop) { + attr['top'] = 1; + } + + // Emit ports (names + directions) + void addPorts(Map portMap, String dir) { + for (final p in portMap.entries) { + ports[p.key] = {'direction': dir, 'bits': []}; + } + } + + addPorts(module.inputs, 'input'); + addPorts(module.outputs, 'output'); + addPorts(module.inOuts, 'inout'); + + // Assign IDs to internal nets (child outputs + constants) + final internalNetIds = >{}; + final maxPortId = map.portLogics.values + .expand((ids) => ids) + .whereType() + .fold(-1, (m, id) => id > m ? id : m); + var nextId = maxPortId + 1; + + // Assign IDs to each child's output ports + for (final childMap in map.submodules.values) { + final childModule = childMap.module; + for (final output in childModule.outputs.values) { + if (childMap.globalLogics.contains(output)) { + continue; + } + final ids = List.generate(output.width, (_) => nextId++); + internalNetIds[output] = ids; + } + } + + // Collect constants + final nextIdRef = [nextId]; + final constHandler = ConstantHandler(); + final constResult = constHandler.collectConstants( + module: module, + map: map, + internalNetIds: internalNetIds, + ports: ports, + nextIdRef: nextIdRef, + isTop: isTop, + filterConstInputsToCombinational: filterConstInputsToCombinational, + ); + nextId = nextIdRef[0]; + + // Collect pass-through connections + final passHandler = PassThroughHandler(); + final passResult = passHandler.collectPassThroughs( + module: module, + map: map, + internalNetIds: internalNetIds, + ports: ports, + nextIdRef: nextIdRef, + ); + nextId = nextIdRef[0]; + + final syntheticNets = >{}; + for (final e in passResult.syntheticNets.entries) { + syntheticNets[e.key] = e.value; + } + + // Collect intermediate logics + final intermediateLogics = {}; + void collectIntermediates(Logic logic, Set visited) { + if (!visited.add(logic)) { + return; + } + if (map.portLogics.containsKey(logic) || + internalNetIds.containsKey(logic)) { + return; + } + intermediateLogics.add(logic); + for (final src in logic.srcConnections) { + collectIntermediates(src, visited); + } + } + + for (final childMap in map.submodules.values) { + for (final input in childMap.module.inputs.values) { + final visited = {}; + for (final src in input.srcConnections) { + collectIntermediates(src, visited); + } + } + } + + for (final portLogic in map.portLogics.keys) { + if (module.outputs.values.contains(portLogic) && + portLogic is! LogicStructure) { + final visited = {}; + for (final src in portLogic.srcConnections) { + collectIntermediates(src, visited); + } + } + } + + // Build union-find on all Logics + final allLogics = [ + ...map.portLogics.keys, + ...internalNetIds.keys, + ...intermediateLogics, + ]; + final logicIndex = { + for (var i = 0; i < allLogics.length; i++) allLogics[i]: i + }; + + final cellUnions = >[]; + for (var i = 0; i < allLogics.length; i++) { + final logic = allLogics[i]; + for (final conn in [...logic.srcConnections, ...logic.dstConnections]) { + final j = logicIndex[conn]; + if (j != null) { + cellUnions.add([i, j]); + } + } + } + + final cellRoots = computeComponents(allLogics.length, cellUnions); + + // Build root → canonical IDs mapping + final rootToIds = >{}; + + for (final portLogic in map.portLogics.keys) { + final idx = logicIndex[portLogic]; + if (idx == null) { + continue; + } + final root = cellRoots[idx]; + final ids = map.portLogics[portLogic]; + if (ids != null && ids.isNotEmpty) { + rootToIds.putIfAbsent(root, () => ids); + } + } + + for (final childOutput in internalNetIds.keys) { + final idx = logicIndex[childOutput]; + if (idx == null) { + continue; + } + final root = cellRoots[idx]; + if (!rootToIds.containsKey(root)) { + final ids = internalNetIds[childOutput]; + if (ids != null && ids.isNotEmpty) { + rootToIds[root] = ids; + } + } + } + + for (final e in passResult.passThroughConnections.entries) { + final outLogic = e.key; + final idx = logicIndex[outLogic]; + if (idx == null) { + continue; + } + final root = cellRoots[idx]; + final synth = internalNetIds[outLogic]; + if (synth != null && synth.isNotEmpty) { + rootToIds[root] = synth; + } + } + + // Helper to get IDs for any child port + List idsForChildLogic(Logic childLogic) { + List tryFromRootOrMaps(Logic l) { + final idx = logicIndex[l]; + if (idx != null) { + return rootToIds[cellRoots[idx]] ?? + map.portLogics[l] ?? + internalNetIds[l] ?? + []; + } + return map.portLogics[l] ?? internalNetIds[l] ?? []; + } + + if (internalNetIds.containsKey(childLogic)) { + final idx = logicIndex[childLogic]; + if (idx != null) { + return rootToIds[cellRoots[idx]] ?? internalNetIds[childLogic]!; + } + return internalNetIds[childLogic]!; + } + + for (final src in childLogic.srcConnections) { + final ids = tryFromRootOrMaps(src); + if (ids.isNotEmpty) { + return ids; + } + } + + for (final dst in childLogic.dstConnections) { + final ids = tryFromRootOrMaps(dst); + if (ids.isNotEmpty) { + return ids; + } + } + + return []; + } + + var nextInternalNetId = 0; + for (final ids in map.portLogics.values) { + for (final id in ids) { + if (id >= nextInternalNetId) { + nextInternalNetId = id + 1; + } + } + } + for (final ids in internalNetIds.values) { + for (final id in ids) { + if (id is int && id >= nextInternalNetId) { + nextInternalNetId = id + 1; + } + } + } + + // Emit cells + for (final childMap in map.submodules.values) { + final childModule = childMap.module; + final cellKey = childModule.hasBuilt + ? childModule.uniqueInstanceName + : childModule.name; + + // Check if module uses Schematic mixin with custom cell generation + if (childModule is Schematic) { + final instanceType = getInstanceTypeOfModule(childModule); + final cellDef = childModule.schematicCell( + instanceType, + cellKey, + childModule.ports, + ); + + if (cellDef != null) { + // Use custom cell definition from mixin + final connMap = >{}; + childModule.ports.forEach((pname, logic) { + final ids = idsForChildLogic(logic); + if (ids.isNotEmpty) { + connMap[pname] = ids.cast(); + } + }); + + cells[cellKey] = { + 'hide_name': 0, + 'type': cellDef.type, + 'parameters': cellDef.parameters, + 'attributes': cellDef.attributes, + 'port_directions': cellDef.portDirections.isNotEmpty + ? cellDef.portDirections + : { + for (final e in childModule.ports.entries) + e.key: e.value.isInput + ? 'input' + : e.value.isOutput + ? 'output' + : 'inout' + }, + 'connections': connMap, + }; + continue; + } + + // If schematicCell returns null but isSchematicPrimitive is true, + // fall through to primitive handling + if (childModule.isSchematicPrimitive) { + final prim = Primitives.instance.lookupForModule(childModule); + if (prim != null) { + _emitPrimitiveCell( + childModule: childModule, + childMap: childMap, + cellKey: cellKey, + prim: prim, + cells: cells, + idsForChildLogic: idsForChildLogic, + constResult: constResult, + syntheticNets: syntheticNets, + nextInternalNetIdGetter: () => nextInternalNetId, + nextInternalNetIdSetter: (v) => nextInternalNetId = v, + internalNetIds: internalNetIds, + ); + continue; + } + } + } + + // Check if this is a primitive - if so, skip emitting a module instance + // and instead emit the primitive cell directly + final prim = Primitives.instance.lookupForModule(childModule); + if (prim != null) { + // Handle primitive cells + _emitPrimitiveCell( + childModule: childModule, + childMap: childMap, + cellKey: cellKey, + prim: prim, + cells: cells, + idsForChildLogic: idsForChildLogic, + constResult: constResult, + syntheticNets: syntheticNets, + nextInternalNetIdGetter: () => nextInternalNetId, + nextInternalNetIdSetter: (v) => nextInternalNetId = v, + internalNetIds: internalNetIds, + ); + continue; + } + + // Non-primitive module instance - use the instance type from SynthBuilder + final instanceType = getInstanceTypeOfModule(childModule); + + final connMap = >{}; + final portDirs = { + for (final e in childModule.ports.entries) + e.key: e.value.isInput + ? 'input' + : e.value.isOutput + ? 'output' + : 'inout' + }; + + childModule.ports.forEach((pname, logic) { + final ids = idsForChildLogic(logic); + if (ids.isNotEmpty) { + connMap[pname] = ids.cast(); + } + }); + + cells[cellKey] = { + 'hide_name': 0, + 'type': instanceType, + 'parameters': {}, + 'attributes': {}, + 'port_directions': portDirs, + 'connections': connMap, + }; + } + + // Build netnames from component IDs + final signals = List.from(module.signals); + final indexOf = {for (var i = 0; i < signals.length; i++) signals[i]: i}; + final unions = >[]; + for (var i = 0; i < signals.length; i++) { + final s = signals[i]; + for (final conn in [...s.srcConnections, ...s.dstConnections]) { + final j = indexOf[conn]; + if (j != null) { + unions.add([i, j]); + } + } + } + final roots = computeComponents(signals.length, unions); + + final bitIdToLogic = {}; + for (final e in map.portLogics.entries) { + for (final bitId in e.value) { + bitIdToLogic[bitId] = e.key; + } + } + for (final e in map.internalLogics.entries) { + for (final bitId in e.value) { + bitIdToLogic[bitId] = e.key; + } + } + for (final e in internalNetIds.entries) { + for (final bitId in e.value) { + if (bitId is int) { + bitIdToLogic[bitId] = e.key; + } + } + } + + final compToIds = >{}; + for (final entry in bitIdToLogic.entries) { + final bitId = entry.key; + final logic = entry.value; + final idx = indexOf[logic]; + if (idx == null) { + continue; + } + final root = roots[idx]; + compToIds.putIfAbsent(root, () => []).add(bitId); + } + + final rootToPreferred = {}; + final rootToCanonicalIds = >{}; + + // Process INPUT ports + for (final entry in ports.entries) { + final pname = entry.key; + final direction = entry.value['direction'] as String?; + if (direction != 'input') { + continue; + } + + final logic = module.ports[pname]; + if (logic == null) { + continue; + } + + final portBitIds = List.from(map.portLogics[logic] ?? []); + entry.value['bits'] = portBitIds; + + final idx = indexOf[logic]; + if (idx != null && portBitIds.isNotEmpty) { + final root = roots[idx]; + rootToPreferred.putIfAbsent(root, () => pname); + rootToCanonicalIds.putIfAbsent(root, () => portBitIds); + } + } + + // Process OUTPUT ports + for (final entry in ports.entries) { + final pname = entry.key; + final direction = entry.value['direction'] as String?; + if (direction != 'output') { + continue; + } + + final logic = module.ports[pname]; + if (logic == null) { + continue; + } + + final portBitIds = passResult.passThroughConnections.containsKey(logic) + ? (internalNetIds[logic]?.whereType().toList() ?? + (map.portLogics[logic] ?? [])) + : (map.portLogics[logic] ?? []); + + entry.value['bits'] = portBitIds; + + final idx = indexOf[logic]; + if (idx != null && portBitIds.isNotEmpty) { + final root = roots[idx]; + rootToPreferred.putIfAbsent(root, () => pname); + rootToCanonicalIds.putIfAbsent(root, () => portBitIds); + } + } + + // Process INOUT ports + for (final entry in ports.entries) { + final pname = entry.key; + final direction = entry.value['direction'] as String?; + if (direction == 'input' || direction == 'output') { + continue; + } + + final logic = module.ports[pname]; + if (logic == null) { + continue; + } + + final portBitIds = map.portLogics[logic] ?? []; + entry.value['bits'] = portBitIds; + + final idx = indexOf[logic]; + if (idx != null && portBitIds.isNotEmpty) { + final root = roots[idx]; + rootToPreferred.putIfAbsent(root, () => pname); + rootToCanonicalIds.putIfAbsent(root, () => portBitIds); + } + } + + // Add named internal signals to rootToPreferred + final portLogicsSet = map.portLogics.keys.toSet(); + signals.asMap().entries.where((e) { + final logic = e.value; + return !portLogicsSet.contains(logic) && + logic.naming != Naming.unnamed && + !Naming.isUnpreferred(logic.name); + }).forEach( + (e) => rootToPreferred.putIfAbsent(roots[e.key], () => e.value.name)); + + // Add buffer cells for pass-through connections + for (final e in passResult.passThroughConnections.entries) { + final out = e.key; + final inn = e.value; + final outName = out.name; + final inName = passResult.passThroughNames[outName] ?? inn.name; + final inIds = map.portLogics[inn] ?? []; + final outIds = internalNetIds[out] ?? map.portLogics[out] ?? []; + if (inIds.isEmpty || outIds.isEmpty) { + continue; + } + cells['passthrough_${inName}_to_$outName'] = { + 'hide_name': 0, + 'type': r'$buf', + 'parameters': {'WIDTH': inn.width}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': >{'A': inIds, 'Y': outIds}, + }; + } + + List uniquePreserve(Iterable items) { + final seen = {}; + return items.where(seen.add).toList(); + } + + compToIds.forEach((root, ids) { + final name = rootToPreferred[root] ?? 'net_$root'; + final existing = netnames.putIfAbsent( + name, () => {'bits': ids, 'attributes': {}})! + as Map; + final existingBits = (existing['bits']! as List).cast(); + existing['bits'] = uniquePreserve([...existingBits, ...ids]); + }); + + // Add child output IDs as netnames + final coveredIds = {}; + for (final nn in netnames.values) { + final bits = (nn! as Map)['bits'] as List?; + if (bits != null) { + for (final b in bits) { + if (b is int) { + coveredIds.add(b); + } + } + } + } + + for (final entry in internalNetIds.entries) { + final outputLogic = entry.key; + final ids = entry.value; + final intIds = ids.whereType().toList(); + if (intIds.isNotEmpty && intIds.every(coveredIds.contains)) { + continue; + } + + final preferredName = + outputLogic.dstConnections.cast().firstWhere((l) { + if (l == null) { + return false; + } + final idx = indexOf[l]; + return idx != null && + l.naming != Naming.unnamed && + !Naming.isUnpreferred(l.name); + }, orElse: () => null)?.name; + + final netName = + (preferredName != null && !netnames.containsKey(preferredName)) + ? preferredName + : '${outputLogic.parentModule?.uniqueInstanceName ?? 'unknown'}_' + '${outputLogic.name}'; + + if (!netnames.containsKey(netName)) { + netnames[netName] = { + 'bits': ids, + 'attributes': {} + }; + intIds.forEach(coveredIds.add); + } + } + + syntheticNets.forEach((name, ids) { + netnames.putIfAbsent( + name, () => {'bits': ids, 'attributes': {}}); + }); + + // Handle LogicStructure module outputs + for (final outEntry in module.outputs.entries) { + final outName = outEntry.key; + final outLogic = outEntry.value; + if (outLogic is! LogicStructure) { + continue; + } + + final struct = outLogic; + final combined = []; + var allFound = true; + + List? findElemIds(Logic elem) { + if (internalNetIds.containsKey(elem)) { + return internalNetIds[elem]; + } + + for (final e in internalNetIds.entries) { + if (identical(_getCanonicalLogic(e.key), _getCanonicalLogic(elem))) { + return e.value; + } + } + + for (final e in internalNetIds.entries) { + if (e.key is! LogicStructure) { + continue; + } + final childStruct = e.key as LogicStructure; + final idx = childStruct.elements.indexWhere((childElem) => + identical(childElem, elem) || + identical( + _getCanonicalLogic(childElem), _getCanonicalLogic(elem))); + if (idx == -1) { + continue; + } + final bitOffset = childStruct.elements + .take(idx) + .fold(0, (s, el) => s + el.width); + final childElemWidth = childStruct.elements[idx].width; + final ids = e.value; + if (ids.length >= bitOffset + childElemWidth) { + final elemIds = ids.sublist(bitOffset, bitOffset + childElemWidth); + internalNetIds[elem] = List.from(elemIds); + return elemIds; + } + } + + return map.portLogics[elem] ?? map.internalLogics[elem]; + } + + final elemLists = struct.elements.map(findElemIds).toList(); + if (elemLists.any((l) => l == null)) { + allFound = false; + } else { + combined.addAll(elemLists.expand((l) => l!)); + } + + if (allFound && combined.length == struct.width) { + ports[outName] = { + 'direction': 'output', + 'bits': List.from(combined), + }; + netnames[outName] = { + 'bits': List.from(combined), + 'attributes': {}, + }; + } + } + + // Add const netnames + constHandler.emitConstNetnames( + constResult: constResult, + netnames: netnames, + ); + + // Create $const driver cells + final referencedIds = { + ...ports.values.expand((p) => (p['bits']! as List).cast()), + ...cells.values + .where((c) => c['type'] != r'$const') + .map((c) => c['connections'] as Map?) + .where((conns) => conns != null) + .expand((conns) => conns!.values + .whereType>() + .expand((l) => l) + .whereType()), + }; + + constHandler.emitConstCells( + constResult: constResult, + cells: cells, + referencedIds: referencedIds, + ); + + return SchematicSynthesisResult( + module, + getInstanceTypeOfModule, + ports: ports, + cells: cells, + netnames: netnames, + attributes: attr, + ); + } + + /// Emits a primitive cell into [cells]. + void _emitPrimitiveCell({ + required Module childModule, + required ModuleMap childMap, + required String cellKey, + required PrimitiveDescriptor prim, + required Map> cells, + required List Function(Logic) idsForChildLogic, + required ConstantCollectionResult constResult, + required Map> syntheticNets, + required int Function() nextInternalNetIdGetter, + required void Function(int) nextInternalNetIdSetter, + required Map> internalNetIds, + }) { + // Handle Sequential modules + final seqHandler = SequentialHandler(); + final handled = seqHandler.handleSequential( + childModule: childModule, + ports: childModule.ports, + internalNetIds: internalNetIds, + idsForChildLogic: idsForChildLogic, + cells: cells, + syntheticNets: syntheticNets, + nextInternalNetIdGetter: nextInternalNetIdGetter, + nextInternalNetIdSetter: nextInternalNetIdSetter, + ); + if (handled) { + return; + } + + if (prim.useRawPortNames) { + final connMap = >{}; + final portDirs = { + for (final e in childModule.ports.entries) + e.value.name: e.value.isInput + ? 'input' + : e.value.isOutput + ? 'output' + : 'inout' + }; + childModule.ports.forEach((_, logic) { + final ids = idsForChildLogic(logic); + if (ids.isNotEmpty) { + connMap[logic.name] = ids; + } + }); + + if (filterConstInputsToCombinational && + childModule.definitionName == 'Combinational') { + connMap.removeWhere((pname, ids) => + ids.isNotEmpty && + ids.whereType().isNotEmpty && + ids.whereType().every(constResult.blockedIds.contains)); + portDirs.removeWhere((k, _) => !connMap.containsKey(k)); + } + + cells[cellKey] = { + 'hide_name': 0, + 'type': prim.primitiveName, + 'parameters': {'CLK_POLARITY': 1}, + 'attributes': {}, + 'port_directions': portDirs, + 'connections': connMap, + }; + return; + } + + final primCell = + Primitives.instance.computePrimitiveCell(childModule, prim); + final portDirs = Map.from( + (primCell['port_directions']! as Map).cast()); + + final connMap = Primitives.instance + .buildPrimitiveConnectionsWithChildLogicLookup( + childModule, + prim, + (primCell['parameters']! as Map).cast(), + portDirs, + (m) => map.submodules[m], + idsForChildLogic); + + if (filterConstInputsToCombinational && + childModule.definitionName == 'Combinational') { + connMap.removeWhere((pname, ids) => + ids.isNotEmpty && + ids.whereType().isNotEmpty && + ids.whereType().every(constResult.blockedIds.contains)); + portDirs.removeWhere((k, _) => !connMap.containsKey(k)); + } + + cells[cellKey] = { + 'hide_name': 0, + 'type': primCell['type'], + 'parameters': primCell['parameters'], + 'attributes': {}, + 'port_directions': portDirs, + 'connections': connMap, + }; + } +} + +/// Helper to get canonical Logic following srcConnection chain. +Logic _getCanonicalLogic(Logic logic) { + var cur = logic; + final visited = {}; + while (cur.srcConnection != null && !visited.contains(cur.hashCode)) { + visited.add(cur.hashCode); + cur = cur.srcConnection!; + } + return cur; +} diff --git a/lib/src/synthesizers/schematic/schematic_synthesizer.dart b/lib/src/synthesizers/schematic/schematic_synthesizer.dart new file mode 100644 index 000000000..20cc10690 --- /dev/null +++ b/lib/src/synthesizers/schematic/schematic_synthesizer.dart @@ -0,0 +1,163 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// schematic_synthesizer.dart +// Synthesizer for schematic generation. +// +// 2025 December 18 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/schematic/schematic.dart'; + +/// A [Synthesizer] that generates schematic output (Yosys JSON format). +/// +/// Unlike the standalone `SchematicDumper`, this synthesizer integrates with +/// [SynthBuilder] to handle module hierarchy recursion automatically. Each +/// [synthesize] call produces a [SchematicSynthesisResult] for one module +/// level. +/// +/// ## Options +/// +/// - [filterConstInputsToCombinational]: When true, filters out constant-only +/// inputs to combinational primitives from the output. +/// +/// - [globalPortNames]: A list of port names on the **top module** that should +/// be treated as "global" signals (e.g., clock, reset). These signals and any +/// signals reachable from them will be excluded from connectivity generation. +/// The names are resolved in [prepare] when SynthBuilder starts. +/// +/// - [globalLogics]: Alternatively, you can directly provide a set of [Logic] +/// objects to treat as global. This takes precedence over [globalPortNames]. +/// +/// Example: +/// ```dart +/// final synth = SchematicSynthesizer( +/// filterConstInputsToCombinational: true, +/// globalPortNames: ['clk', 'reset'], +/// ); +/// final builder = SynthBuilder(topModule, synth); +/// ``` +class SchematicSynthesizer extends Synthesizer { + /// Whether to filter const-only inputs to combinational primitives. + final bool filterConstInputsToCombinational; + + /// Port names on the top module to treat as global signals. + /// + /// These are resolved to [Logic] objects in [prepare] when SynthBuilder + /// starts synthesis. Signals reachable from these ports will be excluded + /// from connectivity generation. + final List globalPortNames; + + /// Explicit set of [Logic] objects to treat as global. + /// + /// If provided, this takes precedence over [globalPortNames]. These signals + /// and any signals reachable from them will be excluded from connectivity + /// generation. + final Set? globalLogics; + + /// Resolved global logics, computed in [prepare]. + Set _resolvedGlobalLogics = {}; + + /// Creates a [SchematicSynthesizer]. + /// + /// - [filterConstInputsToCombinational]: When true, filters out constant-only + /// inputs to combinational primitives. + /// - [globalPortNames]: Port names on the top module to treat as global. + /// - [globalLogics]: Explicit [Logic] objects to treat as global (takes + /// precedence over [globalPortNames]). + SchematicSynthesizer({ + this.filterConstInputsToCombinational = false, + this.globalPortNames = const [], + this.globalLogics, + }); + + @override + void prepare(List tops) { + // Resolve global logics from the top module(s) + _resolvedGlobalLogics = {}; + + // If explicit globalLogics provided, use them + if (globalLogics != null && globalLogics!.isNotEmpty) { + _resolvedGlobalLogics = Set.from(globalLogics!); + return; + } + + // Otherwise resolve from port names on the first top module + if (globalPortNames.isEmpty || tops.isEmpty) { + return; + } + + final topModule = tops.first; + for (final name in globalPortNames) { + // Check inputs, outputs, and inOuts + final port = topModule.inputs[name] ?? + topModule.outputs[name] ?? + topModule.inOuts[name]; + if (port != null) { + _resolvedGlobalLogics.add(port); + } + } + + if (_resolvedGlobalLogics.isEmpty && globalPortNames.isNotEmpty) { + throw StateError( + 'No top-level ports found matching globalPortNames $globalPortNames. ' + 'Ensure the top module declares ports with these names.', + ); + } + } + + @override + bool generatesDefinition(Module module) { + // Check if module uses Schematic mixin and controls definition generation + if (module is Schematic) { + return module.schematicDefinitionType != + SchematicDefinitionGenerationType.none; + } + + // Primitives don't generate separate definitions - they're inlined + final prim = Primitives.instance.lookupForModule(module); + return prim == null; + } + + @override + SynthesisResult synthesize( + Module module, String Function(Module module) getInstanceTypeOfModule) { + // Create a ModuleMap for this single module level (no recursive submodules) + // The SynthBuilder handles the recursion. + final map = ModuleMap( + module, + includeInternals: true, + globalLogics: + _resolvedGlobalLogics.isNotEmpty ? _resolvedGlobalLogics : null, + ); + + // Validate the ModuleMap hierarchy and connectivity similarly to + // SchematicDumper to provide early, clear errors for cycles/invalid maps. + try { + map + ..validateHierarchy(visited: >{}) + ..validate(); + final idErrors = map.validateIdConnectivity(); + if (idErrors.isNotEmpty) { + final buf = StringBuffer()..writeln('ID connectivity errors:'); + for (final e in idErrors) { + buf.writeln(' - $e'); + } + throw StateError(buf.toString()); + } + } catch (e) { + throw StateError( + 'ModuleMap validation failed before schematic synth: $e'); + } + + final builder = SchematicSynthesisResultBuilder( + module: module, + map: map, + getInstanceTypeOfModule: getInstanceTypeOfModule, + filterConstInputsToCombinational: filterConstInputsToCombinational, + ); + + return builder.build(); + } +} diff --git a/lib/src/synthesizers/schematic/sequential_handler.dart b/lib/src/synthesizers/schematic/sequential_handler.dart new file mode 100644 index 000000000..df66dd3c7 --- /dev/null +++ b/lib/src/synthesizers/schematic/sequential_handler.dart @@ -0,0 +1,199 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// sequential_handler.dart +// Class for handling [Sequential] child modules in the schematic +// dumper. This encapsulates simple-vs-complex mapping and synthetic net +// allocation for mux/dff generation. +// +// 2025 December 16 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Handler to process [Sequential] child modules in the schematic +/// dumper. +class SequentialHandler { + /// Creates a [SequentialHandler]. + SequentialHandler(); + + /// Process a [Sequential]-type [Module] child, emitting cells and registering + /// any synthetic nets into [syntheticNets]. Returns true if the child was + /// processed (and the caller should `continue`), false otherwise. + bool handleSequential({ + required Module childModule, + required Map ports, + required Map> internalNetIds, + required List Function(Logic) idsForChildLogic, + required Map> cells, + required Map> syntheticNets, + required int Function() nextInternalNetIdGetter, + required void Function(int) nextInternalNetIdSetter, + }) { + if (childModule.definitionName != 'Sequential') { + return false; + } + + final triggers = {}; + final dataInputs = {}; + final dataOutputs = {}; + + for (final port in ports.entries) { + final name = port.key; + final logic = port.value; + if (name.startsWith('_trigger')) { + triggers[name] = logic; + } else if (name.startsWith('_in')) { + dataInputs[name] = logic; + } else if (name.startsWith('_out')) { + dataOutputs[name] = logic; + } + } + + final cellKey = childModule.hasBuilt + ? childModule.uniqueInstanceName + : childModule.name; + + // Simple case: 1 trigger, 1 data input, 1 output. + if (triggers.length == 1 && + dataInputs.length == 1 && + dataOutputs.length == 1) { + final clkLogic = triggers.values.first; + final dLogic = dataInputs.values.first; + final qLogic = dataOutputs.values.first; + + final connMap = >{ + 'CLK': idsForChildLogic(clkLogic), + 'D': idsForChildLogic(dLogic), + 'Q': idsForChildLogic(qLogic), + }; + + cells[cellKey] = { + 'hide_name': 0, + 'type': r'$dff', + 'parameters': { + 'CLK_POLARITY': 1, + 'WIDTH': qLogic.width, + }, + 'attributes': {}, + 'port_directions': { + 'CLK': 'input', + 'D': 'input', + 'Q': 'output', + }, + 'connections': connMap, + }; + return true; + } + + // Complex case: try to synthesize mux + dff + final clkIds = triggers.isNotEmpty + ? idsForChildLogic(triggers.values.first) + : []; + + Logic? conditionInput; + final dataOnlyInputs = {}; + for (final entry in dataInputs.entries) { + final name = entry.key; + if (name.contains('greaterThan') || + name.contains('lessThan') || + name.contains('equal') || + name.contains('_cond') || + (name.startsWith('_in0_') && entry.value.width == 1)) { + conditionInput ??= entry.value; + } else if (!name.contains('const')) { + dataOnlyInputs[name] = entry.value; + } + } + + if (conditionInput != null && dataOnlyInputs.length >= 2) { + final dataList = dataOnlyInputs.values.toList(); + final condIds = idsForChildLogic(conditionInput); + var outputIdx = 0; + + for (final outLogic in dataOutputs.values) { + final outIds = idsForChildLogic(outLogic); + final width = outLogic.width; + + final aInput = dataList[outputIdx % dataList.length]; + final bInput = dataList[(outputIdx + 1) % dataList.length]; + + final nextIdStart = nextInternalNetIdGetter(); + final muxOutIds = List.generate(width, (i) => nextIdStart + i); + nextInternalNetIdSetter(nextIdStart + width); + + syntheticNets['${cellKey}_mux${outputIdx}_out'] = muxOutIds; + + cells['${cellKey}_mux_$outputIdx'] = { + 'hide_name': 0, + 'type': r'$mux', + 'parameters': {'WIDTH': width}, + 'attributes': {}, + 'port_directions': { + 'A': 'input', + 'B': 'input', + 'S': 'input', + 'Y': 'output', + }, + 'connections': >{ + 'A': idsForChildLogic(aInput), + 'B': idsForChildLogic(bInput), + 'S': condIds, + 'Y': muxOutIds, + }, + }; + + cells['${cellKey}_dff_$outputIdx'] = { + 'hide_name': 0, + 'type': r'$dff', + 'parameters': { + 'CLK_POLARITY': 1, + 'WIDTH': width, + }, + 'attributes': {}, + 'port_directions': { + 'CLK': 'input', + 'D': 'input', + 'Q': 'output', + }, + 'connections': >{ + 'CLK': clkIds, + 'D': muxOutIds, + 'Q': outIds, + }, + }; + + outputIdx++; + } + return true; + } + + // Fallback: emit generic $sequential cell mapping raw ports + final connMap = >{}; + final portDirs = {}; + for (final port in ports.entries) { + final logic = port.value; + final dir = logic.isInput + ? 'input' + : logic.isOutput + ? 'output' + : 'inout'; + portDirs[port.key] = dir; + final ids = idsForChildLogic(logic); + if (ids.isNotEmpty) { + connMap[port.key] = ids; + } + } + + cells[cellKey] = { + 'hide_name': 0, + 'type': r'$sequential', + 'parameters': {'CLK_POLARITY': 1}, + 'attributes': {}, + 'port_directions': portDirs, + 'connections': connMap, + }; + + return true; + } +} diff --git a/lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs b/lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs new file mode 100644 index 000000000..40354ca21 --- /dev/null +++ b/lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs @@ -0,0 +1,44 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// _yosys_loader_runner.mjs +// Javascript program for loading yosys JSON files in D3 ELK format. +// +// Usage: node _yosys_loader_runner.mjs path/to/yosys.json +// +// 2025 December 12 +// Author: Desmond Kirkpatrick + +import fs from 'fs'; +import path from 'path'; + +async function main() { + const args = process.argv.slice(2); + if (args.length < 1) { + console.error(JSON.stringify({success: false, error: 'missing json path'})); + process.exit(2); + } + const jsonPath = path.resolve(args[0]); + try { + const yosysModulePath = path.resolve(new URL('d3-yosys/src/yosys.js', import.meta.url).pathname); + const { yosys } = await import('file://' + yosysModulePath); + const raw = fs.readFileSync(jsonPath, 'utf8'); + const yosysJson = JSON.parse(raw); + const out = yosys(yosysJson); + const topChild = out.children && out.children[0] ? out.children[0] : null; + const res = { + success: true, + rootChildren: out.children ? out.children.length : 0, + topNodeId: topChild ? topChild.id : null, + topNodePorts: topChild ? (topChild.ports ? topChild.ports.length : 0) : null + }; + console.log(JSON.stringify(res)); + process.exit(0); + } catch (e) { + const err = {success: false, error: String(e), stack: e && e.stack ? e.stack : null}; + console.error(JSON.stringify(err)); + process.exit(2); + } +} + +main(); diff --git a/lib/src/synthesizers/schematic/yosys/d3-yosys/LICENSE b/lib/src/synthesizers/schematic/yosys/d3-yosys/LICENSE new file mode 100644 index 000000000..d3087e4c5 --- /dev/null +++ b/lib/src/synthesizers/schematic/yosys/d3-yosys/LICENSE @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/lib/src/synthesizers/schematic/yosys/d3-yosys/README.md b/lib/src/synthesizers/schematic/yosys/d3-yosys/README.md new file mode 100644 index 000000000..d6840694c --- /dev/null +++ b/lib/src/synthesizers/schematic/yosys/d3-yosys/README.md @@ -0,0 +1,3 @@ +This is a set of JavaScript files taken from the [d3-hwschematic package] +() to help sanitize the loading of the +produced [ELK-JS]() format. diff --git a/lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosys.js b/lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosys.js new file mode 100644 index 000000000..9d62d2d9e --- /dev/null +++ b/lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosys.js @@ -0,0 +1,573 @@ +import {yosysTranslateIcons} from "./yosysIcons.js"; +import { + getPortSide, + getPortNameSplice, + isConst, + addEdge, + getConstNodeName, + getTopModuleName, + getNetNamesDict, + orderClkAndRstPorts, + hideChildrenAndNodes, + getSourceAndTarget2, + getSourceAndTargetForCell, + convertPortOrderingFromYosysToElk +} from "./yosysUtills.js"; + +function getNodePorts(node, dict){ + for (let port of node.ports) { + dict[port.id] = port; + } + +} +function getPortIdToPortDict(node) { + let PortIdToPortDict = {}; + + getNodePorts(node, PortIdToPortDict); + for (let child of node.children) { + getNodePorts(child, PortIdToPortDict); + } + + return PortIdToPortDict; +} +function getNodeIdToNodeDict(node,) { + let nodeIdToNodeDict = {}; + nodeIdToNodeDict[node.id] = node; + for (let child of node.children) { + nodeIdToNodeDict[child.id] = child; + } + return nodeIdToNodeDict; +} + +function getPortToEdgeDict(edges) { + let portToEdgeDict = {}; + for (let edge of edges) { + let targets = edge.targets; + let sources = edge.sources; + for (let [_, portId] of sources) { + portToEdgeDict[portId] = edge; + } + + for (let [_, portId] of targets) { + portToEdgeDict[portId] = edge; + } + } + return portToEdgeDict; +} + +function getChildSourcePorts(ports) { + let sourcePorts = []; + for(let port of ports) { + if (port !== undefined && port.direction === "INPUT") { + sourcePorts.push(port); + } + } + + return sourcePorts; +} + +function getEdgeTargetsIndex(targets, portId) { + for(let i = 0; i < targets.length; ++i) { + let target = targets[i]; + let [_, targetPortId] = target; + + if (portId === targetPortId) { + return i; + } + } + throw new Error("PortId was not found"); + +} +function aggregateTwoNodes(childSourcePorts, targetNode, targetPort, portIdToEdgeDict) { + let i = 0; + if (targetPort.properties.index !== 0) { + throw new Error("Port index is not zero, need to regenerate indices in port labels"); + } + for (let oldTargetPort of childSourcePorts) { + let oldTargetPortId = oldTargetPort.id; + let edge = portIdToEdgeDict[oldTargetPortId]; + let edgeTargetsIndex = getEdgeTargetsIndex(edge.targets, oldTargetPortId); + edge.targets[edgeTargetsIndex][0] = targetNode.id; + let newTargetPortIndex = targetPort.properties.index + i; + if (i === 0) { + targetNode.ports[newTargetPortIndex] = oldTargetPort; + } + else { + targetNode.ports.splice(newTargetPortIndex, 0, oldTargetPort) + } + oldTargetPort.properties.index = newTargetPortIndex; + ++i; + } + + +} + +function getChildTargetPortId(child) { + for (let port of child.ports) { + if (port !== undefined && port.direction === "OUTPUT") + { + return port.id; + } + } + + throw new Error("Concat child has no target"); +} + +function aggregate(node, childrenConcats, portIdToEdgeDict, portIdToPortDict, nodeIdToNodeDict) { + let edgesToDelete = new Set(); + let childrenToDelete = new Set(); + + for (let child of childrenConcats) { + let childTargetPortId = getChildTargetPortId(child); + let edge = portIdToEdgeDict[childTargetPortId]; + if (edge === undefined) { + continue; + } + let targets = edge.targets; + + if (targets !== undefined && targets.length === 1) { + let [nodeId, portId] = targets[0]; + let targetNode = nodeIdToNodeDict[nodeId]; + let targetPort = portIdToPortDict[portId]; + let childSourcePorts = getChildSourcePorts(child.ports); + if (targetNode === undefined) { + throw new Error("Target node of target port is undefined"); + } + if (targetNode.hwMeta.cls === "Operator" && targetNode.hwMeta.name === "CONCAT") { + aggregateTwoNodes(childSourcePorts, targetNode, targetPort, portIdToEdgeDict) + edgesToDelete.add(edge.id); + childrenToDelete.add(child.id); + } + } + } + node.children = node.children.filter((c) => { + return !childrenToDelete.has(c.id); + }); + node.edges = node.edges.filter((e) => { + return !edgesToDelete.has(e.id); + }); +} + +function fillConcats(children) { + let concats = []; + for (let child of children) { + if (child.hwMeta.cls === "Operator" && child.hwMeta.name === "CONCAT") { + concats.push(child); + } + } + return concats; + +} + +function aggregateConcants(node) { + let concats = fillConcats(node.children); + let portIdToEdgeDict = getPortToEdgeDict(node.edges); + let portIdToPortDict = getPortIdToPortDict(node); + let nodeIdToNodeDict = getNodeIdToNodeDict(node); + aggregate(node, concats, portIdToEdgeDict, portIdToPortDict, nodeIdToNodeDict); +} + +class LNodeMaker { + constructor(name, yosysModule, idCounter, yosysModules, hierarchyLevel, nodePortNames) { + this.name = name; + this.yosysModule = yosysModule; + this.idCounter = idCounter; + this.yosysModules = yosysModules; + this.hierarchyLevel = hierarchyLevel; + this.nodePortNames = nodePortNames; + this.childrenWithoutPortArray = []; + this.nodeIdToCell = {}; + } + + make() { + if (this.name === undefined) { + throw new Error("Name is undefined"); + } + + let node = this.makeNode(this.name); + + if (this.yosysModule) { + // cell with module definition, load ports, edges and children from module def. recursively + this.fillPorts(node, this.yosysModule.ports, (p) => { + return p.direction + }, undefined); + this.fillChildren(node); + this.fillEdges(node); + + if (node.children !== undefined && node.children.length > 0) { + aggregateConcants(node); + } + + } + + if (node.children !== undefined) { + for (let child of node.children) { + convertPortOrderingFromYosysToElk(child); + if (child.hwMeta.cls === "Operator" && child.hwMeta.name.startsWith("FF")) { + orderClkAndRstPorts(child); + } + } + } + + if (this.hierarchyLevel > 1) { + hideChildrenAndNodes(node, this.yosysModule); + } + + node.hwMeta.maxId = this.idCounter - 1; + return node; + } + makeNode(name) { + let node = { + "id": this.idCounter.toString(), //generate, each component has unique id + "hwMeta": { // [d3-hwschematic specific] + "name": name, // optional str + "cls": "", // optional str + "maxId": 2, // max id of any object in this node used to avoid re-counting object if new object is generated + }, + "properties": { // recommended renderer settings + "org.eclipse.elk.portConstraints": "FIXED_ORDER", // can be also "FREE" or other value accepted by ELK + "org.eclipse.elk.layered.mergeEdges": 1 + }, + "ports": [], // list of LPort + "edges": [], // list of LEdge + "children": [], // list of LNode + }; + ++this.idCounter; + return node; + } + + fillPorts(node, ports, getPortDirectionFn, cellObj) { + const isSplit = cellObj !== undefined && cellObj.type === "$slice"; + const isConcat = cellObj !== undefined && cellObj.type === "$concat"; + let portByName = this.nodePortNames[node.id]; + if (portByName === undefined) { + portByName = {}; + this.nodePortNames[node.id] = portByName; + } + for (let [portName, portObj] of Object.entries(ports)) { + let originalPortName = portName; + if (isSplit || isConcat) { + if (portName === "Y") { + portName = ""; + } + if (isSplit) { + if (portName === "A") { + portName = getPortNameSplice(cellObj.parameters.OFFSET, cellObj.parameters.Y_WIDTH); + } + } else if (isConcat) { + let par = cellObj.parameters; + if (portName === "A") { + portName = getPortNameSplice(0, par.A_WIDTH); + } else if (portName === "B") { + portName = getPortNameSplice(par.A_WIDTH, par.B_WIDTH); + } + } + } + let direction = getPortDirectionFn(portObj); + this.makeLPort(node.ports, portByName, originalPortName, portName, direction, node.hwMeta.name); + } + } + + makeLPort(portList, portByName, originalName, name, direction, nodeName) { + if (name === undefined) { + throw new Error("Name is undefined"); + } + + let portSide = getPortSide(name, direction, nodeName); + let port = { + "id": this.idCounter.toString(), + "hwMeta": { // [d3-hwschematic specific] + "name": name, + }, + "direction": direction.toUpperCase(), // [d3-hwschematic specific] controls direction marker + "properties": { + "side": portSide, + "index": 0 // The order is assumed as clockwise, starting with the leftmost port on the top side. + // Required only for components with "org.eclipse.elk.portConstraints": "FIXED_ORDER" + }, + "children": [], // list of LPort, if the port should be collapsed rename this property to "_children" + }; + port.properties.index = portList.length; + portList.push(port); + portByName[originalName] = port; + ++this.idCounter; + return port; + } + + fillChildren(node) { + // iterate all cells and lookup for modules and construct them recursively + for (const [cellName, cellObj] of Object.entries(this.yosysModule.cells)) { + let moduleName = cellObj.type; //module name + let cellModuleObj = this.yosysModules[moduleName]; + let nodeBuilder = new LNodeMaker(cellName, cellModuleObj, this.idCounter, this.yosysModules, + this.hierarchyLevel + 1, this.nodePortNames); + let subNode = nodeBuilder.make(); + this.idCounter = nodeBuilder.idCounter; + node.children.push(subNode); + yosysTranslateIcons(subNode, cellObj); + this.nodeIdToCell[subNode.id] = cellObj; + if (cellModuleObj === undefined) { + if (cellObj.port_directions === undefined) { + // throw new Error("[Todo] if modules does not have definition in modules and its name does not \ + // start with $, then it does not have port_directions. Must add port to sources and targets of an edge") + + this.childrenWithoutPortArray.push([cellObj, subNode]); + continue; + } + this.fillPorts(subNode, cellObj.port_directions, (p) => { + return p; + }, cellObj); + } + } + } + + fillEdges(node) { + + let edgeTargetsDict = {}; + let edgeSourcesDict = {}; + let constNodeDict = {}; + let [edgeDict, edgeArray] = this.getEdgeDictFromPorts( + node, constNodeDict, edgeTargetsDict, edgeSourcesDict); + let netnamesDict = getNetNamesDict(this.yosysModule); + + function getPortName(bit) { + return netnamesDict[bit]; + } + + for (let i = 0; i < node.children.length; i++) { + const subNode = node.children[i]; + if (constNodeDict[subNode.id] === 1) { + //skip constants to iterate original cells + continue; + } + + let cell = this.nodeIdToCell[subNode.id]; + if (cell.port_directions === undefined) { + continue; + } + let connections = cell.connections; + let portDirections = cell.port_directions; + + + if (connections === undefined) { + throw new Error("Cannot find cell for subNode" + subNode.hwMeta.name); + } + + let portI = 0; + let portByName = this.nodePortNames[subNode.id]; + for (const [portName, bits] of Object.entries(connections)) { + let portObj; + let direction; + if (portName.startsWith("$")) { + portObj = subNode.ports[portI++] + direction = portObj.direction.toLowerCase(); //use direction from module port definition + } else { + portObj = portByName[portName]; + if (portObj === undefined) { + console.error(`DEBUG: portByName[${portName}] is undefined for subNode ${subNode.hwMeta.name}, cell type=${cell.type}`); + console.error(`DEBUG: portByName keys: ${Object.keys(portByName || {}).join(', ')}`); + console.error(`DEBUG: connections keys: ${Object.keys(connections).join(', ')}`); + } + direction = portDirections[portName]; + } + + this.loadNets(node, subNode.id, portObj.id, bits, direction, edgeDict, constNodeDict, + edgeArray, getPortName, getSourceAndTargetForCell, edgeTargetsDict, edgeSourcesDict); + + } + } + // source null target null == direction is output + + for (const [cellObj, subNode] of this.childrenWithoutPortArray) { + for (const [portName, bits] of Object.entries(cellObj.connections)) { + let port = null; + for (const bit of bits) { + let edge = edgeDict[bit]; + if (edge === undefined) { + throw new Error("[Todo] create edge"); + } + let edgePoints; + let direction; + if (edge.sources.length === 0 && edge.targets.length === 0) { + direction = "output"; + edgePoints = edge.sources; + } else if (edge.sources.length === 0) { + // no sources -> add as source + direction = "output"; + edgePoints = edge.sources; + } else { + direction = "input"; + edgePoints = edge.targets; + } + + if (port === null) { + let portByName = this.nodePortNames[subNode.id]; + if (portByName === undefined) { + portByName = {}; + this.nodePortNames[subNode.id] = portByName; + } + port = this.makeLPort(subNode.ports, portByName, portName, portName, direction, subNode.hwMeta.name); + } + + edgePoints.push([subNode.id, port.id]); + } + } + + } + + let edgeSet = {}; // [sources, targets]: true + for (const edge of edgeArray) { + let key = [edge.sources, null, edge.targets] + if (!edgeSet[key]) // filter duplicities + { + edgeSet[key] = true; + node.edges.push(edge); + } + } + + } + + getEdgeDictFromPorts(node, constNodeDict, edgeTargetsDict, edgeSourcesDict) { + let edgeDict = {}; // yosys bits (netId): LEdge + let edgeArray = []; + let portsIndex = 0; + for (const [portName, portObj] of Object.entries(this.yosysModule.ports)) { + let port = node.ports[portsIndex]; + portsIndex++; + + function getPortName2() { + return portName; + } + + this.loadNets(node, node.id, port.id, portObj.bits, portObj.direction, + edgeDict, constNodeDict, edgeArray, getPortName2, getSourceAndTarget2, + edgeTargetsDict, edgeSourcesDict) + + } + return [edgeDict, edgeArray]; + } + + /* + * Iterate bits representing yosys net names, which are used to get edges from the edgeDict. + * If edges are not present in the dictionary, they are created and inserted into it. Eventually, + * nodes are completed by filling sources and targets properties of LEdge. + */ + loadNets(node, nodeId, portId, bits, direction, edgeDict, constNodeDict, edgeArray, + getPortName, getSourceAndTarget, edgeTargetsDict, edgeSourcesDict) { + for (let i = 0; i < bits.length; ++i) { + let startIndex = i; + let width = 1; + let bit = bits[i]; + let portName = getPortName(bit); + let edge = edgeDict[bit]; + let netIsConst = isConst(bit); + if (netIsConst || edge === undefined) { + // create edge if it is not in edgeDict + if (portName === undefined) { + if (!netIsConst) { + throw new Error("Netname is undefined"); + } + portName = bit; + } + edge = this.makeLEdge(portName); + edgeDict[bit] = edge; + edgeArray.push(edge); + if (netIsConst) { + i = this.addConstNodeToSources(node, bits, edge.sources, i, constNodeDict); + width = i - startIndex + 1; + } + } + + let [a, b, targetA, targetB] = getSourceAndTarget(edge); + if (direction === "input") { + a.push([nodeId, portId]); + if (targetA) { + addEdge(edge, portId, edgeTargetsDict, startIndex, width); + } else { + addEdge(edge, portId, edgeSourcesDict, startIndex, width) + } + } else if (direction === "output") { + b.push([nodeId, portId]); + if (targetB) { + addEdge(edge, portId, edgeTargetsDict, startIndex, width); + } else { + addEdge(edge, portId, edgeSourcesDict, startIndex, width); + } + } else { + throw new Error("Unknown direction " + direction); + } + } + } + + makeLEdge(name) { + if (name === undefined) { + throw new Error("Name is undefined"); + } + let edge = { + "id": this.idCounter.toString(), + "sources": [], + "targets": [], // [id of LNode, id of LPort] + "hwMeta": { // [d3-hwschematic specific] + "name": name, // optional string, displayed on mouse over + } + }; + ++this.idCounter; + return edge; + } + + addConstNodeToSources(node, bits, sources, i, constNodeDict) { + let nameArray = []; + for (i; i < bits.length; ++i) { + let bit = bits[i]; + if (isConst(bit)) { + nameArray.push(bit); + } else { + break; + } + } + --i; + // If bit is a constant, create a node with constant + let nodeName = getConstNodeName(nameArray); + let constSubNode; + let port; + [constSubNode, port] = this.addConstNode(node, nodeName, constNodeDict); + sources.push([constSubNode.id, port.id]); + return i; + } + + addConstNode(node, nodeName, constNodeDict) { + let port; + + let nodeBuilder = new LNodeMaker(nodeName, undefined, this.idCounter, null, + this.hierarchyLevel + 1, this.nodePortNames); + let subNode = nodeBuilder.make(); + this.idCounter = nodeBuilder.idCounter; + + let portByName = this.nodePortNames[subNode.id] = {}; + port = this.makeLPort(subNode.ports, portByName, "O0", "O0", "output", subNode.hwMeta.name); + node.children.push(subNode); + constNodeDict[subNode.id] = 1; + + return [subNode, port]; + } + + +} + +export function yosys(yosysJson) { + let nodePortNames = {}; + let rootNodeBuilder = new LNodeMaker("root", null, 0, null, 0, nodePortNames); + let output = rootNodeBuilder.make(); + let topModuleName = getTopModuleName(yosysJson); + + let nodeBuilder = new LNodeMaker(topModuleName, yosysJson.modules[topModuleName], rootNodeBuilder.idCounter, + yosysJson.modules, 1, nodePortNames); + let node = nodeBuilder.make(); + output.children.push(node); + output.hwMeta.maxId = nodeBuilder.idCounter - 1; + //yosysTranslateIcons(output); + //print output to console + //console.log(JSON.stringify(output, null, 2)); + + return output; +} diff --git a/lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosysIcons.js b/lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosysIcons.js new file mode 100644 index 000000000..31477b2b7 --- /dev/null +++ b/lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosysIcons.js @@ -0,0 +1,85 @@ +export function yosysTranslateIcons(node, cell) { + let meta = node.hwMeta; + const t = cell.type; + + if (t === "$mux" || t === "$pmux") { + meta.cls = "Operator"; + meta.name = "MUX"; + } else if (t === "$gt") { + meta.cls = "Operator"; + meta.name = "GT"; + } else if (t === "$lt") { + meta.cls = "Operator"; + meta.name = "LT"; + } else if (t === "$ge") { + meta.cls = "Operator"; + meta.name = "GE"; + } else if (t === "$le") { + meta.cls = "Operator"; + meta.name = "LE"; + } else if (t === "$not" || t === "$logic_not") { + meta.cls = "Operator"; + meta.name = "NOT"; + } else if (t === "$logic_and" || t === "$and") { + meta.cls = "Operator"; + meta.name = "AND"; + } else if (t === "$logic_or" || t === "$or") { + meta.cls = "Operator"; + meta.name = "OR"; + } else if (t === "$xor") { + meta.cls = "Operator"; + meta.name = "XOR"; + } else if (t === "$eq") { + meta.cls = "Operator"; + meta.name = "EQ"; + } else if (t === "$ne") { + meta.cls = "Operator"; + meta.name = "NE"; + } else if (t === "$add") { + meta.cls = "Operator"; + meta.name = "ADD"; + } else if (t === "$sub") { + meta.cls = "Operator"; + meta.name = "SUB"; + } else if (t === "$mul") { + meta.cls = "Operator"; + meta.name = "MUL"; + } else if (t === "$div") { + meta.cls = "Operator"; + meta.name = "DIV"; + } else if (t === "$slice") { + meta.cls = "Operator"; + meta.name = "SLICE"; + } else if (t === "$concat") { + meta.cls = "Operator"; + meta.name = "CONCAT"; + } else if (t === "$adff") { + meta.cls = "Operator"; + let arstPolarity = cell.parameters["ARST_POLARITY"]; + let clkPolarity = cell.parameters["CLK_POLARITY"]; + if (clkPolarity && arstPolarity) { + meta.name = "FF_ARST_clk1_rst1"; + } else if (clkPolarity) { + meta.name = "FF_ARST_clk1_rst0"; + } else if (arstPolarity) { + meta.name = "FF_ARST_clk0_rst1"; + } else { + meta.name = "FF_ARST_clk0_rst0"; + } + } else if (t === "$dff") { + meta.cls = "Operator"; + meta.name = "FF"; + } else if (t === "$shift" || t === "$shiftx") { + meta.cls = "Operator"; + meta.name = "SHIFT"; + } else if (t === "$dlatch") { + meta.cls = "Operator"; + let enPolarity = cell.parameters["EN_POLARITY"]; + if (enPolarity) { + meta.name = "DLATCH_en1"; + } else { + meta.name = "DLATCH_en0"; + + } + } +} \ No newline at end of file diff --git a/lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosysUtills.js b/lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosysUtills.js new file mode 100644 index 000000000..3535385c3 --- /dev/null +++ b/lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosysUtills.js @@ -0,0 +1,167 @@ +export function getPortSide(portName, direction, nodeName) { + if (direction === "input" && nodeName === "MUX" && portName === "S") { + return "SOUTH"; + } + if (direction === "output") { + return "EAST"; + } + if (direction === "input") { + return "WEST"; + } + throw new Error("Unknown direction " + direction); +} + +export function orderClkAndRstPorts(node) { + let index = 0; + for (let port of node.ports) { + let dstIndex = index; + if (port.hwMeta.name === "CLK") { + dstIndex = node.ports.length - 1; + } else if (port.hwMeta.name === "ARST") { + dstIndex = node.ports.length - 2; + } + if (index !== dstIndex) { + let otherPort = node.ports[dstIndex]; + node.ports[dstIndex] = port; + node.ports[index] = otherPort; + otherPort.properties.index = port.properties.index; + port.properties.index = dstIndex; + } + ++index; + } +} + +function iterNetnameBits(netnames, fn) { + for (const [netname, netObj] of Object.entries(netnames)) { + for (const bit of netObj.bits) { + fn(netname, bit, Number.isInteger(bit), isConst(bit)); + } + } +} + +export function getNetNamesDict(yosysModule) { + let netnamesDict = {}; // yosys bits (netId): netname + + iterNetnameBits(yosysModule.netnames, (netname, bit, isInt, isStr) => { + if (isInt) { + netnamesDict[bit] = netname; + } else if (!isStr) { + throw new Error("Invalid type in bits: " + typeof bit); + } + }); + return netnamesDict; +} + +export function isConst(val) { + return (typeof val === "string"); +} + +export function getConstNodeName(nameArray) { + let nodeName = nameArray.reverse().join(""); + nodeName = ["0b", nodeName].join(""); + if (nodeName.match(/^0b[01]+$/g)) { + let res = BigInt(nodeName).toString(16); + return ["0x", res].join(""); + } + return nodeName; +} + +export function addEdge(edge, portId, edgeDict, startIndex, width) { + let edgeArr = edgeDict[portId]; + if (edgeArr === undefined) { + edgeArr = edgeDict[portId] = []; + } + edgeArr[startIndex] = [edge, width]; +} + +export function getSourceAndTarget2(edge) { + return [edge.sources, edge.targets, false, true]; +} + +export function getSourceAndTargetForCell(edge) { + return [edge.targets, edge.sources, true, false]; +} + +export function getPortNameSplice(startIndex, width) { + if (width === 1) { + return `[${startIndex}]`; + } else if (width > 1) { + let endIndex = startIndex + width; + return `[${endIndex}:${startIndex}]`; + } + + throw new Error("Incorrect width" + width); + +} + + +export function hideChildrenAndNodes(node, yosysModule) { + if (yosysModule !== null) { + if (node.children.length === 0 && node.edges.length === 0) { + delete node.children + delete node.edges; + + } else { + node._children = node.children; + delete node.children + node._edges = node.edges; + delete node.edges; + } + } +} + + +function updatePortIndices(ports) { + let index = 0; + for (let port of ports) { + port.properties.index = index; + ++index; + } +} + +function dividePorts(ports) { + let north = []; + let east = []; + let south = []; + let west = []; + + for (let port of ports) { + let side = port.properties.side; + if (side === "NORTH") { + north.push(port); + } else if (side === "EAST") { + east.push(port); + } else if (side === "SOUTH") { + south.push(port); + } else if (side === "WEST") { + west.push(port); + } else { + throw new Error("Invalid port side: " + side); + } + } + + return [north, east, south, west]; +} + +export function convertPortOrderingFromYosysToElk(node) { + let [north, east, south, west] = dividePorts(node.ports); + node.ports = north.concat(east, south.reverse(), west.reverse()); + updatePortIndices(node.ports); + +} + +export function getTopModuleName(yosysJson) { + let topModuleName = undefined; + for (const [moduleName, moduleObj] of Object.entries(yosysJson.modules)) { + if (moduleObj.attributes.top) { + topModuleName = moduleName; + break; + } + } + + if (topModuleName === undefined) { + throw new Error("Cannot find top"); + } + + return topModuleName; +} diff --git a/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart b/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart new file mode 100644 index 000000000..792371633 --- /dev/null +++ b/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart @@ -0,0 +1,97 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// yosys_loader_helper.dart +// A helper routine to load a Yosys JSON file using the D3 ELK loader. +// +// 2025 December 12 +// Author: Desmond Kirkpatrick + +// ignore_for_file: avoid_print + +import 'dart:convert'; +import 'dart:io'; + +/// Result of running the Yosys loader. +class YosysLoaderResult { + /// Whether the load was successful. + final bool success; + + /// Number of root children modules. + final int? rootChildren; + + /// Top node ID. + final String? topNodeId; + + /// Number of top node ports. + final int? topNodePorts; + + /// Error message if unsuccessful. + final String? error; + + /// Stack trace if available. + final String? stack; + + /// Creates a [YosysLoaderResult]. + YosysLoaderResult( + {required this.success, + this.rootChildren, + this.topNodeId, + this.topNodePorts, + this.error, + this.stack}); + + /// Creates a [YosysLoaderResult] from JSON map [j]. + factory YosysLoaderResult.fromJson(Map j) => + YosysLoaderResult( + success: j['success'] as bool, + rootChildren: j['rootChildren'] as int?, + topNodeId: j['topNodeId'] as String?, + topNodePorts: j['topNodePorts'] as int?, + error: j['error'] as String?, + stack: j['stack'] as String?); +} + +/// Run the Node ESM loader on [jsonPath]. Returns a parsed [YosysLoaderResult]. +Future runYosysLoader(String jsonPath) async { + const node = 'node'; + const script = + 'lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs'; + final result = await Process.run(node, [script, jsonPath]); + if (result.exitCode == 0) { + final out = result.stdout.toString().trim(); + final map = json.decode(out) as Map; + return YosysLoaderResult.fromJson(map); + } else { + final errOut = result.stderr.toString().trim(); + try { + final map = json.decode(errOut) as Map; + return YosysLoaderResult.fromJson(map); + } on FormatException { + return YosysLoaderResult( + success: false, + error: 'node runner failed: ${result.stderr}\n${result.stdout}'); + } + } +} + +/// Main entry point for command-line testing. +Future main(List args) async { + if (args.isEmpty) { + print( + 'Usage: dart run lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart '); + exit(1); + } + final result = await runYosysLoader(args[0]); + print('success: ${result.success}'); + if (result.success) { + print('rootChildren: ${result.rootChildren}'); + print('topNodeId: ${result.topNodeId}'); + print('topNodePorts: ${result.topNodePorts}'); + } else { + print('error: ${result.error}'); + if (result.stack != null) { + print('stack:\n${result.stack}'); + } + } +} diff --git a/lib/src/synthesizers/synth_builder.dart b/lib/src/synthesizers/synth_builder.dart index 54e312ab3..c571b0185 100644 --- a/lib/src/synthesizers/synth_builder.dart +++ b/lib/src/synthesizers/synth_builder.dart @@ -56,6 +56,9 @@ class SynthBuilder { } } + // Allow the synthesizer to prepare with knowledge of top module(s) + synthesizer.prepare(this.tops); + final modulesToParse = [...tops]; for (var i = 0; i < modulesToParse.length; i++) { final moduleI = modulesToParse[i]; diff --git a/lib/src/synthesizers/synthesizer.dart b/lib/src/synthesizers/synthesizer.dart index b70c9338e..523e37644 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-2025 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synthesizer.dart @@ -12,6 +12,16 @@ import 'package:rohd/rohd.dart'; /// An object capable of converting a module into some new output format abstract class Synthesizer { + /// Called by [SynthBuilder] before synthesis begins, with the top-level + /// module(s) being synthesized. + /// + /// Override this method to perform any initialization that requires + /// knowledge of the top module, such as resolving port names to [Logic] + /// objects, or computing global signal sets. + /// + /// The default implementation does nothing. + void prepare(List tops) {} + /// Determines whether [module] needs a separate definition or can just be /// described in-line. bool generatesDefinition(Module module); diff --git a/test/schematic_example_test.dart b/test/schematic_example_test.dart new file mode 100644 index 000000000..f6395bd20 --- /dev/null +++ b/test/schematic_example_test.dart @@ -0,0 +1,263 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// schematic_example_test.dart +// Convert examples to schematic for and check the produced JSON. + +// 2025 December 18 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/schematic/schematic.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart'; +import '../example/fir_filter.dart'; +import '../example/logic_array.dart'; +import '../example/oven_fsm.dart'; +import '../example/tree.dart'; + +void main() { + test('Schematic dump for example Counter', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final counter = Counter(en, reset, clk); + await counter.build(); + + const outPath = 'build/Counter.rohd.json'; + SchematicDumper(counter, + outputPath: outPath, filterConstInputsToCombinational: true); + + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + + final r = await runYosysLoader(outPath); + expect(r.success, isTrue, + reason: 'loader should load Counter: ${r.error ?? r}'); + }); + + group('SynthBuilder schematic generation for examples', () { + Future writeCombined( + SynthBuilder synth, Module top, String out) async { + final allModules = >{}; + for (final result in synth.synthesisResults) { + if (result is SchematicSynthesisResult) { + final typeName = result.instanceTypeName; + final attrs = Map.from(result.attributes); + if (result.module == top) { + attrs['top'] = 1; + } + allModules[typeName] = { + 'attributes': attrs, + 'ports': result.ports, + 'cells': result.cells, + 'netnames': result.netnames, + }; + } + } + final combined = { + 'creator': 'SchematicSynthesizer via SynthBuilder (rohd)', + 'modules': allModules, + }; + final json = const JsonEncoder.withIndent(' ').convert(combined); + await File(out).writeAsString(json); + } + + test('SynthBuilder schematic for Counter', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final counter = Counter(en, reset, clk); + await counter.build(); + + final synth = SynthBuilder(counter, SchematicSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + const outPath = 'build/Counter.synth.rohd.json'; + await writeCombined(synth, counter, outPath); + final f = File(outPath); + expect(f.existsSync(), isTrue); + + final r = await runYosysLoader(outPath); + expect(r.success, isTrue, + reason: 'loader should load Counter synth: ${r.error ?? r}'); + }); + + test('SynthBuilder schematic for FIR filter example', () async { + final en = Logic(name: 'en'); + final resetB = Logic(name: 'resetB'); + final clk = SimpleClockGenerator(10).clk; + final inputVal = Logic(name: 'inputVal', width: 8); + + final fir = + FirFilter(en, resetB, clk, inputVal, [0, 0, 0, 1], bitWidth: 8); + await fir.build(); + + final synth = SynthBuilder(fir, SchematicSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + const outPath = 'build/FirFilter.synth.rohd.json'; + await writeCombined(synth, fir, outPath); + final f = File(outPath); + expect(f.existsSync(), isTrue); + + final r = await runYosysLoader(outPath); + expect(r.success, isTrue, + reason: 'loader should load FirFilter synth: ${r.error ?? r}'); + }); + + test('SynthBuilder schematic for LogicArray example', () async { + final arrayA = LogicArray([4], 8, name: 'arrayA'); + final id = Logic(name: 'id', width: 3); + final selectIndexValue = Logic(name: 'selectIndexValue', width: 8); + final selectFromValue = Logic(name: 'selectFromValue', width: 8); + + final la = + LogicArrayExample(arrayA, id, selectIndexValue, selectFromValue); + await la.build(); + + final synth = SynthBuilder(la, SchematicSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + const outPath = 'build/LogicArrayExample.synth.rohd.json'; + await writeCombined(synth, la, outPath); + final f = File(outPath); + expect(f.existsSync(), isTrue); + + final r = await runYosysLoader(outPath); + expect(r.success, isTrue, + reason: + 'loader should load LogicArrayExample synth: ${r.error ?? r}'); + }); + + test('SynthBuilder schematic for OvenModule example', () async { + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final oven = OvenModule(button, reset, clk); + await oven.build(); + + final synth = SynthBuilder(oven, SchematicSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + const outPath = 'build/OvenModule.synth.rohd.json'; + await writeCombined(synth, oven, outPath); + final f = File(outPath); + expect(f.existsSync(), isTrue); + + final r = await runYosysLoader(outPath); + expect(r.success, isTrue, + reason: 'loader should load OvenModule synth: ${r.error ?? r}'); + }); + + test('SynthBuilder schematic for TreeOfTwoInputModules example', () async { + final seq = List.generate(4, (_) => Logic(width: 8)); + final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); + await tree.build(); + + final synth = SynthBuilder(tree, SchematicSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + const outPath = 'build/TreeOfTwoInputModules.synth.rohd.json'; + await writeCombined(synth, tree, outPath); + final f = File(outPath); + expect(f.existsSync(), isTrue); + + // Skip loader validation for the tree as it may be deeply nested. + }); + }); + + test('Schematic dump for FIR filter example', () async { + final en = Logic(name: 'en'); + final resetB = Logic(name: 'resetB'); + final clk = SimpleClockGenerator(10).clk; + final inputVal = Logic(name: 'inputVal', width: 8); + + final fir = FirFilter(en, resetB, clk, inputVal, [0, 0, 0, 1], bitWidth: 8); + await fir.build(); + + const outPath = 'build/FirFilter.rohd.json'; + SchematicDumper(fir, + outputPath: outPath, filterConstInputsToCombinational: true); + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + + final r = await runYosysLoader(outPath); + expect(r.success, isTrue, + reason: 'loader should load FirFilter: ${r.error ?? r}'); + }); + + test('Schematic dump for LogicArray example', () async { + final arrayA = LogicArray([4], 8, name: 'arrayA'); + final id = Logic(name: 'id', width: 3); + final selectIndexValue = Logic(name: 'selectIndexValue', width: 8); + final selectFromValue = Logic(name: 'selectFromValue', width: 8); + + final la = LogicArrayExample(arrayA, id, selectIndexValue, selectFromValue); + await la.build(); + + const outPath = 'build/LogicArrayExample.rohd.json'; + SchematicDumper(la, + outputPath: outPath, filterConstInputsToCombinational: true); + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + + final r = await runYosysLoader(outPath); + expect(r.success, isTrue, + reason: 'loader should load LogicArrayExample: ${r.error ?? r}'); + }); + + test('Schematic dump for OvenModule example', () async { + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final oven = OvenModule(button, reset, clk); + await oven.build(); + + const outPath = 'build/OvenModule.rohd.json'; + SchematicDumper(oven, + outputPath: outPath, filterConstInputsToCombinational: true); + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + + final r = await runYosysLoader(outPath); + expect(r.success, isTrue, + reason: 'loader should load OvenModule: ${r.error ?? r}'); + }); + + test('Schematic dump for TreeOfTwoInputModules example', () async { + final seq = List.generate(4, (_) => Logic(width: 8)); + final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); + await tree.build(); + + const outPath = 'build/TreeOfTwoInputModules.rohd.json'; + SchematicDumper(tree, + outputPath: outPath, filterConstInputsToCombinational: true); + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + + // The loader can hit a recursion/stack overflow on deeply nested + // generated structures for the tree example. For now, ensure the + // ROHD JSON was produced and is non-empty; loader validation is + // skipped to avoid flaky failures. + // If desired, re-enable loader checks with a smaller tree size. + }); +} diff --git a/test/synth_builder_test.dart b/test/synth_builder_test.dart index 3cda6989b..9246aa86f 100644 --- a/test/synth_builder_test.dart +++ b/test/synth_builder_test.dart @@ -8,6 +8,7 @@ // Author: Yao Jing Quek import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/schematic/schematic.dart'; import 'package:test/test.dart'; class TopModule extends Module { @@ -104,4 +105,89 @@ void main() { expect(synthResults.where((e) => e.module is BModule).length, 1); }); }); + + group('schematic synthesizer', () { + test('should throw exception if module is not built', () async { + final mod = TopModule(Logic(width: 4), Logic()); + expect(() { + SynthBuilder(mod, SchematicSynthesizer()); + }, throwsA((dynamic e) => e is ModuleNotBuiltException)); + }); + + test('should generate schematic for simple module', () async { + final mod = TopModule(Logic(width: 4), Logic()); + await mod.build(); + + final synth = SynthBuilder(mod, SchematicSynthesizer()); + final results = synth.synthesisResults; + + // Should have results for TopModule, AModule, and BModule + expect(results.isNotEmpty, isTrue); + + // Check that we can get file contents + final files = synth.getSynthFileContents(); + expect(files.isNotEmpty, isTrue); + + // Each result should be a SchematicSynthesisResult + for (final result in results) { + expect(result, isA()); + final schematicResult = result as SchematicSynthesisResult; + expect(schematicResult.ports, isNotEmpty); + } + }); + + test('schematic synthesizer generates JSON output', () async { + final mod = AModule(Logic(width: 8)); + await mod.build(); + + final synth = SynthBuilder(mod, SchematicSynthesizer()); + final files = synth.getSynthFileContents(); + + expect(files.length, 1); + expect(files.first.name, contains('AModule')); + expect(files.first.name, endsWith('.rohd.json')); + expect(files.first.contents, contains('"modules"')); + expect(files.first.contents, contains('"ports"')); + }); + + test('schematic synthesizer handles module hierarchy', () async { + final mod = TopModule(Logic(width: 4), Logic()); + await mod.build(); + + final synth = SynthBuilder(mod, SchematicSynthesizer()); + final results = synth.synthesisResults; + + // Should have results for TopModule, AModule, and BModule + final moduleTypes = results.map((r) => r.module.definitionName).toSet(); + expect(moduleTypes, contains('TopModule')); + }); + + test('schematic synthesizer deduplicates identical modules', () async { + final mod = TopModule(Logic(width: 4), Logic()); + await mod.build(); + + final synth = SynthBuilder(mod, SchematicSynthesizer()); + final results = synth.synthesisResults; + + // BModule is instantiated twice with same width, should be deduplicated + final bModuleResults = + results.where((r) => r.module.definitionName == 'BModule'); + expect(bModuleResults.length, 1); + }); + + test('multi-top schematic synthesizer works', () async { + final top1 = TopModule(Logic(), Logic()); + final top2 = TopModule(Logic(width: 8), Logic()); + + await top1.build(); + await top2.build(); + + final synthBuilder = + SynthBuilder.multi([top1, top2], SchematicSynthesizer()); + final synthResults = synthBuilder.synthesisResults; + + expect(synthResults.where((e) => e.module == top1).length, 1); + expect(synthResults.where((e) => e.module == top2).length, 1); + }); + }); } From 4dc8bcfd264cde33f9cce9f828fb68c521739af2 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 18 Dec 2025 10:34:42 -0800 Subject: [PATCH 2/9] add npm to build for schematic verification --- .../schematic/yosys/yosys_loader_helper.dart | 11 ++++++++++- tool/gh_actions/install_dependencies.sh | 8 ++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart b/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart index 792371633..84d73deb9 100644 --- a/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart +++ b/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart @@ -57,7 +57,16 @@ Future runYosysLoader(String jsonPath) async { const node = 'node'; const script = 'lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs'; - final result = await Process.run(node, [script, jsonPath]); + ProcessResult result; + try { + result = await Process.run(node, [script, jsonPath]); + } on UnsupportedError catch (e) { + return YosysLoaderResult( + success: false, + error: 'Yosys loader unavailable in this environment: ${e.message}'); + } on Exception catch (e) { + return YosysLoaderResult(success: false, error: 'Process.run failed: $e'); + } if (result.exitCode == 0) { final out = result.stdout.toString().trim(); final map = json.decode(out) as Map; diff --git a/tool/gh_actions/install_dependencies.sh b/tool/gh_actions/install_dependencies.sh index 7aef34082..d4130d292 100755 --- a/tool/gh_actions/install_dependencies.sh +++ b/tool/gh_actions/install_dependencies.sh @@ -12,3 +12,11 @@ set -euo pipefail dart pub get + +# If npm is available, install the JS loader dependency used by tests. +if command -v npm >/dev/null 2>&1; then + echo "Installing Node dependencies (d3-yosys)..." + npm install d3-yosys +else + echo "npm not found; skipping Node dependency installation" +fi From f9c9a46ff72a8c56700b813693c57c5fb91101b1 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 18 Dec 2025 11:27:45 -0800 Subject: [PATCH 3/9] fixes for the yosys loader --- .../schematic/yosys/_yosys_loader_runner.mjs | 60 ++++++++++++++++++- tool/gh_actions/install_dependencies.sh | 8 --- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs b/lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs index 40354ca21..1616cd3fe 100644 --- a/lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs +++ b/lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs @@ -11,6 +11,7 @@ import fs from 'fs'; import path from 'path'; +import { fileURLToPath } from 'url'; async function main() { const args = process.argv.slice(2); @@ -20,11 +21,64 @@ async function main() { } const jsonPath = path.resolve(args[0]); try { - const yosysModulePath = path.resolve(new URL('d3-yosys/src/yosys.js', import.meta.url).pathname); - const { yosys } = await import('file://' + yosysModulePath); + // Resolve paths relative to this file to allow a local `d3-yosys` directory + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + // Find repository root by searching upwards for pubspec.yaml, fallback to a parent heuristic. + function findRepoRoot(startDir) { + let dir = startDir; + while (true) { + if (fs.existsSync(path.join(dir, 'pubspec.yaml'))) return dir; + const parent = path.dirname(dir); + if (parent === dir) return null; + dir = parent; + } + } + + const repoRoot = findRepoRoot(__dirname) || path.resolve(__dirname, '../../../../..'); + + // Prefer a local copy of d3-yosys located next to this loader (lib/.../yosys/d3-yosys). + // This matches the repository layout where d3-yosys is nested under the loader folder. + let yosysFn = null; + try { + const localRelative = path.join(__dirname, 'd3-yosys', 'src', 'yosys.js'); + if (fs.existsSync(localRelative)) { + const mod = await import('file://' + localRelative); + yosysFn = mod.yosys; + } + } catch (e) { + // ignore and try other locations + } + + // Next, try a d3-yosys directory at the repository root (legacy location). + if (!yosysFn) { + try { + const localYosys = repoRoot ? path.join(repoRoot, 'd3-yosys', 'src', 'yosys.js') : null; + if (localYosys && fs.existsSync(localYosys)) { + const mod = await import('file://' + localYosys); + yosysFn = mod.yosys; + } + } catch (e) { + // ignore and fall back + } + } + + // Finally, try resolving from installed packages (node_modules) or package specifier. + if (!yosysFn) { + try { + const yosysModulePath = path.resolve(new URL('d3-yosys/src/yosys.js', import.meta.url).pathname); + const mod = await import('file://' + yosysModulePath); + yosysFn = mod.yosys; + } catch (e) { + const mod = await import('d3-yosys/src/yosys.js'); + yosysFn = mod.yosys; + } + } + const raw = fs.readFileSync(jsonPath, 'utf8'); const yosysJson = JSON.parse(raw); - const out = yosys(yosysJson); + const out = yosysFn(yosysJson); const topChild = out.children && out.children[0] ? out.children[0] : null; const res = { success: true, diff --git a/tool/gh_actions/install_dependencies.sh b/tool/gh_actions/install_dependencies.sh index d4130d292..7aef34082 100755 --- a/tool/gh_actions/install_dependencies.sh +++ b/tool/gh_actions/install_dependencies.sh @@ -12,11 +12,3 @@ set -euo pipefail dart pub get - -# If npm is available, install the JS loader dependency used by tests. -if command -v npm >/dev/null 2>&1; then - echo "Installing Node dependencies (d3-yosys)..." - npm install d3-yosys -else - echo "npm not found; skipping Node dependency installation" -fi From 715d859ead71e0b5cc174f335151eddd2f2e6384 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 18 Dec 2025 11:34:32 -0800 Subject: [PATCH 4/9] remove catch block that is not recommended --- lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart b/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart index 84d73deb9..19682e580 100644 --- a/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart +++ b/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart @@ -60,10 +60,6 @@ Future runYosysLoader(String jsonPath) async { ProcessResult result; try { result = await Process.run(node, [script, jsonPath]); - } on UnsupportedError catch (e) { - return YosysLoaderResult( - success: false, - error: 'Yosys loader unavailable in this environment: ${e.message}'); } on Exception catch (e) { return YosysLoaderResult(success: false, error: 'Process.run failed: $e'); } From a1e859d8d881010fa295530da8f514666ec0c6f2 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 18 Dec 2025 22:52:35 -0800 Subject: [PATCH 5/9] yosys loader should be portable to node v20 --- .../schematic/passthrough_handler.dart | 2 +- .../schematic/schematic_dumper.dart | 34 ++- .../schematic/yosys/_yosys_loader_runner.mjs | 20 +- .../schematic/yosys/yosys_loader_helper.dart | 96 +------ .../schematic/yosys/yosys_loader_io.dart | 141 ++++++++++ .../schematic/yosys/yosys_loader_web.dart | 249 ++++++++++++++++++ test/schematic_example_test.dart | 189 ++++++++----- 7 files changed, 561 insertions(+), 170 deletions(-) create mode 100644 lib/src/synthesizers/schematic/yosys/yosys_loader_io.dart create mode 100644 lib/src/synthesizers/schematic/yosys/yosys_loader_web.dart diff --git a/lib/src/synthesizers/schematic/passthrough_handler.dart b/lib/src/synthesizers/schematic/passthrough_handler.dart index 1415d9970..5810a47b3 100644 --- a/lib/src/synthesizers/schematic/passthrough_handler.dart +++ b/lib/src/synthesizers/schematic/passthrough_handler.dart @@ -99,7 +99,7 @@ class PassThroughHandler { final passThroughNames = { for (final e in passThroughConnections.entries) - e.key.name: (inputsByLogic[e.value] ?? e.value.name), + e.key.name: inputsByLogic[e.value] ?? e.value.name, }; nextIdRef[0] = nextId; diff --git a/lib/src/synthesizers/schematic/schematic_dumper.dart b/lib/src/synthesizers/schematic/schematic_dumper.dart index 08e11a918..3a36e2b27 100644 --- a/lib/src/synthesizers/schematic/schematic_dumper.dart +++ b/lib/src/synthesizers/schematic/schematic_dumper.dart @@ -7,11 +7,11 @@ // 2025 December 12 // Author: Desmond Kirkpatrick +import 'dart:async' show unawaited; import 'dart:convert'; import 'dart:io'; import 'package:rohd/rohd.dart'; - import 'package:rohd/src/synthesizers/schematic/schematic.dart'; /// Helper: follow srcConnection chain to return canonical driver Logic. @@ -1020,9 +1020,35 @@ class SchematicDumper { }; // (Diagnostics removed.) - File(outPath) - ..createSync(recursive: true) - ..writeAsStringSync(const JsonEncoder.withIndent(' ').convert(out)); + final outJson = const JsonEncoder.withIndent(' ').convert(out); + try { + // Attempt normal file write (works on Dart VM). + File(outPath) + ..createSync(recursive: true) + ..writeAsStringSync(outJson); + } on Exception catch (_) { + // Running in JS platform (Node) — filesystem operations may be + // unsupported. Instead of falling back to printing the entire JSON, + // validate the generated JSON by invoking the Yosys loader in-process + // (the JS implementation will import the d3-yosys module). This keeps + // tests that run under --platform node able to validate dumps. + unawaited(runYosysLoaderFromString(outJson).then((res) { + if (!res.success) { + final msg = + 'Yosys loader validation failed for $outPath: ${res.error}' + '${res.stack != null ? '\n${res.stack}' : ''}'; + throw Exception(msg); + } + return res; + }).catchError((Object e, StackTrace? st) { + final msg = 'Yosys loader invocation failed for $outPath: $e' + '${st != null ? '\n$st' : ''}'; + throw Exception(msg); + })); + } catch (e) { + // Re-throw unexpected errors to keep behavior unchanged. + rethrow; + } } /// Synchronous accessor for the top module map. diff --git a/lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs b/lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs index 1616cd3fe..0af75e7c4 100644 --- a/lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs +++ b/lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs @@ -19,7 +19,11 @@ async function main() { console.error(JSON.stringify({success: false, error: 'missing json path'})); process.exit(2); } - const jsonPath = path.resolve(args[0]); + // Treat a single dash '-' as shorthand for stdin. Do not resolve it + // into an absolute path because path.resolve('-') would produce a + // filesystem path named '-' and break the stdin detection. + const rawArg = args[0]; + const jsonPath = rawArg && rawArg !== '-' ? path.resolve(rawArg) : null; try { // Resolve paths relative to this file to allow a local `d3-yosys` directory const __filename = fileURLToPath(import.meta.url); @@ -76,7 +80,19 @@ async function main() { } } - const raw = fs.readFileSync(jsonPath, 'utf8'); + let raw; + if (!jsonPath || jsonPath === '-') { + // Read JSON from stdin + raw = await new Promise((resolve, reject) => { + let data = ''; + process.stdin.setEncoding('utf8'); + process.stdin.on('data', chunk => data += chunk); + process.stdin.on('end', () => resolve(data)); + process.stdin.on('error', err => reject(err)); + }); + } else { + raw = fs.readFileSync(jsonPath, 'utf8'); + } const yosysJson = JSON.parse(raw); const out = yosysFn(yosysJson); const topChild = out.children && out.children[0] ? out.children[0] : null; diff --git a/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart b/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart index 19682e580..39608abaf 100644 --- a/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart +++ b/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart @@ -7,96 +7,6 @@ // 2025 December 12 // Author: Desmond Kirkpatrick -// ignore_for_file: avoid_print - -import 'dart:convert'; -import 'dart:io'; - -/// Result of running the Yosys loader. -class YosysLoaderResult { - /// Whether the load was successful. - final bool success; - - /// Number of root children modules. - final int? rootChildren; - - /// Top node ID. - final String? topNodeId; - - /// Number of top node ports. - final int? topNodePorts; - - /// Error message if unsuccessful. - final String? error; - - /// Stack trace if available. - final String? stack; - - /// Creates a [YosysLoaderResult]. - YosysLoaderResult( - {required this.success, - this.rootChildren, - this.topNodeId, - this.topNodePorts, - this.error, - this.stack}); - - /// Creates a [YosysLoaderResult] from JSON map [j]. - factory YosysLoaderResult.fromJson(Map j) => - YosysLoaderResult( - success: j['success'] as bool, - rootChildren: j['rootChildren'] as int?, - topNodeId: j['topNodeId'] as String?, - topNodePorts: j['topNodePorts'] as int?, - error: j['error'] as String?, - stack: j['stack'] as String?); -} - -/// Run the Node ESM loader on [jsonPath]. Returns a parsed [YosysLoaderResult]. -Future runYosysLoader(String jsonPath) async { - const node = 'node'; - const script = - 'lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs'; - ProcessResult result; - try { - result = await Process.run(node, [script, jsonPath]); - } on Exception catch (e) { - return YosysLoaderResult(success: false, error: 'Process.run failed: $e'); - } - if (result.exitCode == 0) { - final out = result.stdout.toString().trim(); - final map = json.decode(out) as Map; - return YosysLoaderResult.fromJson(map); - } else { - final errOut = result.stderr.toString().trim(); - try { - final map = json.decode(errOut) as Map; - return YosysLoaderResult.fromJson(map); - } on FormatException { - return YosysLoaderResult( - success: false, - error: 'node runner failed: ${result.stderr}\n${result.stdout}'); - } - } -} - -/// Main entry point for command-line testing. -Future main(List args) async { - if (args.isEmpty) { - print( - 'Usage: dart run lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart '); - exit(1); - } - final result = await runYosysLoader(args[0]); - print('success: ${result.success}'); - if (result.success) { - print('rootChildren: ${result.rootChildren}'); - print('topNodeId: ${result.topNodeId}'); - print('topNodePorts: ${result.topNodePorts}'); - } else { - print('error: ${result.error}'); - if (result.stack != null) { - print('stack:\n${result.stack}'); - } - } -} +// Conditional export: use IO implementation on VM, JS implementation on web +export 'yosys_loader_io.dart' + if (dart.library.js_interop) 'yosys_loader_web.dart'; diff --git a/lib/src/synthesizers/schematic/yosys/yosys_loader_io.dart b/lib/src/synthesizers/schematic/yosys/yosys_loader_io.dart new file mode 100644 index 000000000..d70bcbff2 --- /dev/null +++ b/lib/src/synthesizers/schematic/yosys/yosys_loader_io.dart @@ -0,0 +1,141 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// yosys_loader_io.dart +// VM implementation using Process to spawn node. + +// ignore_for_file: avoid_print + +import 'dart:convert'; +import 'dart:io'; + +/// Result of running the Yosys loader. +class YosysLoaderResult { + /// Whether the load was successful. + final bool success; + + /// Number of root children modules. + final int? rootChildren; + + /// Top node ID. + final String? topNodeId; + + /// Number of top node ports. + final int? topNodePorts; + + /// Error message if unsuccessful. + final String? error; + + /// Stack trace if available. + final String? stack; + + /// Creates a [YosysLoaderResult]. + YosysLoaderResult({ + required this.success, + this.rootChildren, + this.topNodeId, + this.topNodePorts, + this.error, + this.stack, + }); + + /// Creates a [YosysLoaderResult] from JSON map [j]. + factory YosysLoaderResult.fromJson(Map j) => + YosysLoaderResult( + success: j['success'] as bool, + rootChildren: j['rootChildren'] as int?, + topNodeId: j['topNodeId'] as String?, + topNodePorts: j['topNodePorts'] as int?, + error: j['error'] as String?, + stack: j['stack'] as String?, + ); + + @override + String toString() => success + ? 'YosysLoaderResult(success, rootChildren: $rootChildren, ' + 'topNodeId: $topNodeId, topNodePorts: $topNodePorts)' + : 'YosysLoaderResult(failed: $error)'; +} + +/// VM implementation: run loader on a file path. +Future runYosysLoader(String jsonPath) async { + const node = 'node'; + const script = + 'lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs'; + ProcessResult result; + try { + result = await Process.run(node, [script, jsonPath]); + } on Exception catch (e) { + return YosysLoaderResult(success: false, error: 'Process.run failed: $e'); + } + if (result.exitCode == 0) { + final out = result.stdout.toString().trim(); + final map = json.decode(out) as Map; + return YosysLoaderResult.fromJson(map); + } else { + final errOut = result.stderr.toString().trim(); + try { + final map = json.decode(errOut) as Map; + return YosysLoaderResult.fromJson(map); + } on FormatException { + return YosysLoaderResult( + success: false, + error: 'node runner failed: ${result.stderr}\n${result.stdout}'); + } + } +} + +/// VM implementation: spawn node process and pipe JSON via stdin. +Future runYosysLoaderFromString(String jsonString) async { + const node = 'node'; + const script = + 'lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs'; + ProcessResult result; + try { + final proc = await Process.start(node, [script, '-']); + proc.stdin.write(jsonString); + await proc.stdin.close(); + final exitCode = await proc.exitCode; + final out = await proc.stdout.transform(const Utf8Decoder()).join(); + final err = await proc.stderr.transform(const Utf8Decoder()).join(); + result = ProcessResult(proc.pid, exitCode, out, err); + } on Exception catch (e) { + return YosysLoaderResult(success: false, error: 'Process.start failed: $e'); + } + if (result.exitCode == 0) { + final out = result.stdout.toString().trim(); + final map = json.decode(out) as Map; + return YosysLoaderResult.fromJson(map); + } else { + final errOut = result.stderr.toString().trim(); + try { + final map = json.decode(errOut) as Map; + return YosysLoaderResult.fromJson(map); + } on FormatException { + return YosysLoaderResult( + success: false, + error: 'node runner failed: ${result.stderr}\n${result.stdout}'); + } + } +} + +/// Main entry point for command-line testing. +Future main(List args) async { + if (args.isEmpty) { + print( + 'Usage: dart run lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart '); + return; + } + final result = await runYosysLoader(args[0]); + print('success: ${result.success}'); + if (result.success) { + print('rootChildren: ${result.rootChildren}'); + print('topNodeId: ${result.topNodeId}'); + print('topNodePorts: ${result.topNodePorts}'); + } else { + print('error: ${result.error}'); + if (result.stack != null) { + print('stack:\n${result.stack}'); + } + } +} diff --git a/lib/src/synthesizers/schematic/yosys/yosys_loader_web.dart b/lib/src/synthesizers/schematic/yosys/yosys_loader_web.dart new file mode 100644 index 000000000..755f0384d --- /dev/null +++ b/lib/src/synthesizers/schematic/yosys/yosys_loader_web.dart @@ -0,0 +1,249 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// yosys_loader_web.dart +// JS/Node implementation using direct JS interop to call d3-yosys. + +// ignore_for_file: avoid_print + +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +/// Result of running the Yosys loader. +class YosysLoaderResult { + /// Whether the load was successful. + final bool success; + + /// Number of root children modules. + final int? rootChildren; + + /// Top node ID. + final String? topNodeId; + + /// Number of top node ports. + final int? topNodePorts; + + /// Error message if unsuccessful. + final String? error; + + /// Stack trace if available. + final String? stack; + + /// Creates a [YosysLoaderResult]. + YosysLoaderResult({ + required this.success, + this.rootChildren, + this.topNodeId, + this.topNodePorts, + this.error, + this.stack, + }); + + /// Creates a [YosysLoaderResult] from JSON map [j]. + factory YosysLoaderResult.fromJson(Map j) => + YosysLoaderResult( + success: j['success'] as bool, + rootChildren: j['rootChildren'] as int?, + topNodeId: j['topNodeId'] as String?, + topNodePorts: j['topNodePorts'] as int?, + error: j['error'] as String?, + stack: j['stack'] as String?, + ); + + @override + String toString() => success + ? 'YosysLoaderResult(success, rootChildren: $rootChildren, ' + 'topNodeId: $topNodeId, topNodePorts: $topNodePorts)' + : 'YosysLoaderResult(failed: $error)'; +} + +/// JavaScript interop to access JSON.parse. +@JS('JSON.parse') +external JSObject _jsonParse(JSString jsonString); + +/// JavaScript interop to access process.cwd(). +@JS('process.cwd') +external JSFunction get _jsCwd; + +/// Get current working directory. +String _cwd() { + final result = _jsCwd.callAsFunction(); + return (result! as JSString).toDart; +} + +/// Static interop types to describe the JS shape returned by d3-yosys. +@JS() +@staticInterop +class YosysResult {} + +/// Extensions to access properties of YosysResult. +extension YosysResultExt on YosysResult { + /// Children nodes. + external JSArray? get children; +} + +/// Static interop types to describe the JS shape returned by d3-yosys. +@JS() +@staticInterop +class YosysNode {} + +/// Extensions to access properties of YosysNode. +extension YosysNodeExt on YosysNode { + /// Node ID. + external JSString? get id; + + /// Node ports. + external JSArray? get ports; +} + +/// Load the yosys function from d3-yosys module using dynamic import. +/// Returns a Future since dynamic import is async. +Future _loadYosysFn() async { + // Build list of paths to try - using file:// URLs for ES module import + final paths = []; + + // Try with cwd prefix first (absolute path as file:// URL) + try { + final cwd = _cwd(); + paths.add( + 'file://$cwd/lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosys.js'); + } on Object catch (_) {} + + // Also try relative paths + paths.addAll([ + './lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosys.js', + '../lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosys.js', + 'lib/src/synthesizers/schematic/yosys/d3-yosys/src/yosys.js', + ]); + + for (final p in paths) { + try { + // Use importModule from dart:js_interop for ES modules + final promise = importModule(p.toJS); + final mod = await promise.toDart; + final fn = mod['yosys']; + if (fn != null) { + // Try to use as JSFunction - will throw if not callable + return fn as JSFunction; + } + } on Object catch (_) { + continue; + } + } + return null; +} + +/// JS implementation: call d3-yosys directly in the Node runtime. +Future runYosysLoaderFromString(String jsonString) async { + try { + // Load the yosys function (async for ES module import) + final yosysFn = await _loadYosysFn(); + if (yosysFn == null) { + return YosysLoaderResult( + success: false, + error: 'Could not load d3-yosys module. ' + 'Tried multiple paths but none worked.', + ); + } + + // Parse JSON string using JavaScript's JSON.parse + final jsObj = _jsonParse(jsonString.toJS); + + // Call the yosys function + final result = yosysFn.callAsFunction(null, jsObj); + if (result == null) { + return YosysLoaderResult( + success: false, + error: 'yosys function returned null', + ); + } + + // Convert JS return value to static interop type so we can access + // the expected properties with typed getters. + final resultObj = (result as JSObject) as YosysResult; + + var rootChildren = 0; + String? topNodeId; + int? topNodePorts; + + final children = resultObj.children; + if (children != null) { + final childrenList = children.toDart; + rootChildren = childrenList.length; + + if (childrenList.isNotEmpty) { + final topNode = (childrenList[0]! as JSObject) as YosysNode; + final id = topNode.id; + if (id != null) { + topNodeId = id.toDart; + } + final ports = topNode.ports; + if (ports != null) { + topNodePorts = ports.toDart.length; + } + } + } + + return YosysLoaderResult( + success: true, + rootChildren: rootChildren, + topNodeId: topNodeId, + topNodePorts: topNodePorts, + ); + } on Object catch (e, st) { + return YosysLoaderResult( + success: false, + error: 'JS yosys call failed: $e', + stack: st.toString(), + ); + } +} + +/// JavaScript interop to access Node's require function (for CommonJS modules). +@JS('require') +external JSFunction get _jsRequire; + +/// Call require with a path. +JSAny? _require(String path) => _jsRequire.callAsFunction(null, path.toJS); + +/// JS implementation: run loader on a file path. +/// In JS we read the file using Node's fs module and call d3-yosys directly. +Future runYosysLoader(String jsonPath) async { + try { + // Use Node's fs module to read the file + final fs = _require('fs'); + if (fs == null) { + return YosysLoaderResult( + success: false, + error: 'Could not load Node fs module', + ); + } + + final fsObj = fs as JSObject; + final readFileSync = fsObj['readFileSync'] as JSFunction?; + if (readFileSync == null) { + return YosysLoaderResult( + success: false, + error: 'fs.readFileSync not available', + ); + } + + final content = + readFileSync.callAsFunction(fsObj, jsonPath.toJS, 'utf8'.toJS); + if (content == null) { + return YosysLoaderResult( + success: false, + error: 'Failed to read file: $jsonPath', + ); + } + + final jsonString = (content as JSString).toDart; + return runYosysLoaderFromString(jsonString); + } on Object catch (e, st) { + return YosysLoaderResult( + success: false, + error: 'JS loader failed: $e', + stack: st.toString(), + ); + } +} diff --git a/test/schematic_example_test.dart b/test/schematic_example_test.dart index f6395bd20..a6ce6eb56 100644 --- a/test/schematic_example_test.dart +++ b/test/schematic_example_test.dart @@ -21,6 +21,42 @@ import '../example/oven_fsm.dart'; import '../example/tree.dart'; void main() { + // Detect whether running in JS (dart2js) environment. In JS many + // `dart:io` APIs are unsupported; when running tests with + // `--platform node` we skip filesystem and loader assertions. + const isJS = identical(0, 0.0); + + // Helper: write combined SynthBuilder schematic JSON to `out`. + // Returns the JSON string so callers can pass it to loaders directly. + Future writeCombinedFromSynth( + SynthBuilder synth, Module top, String out) async { + final allModules = >{}; + for (final result in synth.synthesisResults) { + if (result is SchematicSynthesisResult) { + final typeName = result.instanceTypeName; + final attrs = Map.from(result.attributes); + if (result.module == top) { + attrs['top'] = 1; + } + allModules[typeName] = { + 'attributes': attrs, + 'ports': result.ports, + 'cells': result.cells, + 'netnames': result.netnames, + }; + } + } + final combined = { + 'creator': 'SchematicSynthesizer via SynthBuilder (rohd)', + 'modules': allModules, + }; + final json = const JsonEncoder.withIndent(' ').convert(combined); + if (!isJS) { + await File(out).writeAsString(json); + } + return json; + } + test('Schematic dump for example Counter', () async { final en = Logic(name: 'en'); final reset = Logic(name: 'reset'); @@ -28,23 +64,20 @@ void main() { final counter = Counter(en, reset, clk); await counter.build(); + counter.generateSynth(); + final synth = SynthBuilder(counter, SchematicSynthesizer()); const outPath = 'build/Counter.rohd.json'; - SchematicDumper(counter, - outputPath: outPath, filterConstInputsToCombinational: true); + final json = await writeCombinedFromSynth(synth, counter, outPath); - final f = File(outPath); - expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); - final contents = await f.readAsString(); - expect(contents.trim().isNotEmpty, isTrue); - - final r = await runYosysLoader(outPath); + // Always validate the generated JSON with the yosys loader. + final r = await runYosysLoaderFromString(json); expect(r.success, isTrue, - reason: 'loader should load Counter: ${r.error ?? r}'); + reason: 'loader should load Counter from string: ${r.error ?? r}'); }); group('SynthBuilder schematic generation for examples', () { - Future writeCombined( + Future writeCombined( SynthBuilder synth, Module top, String out) async { final allModules = >{}; for (final result in synth.synthesisResults) { @@ -67,7 +100,10 @@ void main() { 'modules': allModules, }; final json = const JsonEncoder.withIndent(' ').convert(combined); - await File(out).writeAsString(json); + if (!isJS) { + await File(out).writeAsString(json); + } + return json; } test('SynthBuilder schematic for Counter', () async { @@ -82,11 +118,8 @@ void main() { expect(synth.synthesisResults.isNotEmpty, isTrue); const outPath = 'build/Counter.synth.rohd.json'; - await writeCombined(synth, counter, outPath); - final f = File(outPath); - expect(f.existsSync(), isTrue); - - final r = await runYosysLoader(outPath); + final json = await writeCombined(synth, counter, outPath); + final r = await runYosysLoaderFromString(json); expect(r.success, isTrue, reason: 'loader should load Counter synth: ${r.error ?? r}'); }); @@ -105,11 +138,8 @@ void main() { expect(synth.synthesisResults.isNotEmpty, isTrue); const outPath = 'build/FirFilter.synth.rohd.json'; - await writeCombined(synth, fir, outPath); - final f = File(outPath); - expect(f.existsSync(), isTrue); - - final r = await runYosysLoader(outPath); + final json = await writeCombined(synth, fir, outPath); + final r = await runYosysLoaderFromString(json); expect(r.success, isTrue, reason: 'loader should load FirFilter synth: ${r.error ?? r}'); }); @@ -128,11 +158,8 @@ void main() { expect(synth.synthesisResults.isNotEmpty, isTrue); const outPath = 'build/LogicArrayExample.synth.rohd.json'; - await writeCombined(synth, la, outPath); - final f = File(outPath); - expect(f.existsSync(), isTrue); - - final r = await runYosysLoader(outPath); + final json = await writeCombined(synth, la, outPath); + final r = await runYosysLoaderFromString(json); expect(r.success, isTrue, reason: 'loader should load LogicArrayExample synth: ${r.error ?? r}'); @@ -150,11 +177,8 @@ void main() { expect(synth.synthesisResults.isNotEmpty, isTrue); const outPath = 'build/OvenModule.synth.rohd.json'; - await writeCombined(synth, oven, outPath); - final f = File(outPath); - expect(f.existsSync(), isTrue); - - final r = await runYosysLoader(outPath); + final json = await writeCombined(synth, oven, outPath); + final r = await runYosysLoaderFromString(json); expect(r.success, isTrue, reason: 'loader should load OvenModule synth: ${r.error ?? r}'); }); @@ -168,9 +192,11 @@ void main() { expect(synth.synthesisResults.isNotEmpty, isTrue); const outPath = 'build/TreeOfTwoInputModules.synth.rohd.json'; - await writeCombined(synth, tree, outPath); - final f = File(outPath); - expect(f.existsSync(), isTrue); + final json = await writeCombined(synth, tree, outPath); + final r = await runYosysLoaderFromString(json); + expect(r.success, isTrue, + reason: 'loader should load TreeOfTwoInputModules synth: ' + '${r.error ?? r}'); // Skip loader validation for the tree as it may be deeply nested. }); @@ -185,17 +211,22 @@ void main() { final fir = FirFilter(en, resetB, clk, inputVal, [0, 0, 0, 1], bitWidth: 8); await fir.build(); + final synth = SynthBuilder(fir, SchematicSynthesizer()); const outPath = 'build/FirFilter.rohd.json'; - SchematicDumper(fir, - outputPath: outPath, filterConstInputsToCombinational: true); - final f = File(outPath); - expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); - final contents = await f.readAsString(); - expect(contents.trim().isNotEmpty, isTrue); - - final r = await runYosysLoader(outPath); - expect(r.success, isTrue, - reason: 'loader should load FirFilter: ${r.error ?? r}'); + final json = await writeCombinedFromSynth(synth, fir, outPath); + if (!isJS) { + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + final r = await runYosysLoader(outPath); + expect(r.success, isTrue, + reason: 'loader should load FirFilter: ${r.error ?? r}'); + } else { + final r = await runYosysLoaderFromString(json); + expect(r.success, isTrue, + reason: 'loader should load FirFilter from string: ${r.error ?? r}'); + } }); test('Schematic dump for LogicArray example', () async { @@ -207,17 +238,23 @@ void main() { final la = LogicArrayExample(arrayA, id, selectIndexValue, selectFromValue); await la.build(); + final synth = SynthBuilder(la, SchematicSynthesizer()); const outPath = 'build/LogicArrayExample.rohd.json'; - SchematicDumper(la, - outputPath: outPath, filterConstInputsToCombinational: true); - final f = File(outPath); - expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); - final contents = await f.readAsString(); - expect(contents.trim().isNotEmpty, isTrue); - - final r = await runYosysLoader(outPath); - expect(r.success, isTrue, - reason: 'loader should load LogicArrayExample: ${r.error ?? r}'); + final json = await writeCombinedFromSynth(synth, la, outPath); + if (!isJS) { + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + final r = await runYosysLoader(outPath); + expect(r.success, isTrue, + reason: 'loader should load LogicArrayExample: ${r.error ?? r}'); + } else { + final r = await runYosysLoaderFromString(json); + expect(r.success, isTrue, + reason: 'loader should load LogicArrayExample from string: ' + '${r.error ?? r}'); + } }); test('Schematic dump for OvenModule example', () async { @@ -228,17 +265,22 @@ void main() { final oven = OvenModule(button, reset, clk); await oven.build(); + final synth = SynthBuilder(oven, SchematicSynthesizer()); const outPath = 'build/OvenModule.rohd.json'; - SchematicDumper(oven, - outputPath: outPath, filterConstInputsToCombinational: true); - final f = File(outPath); - expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); - final contents = await f.readAsString(); - expect(contents.trim().isNotEmpty, isTrue); - - final r = await runYosysLoader(outPath); - expect(r.success, isTrue, - reason: 'loader should load OvenModule: ${r.error ?? r}'); + final json = await writeCombinedFromSynth(synth, oven, outPath); + if (!isJS) { + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + final r = await runYosysLoader(outPath); + expect(r.success, isTrue, + reason: 'loader should load OvenModule: ${r.error ?? r}'); + } else { + final r = await runYosysLoaderFromString(json); + expect(r.success, isTrue, + reason: 'loader should load OvenModule from string: ${r.error ?? r}'); + } }); test('Schematic dump for TreeOfTwoInputModules example', () async { @@ -246,13 +288,20 @@ void main() { final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); await tree.build(); + final synth = SynthBuilder(tree, SchematicSynthesizer()); const outPath = 'build/TreeOfTwoInputModules.rohd.json'; - SchematicDumper(tree, - outputPath: outPath, filterConstInputsToCombinational: true); - final f = File(outPath); - expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); - final contents = await f.readAsString(); - expect(contents.trim().isNotEmpty, isTrue); + final json = await writeCombinedFromSynth(synth, tree, outPath); + if (!isJS) { + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + } else { + final r = await runYosysLoaderFromString(json); + expect(r.success, isTrue, + reason: 'loader should load TreeOfTwoInputModules from string: ' + '${r.error ?? r}'); + } // The loader can hit a recursion/stack overflow on deeply nested // generated structures for the tree example. For now, ensure the From 039e4cffbf7dc500b01e10add6635f1bc25017f5 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 18 Dec 2025 23:12:38 -0800 Subject: [PATCH 6/9] remove avoid_print --- lib/src/synthesizers/schematic/yosys/yosys_loader_web.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/synthesizers/schematic/yosys/yosys_loader_web.dart b/lib/src/synthesizers/schematic/yosys/yosys_loader_web.dart index 755f0384d..665040d48 100644 --- a/lib/src/synthesizers/schematic/yosys/yosys_loader_web.dart +++ b/lib/src/synthesizers/schematic/yosys/yosys_loader_web.dart @@ -4,8 +4,6 @@ // yosys_loader_web.dart // JS/Node implementation using direct JS interop to call d3-yosys. -// ignore_for_file: avoid_print - import 'dart:js_interop'; import 'dart:js_interop_unsafe'; From 3968f5b129a3782defc32185d695986c7298231e Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 18 Dec 2025 23:29:47 -0800 Subject: [PATCH 7/9] create file --- test/schematic_example_test.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/schematic_example_test.dart b/test/schematic_example_test.dart index a6ce6eb56..06b6b864c 100644 --- a/test/schematic_example_test.dart +++ b/test/schematic_example_test.dart @@ -52,7 +52,9 @@ void main() { }; final json = const JsonEncoder.withIndent(' ').convert(combined); if (!isJS) { - await File(out).writeAsString(json); + final file = File(out); + await file.create(recursive: true); + await file.writeAsString(json); } return json; } @@ -101,7 +103,9 @@ void main() { }; final json = const JsonEncoder.withIndent(' ').convert(combined); if (!isJS) { - await File(out).writeAsString(json); + final file = File(out); + await file.create(recursive: true); + await file.writeAsString(json); } return json; } From 58782b44f4413299930eec8765f7c104cec815b9 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 19 Dec 2025 15:56:04 -0800 Subject: [PATCH 8/9] refactored to remove dumper flow and just use the Synthesizer flow --- .../schematic/constant_handler.dart | 31 +- .../synthesizers/schematic/module_map.dart | 335 ------ .../schematic/passthrough_handler.dart | 6 +- lib/src/synthesizers/schematic/schematic.dart | 2 - .../schematic/schematic_dumper.dart | 1066 ----------------- .../schematic/schematic_primitives.dart | 42 +- .../schematic/schematic_synthesis_result.dart | 265 +++- .../schematic/schematic_synthesizer.dart | 430 ++++++- lib/src/synthesizers/synth_builder.dart | 26 +- lib/src/synthesizers/synthesizer.dart | 9 +- .../systemverilog_synthesizer.dart | 4 +- test/schematic_example_test.dart | 167 +-- 12 files changed, 750 insertions(+), 1633 deletions(-) delete mode 100644 lib/src/synthesizers/schematic/module_map.dart delete mode 100644 lib/src/synthesizers/schematic/schematic_dumper.dart diff --git a/lib/src/synthesizers/schematic/constant_handler.dart b/lib/src/synthesizers/schematic/constant_handler.dart index 043113881..d54f32375 100644 --- a/lib/src/synthesizers/schematic/constant_handler.dart +++ b/lib/src/synthesizers/schematic/constant_handler.dart @@ -11,8 +11,8 @@ import 'package:rohd/rohd.dart'; -import 'package:rohd/src/synthesizers/schematic/module_map.dart'; import 'package:rohd/src/synthesizers/schematic/schematic_primitives.dart'; +import 'package:rohd/src/synthesizers/schematic/schematic_synthesis_result.dart'; bool _listEquals(List a, List b) { if (a.length != b.length) { @@ -81,15 +81,14 @@ class ConstantHandler { /// /// Parameters: /// - [module]: The module being processed - /// - [map]: The ModuleMap for this module (kept `dynamic` to avoid - /// circular type dependencies with schematic_dumper) /// - [internalNetIds]: Map to populate with constant Logic → IDs /// - [ports]: The ports map (for name collision avoidance) /// - [nextIdRef]: Reference to the next available ID (will be mutated) /// - [isTop]: Whether this is the top-level module ConstantCollectionResult collectConstants({ required Module module, - required ModuleMap map, + required List childModules, + required List childResultsList, required Map> internalNetIds, required Map> ports, required List nextIdRef, // Use list as mutable reference @@ -114,9 +113,17 @@ class ConstantHandler { return ids; } - // Process each child module - for (final childMap in map.submodules.values) { - final childModule = childMap.module; + // Process each child module (use provided child lists/fallbacks) + for (var ci = 0; ci < childModules.length; ci++) { + final childModule = childModules[ci]; + final childResult = childResultsList[ci]; + + // Derive child input list either from the authoritative child result + // (preferred) or directly from the child's declared inputs (for + // primitive/helper modules that may not produce a result). + final childInputs = (childResult != null) + ? childResult.portLogics.keys.where((l) => l.isInput).toList() + : childModule.inputs.values.toList(); // Scan primitive child's internal signals for `Const` values. // For combinational-like primitives we allocate fresh internal IDs @@ -152,12 +159,6 @@ class ConstantHandler { } // Collect constants used by child inputs. - final childInputs = []; - for (final l in childMap.portLogics.keys) { - if (l.isInput) { - childInputs.add(l); - } - } // Collect per-input `Const`s: process each input's srcConnections and // handle combinational-internalization vs per-input driver creation. for (final input in childInputs) { @@ -198,7 +199,7 @@ class ConstantHandler { .forEach((sig) { final hasScopeConsumer = sig.dstConnections.any((dst) { final pm = dst.parentModule; - return pm != null && (pm == module || map.submodules.containsKey(pm)); + return pm != null && (pm == module || childModules.contains(pm)); }); if (isTop && !hasScopeConsumer) { return; @@ -207,7 +208,7 @@ class ConstantHandler { final dsts = sig.dstConnections; final anyToCombOrSeq = dsts.any((d) { final pm = d.parentModule; - if (pm == null || !map.submodules.containsKey(pm)) { + if (pm == null || !childModules.contains(pm)) { return false; } final pmDefLower = pm.definitionName.toLowerCase(); diff --git a/lib/src/synthesizers/schematic/module_map.dart b/lib/src/synthesizers/schematic/module_map.dart deleted file mode 100644 index 7e1d63303..000000000 --- a/lib/src/synthesizers/schematic/module_map.dart +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright (C) 2025 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// -// module_map.dart -// Extracted ModuleMap, computeComponents, and listEquals helpers so -// other handlers can reference them without circular imports. -// -// 2025 December 14 -// Author: Desmond Kirkpatrick - -import 'package:rohd/rohd.dart'; -import 'package:rohd/src/synthesizers/schematic/schematic_primitives.dart'; - -/// Compute connected-component roots for `n` items given a list of union -/// operations as index pairs. Returns a list `roots` where `roots[i]` is the -/// canonical root index for element `i`. -List computeComponents(int n, Iterable> unions, - {List? priority}) { - final parent = List.generate(n, (i) => i); - // If a priority list is provided, ensure it has length n by - // padding with zeros if necessary. - var pri = priority ?? List.filled(n, 0); - if (pri.length < n) { - pri = [...pri, ...List.filled(n - pri.length, 0)]; - } - int find(int x) { - var r = x; - while (parent[r] != r) { - parent[r] = parent[parent[r]]; - r = parent[r]; - } - return r; - } - - void unite(int a, int b) { - final ra = find(a); - final rb = find(b); - if (ra == rb) { - return; - } - // Prefer the root with higher priority; on tie, pick the smaller index. - final pra = pri[ra]; - final prb = pri[rb]; - final winner = (pra > prb) ? ra : (prb > pra ? rb : (ra < rb ? ra : rb)); - final loser = (winner == ra) ? rb : ra; - parent[loser] = winner; - } - - for (final u in unions) { - if (u.length >= 2) { - unite(u[0], u[1]); - } - } - - return List.generate(n, find); -} - -/// Deep list equality (compare contents, not identity). -bool listEquals(List a, List b) { - if (a.length != b.length) { - return false; - } - for (var i = 0; i < a.length; i++) { - if (a[i] != b[i]) { - return false; - } - } - return true; -} - -/// Minimal recursive representation of a Module hierarchy. -class ModuleMap { - /// The Module this map was constructed from. - final Module _module; - - /// Public accessor for the underlying Module. - Module get module => _module; - - /// The unique name of the module this map represents. - final String uniqueName; - - /// Maps of port logics to a list of schematic bit-ids. - final Map> portLogics = {}; - - /// Maps of internal logics to a list of schematic bit-ids. - final Map> internalLogics = {}; - - /// Maps of submodule unique names to their ModuleMaps. - final Map submodules = {}; - - /// Set of Logic objects in this module that are considered "global". - /// Global Logics will not have connectivity generated for them and any - /// signals reachable from these Logics will be excluded from the - /// ModuleMap's port/internal id assignments. - final Set globalLogics = {}; - - /// Creates a ModuleMap for [module]. - ModuleMap(Module module, - {bool includeInternals = false, - bool includeChildPorts = true, - Set? globalLogics}) - : _module = module, - uniqueName = module.hasBuilt ? module.uniqueInstanceName : module.name { - var nextId = 0; - // Collect declared ports (inputs/outputs/inouts) as the logical - // port set for the module. - // Initialize globalLogics set if provided. - if (globalLogics != null) { - this.globalLogics.addAll(globalLogics); - } - - final portLogicsCandidates = [ - ...module.inputs.values, - ...module.outputs.values, - ...module.inOuts.values - ]; - - // If global logics were provided, we want to compute the transitive set - // of signals reachable from them (following dstConnections) so we can - // exclude those from connectivity/id assignment. - final reachableFromGlobals = {}; - if (this.globalLogics.isNotEmpty) { - final visitQueue = [...this.globalLogics]; - while (visitQueue.isNotEmpty) { - final cur = visitQueue.removeLast(); - if (reachableFromGlobals.contains(cur)) { - continue; - } - reachableFromGlobals.add(cur); - for (final dst in cur.dstConnections) { - if (!reachableFromGlobals.contains(dst)) { - visitQueue.add(dst); - } - } - } - } - for (final logic in portLogicsCandidates) { - // Skip any ports that are reachable from the global set; these are - // intentionally hidden and should not have connectivity generated. - if (reachableFromGlobals.contains(logic)) { - continue; - } - final ids = List.generate(logic.width, (_) => nextId++); - portLogics[logic] = ids; - } - - if (includeInternals) { - final internalSignals = [ - for (final s in module.signals) - if (!portLogics.containsKey(s)) s - ]; - for (final sig in internalSignals) { - // Skip any internal signals reachable from globals. - if (reachableFromGlobals.contains(sig)) { - continue; - } - final ids = List.generate(sig.width, (_) => nextId++); - internalLogics[sig] = ids; - } - - for (final sub in module.subModules) { - // Determine child input ports that should be considered global - // within the child. For each input port on the child, check its - // srcConnections and if any source is within our reachableFromGlobals - // set (i.e., parent-side global), mark that child input as global. - final childGlobals = {}; - if (reachableFromGlobals.isNotEmpty) { - for (final input in sub.inputs.values) { - for (final src in input.srcConnections) { - if (reachableFromGlobals.contains(src)) { - childGlobals.add(input); - break; - } - } - } - } - - submodules[sub] = ModuleMap(sub, - includeInternals: includeInternals, - includeChildPorts: includeChildPorts, - globalLogics: childGlobals.isEmpty ? null : childGlobals); - } - } - } - - /// Validates that the ModuleMap is internally consistent. - void validate() { - final logicToIds = >{} - ..addAll(portLogics) - ..addAll(internalLogics); - final allLogics = [...portLogics.keys, ...internalLogics.keys]; - for (final l in allLogics) { - if (!logicToIds.containsKey(l)) { - throw StateError('Logic $l missing ids in module $uniqueName'); - } - } - - final bitIdToMembers = >{}; - for (final e in logicToIds.entries) { - for (final bitId in e.value) { - bitIdToMembers.putIfAbsent(bitId, () => []).add(e.key); - } - } - - final signals = [...portLogics.keys, ...internalLogics.keys]; - final indexOf = {for (var i = 0; i < signals.length; i++) signals[i]: i}; - final unions = >[ - for (var i = 0; i < signals.length; i++) - for (final conn in [ - ...signals[i].srcConnections, - ...signals[i].dstConnections - ]) - if (indexOf[conn] != null) [i, indexOf[conn]!] - ]; - final roots = computeComponents(signals.length, unions); - - for (final members in bitIdToMembers.values) { - if (members.length <= 1) { - continue; - } - final root0 = roots[indexOf[members.first]!]; - for (final other in members.skip(1)) { - final rootN = roots[indexOf[other]!]; - if (root0 != rootN) { - final buf = StringBuffer() - ..writeln( - 'Members ${members.first} and $other share bit-id but are ' - 'not in same component in $uniqueName') - ..writeln('Member info:'); - for (final m in members) { - buf.writeln( - ' - $m (ids=${logicToIds[m]}, root=${roots[indexOf[m]!]}'); - } - throw StateError(buf.toString()); - } - } - } - - for (final sub in submodules.values) { - sub.validate(); - } - } - - /// Validates that the ModuleMap hierarchy is acyclic and unique. - /// - /// This implementation detects cycles by tracking `Module` instances in the - /// current ancestor chain. Using `ModuleMap` identity alone is insufficient - /// because different `ModuleMap` objects may be created for the same - /// underlying `Module` when the same module is instantiated in multiple - /// places; we want to detect the case where a module becomes a submodule of - /// itself (directly or transitively). - void validateHierarchy( - {required Map> visited, - List hierarchy = const []}) { - final newHierarchy = [...hierarchy, this]; - - // Detect cycles by module identity: if any ancestor in the hierarchy - // refers to the same Module object as `this.module`, we've created a - // recursive instantiation and must error. - if (hierarchy.any((m) => m.module == module)) { - final loop = newHierarchy.map((m) => m.uniqueName).join('.'); - throw StateError('Module $uniqueName is a submodule of itself: $loop'); - } - - // Detect if the same Module appears in more than one place in the - // hierarchy (different paths). Record the current path for this module - // so that subsequent occurrences can report both locations. - if (visited.containsKey(module)) { - final other = visited[module]!; - final otherStr = other.map((m) => m.uniqueName).join('.'); - final thisStr = hierarchy.map((m) => m.uniqueName).join('.'); - throw StateError( - 'Module $uniqueName exists at more than one hierarchy: $otherStr ' - 'and $thisStr'); - } - visited[module] = newHierarchy; - - for (final sub in submodules.values) { - sub.validateHierarchy(visited: visited, hierarchy: newHierarchy); - } - } - - /// Validates that the ModuleMap's schematic IDs are connected properly. - - List validateIdConnectivity() { - final errors = []; - final allIds = {}; - void checkIds(Logic logic, List ids, String context) { - for (final id in ids) { - if (id < 0) { - errors.add('$context: Logic "${logic.name}" has negative id $id'); - } - final existing = allIds[id]; - if (existing != null && existing != logic) { - final connected = logic.srcConnections.contains(existing) || - logic.dstConnections.contains(existing) || - existing.srcConnections.contains(logic) || - existing.dstConnections.contains(logic); - if (!connected) { - errors.add('$context: ID $id assigned to both "${logic.name}" and ' - '"${existing.name}" but they are not connected'); - } - } - allIds[id] = logic; - } - } - - for (final entry in portLogics.entries) { - checkIds(entry.key, entry.value, uniqueName); - } - for (final entry in internalLogics.entries) { - checkIds(entry.key, entry.value, '$uniqueName (internal)'); - } - for (final sub in submodules.values) { - errors.addAll(sub.validateIdConnectivity()); - } - for (final sub in submodules.values) { - var prim = - Primitives.instance.lookupByDefinitionName(sub.module.definitionName); - if (prim == null && sub.submodules.isEmpty) { - prim = Primitives.instance.lookupForModule(sub.module); - } - if (prim == null) { - continue; - } - for (final inLogic in sub.module.inputs.values) { - if (inLogic.srcConnections.isEmpty) { - errors.add('$uniqueName: Primitive ${sub.uniqueName} input ' - '"${inLogic.name}" has no driver'); - } - } - } - return errors; - } -} diff --git a/lib/src/synthesizers/schematic/passthrough_handler.dart b/lib/src/synthesizers/schematic/passthrough_handler.dart index 5810a47b3..a2f92519a 100644 --- a/lib/src/synthesizers/schematic/passthrough_handler.dart +++ b/lib/src/synthesizers/schematic/passthrough_handler.dart @@ -10,6 +10,7 @@ // Author: Desmond Kirkpatrick import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/schematic/schematic_synthesis_result.dart'; /// Result of pass-through collection. class PassThroughResult { @@ -34,9 +35,10 @@ class PassThroughHandler { /// nets. PassThroughResult collectPassThroughs({ required Module module, - required dynamic map, + required List childModules, + required List childResultsList, required Map> internalNetIds, - required Map ports, + required Map> ports, required List nextIdRef, }) { final passThroughConnections = {}; diff --git a/lib/src/synthesizers/schematic/schematic.dart b/lib/src/synthesizers/schematic/schematic.dart index 88920e3e6..59a114414 100644 --- a/lib/src/synthesizers/schematic/schematic.dart +++ b/lib/src/synthesizers/schematic/schematic.dart @@ -2,10 +2,8 @@ // SPDX-License-Identifier: BSD-3-Clause export 'constant_handler.dart'; -export 'module_map.dart'; export 'module_utils.dart'; export 'passthrough_handler.dart'; -export 'schematic_dumper.dart'; export 'schematic_mixins.dart'; export 'schematic_primitives.dart'; export 'schematic_synthesis_result.dart'; diff --git a/lib/src/synthesizers/schematic/schematic_dumper.dart b/lib/src/synthesizers/schematic/schematic_dumper.dart deleted file mode 100644 index 3a36e2b27..000000000 --- a/lib/src/synthesizers/schematic/schematic_dumper.dart +++ /dev/null @@ -1,1066 +0,0 @@ -// Copyright (C) 2025 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// -// schematic_dumper.dart -// Schematic dumping into ELK-JSON Yosys format. - -// 2025 December 12 -// Author: Desmond Kirkpatrick - -import 'dart:async' show unawaited; -import 'dart:convert'; -import 'dart:io'; - -import 'package:rohd/rohd.dart'; -import 'package:rohd/src/synthesizers/schematic/schematic.dart'; - -/// Helper: follow srcConnection chain to return canonical driver Logic. -Logic getCanonicalLogic(Logic logic) { - var cur = logic; - final visited = {}; - while (cur.srcConnection != null && !visited.contains(cur.hashCode)) { - visited.add(cur.hashCode); - cur = cur.srcConnection!; - } - return cur; -} - -/// Lightweight schematic dumper similar in intent to WaveDumper. -class SchematicDumper { - /// The top-level module provided to the dumper. - final Module topModule; - - /// The constructed module map for the top module. - final ModuleMap topMap; - - /// Optional output path (mirrors WaveDumper's `outputPath` argument). - final String? outputPath; - - /// Whether to filter out input ports driven only by constants. - - final bool filterConstInputsToCombinational; - - /// Construct a `SchematicDumper` directly from [module]. The - /// [includeInternals] flag controls whether internal signals and submodules - /// are mapped. - SchematicDumper(Module module, - {bool includeInternals = true, - this.outputPath, - this.filterConstInputsToCombinational = false, - List? globalPortNames}) - : topModule = module, - topMap = (() { - // Build an initial set of global Logics based on explicit top-level - // port names. - final gnames = globalPortNames ?? []; - final globals = {}; - if (gnames.isNotEmpty) { - globals - .addAll(gnames.map((n) => module.ports[n]).whereType()); - if (globals.isEmpty) { - throw StateError( - 'No top-level ports found matching globalPortNames $gnames. ' - 'Ensure the top module declares ports with these names or ' - 'pass appropriate Logic ports to the dumper.'); - } - } - - return ModuleMap(module, - includeInternals: includeInternals, - globalLogics: globals.isEmpty ? null : globals); - })() { - if (outputPath != null) { - // Synchronous export: require the module to already be built. - if (!module.hasBuilt) { - throw StateError('Top module must be built before constructor export'); - } - final out = outputPath!; - _exportYosysJson(out); - } - } - - /// Private implementation that writes a Yosys-style JSON file to [outPath]. - /// Synchronous version: callers must ensure `topModule.hasBuilt` is true - /// before calling this method. Throws if validation fails. - void _exportYosysJson(String outPath) { - // Validate the ModuleMap hierarchy before exporting to catch issues - // early and provide a clear error message rather than producing - // malformed JSON that can crash downstream tools. - try { - topMap - ..validateHierarchy(visited: >{}) - ..validate(); - final idErrors = topMap.validateIdConnectivity(); - if (idErrors.isNotEmpty) { - final buf = StringBuffer()..writeln('ID connectivity errors:'); - for (final e in idErrors) { - buf.writeln(' - $e'); - } - throw Exception(buf.toString()); - } - } catch (e) { - throw StateError('ModuleMap validation failed before export: $e'); - } - - final modulesOut = >{}; - - // Use the module's `definitionName` as the stable type key for module - // definitions and for cell `type` fields so the output matches Yosys - // semantics where `type` is the module's definition name. - - Map buildModuleEntryHierarchy(ModuleMap map, - {bool isTop = false}) { - final module = map.module; - - // Emit ports (names + directions) but do not attempt to compute bits - // or connections yet. Combine input/output/inout emission. - final ports = >{}; - void addPorts(Map map, String dir) { - for (final p in map.entries) { - ports[p.key] = {'direction': dir, 'bits': []}; - } - } - - addPorts(module.inputs, 'input'); - addPorts(module.outputs, 'output'); - addPorts(module.inOuts, 'inout'); - - // Optionally remove input ports for combinational-like module - // definitions when those inputs are driven only by Const sources. - if (filterConstInputsToCombinational && - module.definitionName == 'Combinational') { - module.inputs.forEach((pname, logic) { - if (logic.srcConnections.isNotEmpty && - logic.srcConnections.every((s) => s is Const)) { - ports.remove(pname); - } - }); - } - - // --- STEP 1: Assign IDs to internal nets (child outputs + constants) --- - // - // Symmetry principle for understanding the data flow: - // Module inputs ↔ Child outputs (both are PRODUCERS in module scope) - // Module outputs ↔ Child inputs (both are CONSUMERS in module scope) - // - // Producers get fresh IDs allocated here. Consumers look up IDs from - // their sources via union-find. - // - // This map covers: - // - Child outputs (producers of internal nets) - get fresh IDs - // - Constants - get fresh IDs - // - (Module ports are in map.portLogics, also producers) - final internalNetIds = >{}; - - // Find the next available ID (after all port IDs) - final maxPortId = map.portLogics.values - .expand((ids) => ids) - .whereType() - .fold(-1, (m, id) => id > m ? id : m); - var nextId = maxPortId + 1; - - // Assign IDs to each child's output ports - for (final childMap in map.submodules.values) { - final childModule = childMap.module; - for (final output in childModule.outputs.values) { - // If this child port is marked global (the child's ModuleMap - // indicates it), skip allocating internal net IDs so no - // connectivity/netnames are generated for it. - if (childMap.globalLogics.contains(output)) { - continue; - } - final ids = List.generate(output.width, (_) => nextId++); - internalNetIds[output] = ids; - } - } - - // Collect constants using the ConstantHandler - final nextIdRef = [nextId]; - final constHandler = ConstantHandler(); - final constResult = constHandler.collectConstants( - module: module, - map: map, - internalNetIds: internalNetIds, - ports: ports, - nextIdRef: nextIdRef, - isTop: isTop, - filterConstInputsToCombinational: filterConstInputsToCombinational, - ); - nextId = nextIdRef[0]; - - // Collect pass-through connections and allocate synthetic nets - final passHandler = PassThroughHandler(); - final passResult = passHandler.collectPassThroughs( - module: module, - map: map, - internalNetIds: internalNetIds, - ports: ports, - nextIdRef: nextIdRef, - ); - final passThroughConnections = passResult.passThroughConnections; - final syntheticNetsFromPass = passResult.syntheticNets; - nextId = nextIdRef[0]; - - // Merge synthetic nets from pass-through handler into local map used - // later when emitting netnames and connections. - final syntheticNets = >{}; - for (final e in syntheticNetsFromPass.entries) { - syntheticNets[e.key] = e.value; - } - - // --- STEP 2: Collect transitive closure of intermediate Logics --- - // Starting from child inputs, trace srcConnections to find all - // intermediate Logics that connect to sources (module ports, child - // outputs, or constants). This keeps the scope per-module. - final intermediateLogics = {}; - void collectIntermediates(Logic logic, Set visited) { - if (!visited.add(logic)) { - return; - } - // Skip if already in our ID maps (ports or child outputs) - if (map.portLogics.containsKey(logic) || - internalNetIds.containsKey(logic)) { - return; - } - // Add as intermediate - intermediateLogics.add(logic); - // Continue tracing - for (final src in logic.srcConnections) { - collectIntermediates(src, visited); - } - } - - // Trace from each child input's srcConnections - for (final childMap in map.submodules.values) { - for (final input in childMap.module.inputs.values) { - final visited = {}; - for (final src in input.srcConnections) { - collectIntermediates(src, visited); - } - } - } - - // Also trace from non-LogicStructure module output srcConnections. - // This finds intermediate Logics between child outputs and module - // outputs. We skip LogicStructure outputs because their IDs get - // replaced later by element-based resolution. - for (final portLogic in map.portLogics.keys) { - if (module.outputs.values.contains(portLogic) && - portLogic is! LogicStructure) { - final visited = {}; - for (final src in portLogic.srcConnections) { - collectIntermediates(src, visited); - } - } - } - - // --- STEP 3: Build union-find on all Logics --- - // Collect all Logics: module ports + child outputs + intermediates - // Note: We intentionally exclude map.internalLogics to avoid using - // internal signal IDs that may be replaced later (e.g., LogicStructure - // port IDs get replaced by element-based IDs). - final allLogics = [ - ...map.portLogics.keys, - ...internalNetIds.keys, - ...intermediateLogics, - ]; - final logicIndex = { - for (var i = 0; i < allLogics.length; i++) allLogics[i]: i - }; - - // Build union pairs from srcConnections/dstConnections - final cellUnions = >[]; - for (var i = 0; i < allLogics.length; i++) { - final logic = allLogics[i]; - for (final conn in [...logic.srcConnections, ...logic.dstConnections]) { - final j = logicIndex[conn]; - if (j != null) { - cellUnions.add([i, j]); - } - } - } - - // Compute connected components - final cellRoots = computeComponents(allLogics.length, cellUnions); - - // Build root -> canonical IDs mapping. We explicitly prioritize: - // 1. Module ports (top-level declared Logics with names) - // 2. Child outputs (internal nets between cells) - // This ensures module-level signals are used as canonical IDs for their - // connected components, not intermediate or internal logics. - final rootToIds = >{}; - - // First pass: set canonical IDs from module ports only - for (final portLogic in map.portLogics.keys) { - final idx = logicIndex[portLogic]; - if (idx == null) { - continue; - } - final root = cellRoots[idx]; - final ids = map.portLogics[portLogic]; - if (ids != null && ids.isNotEmpty) { - rootToIds.putIfAbsent(root, () => ids); - } - } - - // Second pass: fill in remaining roots from child outputs - for (final childOutput in internalNetIds.keys) { - final idx = logicIndex[childOutput]; - if (idx == null) { - continue; - } - final root = cellRoots[idx]; - if (!rootToIds.containsKey(root)) { - final ids = internalNetIds[childOutput]; - if (ids != null && ids.isNotEmpty) { - rootToIds[root] = ids; - } - } - } - - // If the PassThroughHandler allocated synthetic internal IDs for a - // module output that is part of a connected component, prefer those - // synthetic IDs as the canonical IDs for the component so that the - // pass-through buffer's Y IDs are used by netnames and consumers. - for (final e in passThroughConnections.entries) { - final outLogic = e.key; - final idx = logicIndex[outLogic]; - if (idx == null) { - continue; - } - final root = cellRoots[idx]; - final synth = internalNetIds[outLogic]; - if (synth != null && synth.isNotEmpty) { - rootToIds[root] = synth; - } - } - - // --- STEP 4: Helper to get IDs for any child port (now a simple lookup) - // --- - List idsForChildLogic(Logic childLogic) { - List tryFromRootOrMaps(Logic l) { - final idx = logicIndex[l]; - if (idx != null) { - return rootToIds[cellRoots[idx]] ?? - map.portLogics[l] ?? - internalNetIds[l] ?? - []; - } - return map.portLogics[l] ?? internalNetIds[l] ?? []; - } - - // Directly assigned internal net IDs (child outputs / constants) - if (internalNetIds.containsKey(childLogic)) { - final idx = logicIndex[childLogic]; - if (idx != null) { - return rootToIds[cellRoots[idx]] ?? internalNetIds[childLogic]!; - } - return internalNetIds[childLogic]!; - } - - // Check immediate parent sources first (for child inputs) - for (final src in childLogic.srcConnections) { - final ids = tryFromRootOrMaps(src); - if (ids.isNotEmpty) { - return ids; - } - } - - // Then check downstream destinations (for child outputs) - for (final dst in childLogic.dstConnections) { - final ids = tryFromRootOrMaps(dst); - if (ids.isNotEmpty) { - return ids; - } - } - - return []; - } - - // Emit cells with type and connections - final cells = >{}; - - // Track next available internal net ID for synthetic wires - // Compute max ID from all assigned IDs (port IDs and internal net IDs) - var nextInternalNetId = 0; - for (final ids in map.portLogics.values) { - for (final id in ids) { - if (id >= nextInternalNetId) { - nextInternalNetId = id + 1; - } - } - } - for (final ids in internalNetIds.values) { - for (final id in ids) { - if (id is int && id >= nextInternalNetId) { - nextInternalNetId = id + 1; - } - } - } - - for (final childMap in map.submodules.values) { - final childModule = childMap.module; - final cellKey = childModule.hasBuilt - ? childModule.uniqueInstanceName - : childModule.name; // instance name (cell key) — keep as-is - // Default cell type is the child module's definition name. - final cellType = childModule.definitionName; - final parameters = {}; - - // Delegate Sequential handling to the refactored SequentialHandler - final seqHandler = SequentialHandler(); - final handled = seqHandler.handleSequential( - childModule: childModule, - ports: childModule.ports, - internalNetIds: internalNetIds, - idsForChildLogic: idsForChildLogic, - cells: cells, - syntheticNets: syntheticNets, - nextInternalNetIdGetter: () => nextInternalNetId, - nextInternalNetIdSetter: (v) => nextInternalNetId = v, - ); - if (handled) { - continue; - } - - // Try exact definitionName mapping first (map true helper modules - // like 'Swizzle'/'BusSubset' even if they contain small internals). - // For other cases, only apply the flexible lookup for leaf modules - // to avoid accidentally matching composite modules like Adders. - var prim = Primitives.instance - .lookupByDefinitionName(childModule.definitionName); - if (prim == null && childMap.submodules.isEmpty) { - prim = Primitives.instance.lookupForModule(childModule); - } - - if (prim != null) { - // For primitives with useRawPortNames (like Add/Combinational), use the - // actual ROHD port names directly instead of generic A/B/Y names - if (prim.useRawPortNames) { - final connMap = >{}; - final portDirs = { - for (final e in childModule.ports.entries) - e.value.name: e.value.isInput - ? 'input' - : e.value.isOutput - ? 'output' - : 'inout' - }; - childModule.ports.forEach((_, logic) { - final ids = idsForChildLogic(logic); - if (ids.isNotEmpty) { - connMap[logic.name] = ids; - } - }); - - // Connectivity check: ensure not all outputs are floating. - if (childModule.outputs.isNotEmpty) { - final anyOutputDrives = childModule.outputs.values - .any((logic) => idsForChildLogic(logic).isNotEmpty); - if (!anyOutputDrives) { - throw StateError( - 'Submodule ${childModule.uniqueInstanceName} has outputs ' - 'but none drive any nets'); - } - } - - // Optionally remove ports that are driven only by constants for - // combinational-like primitives when the dumper option requests - // const-input filtering. - if (filterConstInputsToCombinational) { - if (childModule.definitionName == 'Combinational') { - connMap.removeWhere((pname, ids) => - ids.isNotEmpty && - ids.whereType().isNotEmpty && - ids - .whereType() - .every(constResult.blockedIds.contains)); - portDirs.removeWhere((k, _) => !connMap.containsKey(k)); - } - } - - cells[cellKey] = { - 'hide_name': 0, - 'type': prim.primitiveName, - 'parameters': {'CLK_POLARITY': 1}, - 'attributes': {}, - 'port_directions': portDirs, - 'connections': connMap, - }; - continue; - } - - final primCell = - Primitives.instance.computePrimitiveCell(childModule, prim); - - final portDirs = Map.from( - (primCell['port_directions']! as Map).cast()); - - // Build the primitive connection map using the centralized helper - // in the Primitives registry. Provide a small adapter to lookup - // ROHD-port ids from this dumper's `idsForChildLogic` helper. - final connMap = Primitives.instance - .buildPrimitiveConnectionsWithChildLogicLookup( - childModule, - prim, - (primCell['parameters']! as Map).cast(), - portDirs, - (m) => map.submodules[m], - idsForChildLogic); - - // Connectivity check: ensure not all outputs are floating. - if (childModule.outputs.isNotEmpty) { - final anyOutputDrives = childModule.outputs.values - .any((logic) => idsForChildLogic(logic).isNotEmpty); - if (!anyOutputDrives) { - throw StateError( - 'Submodule ${childModule.uniqueInstanceName} has outputs ' - 'but none drive any nets'); - } - } - - // Optionally remove const-only ports for combinational primitives - // when requested. Only apply this filtering for modules whose - // definitionName is exactly 'Combinational' to avoid affecting - // comparators and other primitives. - if (filterConstInputsToCombinational) { - if (childModule.definitionName == 'Combinational') { - connMap.removeWhere((pname, ids) => - ids.isNotEmpty && - ids.whereType().isNotEmpty && - ids.whereType().every(constResult.blockedIds.contains)); - portDirs.removeWhere((k, _) => !connMap.containsKey(k)); - } - } - - cells[cellKey] = { - 'hide_name': 0, - 'type': primCell['type'], - 'parameters': primCell['parameters'], - 'attributes': {}, - 'port_directions': portDirs, - 'connections': connMap, - }; - continue; - } - - final connMap = >{}; - final portDirs = { - for (final e in childModule.ports.entries) - e.key: e.value.isInput - ? 'input' - : e.value.isOutput - ? 'output' - : 'inout' - }; - childModule.ports.forEach((pname, logic) { - final ids = idsForChildLogic(logic); - if (ids.isNotEmpty) { - connMap[pname] = ids.cast(); - } - }); - - // Connectivity check: ensure not all outputs are floating. - if (childModule.outputs.isNotEmpty) { - final anyOutputDrives = childModule.outputs.values - .any((logic) => idsForChildLogic(logic).isNotEmpty); - if (!anyOutputDrives) { - throw StateError( - 'Submodule ${childModule.uniqueInstanceName} has outputs but ' - 'none drive any nets'); - } - } - - cells[cellKey] = { - 'hide_name': 0, - 'type': cellType, - 'parameters': parameters, - 'attributes': {}, - 'port_directions': portDirs, - 'connections': connMap, - }; - } - - final attr = {'src': 'generated'}; - if (isTop) { - attr['top'] = 1; - } - - // Compute bit-id -> Logic map for this module (for netnames and port - // bits) Since each Logic now has multiple bit-ids, we map each bit-id to - // its Logic. - final bitIdToLogic = {}; - for (final e in map.portLogics.entries) { - for (final bitId in e.value) { - bitIdToLogic[bitId] = e.key; - } - } - for (final e in map.internalLogics.entries) { - for (final bitId in e.value) { - bitIdToLogic[bitId] = e.key; - } - } - // Also add child output IDs to the bit->Logic mapping so they appear in - // netnames. Only numeric bit-ids can be keys in `bitIdToLogic`; string - // tokens representing constant bit values are not added here and will - // instead be included directly in netname bit lists when appropriate. - for (final e in internalNetIds.entries) { - for (final bitId in e.value) { - if (bitId is int) { - bitIdToLogic[bitId] = e.key; - } - } - } - - // Build connectivity across module.signals and compute components - final signals = List.from(module.signals); - final indexOf = {for (var i = 0; i < signals.length; i++) signals[i]: i}; - final unions = >[]; - for (var i = 0; i < signals.length; i++) { - final s = signals[i]; - for (final conn in [...s.srcConnections, ...s.dstConnections]) { - final j = indexOf[conn]; - if (j != null) { - unions.add([i, j]); - } - } - } - final roots = computeComponents(signals.length, unions); - - // Group bit-ids by component root (using Logic -> root mapping) - final compToIds = >{}; - for (final entry in bitIdToLogic.entries) { - final bitId = entry.key; - final logic = entry.value; - final idx = indexOf[logic]; - if (idx == null) { - continue; - } - final root = roots[idx]; - compToIds.putIfAbsent(root, () => []).add(bitId); - } - - // Fill ports.bits and build a mapping from component root -> preferred - // net name (prefer port names). We'll then ensure every component has - // a netname entry so cell `connections` ids always reference a net. - // - // Each port keeps its own unique IDs. For pass-through connections - // (output directly connected to input), we'll add explicit buffer cells - // in the cells section to show the internal wiring. - final rootToPreferred = {}; - final rootToCanonicalIds = >{}; - - // passThroughConnections provided by PassThroughHandler - - // First pass: process INPUT ports to establish canonical IDs - for (final entry in ports.entries) { - final pname = entry.key; - final pdata = entry.value; - final direction = pdata['direction'] as String?; - if (direction != 'input') { - continue; - } - - final logic = map.module.ports[pname]; - if (logic == null) { - continue; - } - - // Input port bit lists are guaranteed to be List from - // ModuleMap.portLogics. Use them directly. - final portBitIds = List.from(map.portLogics[logic] ?? []); - entry.value['bits'] = portBitIds; - - final idx = indexOf[logic]; - if (idx != null && portBitIds.isNotEmpty) { - final root = roots[idx]; - rootToPreferred.putIfAbsent(root, () => pname); - rootToCanonicalIds.putIfAbsent(root, () => portBitIds); - } - } - - // Second pass: process OUTPUT ports with their own IDs. Prefer - // synthetic/internal IDs allocated for pass-throughs when present. - for (final entry in ports.entries) { - final pname = entry.key; - final pdata = entry.value; - final direction = pdata['direction'] as String?; - if (direction != 'output') { - continue; - } - - final logic = map.module.ports[pname]; - if (logic == null) { - continue; - } - - // Normalize to List (port bits must be integers). Prefer - // synthetic IDs from PassThroughHandler when available; otherwise - // use ModuleMap-assigned port bits. - final portBitIds = passThroughConnections.containsKey(logic) - ? (internalNetIds[logic]?.whereType().toList() ?? - (map.portLogics[logic] ?? [])) - : (map.portLogics[logic] ?? []); - - entry.value['bits'] = portBitIds; - - final idx = indexOf[logic]; - if (idx != null && portBitIds.isNotEmpty) { - final root = roots[idx]; - rootToPreferred.putIfAbsent(root, () => pname); - rootToCanonicalIds.putIfAbsent(root, () => portBitIds); - } - } - - // pass-through detection/allocation handled by PassThroughHandler - - // Third pass: handle inout ports - for (final entry in ports.entries) { - final pname = entry.key; - final pdata = entry.value; - final direction = pdata['direction'] as String?; - if (direction == 'input' || direction == 'output') { - continue; - } - - final logic = map.module.ports[pname]; - if (logic == null) { - continue; - } - - final portBitIds = map.portLogics[logic] ?? []; - entry.value['bits'] = portBitIds; - - final idx = indexOf[logic]; - if (idx != null && portBitIds.isNotEmpty) { - final root = roots[idx]; - rootToPreferred.putIfAbsent(root, () => pname); - rootToCanonicalIds.putIfAbsent(root, () => portBitIds); - } - } - - // Add named internal Logic signals to rootToPreferred. Priority is lower - // than parent ports but higher than child port names. Iterate over all - // signals from module.signals (already in `signals` list) and add named - // ones that aren't ports. - final portLogicsSet = map.portLogics.keys.toSet(); - signals.asMap().entries.where((e) { - final logic = e.value; - return !portLogicsSet.contains(logic) && - logic.naming != Naming.unnamed && - !Naming.isUnpreferred(logic.name); - }).forEach( - (e) => rootToPreferred.putIfAbsent(roots[e.key], () => e.value.name)); - - // Add buffer cells for pass-through connections (input → output) - // This makes the internal wiring visible in the schematic. Use the - // `passThroughNames` map returned by the handler to avoid scanning - // the module's input/output maps here. - for (final e in passThroughConnections.entries) { - final out = e.key; - final inn = e.value; - final outName = out.name; - final inName = passResult.passThroughNames[outName] ?? inn.name; - final inIds = map.portLogics[inn] ?? []; - final outIds = internalNetIds[out] ?? map.portLogics[out] ?? []; - if (inIds.isEmpty || outIds.isEmpty) { - continue; - } - cells['passthrough_${inName}_to_$outName'] = { - 'hide_name': 0, - 'type': r'$buf', - 'parameters': {'WIDTH': inn.width}, - 'attributes': {}, - 'port_directions': {'A': 'input', 'Y': 'output'}, - 'connections': >{'A': inIds, 'Y': outIds}, - }; - } - - final netnames = {}; - - List uniquePreserve(Iterable items) { - final seen = {}; - return items.where(seen.add).toList(); - } - - compToIds.forEach((root, ids) { - final name = rootToPreferred[root] ?? 'net_$root'; - final existing = netnames.putIfAbsent( - name, () => {'bits': ids, 'attributes': {}})! - as Map; - final existingBits = (existing['bits']! as List).cast(); - existing['bits'] = uniquePreserve([...existingBits, ...ids]); - }); - - // Add child output IDs as netnames (internal wires between cells) - // These IDs connect child outputs to child inputs. - // Priority: parent port names > named() Logic names > child port names - // Only add child-derived netnames if the IDs are not already covered - // by a higher-priority netname. - // - // Build a set of IDs already covered by existing netnames (from - // union-find preferred names which include parent ports and named - // Logics). - final coveredIds = {}; - for (final nn in netnames.values) { - final bits = (nn! as Map)['bits'] as List?; - if (bits != null) { - for (final b in bits) { - if (b is int) { - coveredIds.add(b); - } - } - } - } - - // Add child output IDs as netnames unless already covered by higher- - // priority netnames. Prefer a connected parent signal name when one - // exists and is meaningful; otherwise fall back to "_". - for (final entry in internalNetIds.entries) { - final outputLogic = entry.key; - final ids = entry.value; - final intIds = ids.whereType().toList(); - if (intIds.isNotEmpty && intIds.every(coveredIds.contains)) { - continue; - } - - final preferredName = - outputLogic.dstConnections.cast().firstWhere((l) { - if (l == null) { - return false; - } - final idx = indexOf[l]; - return idx != null && - l.naming != Naming.unnamed && - !Naming.isUnpreferred(l.name); - }, orElse: () => null)?.name; - - final netName = (preferredName != null && - !netnames.containsKey(preferredName)) - ? preferredName - : '${outputLogic.parentModule?.uniqueInstanceName ?? 'unknown'}_' - '${outputLogic.name}'; - - if (!netnames.containsKey(netName)) { - netnames[netName] = { - 'bits': ids, - 'attributes': {} - }; - intIds.forEach(coveredIds.add); - } - } - - // Merge synthetic nets created during Sequential expansion when absent - syntheticNets.forEach((name, ids) { - netnames.putIfAbsent( - name, () => {'bits': ids, 'attributes': {}}); - }); - - // Attempt element-based resolution for LogicStructure module outputs. - // If a module output is a LogicStructure, try to build its bitlist from - // child outputs that are structures or from direct element producers. - for (final outEntry in module.outputs.entries) { - final outName = outEntry.key; - final outLogic = outEntry.value; - if (outLogic is! LogicStructure) { - continue; - } - - final struct = outLogic; - final combined = []; - var allFound = true; - - List? findElemIds(Logic elem) { - // Direct child output mapping - if (internalNetIds.containsKey(elem)) { - return internalNetIds[elem]; - } - - // Direct canonical match (same canonical logic) - for (final e in internalNetIds.entries) { - if (identical(getCanonicalLogic(e.key), getCanonicalLogic(elem))) { - return e.value; - } - } - - // If a child exported a full LogicStructure, slice its ids - for (final e in internalNetIds.entries) { - if (e.key is! LogicStructure) { - continue; - } - final childStruct = e.key as LogicStructure; - // Find the index of the matching child element and compute its - // bit offset by summing widths of preceding elements. - final idx = childStruct.elements.indexWhere((childElem) => - identical(childElem, elem) || - identical( - getCanonicalLogic(childElem), getCanonicalLogic(elem))); - if (idx == -1) { - continue; - } - final bitOffset = childStruct.elements - .take(idx) - .fold(0, (s, el) => s + el.width); - final childElemWidth = childStruct.elements[idx].width; - final ids = e.value; - if (ids.length >= bitOffset + childElemWidth) { - final elemIds = - ids.sublist(bitOffset, bitOffset + childElemWidth); - internalNetIds[elem] = List.from(elemIds); - return elemIds; - } - } - - // Fall back: check module-level port or internal mapping - return map.portLogics[elem] ?? map.internalLogics[elem]; - } - - final elemLists = struct.elements.map(findElemIds).toList(); - if (elemLists.any((l) => l == null)) { - allFound = false; - } else { - combined.addAll(elemLists.expand((l) => l!)); - } - - if (allFound && combined.length == struct.width) { - // Replace the port bits and create a netname so viewers show - // the module output connected to upstream producers. - ports[outName] = { - 'direction': 'output', - 'bits': List.from(combined), - }; - netnames[outName] = { - 'bits': List.from(combined), - 'attributes': {}, - }; - } - } - - // Add const netnames (per-input and pattern-level) using ConstantHandler - // so constant handling logic is consolidated in one place. - constHandler.emitConstNetnames( - constResult: constResult, - netnames: netnames, - ); - // Create $const driver cells using the ConstantHandler - final referencedIds = { - ...ports.values - .expand((p) => (p['bits']! as List).cast()), - ...cells.values - .where((c) => c['type'] != r'$const') - .map((c) => c['connections'] as Map?) - .where((conns) => conns != null) - .expand((conns) => conns!.values - .whereType>() - .expand((l) => l) - .whereType()), - }; - - constHandler.emitConstCells( - constResult: constResult, - cells: cells, - referencedIds: referencedIds, - ); - - return { - 'attributes': attr, - 'ports': ports, - 'cells': cells, - 'netnames': netnames, - }; - } - - // Walk module tree and emit hierarchy entries. Use the module's type - // name (`module.name`) as the key so all instances of the same type - // share the same module definition (Yosys-style). - // Skip modules that are primitives - they don't need module definitions. - void walkHierarchy(ModuleMap map, {bool isTop = false}) { - final typeName = map.module.definitionName; - - // Check if this module is a primitive - if so, don't emit a module - // definition. - final prim = Primitives.instance.lookupForModule(map.module); - if (prim != null) { - // Primitives don't get module definitions - they're handled by - // port_directions in cells. - return; - } - - if (!modulesOut.containsKey(typeName)) { - modulesOut[typeName] = buildModuleEntryHierarchy(map, isTop: isTop); - } else { - // Merge ports from this instance into the existing module definition - // This handles cases where different instances have different optional - // ports. - final existing = modulesOut[typeName]!; - final existingPorts = existing['ports'] as Map? ?? {}; - final module = map.module; - - for (final name in module.inputs.keys) { - existingPorts.putIfAbsent( - name, () => {'direction': 'input', 'bits': []}); - } - for (final name in module.outputs.keys) { - existingPorts.putIfAbsent( - name, () => {'direction': 'output', 'bits': []}); - } - for (final name in module.inOuts.keys) { - existingPorts.putIfAbsent( - name, () => {'direction': 'inout', 'bits': []}); - } - } - map.submodules.values.forEach(walkHierarchy); - } - - walkHierarchy(topMap, isTop: true); - - final out = { - 'creator': 'SchematicDumper (rohd_hcl)', - 'modules': modulesOut - }; - - // (Diagnostics removed.) - final outJson = const JsonEncoder.withIndent(' ').convert(out); - try { - // Attempt normal file write (works on Dart VM). - File(outPath) - ..createSync(recursive: true) - ..writeAsStringSync(outJson); - } on Exception catch (_) { - // Running in JS platform (Node) — filesystem operations may be - // unsupported. Instead of falling back to printing the entire JSON, - // validate the generated JSON by invoking the Yosys loader in-process - // (the JS implementation will import the d3-yosys module). This keeps - // tests that run under --platform node able to validate dumps. - unawaited(runYosysLoaderFromString(outJson).then((res) { - if (!res.success) { - final msg = - 'Yosys loader validation failed for $outPath: ${res.error}' - '${res.stack != null ? '\n${res.stack}' : ''}'; - throw Exception(msg); - } - return res; - }).catchError((Object e, StackTrace? st) { - final msg = 'Yosys loader invocation failed for $outPath: $e' - '${st != null ? '\n$st' : ''}'; - throw Exception(msg); - })); - } catch (e) { - // Re-throw unexpected errors to keep behavior unchanged. - rethrow; - } - } - - /// Synchronous accessor for the top module map. - ModuleMap get moduleMap => topMap; - - /// Public export helper. Call this to write a Yosys-style JSON file to - /// [outPath]. The constructor no longer triggers automatic exports. - /// Synchronous export. Throws if `topModule.hasBuilt` is false. - void exportYosysJson(String outPath) { - if (!topModule.hasBuilt) { - throw StateError('Top module must be built before exporting JSON'); - } - _exportYosysJson(outPath); - } -} diff --git a/lib/src/synthesizers/schematic/schematic_primitives.dart b/lib/src/synthesizers/schematic/schematic_primitives.dart index 27cce754f..e17039354 100644 --- a/lib/src/synthesizers/schematic/schematic_primitives.dart +++ b/lib/src/synthesizers/schematic/schematic_primitives.dart @@ -8,8 +8,8 @@ // Author: Desmond Kirkpatrick import 'package:rohd/rohd.dart'; -import 'package:rohd/src/synthesizers/schematic/module_map.dart'; import 'package:rohd/src/synthesizers/schematic/module_utils.dart'; +import 'package:rohd/src/synthesizers/schematic/schematic_synthesis_result.dart'; /// Descriptor describing how a ROHD helper module maps to a Yosys /// primitive type. @@ -508,25 +508,43 @@ class Primitives { } /// Convenience wrapper used by the dumper when the lookup for ROHD port - /// ids needs to resolve ports via a child ModuleMap (or fallback to the - /// child module's own ports). The [idsForChildLogic] callback should - /// accept a `Logic` and return the corresponding bit id list. The - /// [childMapLookup] callback, when provided, should return the ModuleMap - /// for a given child module or null if not present. + /// ids needs to resolve ports via either a previously-produced + /// `SynthesisResult` for the child (preferred) or a child `ModuleMap` + /// (fallback). The [idsForChildLogic] callback should accept a `Logic` + /// and return the corresponding bit id list. The [childResultLookup] + /// callback, when provided, should return the `SynthesisResult` for a + /// given child module or null if not present. This allows using cached + /// synthesis outputs rather than rebuilding ModuleMaps. Map> buildPrimitiveConnectionsWithChildLogicLookup( Module childModule, PrimitiveDescriptor prim, Map parameters, Map portDirs, - ModuleMap? Function(Module) childMapLookup, + SynthesisResult? Function(Module) childResultLookup, List Function(Logic) idsForChildLogic) { // Adapter: convert rohdName -> idsForRohd by resolving the Logic - // either from the child ModuleMap (if available) or directly from - // the child module. + // either from the SchematicSynthesisResult (if available), the + // child ModuleMap contained within that result, or directly from + // the child module as a last resort. List idsForRohd(String rohdName) { - final childMap = childMapLookup(childModule); - final logic = - childMap?.module.ports[rohdName] ?? childModule.ports[rohdName]; + final res = childResultLookup(childModule); + // If we have a SchematicSynthesisResult, try to use its port map. + if (res is SchematicSynthesisResult) { + final ports = res.ports; + if (ports.containsKey(rohdName)) { + // ports[rohdName]['bits'] is a List of bit ids + final bits = (ports[rohdName]! as Map)['bits']; + if (bits is List) { + return bits.cast(); + } + } + } + + // Fallback: try to obtain the logic from the child's ModuleMap if + // the result provides a way (many SchematicSynthesisResults do not + // expose ModuleMap directly), otherwise use the module's port + // reference and resolve via idsForChildLogic. + final logic = childModule.ports[rohdName]; if (logic == null) { return []; } diff --git a/lib/src/synthesizers/schematic/schematic_synthesis_result.dart b/lib/src/synthesizers/schematic/schematic_synthesis_result.dart index b02d87ebf..a805cbdae 100644 --- a/lib/src/synthesizers/schematic/schematic_synthesis_result.dart +++ b/lib/src/synthesizers/schematic/schematic_synthesis_result.dart @@ -11,6 +11,7 @@ import 'dart:convert'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/schematic/schematic.dart'; +// ModuleMap removed — migration to child SchematicSynthesisResult objects. /// A [SynthesisResult] representing schematic output for a single [Module]. /// @@ -20,6 +21,12 @@ class SchematicSynthesisResult extends SynthesisResult { /// The ports map: name → {direction, bits}. final Map> ports; + /// Mapping of `Logic` port objects to assigned schematic bit ids. + final Map> portLogics; + + /// Set of Logic objects considered global for this module result. + final Set globalLogics; + /// The cells map: instance name → cell data. final Map> cells; @@ -29,6 +36,13 @@ class SchematicSynthesisResult extends SynthesisResult { /// Attributes for this module (e.g., top marker). final Map attributes; + /// Note: ModuleMap was removed from result; builder keeps a local fallback. + + /// List of child module SchematicSynthesisResults (ordered like + /// `ModuleMap.submodules.keys`). Elements may be null when no existing + /// result was available for a child module. + final List childResults; + /// Cached JSON string for comparison and output. late final String _cachedJson = _buildJson(); @@ -37,11 +51,68 @@ class SchematicSynthesisResult extends SynthesisResult { super.module, super.getInstanceTypeOfModule, { required this.ports, + required this.portLogics, + required this.globalLogics, required this.cells, required this.netnames, this.attributes = const {}, + this.childResults = const [], }); + /// Compute connected-component roots for `n` items given a list of union + /// operations as index pairs. Returns a list `roots` where `roots[i]` is the + /// canonical root index for element `i`. + static List computeComponents(int n, Iterable> unions, + {List? priority}) { + final parent = List.generate(n, (i) => i); + var pri = priority ?? List.filled(n, 0); + if (pri.length < n) { + pri = [...pri, ...List.filled(n - pri.length, 0)]; + } + int find(int x) { + var r = x; + while (parent[r] != r) { + parent[r] = parent[parent[r]]; + r = parent[r]; + } + return r; + } + + void unite(int a, int b) { + final ra = find(a); + final rb = find(b); + if (ra == rb) { + return; + } + final pra = pri[ra]; + final prb = pri[rb]; + final winner = (pra > prb) ? ra : (prb > pra ? rb : (ra < rb ? ra : rb)); + final loser = (winner == ra) ? rb : ra; + parent[loser] = winner; + } + + for (final u in unions) { + if (u.length >= 2) { + unite(u[0], u[1]); + } + } + + return List.generate(n, find); + } + + /// Deep list equality (compare contents, not identity). + static bool listEquals(List a, List b) { + if (a.length != b.length) { + return false; + } + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; + } + String _buildJson() { final moduleEntry = { 'attributes': attributes, @@ -89,28 +160,37 @@ class SchematicSynthesisResult extends SynthesisResult { } } -/// Factory helper to build [SchematicSynthesisResult] from a [ModuleMap]. +/// Factory helper to build [SchematicSynthesisResult]. /// -/// This extracts the logic from SchematicDumper.buildModuleEntryHierarchy to -/// handle one module level without recursion. +/// Extracted from SchematicDumper.buildModuleEntryHierarchy to handle one +/// module level without recursion. class SchematicSynthesisResultBuilder { /// The module to synthesize. final Module module; - /// The ModuleMap for this module. - final ModuleMap map; - /// Whether to filter const-only inputs to combinational primitives. final bool filterConstInputsToCombinational; + /// Optional set of resolved global logics (from synthesizer.prepare()) + /// to be considered when computing reachable-from-global sets. + final Set resolvedGlobalLogics; + /// Function to get instance type names for submodules. final String Function(Module) getInstanceTypeOfModule; + /// Optional callback to lookup an existing `SynthesisResult` for a module. + final SynthesisResult? Function(Module module)? lookupExistingResult; + + /// Optional map of existing results keyed by Module for fast access. + final Map? existingResults; + /// Creates a builder for [module]. SchematicSynthesisResultBuilder({ required this.module, - required this.map, required this.getInstanceTypeOfModule, + this.resolvedGlobalLogics = const {}, + this.lookupExistingResult, + this.existingResults, this.filterConstInputsToCombinational = false, }); @@ -125,6 +205,35 @@ class SchematicSynthesisResultBuilder { attr['top'] = 1; } + // Prepare child SchematicSynthesisResults lookup aligned with the + // module's declared submodules so iteration matches the Module's + // declared child order (helps when ModuleMap ordering differs). + final childModules = module.subModules.toList(); + final childResultsList = [ + for (final m in childModules) + (existingResults != null) + ? existingResults![m] as SchematicSynthesisResult? + : (lookupExistingResult != null) + ? lookupExistingResult!(m) as SchematicSynthesisResult? + : null + ]; + // Allow null entries for children that do not generate definitions + // (primitives or modules synthesized inline). For children that do + // generate definitions, a SchematicSynthesisResult should already + // be present in `existingResults` (provided by SynthBuilder). + for (var i = 0; i < childModules.length; i++) { + final child = childModules[i]; + final res = childResultsList[i]; + final typeName = getInstanceTypeOfModule(child); + if (typeName != '*NONE*' && res == null) { + throw StateError('Missing SchematicSynthesisResult for child ' + '${child.name}; builder requires child results to be available.'); + } + } + // No ModuleMap fallbacks: require child results to be present. + final internalLogicsFallback = >{}; + // childResultsList is aligned with childModules; iterate by index. + // Emit ports (names + directions) void addPorts(Map portMap, String dir) { for (final p in portMap.entries) { @@ -138,17 +247,64 @@ class SchematicSynthesisResultBuilder { // Assign IDs to internal nets (child outputs + constants) final internalNetIds = >{}; - final maxPortId = map.portLogics.values - .expand((ids) => ids) - .whereType() - .fold(-1, (m, id) => id > m ? id : m); - var nextId = maxPortId + 1; - - // Assign IDs to each child's output ports - for (final childMap in map.submodules.values) { - final childModule = childMap.module; + + // Compute port-level schematic ids locally (same algorithm used by + // ModuleMap) so the builder can operate without consulting + // `moduleMap.portLogics` directly. + final portLogicsLocal = >{}; + final portLogicsCandidates = [ + ...module.inputs.values, + ...module.outputs.values, + ...module.inOuts.values + ]; + + // Compute transitive set of signals reachable from globals aggregated + // from child results. Require child results to be present; throw early + // if any are missing. + // Start with synthesizer-provided resolved globals if any. + final reachableFromGlobals = {}..addAll(resolvedGlobalLogics); + for (var ci = 0; ci < childModules.length; ci++) { + final childResult = childResultsList[ci]; + if (childResult == null) { + continue; + } + for (final g in childResult.globalLogics) { + if (!reachableFromGlobals.contains(g)) { + final visitQueue = [g]; + while (visitQueue.isNotEmpty) { + final cur = visitQueue.removeLast(); + if (reachableFromGlobals.contains(cur)) { + continue; + } + reachableFromGlobals.add(cur); + for (final dst in cur.dstConnections) { + if (!reachableFromGlobals.contains(dst)) { + visitQueue.add(dst); + } + } + } + } + } + } + + var nextId = 0; + for (final logic in portLogicsCandidates) { + if (reachableFromGlobals.contains(logic)) { + continue; + } + final ids = List.generate(logic.width, (_) => nextId++); + portLogicsLocal[logic] = ids; + } + + // Assign IDs to each child's output ports by walking the child results + // list produced by recursion so we rely on synthesized results rather + // than performing fresh lookups. + for (var ci = 0; ci < childModules.length; ci++) { + final childModule = childModules[ci]; + final childResult = childResultsList[ci]; + final childGlobalSet = childResult?.globalLogics ?? {}; for (final output in childModule.outputs.values) { - if (childMap.globalLogics.contains(output)) { + if (childGlobalSet.contains(output)) { continue; } final ids = List.generate(output.width, (_) => nextId++); @@ -161,7 +317,8 @@ class SchematicSynthesisResultBuilder { final constHandler = ConstantHandler(); final constResult = constHandler.collectConstants( module: module, - map: map, + childModules: childModules, + childResultsList: childResultsList, internalNetIds: internalNetIds, ports: ports, nextIdRef: nextIdRef, @@ -174,7 +331,8 @@ class SchematicSynthesisResultBuilder { final passHandler = PassThroughHandler(); final passResult = passHandler.collectPassThroughs( module: module, - map: map, + childModules: childModules, + childResultsList: childResultsList, internalNetIds: internalNetIds, ports: ports, nextIdRef: nextIdRef, @@ -192,7 +350,7 @@ class SchematicSynthesisResultBuilder { if (!visited.add(logic)) { return; } - if (map.portLogics.containsKey(logic) || + if (portLogicsLocal.containsKey(logic) || internalNetIds.containsKey(logic)) { return; } @@ -202,8 +360,13 @@ class SchematicSynthesisResultBuilder { } } - for (final childMap in map.submodules.values) { - for (final input in childMap.module.inputs.values) { + for (var ci = 0; ci < childModules.length; ci++) { + final childModule = childModules[ci]; + final childResult = childResultsList[ci]; + final inputs = childResult != null + ? childResult.module.inputs.values + : childModule.inputs.values; + for (final input in inputs) { final visited = {}; for (final src in input.srcConnections) { collectIntermediates(src, visited); @@ -211,7 +374,7 @@ class SchematicSynthesisResultBuilder { } } - for (final portLogic in map.portLogics.keys) { + for (final portLogic in portLogicsLocal.keys) { if (module.outputs.values.contains(portLogic) && portLogic is! LogicStructure) { final visited = {}; @@ -223,7 +386,7 @@ class SchematicSynthesisResultBuilder { // Build union-find on all Logics final allLogics = [ - ...map.portLogics.keys, + ...portLogicsLocal.keys, ...internalNetIds.keys, ...intermediateLogics, ]; @@ -242,18 +405,19 @@ class SchematicSynthesisResultBuilder { } } - final cellRoots = computeComponents(allLogics.length, cellUnions); + final cellRoots = SchematicSynthesisResult.computeComponents( + allLogics.length, cellUnions); // Build root → canonical IDs mapping final rootToIds = >{}; - for (final portLogic in map.portLogics.keys) { + for (final portLogic in portLogicsLocal.keys) { final idx = logicIndex[portLogic]; if (idx == null) { continue; } final root = cellRoots[idx]; - final ids = map.portLogics[portLogic]; + final ids = portLogicsLocal[portLogic]; if (ids != null && ids.isNotEmpty) { rootToIds.putIfAbsent(root, () => ids); } @@ -292,11 +456,11 @@ class SchematicSynthesisResultBuilder { final idx = logicIndex[l]; if (idx != null) { return rootToIds[cellRoots[idx]] ?? - map.portLogics[l] ?? + portLogicsLocal[l] ?? internalNetIds[l] ?? []; } - return map.portLogics[l] ?? internalNetIds[l] ?? []; + return portLogicsLocal[l] ?? internalNetIds[l] ?? []; } if (internalNetIds.containsKey(childLogic)) { @@ -325,7 +489,7 @@ class SchematicSynthesisResultBuilder { } var nextInternalNetId = 0; - for (final ids in map.portLogics.values) { + for (final ids in portLogicsLocal.values) { for (final id in ids) { if (id >= nextInternalNetId) { nextInternalNetId = id + 1; @@ -341,8 +505,9 @@ class SchematicSynthesisResultBuilder { } // Emit cells - for (final childMap in map.submodules.values) { - final childModule = childMap.module; + for (var ci = 0; ci < childModules.length; ci++) { + final childModule = childModules[ci]; + final cellKey = childModule.hasBuilt ? childModule.uniqueInstanceName : childModule.name; @@ -393,7 +558,6 @@ class SchematicSynthesisResultBuilder { if (prim != null) { _emitPrimitiveCell( childModule: childModule, - childMap: childMap, cellKey: cellKey, prim: prim, cells: cells, @@ -416,7 +580,6 @@ class SchematicSynthesisResultBuilder { // Handle primitive cells _emitPrimitiveCell( childModule: childModule, - childMap: childMap, cellKey: cellKey, prim: prim, cells: cells, @@ -473,19 +636,17 @@ class SchematicSynthesisResultBuilder { } } } - final roots = computeComponents(signals.length, unions); + final roots = + SchematicSynthesisResult.computeComponents(signals.length, unions); final bitIdToLogic = {}; - for (final e in map.portLogics.entries) { - for (final bitId in e.value) { - bitIdToLogic[bitId] = e.key; - } - } - for (final e in map.internalLogics.entries) { + for (final e in portLogicsLocal.entries) { for (final bitId in e.value) { bitIdToLogic[bitId] = e.key; } } + // No internalLogicsFallback entries to add; all internal ids are + // represented in `internalNetIds`. for (final e in internalNetIds.entries) { for (final bitId in e.value) { if (bitId is int) { @@ -522,7 +683,7 @@ class SchematicSynthesisResultBuilder { continue; } - final portBitIds = List.from(map.portLogics[logic] ?? []); + final portBitIds = List.from(portLogicsLocal[logic] ?? []); entry.value['bits'] = portBitIds; final idx = indexOf[logic]; @@ -548,8 +709,8 @@ class SchematicSynthesisResultBuilder { final portBitIds = passResult.passThroughConnections.containsKey(logic) ? (internalNetIds[logic]?.whereType().toList() ?? - (map.portLogics[logic] ?? [])) - : (map.portLogics[logic] ?? []); + (portLogicsLocal[logic] ?? [])) + : (portLogicsLocal[logic] ?? []); entry.value['bits'] = portBitIds; @@ -574,7 +735,7 @@ class SchematicSynthesisResultBuilder { continue; } - final portBitIds = map.portLogics[logic] ?? []; + final portBitIds = portLogicsLocal[logic] ?? []; entry.value['bits'] = portBitIds; final idx = indexOf[logic]; @@ -586,7 +747,7 @@ class SchematicSynthesisResultBuilder { } // Add named internal signals to rootToPreferred - final portLogicsSet = map.portLogics.keys.toSet(); + final portLogicsSet = portLogicsLocal.keys.toSet(); signals.asMap().entries.where((e) { final logic = e.value; return !portLogicsSet.contains(logic) && @@ -601,8 +762,8 @@ class SchematicSynthesisResultBuilder { final inn = e.value; final outName = out.name; final inName = passResult.passThroughNames[outName] ?? inn.name; - final inIds = map.portLogics[inn] ?? []; - final outIds = internalNetIds[out] ?? map.portLogics[out] ?? []; + final inIds = portLogicsLocal[inn] ?? []; + final outIds = internalNetIds[out] ?? portLogicsLocal[out] ?? []; if (inIds.isEmpty || outIds.isEmpty) { continue; } @@ -729,7 +890,7 @@ class SchematicSynthesisResultBuilder { } } - return map.portLogics[elem] ?? map.internalLogics[elem]; + return portLogicsLocal[elem] ?? internalLogicsFallback[elem]; } final elemLists = struct.elements.map(findElemIds).toList(); @@ -783,13 +944,15 @@ class SchematicSynthesisResultBuilder { cells: cells, netnames: netnames, attributes: attr, + portLogics: portLogicsLocal, + globalLogics: reachableFromGlobals, + childResults: childResultsList, ); } /// Emits a primitive cell into [cells]. void _emitPrimitiveCell({ required Module childModule, - required ModuleMap childMap, required String cellKey, required PrimitiveDescriptor prim, required Map> cells, @@ -864,7 +1027,7 @@ class SchematicSynthesisResultBuilder { prim, (primCell['parameters']! as Map).cast(), portDirs, - (m) => map.submodules[m], + lookupExistingResult ?? ((Module _) => null), idsForChildLogic); if (filterConstInputsToCombinational && diff --git a/lib/src/synthesizers/schematic/schematic_synthesizer.dart b/lib/src/synthesizers/schematic/schematic_synthesizer.dart index 20cc10690..8eba217ef 100644 --- a/lib/src/synthesizers/schematic/schematic_synthesizer.dart +++ b/lib/src/synthesizers/schematic/schematic_synthesizer.dart @@ -7,6 +7,8 @@ // 2025 December 18 // Author: Desmond Kirkpatrick +import 'dart:convert'; + import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/schematic/schematic.dart'; @@ -59,6 +61,10 @@ class SchematicSynthesizer extends Synthesizer { /// Resolved global logics, computed in [prepare]. Set _resolvedGlobalLogics = {}; + /// Top-level modules provided in [prepare]. Stored so `synthesize` can + /// determine whether a module is a top-level module. + Set _topModules = {}; + /// Creates a [SchematicSynthesizer]. /// /// - [filterConstInputsToCombinational]: When true, filters out constant-only @@ -74,6 +80,9 @@ class SchematicSynthesizer extends Synthesizer { @override void prepare(List tops) { + // Record top modules for later use in `synthesize`. + _topModules = Set.from(tops); + // Resolve global logics from the top module(s) _resolvedGlobalLogics = {}; @@ -107,6 +116,9 @@ class SchematicSynthesizer extends Synthesizer { } } + // ModuleMap-based helpers removed — Schematic synthesis prefers + // child SchematicSynthesisResult objects and builder-local computations. + @override bool generatesDefinition(Module module) { // Check if module uses Schematic mixin and controls definition generation @@ -122,42 +134,400 @@ class SchematicSynthesizer extends Synthesizer { @override SynthesisResult synthesize( - Module module, String Function(Module module) getInstanceTypeOfModule) { - // Create a ModuleMap for this single module level (no recursive submodules) - // The SynthBuilder handles the recursion. - final map = ModuleMap( - module, - includeInternals: true, - globalLogics: - _resolvedGlobalLogics.isNotEmpty ? _resolvedGlobalLogics : null, + Module module, String Function(Module module) getInstanceTypeOfModule, + {SynthesisResult? Function(Module module)? lookupExistingResult, + Map? existingResults}) { + final builder = SchematicSynthesisResultBuilder( + module: module, + getInstanceTypeOfModule: getInstanceTypeOfModule, + resolvedGlobalLogics: _resolvedGlobalLogics, + filterConstInputsToCombinational: filterConstInputsToCombinational, + lookupExistingResult: lookupExistingResult, + existingResults: existingResults, ); - // Validate the ModuleMap hierarchy and connectivity similarly to - // SchematicDumper to provide early, clear errors for cycles/invalid maps. - try { - map - ..validateHierarchy(visited: >{}) - ..validate(); - final idErrors = map.validateIdConnectivity(); - if (idErrors.isNotEmpty) { - final buf = StringBuffer()..writeln('ID connectivity errors:'); - for (final e in idErrors) { - buf.writeln(' - $e'); + // If this module was one of the tops provided in `prepare`, tell the + // builder so it can set the `top` attribute locally. + final isTop = _topModules.contains(module); + final result = builder.build(isTop: isTop); + + // Per-level structural validation + _validateResult(result); + + // ID connectivity checks + final idErrs = _validateIdConnectivity(result); + if (idErrs.isNotEmpty) { + final buf = StringBuffer() + ..writeln('Schematic ID connectivity errors for ') + ..writeln(' module: ${module.name}') + ..writeln('Errors:'); + for (final e in idErrs) { + buf.writeln(' - $e'); + } + throw StateError(buf.toString()); + } + + // If this is a top-level module, validate the entire hierarchy for + // cycles/duplicate instances. + if (isTop) { + _validateHierarchyForTop(module); + } + + return result; + } + + /// Run basic result validation using a ModuleMap constructed from the + /// result's module. This delegates to the existing ModuleMap.validate() + /// implementation. + void _validateResult(SchematicSynthesisResult result) { + final module = result.module; + final globalLogics = result.globalLogics; + + // Recompute portLogics and internalLogics as ModuleMap did. + final portLogics = >{}; + final internalLogics = >{}; + + // Compute reachable from globals + final reachableFromGlobals = {}; + if (globalLogics.isNotEmpty) { + final visitQueue = [...globalLogics]; + while (visitQueue.isNotEmpty) { + final cur = visitQueue.removeLast(); + if (reachableFromGlobals.contains(cur)) { + continue; + } + reachableFromGlobals.add(cur); + for (final dst in cur.dstConnections) { + if (!reachableFromGlobals.contains(dst)) { + visitQueue.add(dst); + } } - throw StateError(buf.toString()); } - } catch (e) { - throw StateError( - 'ModuleMap validation failed before schematic synth: $e'); } - final builder = SchematicSynthesisResultBuilder( - module: module, - map: map, - getInstanceTypeOfModule: getInstanceTypeOfModule, - filterConstInputsToCombinational: filterConstInputsToCombinational, - ); + var nextId = 0; + final portCandidates = [ + ...module.inputs.values, + ...module.outputs.values, + ...module.inOuts.values + ]; + for (final logic in portCandidates) { + if (reachableFromGlobals.contains(logic)) { + continue; + } + final ids = List.generate(logic.width, (_) => nextId++); + portLogics[logic] = ids; + } + + // internals: include signals that are not ports + final internalSignals = [ + for (final s in module.signals) + if (!portLogics.containsKey(s)) s + ]; + for (final sig in internalSignals) { + if (reachableFromGlobals.contains(sig)) { + continue; + } + final ids = List.generate(sig.width, (_) => nextId++); + internalLogics[sig] = ids; + } + + // Now perform the same validation logic as ModuleMap.validate() + final logicToIds = >{} + ..addAll(portLogics) + ..addAll(internalLogics); + final allLogics = [...portLogics.keys, ...internalLogics.keys]; + for (final l in allLogics) { + if (!logicToIds.containsKey(l)) { + throw StateError( + 'Logic $l missing ids in module ${module.uniqueInstanceName}'); + } + } + + final bitIdToMembers = >{}; + for (final e in logicToIds.entries) { + for (final bitId in e.value) { + bitIdToMembers.putIfAbsent(bitId, () => []).add(e.key); + } + } + + final signals = [...portLogics.keys, ...internalLogics.keys]; + final indexOf = {for (var i = 0; i < signals.length; i++) signals[i]: i}; + final unions = >[ + for (var i = 0; i < signals.length; i++) + for (final conn in [ + ...signals[i].srcConnections, + ...signals[i].dstConnections + ]) + if (indexOf[conn] != null) [i, indexOf[conn]!] + ]; + + final roots = + SchematicSynthesisResult.computeComponents(signals.length, unions); + + for (final members in bitIdToMembers.values) { + if (members.length <= 1) { + continue; + } + final root0 = roots[indexOf[members.first]!]; + for (final other in members.skip(1)) { + final rootN = roots[indexOf[other]!]; + if (root0 != rootN) { + final buf = StringBuffer() + ..writeln('Members ${members.first} and $other share ' + 'bit-id but are not in same component in ') + ..writeln(module.uniqueInstanceName) + ..writeln('Member info:'); + for (final m in members) { + buf.writeln(' - $m (ids=${logicToIds[m]}, ' + 'root=${roots[indexOf[m]!]}'); + } + throw StateError(buf.toString()); + } + } + } + + // Recurse into submodules + for (final sub in module.subModules) { + // Build a result-like check by using existing SchematicSynthesisResult if + // available in synthesis flow; otherwise we still validate the Module + // structure by recursing on the Module object. + // For now, validate using Module semantics recursively. + // (This mirrors ModuleMap's recursive validate()) + // Construct a minimal temporary SchematicSynthesisResult with empty + // mappings and the resolved child globals. + final childGlobals = {}; + if (reachableFromGlobals.isNotEmpty) { + for (final input in sub.inputs.values) { + for (final src in input.srcConnections) { + if (reachableFromGlobals.contains(src)) { + childGlobals.add(input); + break; + } + } + } + } + // Recursively validate child module structure + _validateResult(SchematicSynthesisResult( + sub, + (m) => m.definitionName, + ports: const {}, + portLogics: const {}, + globalLogics: childGlobals, + cells: const {}, + netnames: const {}, + )); + } + } + + /// Compute port and internal schematic id maps for [module], excluding + /// signals reachable from [globalLogics]. Returns a map with keys + /// 'ports' and 'internals'. + Map>> _computePortAndInternalIds( + Module module, Set globalLogics) { + final portLogics = >{}; + final internalLogics = >{}; + + final reachableFromGlobals = {}; + if (globalLogics.isNotEmpty) { + final visitQueue = [...globalLogics]; + while (visitQueue.isNotEmpty) { + final cur = visitQueue.removeLast(); + if (reachableFromGlobals.contains(cur)) { + continue; + } + reachableFromGlobals.add(cur); + for (final dst in cur.dstConnections) { + if (!reachableFromGlobals.contains(dst)) { + visitQueue.add(dst); + } + } + } + } + + var nextId = 0; + final portCandidates = [ + ...module.inputs.values, + ...module.outputs.values, + ...module.inOuts.values + ]; + for (final logic in portCandidates) { + if (reachableFromGlobals.contains(logic)) { + continue; + } + final ids = List.generate(logic.width, (_) => nextId++); + portLogics[logic] = ids; + } + + final internalSignals = [ + for (final s in module.signals) + if (!portLogics.containsKey(s)) s + ]; + for (final sig in internalSignals) { + if (reachableFromGlobals.contains(sig)) { + continue; + } + final ids = List.generate(sig.width, (_) => nextId++); + internalLogics[sig] = ids; + } + + return { + 'ports': portLogics, + 'internals': internalLogics, + }; + } + + /// Validate id connectivity for a single module result by delegating to + /// ModuleMap.validateIdConnectivity(). Returns a list of error messages. + List _validateIdConnectivity(SchematicSynthesisResult result) { + final errors = []; + final module = result.module; + + final maps = _computePortAndInternalIds(module, result.globalLogics); + final portLogics = maps['ports']!; + final internalLogics = maps['internals']!; + + final allIds = {}; + + void checkIds(Logic logic, List ids, String context) { + for (final id in ids) { + if (id < 0) { + errors.add('$context: Logic "${logic.name}" has negative id $id'); + } + final existing = allIds[id]; + if (existing != null && existing != logic) { + final connected = logic.srcConnections.contains(existing) || + logic.dstConnections.contains(existing) || + existing.srcConnections.contains(logic) || + existing.dstConnections.contains(logic); + if (!connected) { + errors.add( + '$context: ID $id assigned to both "${logic.name}" and ' + '"${existing.name}" but they are not connected', + ); + } + } + allIds[id] = logic; + } + } + + for (final entry in portLogics.entries) { + checkIds(entry.key, entry.value, module.uniqueInstanceName); + } + for (final entry in internalLogics.entries) { + checkIds( + entry.key, entry.value, '${module.uniqueInstanceName} (internal)'); + } + + // Recurse into child results if present + for (final child in result.childResults) { + if (child != null) { + errors.addAll(_validateIdConnectivity(child)); + } + } + + // Primitive input driver checks + for (final sub in module.subModules) { + var prim = Primitives.instance.lookupByDefinitionName(sub.definitionName); + if (prim == null && sub.subModules.isEmpty) { + prim = Primitives.instance.lookupForModule(sub); + } + if (prim == null) { + continue; + } + for (final inLogic in sub.inputs.values) { + if (inLogic.srcConnections.isEmpty) { + errors.add( + '${module.uniqueInstanceName}: Primitive ' + '${sub.uniqueInstanceName} input "${inLogic.name}" has no driver', + ); + } + } + } + + return errors; + } + + /// Validate the hierarchical placement of modules starting at [top]. This + /// will raise if cycles or duplicate-hierarchy placements are detected. + void _validateHierarchyForTop(Module top) { + // Validate hierarchy for cycles and duplicate placements. + void visit(Module m, List hierarchy, + Map>> visitedPaths) { + final newHierarchy = [...hierarchy, m]; + + // Detect cycles by module identity + if (hierarchy.any((mm) => mm == m)) { + final loop = newHierarchy.map((x) => x.uniqueInstanceName).join('.'); + throw StateError( + 'Module ${m.uniqueInstanceName} is a submodule of itself: $loop'); + } + + if (visitedPaths.containsKey(m)) { + final otherPaths = + visitedPaths[m]!.map((p) => p.map((i) => i).join('.')).join(','); + final thisStr = hierarchy.map((mm) => mm.uniqueInstanceName).join('.'); + throw StateError( + 'Module ${m.uniqueInstanceName} exists at more than one ' + 'hierarchy: $otherPaths and $thisStr'); + } + + visitedPaths[m] = [newHierarchy.map((x) => x.hashCode).toList()]; + + for (final sub in m.subModules) { + visit(sub, newHierarchy, visitedPaths); + } + } + + visit(top, [], >>{}); + } + + /// Collects a combined modules map from a collection of [SynthesisResult]s + /// suitable for JSON emission (matches previous test helpers). + /// + /// Each entry is keyed by the result's `instanceTypeName` and contains the + /// `attributes`, `ports`, `cells`, and `netnames` maps. If a [topModule] + /// is supplied, the corresponding module's attributes will include + /// `'top': 1`. + Map> collectModuleEntries( + Iterable results, + {Module? topModule}) { + final allModules = >{}; + for (final result in results) { + if (result is SchematicSynthesisResult) { + final typeName = result.instanceTypeName; + final attrs = Map.from(result.attributes); + if (topModule != null && result.module == topModule) { + attrs['top'] = 1; + } + allModules[typeName] = { + 'attributes': attrs, + 'ports': result.ports, + 'cells': result.cells, + 'netnames': result.netnames, + }; + } + } + return allModules; + } + + /// Generate the combined ROHD schematic JSON from a SynthBuilder's + /// `synthesisResults`. Returns the JSON string. + Future generateCombinedJson(SynthBuilder synth, Module top) async { + final modules = + collectModuleEntries(synth.synthesisResults, topModule: top); + final combined = { + 'creator': 'SchematicSynthesizer via SynthBuilder (rohd)', + 'modules': modules, + }; + return const JsonEncoder.withIndent(' ').convert(combined); + } - return builder.build(); + /// Convenience API: synthesize [top] into a combined ROHD JSON string. + /// This builds a `SynthBuilder` internally with `this` synthesizer and + /// returns the full JSON contents (including the `creator` field). + Future synthesizeToJson(Module top) async { + final sb = SynthBuilder(top, this); + return generateCombinedJson(sb, top); } } diff --git a/lib/src/synthesizers/synth_builder.dart b/lib/src/synthesizers/synth_builder.dart index c571b0185..aaf1530c7 100644 --- a/lib/src/synthesizers/synth_builder.dart +++ b/lib/src/synthesizers/synth_builder.dart @@ -34,6 +34,9 @@ class SynthBuilder { /// All the [SynthesisResult]s generated by this [SynthBuilder]. final Set _synthesisResults = {}; + /// Map from Module -> its produced SynthesisResult for fast lookup. + final Map _synthesisResultByModule = {}; + /// All the [SynthesisResult]s generated by this [SynthBuilder]. Set get synthesisResults => UnmodifiableSetView(_synthesisResults); @@ -104,13 +107,26 @@ class SynthBuilder { } var newName = module.definitionName; - final newSynthesisResult = synthesizer.synthesize(module, _getInstanceType); - if (_synthesisResults.contains(newSynthesisResult)) { - // a name for this module already exists - newName = _moduleToInstanceTypeMap[ - _synthesisResults.lookup(newSynthesisResult)!.module]!; + // Provide a lookup closure so synthesizers can access already-produced + // synthesis results for submodules. + final newSynthesisResult = synthesizer.synthesize( + module, + _getInstanceType, + lookupExistingResult: (m) => _synthesisResultByModule[m], + existingResults: _synthesisResultByModule, + ); + // Note: top attribute is set by SchematicSynthesisResultBuilder.build() + // when invoked with `isTop=true` from the SchematicSynthesizer. + final existing = _synthesisResults.lookup(newSynthesisResult); + if (existing != null) { + // a name for this module already exists; reuse mapping + newName = _moduleToInstanceTypeMap[existing.module]!; + // Map this module to the canonical existing result so future + // lookups by module succeed. + _synthesisResultByModule[module] = existing; } else { _synthesisResults.add(newSynthesisResult); + _synthesisResultByModule[module] = newSynthesisResult; newName = _instanceTypeUniquifier.getUniqueName( initialName: newName, reserved: module.reserveDefinitionName); } diff --git a/lib/src/synthesizers/synthesizer.dart b/lib/src/synthesizers/synthesizer.dart index 523e37644..d13e6b819 100644 --- a/lib/src/synthesizers/synthesizer.dart +++ b/lib/src/synthesizers/synthesizer.dart @@ -28,6 +28,13 @@ abstract class Synthesizer { /// Synthesizes [module] into a [SynthesisResult], given the mapping provided /// by [getInstanceTypeOfModule]. + /// + /// Optionally a [lookupExistingResult] callback may be supplied which + /// allows the synthesizer to query already-generated `SynthesisResult`s + /// for child modules (useful when building parent output that needs + /// information from children). SynthesisResult synthesize( - Module module, String Function(Module module) getInstanceTypeOfModule); + Module module, String Function(Module module) getInstanceTypeOfModule, + {SynthesisResult? Function(Module module)? lookupExistingResult, + Map? existingResults}); } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index d8b5bae36..6320cd444 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart @@ -137,7 +137,9 @@ class SystemVerilogSynthesizer extends Synthesizer { @override SynthesisResult synthesize( - Module module, String Function(Module module) getInstanceTypeOfModule) { + Module module, String Function(Module module) getInstanceTypeOfModule, + {SynthesisResult? Function(Module module)? lookupExistingResult, + Map? existingResults}) { assert( module is! SystemVerilog || module.generatedDefinitionType != DefinitionGenerationType.none, diff --git a/test/schematic_example_test.dart b/test/schematic_example_test.dart index 06b6b864c..06c84c5ed 100644 --- a/test/schematic_example_test.dart +++ b/test/schematic_example_test.dart @@ -7,7 +7,6 @@ // 2025 December 18 // Author: Desmond Kirkpatrick -import 'dart:convert'; import 'dart:io'; import 'package:rohd/rohd.dart'; @@ -26,37 +25,23 @@ void main() { // `--platform node` we skip filesystem and loader assertions. const isJS = identical(0, 0.0); - // Helper: write combined SynthBuilder schematic JSON to `out`. - // Returns the JSON string so callers can pass it to loaders directly. - Future writeCombinedFromSynth( - SynthBuilder synth, Module top, String out) async { - final allModules = >{}; - for (final result in synth.synthesisResults) { - if (result is SchematicSynthesisResult) { - final typeName = result.instanceTypeName; - final attrs = Map.from(result.attributes); - if (result.module == top) { - attrs['top'] = 1; - } - allModules[typeName] = { - 'attributes': attrs, - 'ports': result.ports, - 'cells': result.cells, - 'netnames': result.netnames, - }; - } - } - final combined = { - 'creator': 'SchematicSynthesizer via SynthBuilder (rohd)', - 'modules': allModules, - }; - final json = const JsonEncoder.withIndent(' ').convert(combined); + // Tests should call the synthesizer API directly to obtain the final + // combined JSON string and perform VM-only writes themselves. + + // Helper used by the tests to synthesize `top` and optionally write the + // produced JSON to `outPath` when running on VM. Returns the JSON string + // so callers can validate loader compatibility. + Future convertTestWriteSchematic( + Module top, String outPath) async { + final synth = SynthBuilder(top, SchematicSynthesizer()); + final json = + await (synth.synthesizer as SchematicSynthesizer).synthesizeToJson(top); if (!isJS) { - final file = File(out); + final file = File(outPath); await file.create(recursive: true); await file.writeAsString(json); } - return json; + return runYosysLoaderFromString(json); } test('Schematic dump for example Counter', () async { @@ -68,48 +53,17 @@ void main() { await counter.build(); counter.generateSynth(); - final synth = SynthBuilder(counter, SchematicSynthesizer()); - const outPath = 'build/Counter.rohd.json'; - final json = await writeCombinedFromSynth(synth, counter, outPath); + final r = + await convertTestWriteSchematic(counter, 'build/Counter.rohd.json'); - // Always validate the generated JSON with the yosys loader. - final r = await runYosysLoaderFromString(json); - expect(r.success, isTrue, - reason: 'loader should load Counter from string: ${r.error ?? r}'); + expect( + r.success, + isTrue, + reason: 'loader should load Counter from string: ${r.error ?? r}', + ); }); group('SynthBuilder schematic generation for examples', () { - Future writeCombined( - SynthBuilder synth, Module top, String out) async { - final allModules = >{}; - for (final result in synth.synthesisResults) { - if (result is SchematicSynthesisResult) { - final typeName = result.instanceTypeName; - final attrs = Map.from(result.attributes); - if (result.module == top) { - attrs['top'] = 1; - } - allModules[typeName] = { - 'attributes': attrs, - 'ports': result.ports, - 'cells': result.cells, - 'netnames': result.netnames, - }; - } - } - final combined = { - 'creator': 'SchematicSynthesizer via SynthBuilder (rohd)', - 'modules': allModules, - }; - final json = const JsonEncoder.withIndent(' ').convert(combined); - if (!isJS) { - final file = File(out); - await file.create(recursive: true); - await file.writeAsString(json); - } - return json; - } - test('SynthBuilder schematic for Counter', () async { final en = Logic(name: 'en'); final reset = Logic(name: 'reset'); @@ -118,12 +72,8 @@ void main() { final counter = Counter(en, reset, clk); await counter.build(); - final synth = SynthBuilder(counter, SchematicSynthesizer()); - expect(synth.synthesisResults.isNotEmpty, isTrue); - - const outPath = 'build/Counter.synth.rohd.json'; - final json = await writeCombined(synth, counter, outPath); - final r = await runYosysLoaderFromString(json); + final r = await convertTestWriteSchematic( + counter, 'build/Counter.synth.rohd.json'); expect(r.success, isTrue, reason: 'loader should load Counter synth: ${r.error ?? r}'); }); @@ -141,9 +91,8 @@ void main() { final synth = SynthBuilder(fir, SchematicSynthesizer()); expect(synth.synthesisResults.isNotEmpty, isTrue); - const outPath = 'build/FirFilter.synth.rohd.json'; - final json = await writeCombined(synth, fir, outPath); - final r = await runYosysLoaderFromString(json); + final r = await convertTestWriteSchematic( + fir, 'build/FirFilter.synth.rohd.json'); expect(r.success, isTrue, reason: 'loader should load FirFilter synth: ${r.error ?? r}'); }); @@ -161,9 +110,8 @@ void main() { final synth = SynthBuilder(la, SchematicSynthesizer()); expect(synth.synthesisResults.isNotEmpty, isTrue); - const outPath = 'build/LogicArrayExample.synth.rohd.json'; - final json = await writeCombined(synth, la, outPath); - final r = await runYosysLoaderFromString(json); + final r = await convertTestWriteSchematic( + la, 'build/LogicArrayExample.synth.rohd.json'); expect(r.success, isTrue, reason: 'loader should load LogicArrayExample synth: ${r.error ?? r}'); @@ -180,9 +128,8 @@ void main() { final synth = SynthBuilder(oven, SchematicSynthesizer()); expect(synth.synthesisResults.isNotEmpty, isTrue); - const outPath = 'build/OvenModule.synth.rohd.json'; - final json = await writeCombined(synth, oven, outPath); - final r = await runYosysLoaderFromString(json); + final r = await convertTestWriteSchematic( + oven, 'build/OvenModule.synth.rohd.json'); expect(r.success, isTrue, reason: 'loader should load OvenModule synth: ${r.error ?? r}'); }); @@ -195,14 +142,11 @@ void main() { final synth = SynthBuilder(tree, SchematicSynthesizer()); expect(synth.synthesisResults.isNotEmpty, isTrue); - const outPath = 'build/TreeOfTwoInputModules.synth.rohd.json'; - final json = await writeCombined(synth, tree, outPath); - final r = await runYosysLoaderFromString(json); + final r = await convertTestWriteSchematic( + tree, 'build/TreeOfTwoInputModules.synth.rohd.json'); expect(r.success, isTrue, reason: 'loader should load TreeOfTwoInputModules synth: ' '${r.error ?? r}'); - - // Skip loader validation for the tree as it may be deeply nested. }); }); @@ -215,9 +159,8 @@ void main() { final fir = FirFilter(en, resetB, clk, inputVal, [0, 0, 0, 1], bitWidth: 8); await fir.build(); - final synth = SynthBuilder(fir, SchematicSynthesizer()); const outPath = 'build/FirFilter.rohd.json'; - final json = await writeCombinedFromSynth(synth, fir, outPath); + final rStr = await convertTestWriteSchematic(fir, outPath); if (!isJS) { final f = File(outPath); expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); @@ -227,9 +170,12 @@ void main() { expect(r.success, isTrue, reason: 'loader should load FirFilter: ${r.error ?? r}'); } else { - final r = await runYosysLoaderFromString(json); - expect(r.success, isTrue, - reason: 'loader should load FirFilter from string: ${r.error ?? r}'); + expect( + rStr.success, + isTrue, + reason: + 'loader should load FirFilter from string: ${rStr.error ?? rStr}', + ); } }); @@ -242,9 +188,8 @@ void main() { final la = LogicArrayExample(arrayA, id, selectIndexValue, selectFromValue); await la.build(); - final synth = SynthBuilder(la, SchematicSynthesizer()); const outPath = 'build/LogicArrayExample.rohd.json'; - final json = await writeCombinedFromSynth(synth, la, outPath); + final rStr = await convertTestWriteSchematic(la, outPath); if (!isJS) { final f = File(outPath); expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); @@ -254,10 +199,12 @@ void main() { expect(r.success, isTrue, reason: 'loader should load LogicArrayExample: ${r.error ?? r}'); } else { - final r = await runYosysLoaderFromString(json); - expect(r.success, isTrue, - reason: 'loader should load LogicArrayExample from string: ' - '${r.error ?? r}'); + expect( + rStr.success, + isTrue, + reason: 'loader should load LogicArrayExample from string: ' + '${rStr.error ?? rStr}', + ); } }); @@ -269,9 +216,8 @@ void main() { final oven = OvenModule(button, reset, clk); await oven.build(); - final synth = SynthBuilder(oven, SchematicSynthesizer()); const outPath = 'build/OvenModule.rohd.json'; - final json = await writeCombinedFromSynth(synth, oven, outPath); + final rStr = await convertTestWriteSchematic(oven, outPath); if (!isJS) { final f = File(outPath); expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); @@ -281,9 +227,12 @@ void main() { expect(r.success, isTrue, reason: 'loader should load OvenModule: ${r.error ?? r}'); } else { - final r = await runYosysLoaderFromString(json); - expect(r.success, isTrue, - reason: 'loader should load OvenModule from string: ${r.error ?? r}'); + expect( + rStr.success, + isTrue, + reason: + 'loader should load OvenModule from string: ${rStr.error ?? rStr}', + ); } }); @@ -292,25 +241,17 @@ void main() { final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); await tree.build(); - final synth = SynthBuilder(tree, SchematicSynthesizer()); const outPath = 'build/TreeOfTwoInputModules.rohd.json'; - final json = await writeCombinedFromSynth(synth, tree, outPath); + final rStr = await convertTestWriteSchematic(tree, outPath); if (!isJS) { final f = File(outPath); expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); final contents = await f.readAsString(); expect(contents.trim().isNotEmpty, isTrue); } else { - final r = await runYosysLoaderFromString(json); - expect(r.success, isTrue, + expect(rStr.success, isTrue, reason: 'loader should load TreeOfTwoInputModules from string: ' - '${r.error ?? r}'); + '${rStr.error ?? rStr}'); } - - // The loader can hit a recursion/stack overflow on deeply nested - // generated structures for the tree example. For now, ensure the - // ROHD JSON was produced and is non-empty; loader validation is - // skipped to avoid flaky failures. - // If desired, re-enable loader checks with a smaller tree size. }); } From 42f79cd770f9be3608683f5963643dc68214ee5d Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 20 Dec 2025 09:28:44 -0800 Subject: [PATCH 9/9] Refactored to use a mixin approach (almost) for leaf cells --- .../schematic/constant_handler.dart | 8 +- lib/src/synthesizers/schematic/schematic.dart | 4 +- .../schematic/schematic_gates.dart | 244 +++++ .../schematic/schematic_mixins.dart | 851 ++++++++++++++++- .../schematic/schematic_primitives.dart | 856 ------------------ .../schematic/schematic_synthesis_result.dart | 100 +- .../schematic/schematic_synthesizer.dart | 18 +- ...handler.dart => sequential_schematic.dart} | 51 +- 8 files changed, 1179 insertions(+), 953 deletions(-) create mode 100644 lib/src/synthesizers/schematic/schematic_gates.dart delete mode 100644 lib/src/synthesizers/schematic/schematic_primitives.dart rename lib/src/synthesizers/schematic/{sequential_handler.dart => sequential_schematic.dart} (83%) diff --git a/lib/src/synthesizers/schematic/constant_handler.dart b/lib/src/synthesizers/schematic/constant_handler.dart index d54f32375..aca8ba8a7 100644 --- a/lib/src/synthesizers/schematic/constant_handler.dart +++ b/lib/src/synthesizers/schematic/constant_handler.dart @@ -11,7 +11,8 @@ import 'package:rohd/rohd.dart'; -import 'package:rohd/src/synthesizers/schematic/schematic_primitives.dart'; +import 'package:rohd/src/synthesizers/schematic/schematic_gates.dart'; +import 'package:rohd/src/synthesizers/schematic/schematic_mixins.dart'; import 'package:rohd/src/synthesizers/schematic/schematic_synthesis_result.dart'; bool _listEquals(List a, List b) { @@ -129,7 +130,10 @@ class ConstantHandler { // For combinational-like primitives we allocate fresh internal IDs // so they do not become shared pattern-level $const cells; for // other primitives we register const patterns as before. - final childPrimDesc = Primitives.instance.lookupForModule(childModule); + final childPrimDesc = (childModule is Schematic + ? childModule.primitiveDescriptor() + : null) ?? + CoreGatePrimitives.instance.lookupByType(childModule); final childIsPrimitive = childPrimDesc != null; // Only treat explicit ROHD helper modules with definitionName // 'Combinational' or 'Sequential' as candidates for the const-input diff --git a/lib/src/synthesizers/schematic/schematic.dart b/lib/src/synthesizers/schematic/schematic.dart index 59a114414..fa8b7f089 100644 --- a/lib/src/synthesizers/schematic/schematic.dart +++ b/lib/src/synthesizers/schematic/schematic.dart @@ -4,9 +4,9 @@ export 'constant_handler.dart'; export 'module_utils.dart'; export 'passthrough_handler.dart'; +export 'schematic_gates.dart'; export 'schematic_mixins.dart'; -export 'schematic_primitives.dart'; export 'schematic_synthesis_result.dart'; export 'schematic_synthesizer.dart'; -export 'sequential_handler.dart'; +export 'sequential_schematic.dart'; export 'yosys/yosys_loader_helper.dart'; diff --git a/lib/src/synthesizers/schematic/schematic_gates.dart b/lib/src/synthesizers/schematic/schematic_gates.dart new file mode 100644 index 000000000..da5629ed2 --- /dev/null +++ b/lib/src/synthesizers/schematic/schematic_gates.dart @@ -0,0 +1,244 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// schematic_gates.dart +// Schematic primitive descriptors for core gate modules. +// +// 2025 December 20 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/schematic/schematic_mixins.dart'; + +/// Registry of primitive descriptors for core ROHD gate modules. +/// +/// This provides schematic synthesis support for core gates without modifying +/// their implementation. The descriptors are looked up by module type. +class CoreGatePrimitives { + CoreGatePrimitives._() { + _populateDefaults(); + } + + /// Singleton instance. + static final CoreGatePrimitives instance = CoreGatePrimitives._(); + + /// Map from runtime type to primitive descriptor. + final Map _descriptors = {}; + + /// Look up a primitive descriptor for a module by its runtime type. + /// + /// Returns `null` if no descriptor is registered for this type. + PrimitiveDescriptor? lookupByType(Module m) => _descriptors[m.runtimeType]; + + /// Register a primitive descriptor for a module type. + void register(Type type, PrimitiveDescriptor descriptor) { + _descriptors[type] = descriptor; + } + + void _populateDefaults() { + // Two-input gates + register( + And2Gate, + const PrimitiveDescriptor( + primitiveName: r'$and', + portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + Or2Gate, + const PrimitiveDescriptor( + primitiveName: r'$or', + portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + Xor2Gate, + const PrimitiveDescriptor( + primitiveName: r'$xor', + portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + + // Single-input gates + register( + NotGate, + const PrimitiveDescriptor( + primitiveName: r'$not', + portMap: {'A': 're:^in_.+'}, + portDirs: {'A': 'input', 'Y': 'output'}, + )); + + // Unary reduction gates + register( + AndUnary, + const PrimitiveDescriptor( + primitiveName: r'$logic_and', + portDirs: {'A': 'input', 'Y': 'output'}, + )); + register( + OrUnary, + const PrimitiveDescriptor( + primitiveName: r'$logic_or', + portDirs: {'A': 'input', 'Y': 'output'}, + )); + register( + XorUnary, + const PrimitiveDescriptor( + primitiveName: r'$xor', + portDirs: {'A': 'input', 'Y': 'output'}, + )); + + // Comparison gates + register( + Equals, + const PrimitiveDescriptor( + primitiveName: r'$eq', + portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, + paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + NotEquals, + const PrimitiveDescriptor( + primitiveName: r'$ne', + portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, + paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + LessThan, + const PrimitiveDescriptor( + primitiveName: r'$lt', + portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, + paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + LessThanOrEqual, + const PrimitiveDescriptor( + primitiveName: r'$le', + portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, + paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + GreaterThan, + const PrimitiveDescriptor( + primitiveName: r'$gt', + portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, + paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + GreaterThanOrEqual, + const PrimitiveDescriptor( + primitiveName: r'$ge', + portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, + paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + + // Shift operations + register( + LShift, + const PrimitiveDescriptor( + primitiveName: r'$shl', + portMap: {'A': 're:^in_.+', 'B': 're:^shiftAmount_.+'}, + paramFromPort: {'A_WIDTH': 'A'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + RShift, + const PrimitiveDescriptor( + primitiveName: r'$shr', + portMap: {'A': 're:^in_.+', 'B': 're:^shiftAmount_.+'}, + paramFromPort: {'A_WIDTH': 'A'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + ARShift, + const PrimitiveDescriptor( + primitiveName: r'$shiftx', + portMap: {'A': 'A', 'B': 'B', 'Y': 'Y'}, + paramFromPort: {'A_WIDTH': 'A'}, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + + // Bus operations + register( + Swizzle, + const PrimitiveDescriptor( + primitiveName: r'$concat', + portMap: { + 'A': r're:^in\d+_.+', + 'B': r're:^in\d+_.+', + 'Y': r're:^(?:swizzled$|out$)' + }, + portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + register( + BusSubset, + const PrimitiveDescriptor( + primitiveName: r'$slice', + portMap: { + 'A': r're:^in\d*_.+|^in_.+|^A$', + 'Y': r're:.*_subset_\d+_\d+|^out$' + }, + paramFromPort: {'HIGH': 'A', 'LOW': 'A'}, + portDirs: {'A': 'input', 'Y': 'output'}, + )); + + // Mux + register( + Mux, + const PrimitiveDescriptor( + primitiveName: r'$mux', + portMap: { + 'S': + r're:^(?:_?control_.+|_?sel_.+|_?s_.+|in0_.+|in1_.+|A$|.*_subset_\d+_\d+)', + 'A': r're:^(?:d1_.+|B$|d1$|d1_.+)', + 'B': r're:^(?:d0_.+|C$|d0$|d0_.+)', + 'Y': r're:^(?:out$|Y$)' + }, + paramFromPort: {'WIDTH': 'B'}, + portDirs: {'S': 'input', 'A': 'input', 'B': 'input', 'Y': 'output'}, + )); + + // Arithmetic with dynamic ports + register( + Add, + const PrimitiveDescriptor( + primitiveName: r'$add', + useRawPortNames: true, + )); + + // FlipFlop + register( + FlipFlop, + const PrimitiveDescriptor( + primitiveName: r'$dff', + portMap: { + 'd': 'D', + 'q': 'Q', + 'clk': 'CLK', + 'en': 'EN', + 'reset': 'SRST' + }, + portDirs: { + 'd': 'input', + 'q': 'output', + 'clk': 'input', + 'en': 'input', + 'reset': 'input' + }, + )); + + // Combinational (special case) + register( + Combinational, + const PrimitiveDescriptor( + primitiveName: r'$combinational', + useRawPortNames: true, + )); + } +} diff --git a/lib/src/synthesizers/schematic/schematic_mixins.dart b/lib/src/synthesizers/schematic/schematic_mixins.dart index c253cbb6c..61beafbd1 100644 --- a/lib/src/synthesizers/schematic/schematic_mixins.dart +++ b/lib/src/synthesizers/schematic/schematic_mixins.dart @@ -4,11 +4,546 @@ // schematic_mixins.dart // Definition for Schematic Mixins for controlling schematic synthesis. // -// 2025 December +// 2025 December 20 // Author: Desmond Kirkpatrick +// Architecture Overview: +// +// The schematic synthesis system uses a mixin-first architecture where modules +// can control their schematic representation. There are several approaches: +// +// 1. **Core ROHD modules** (And2Gate, Or2Gate, etc.): +// - NOT modified with mixins +// - Registered in CoreGatePrimitives by runtime type +// - Example: CoreGatePrimitives.register(And2Gate, descriptor) +// +// 2. **Simple user primitives**: +// - Use PrimitiveSchematic mixin +// - Override primitiveDescriptor() to provide descriptor +// - Automatic port mapping and parameter inference +// +// 3. **Inline primitives**: +// - Use InlineSchematic mixin +// - Override schematicPrimitiveName and optionally schematicParameters +// - Good for simple wrappers with fixed primitive types +// +// 4. **Complex primitives** (like Sequential): +// - Use Schematic mixin directly +// - Override primitiveDescriptor() and/or emitSchematicCells() +// - Full control over cell emission (can emit multiple cells) +// +// 5. **Custom modules** (rare): +// - Use Schematic mixin +// - Override schematicCell() for custom instantiation +// - Or schematicDefinition() for custom module definitions + import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/schematic/module_utils.dart'; +import 'package:rohd/src/synthesizers/schematic/schematic_gates.dart'; +import 'package:rohd/src/synthesizers/schematic/schematic_synthesis_result.dart'; + +/// Descriptor describing how a ROHD helper module maps to a Yosys +/// primitive type. +class PrimitiveDescriptor { + /// The Yosys primitive type name (e.g. "\\$concat", "\\$dff", "\\$mux"). + final String primitiveName; + + /// Map from the ROHD module's port name to the primitive port name. + final Map portMap; + + /// Map of primitive parameter name -> ROHD port name or expression key. + final Map paramFromPort; + + /// Optional primitive port directions (primitive port -> + /// 'input'|'output'|'inout'). When provided in a descriptor, these directions + /// are used directly and the automatic direction inference is skipped for + /// those ports. + final Map portDirs; + + /// Default parameter values supplied by the descriptor (applied before + /// inference). Use this to move always-1 defaults into registration. + final Map defaultParams; + + /// When true, use the ROHD module's actual port names directly instead of + /// generating generic A/B/Y names. Useful for modules like Sequential that + /// have dynamic port names per instance. + final bool useRawPortNames; + + /// Creates a [PrimitiveDescriptor] for leaf schematic primitive mapping. + const PrimitiveDescriptor( + {required this.primitiveName, + this.portMap = const {}, + this.paramFromPort = const {}, + this.portDirs = const {}, + this.defaultParams = const {}, + this.useRawPortNames = false}); +} + +/// Helper methods for primitive cell computation and connection mapping. +/// +/// These static methods provide the core logic for mapping ROHD modules to +/// schematic primitives, including parameter inference and port mapping. +class PrimitiveHelper { + PrimitiveHelper._(); + + /// Compute the primitive cell representation for a `Module` that maps to + /// a known primitive descriptor. Returns a map containing keys: + /// - 'type' -> String primitive type (e.g. r'$concat') + /// - 'parameters' -> `Map `of parameter values + /// - 'port_directions' -> `Map` mapping primitive port names + /// to directions expected by the loader ('input'/'output'/'inout') + static Map computePrimitiveCell( + Module childModule, PrimitiveDescriptor prim) { + final cellType = prim.primitiveName; + final parameters = {}; + // Apply any descriptor-provided default parameters before inference. + // This lets registrations move always-1 (or other) defaults into the + // descriptor so they don't need to be inferred here. + if (prim.defaultParams.isNotEmpty) { + parameters.addAll(prim.defaultParams); + } + + void ensureIntParam(String k, int defaultVal) { + final v = parameters[k]; + if (v is int) { + if (v <= 0) { + parameters[k] = defaultVal; + } + } else { + parameters[k] = defaultVal; + } + } + + ensureIntParam('A_WIDTH', 1); + ensureIntParam('B_WIDTH', 1); + ensureIntParam('Y_WIDTH', 1); + if (parameters['OFFSET'] == null) { + parameters['OFFSET'] = 0; + } + + final ywVal = parameters['Y_WIDTH']; + if ((parameters['HIGH'] == null || parameters['LOW'] == null) && + ywVal is int) { + parameters['LOW'] = 0; + parameters['HIGH'] = (ywVal - 1) >= 0 ? (ywVal - 1) : 0; + } + + // Initialize `portDirs` from the descriptor. + final portDirs = {}..addAll(prim.portDirs); + + return { + 'type': cellType, + 'parameters': parameters, + 'port_directions': portDirs, + }; + } + + /// Finalize/adjust primitive parameters using the connection map built by + /// the dumper. This allows inference that depends on actual bit-id + /// connections (for example, determining slice offsets/high/low and + /// input widths) rather than only on port names or descriptor defaults. + /// + /// - [childModule] is the module instance for the primitive. + /// - [prim] is the primitive descriptor. + /// - [parameters] is the mutable parameters map produced by + /// `computePrimitiveCell` (will be modified in-place). + /// - [connMap] maps primitive port names (A/B/Y/etc) to lists of bit ids + /// (as produced by the dumper's connection resolution). Bit ids may be + /// integers (net ids) or string tokens for constants. + static void finalizePrimitiveCell( + Module childModule, + PrimitiveDescriptor prim, + Map parameters, + Map> connMap) { + // Apply simple paramFromPort mappings (e.g., A_WIDTH -> A) + prim.paramFromPort.entries + .where((e) => e.key.endsWith('_WIDTH')) + .forEach((e) { + final bits = connMap[e.value]; + if (bits != null) { + parameters[e.key] = bits.length; + } + }); + + // Specialized handling for $slice (BusSubset) primitives. Compute + // OFFSET/HIGH/LOW/Y_WIDTH/A_WIDTH when we have concrete connection ids + // for the source ('A') and the result ('Y'). + if (prim.primitiveName == r'$slice') { + final aBits = connMap['A'] ?? []; + final yBits = connMap['Y'] ?? []; + + // Populate widths if available + if (aBits.isNotEmpty) { + parameters['A_WIDTH'] = aBits.length; + } + if (yBits.isNotEmpty) { + parameters['Y_WIDTH'] = yBits.length; + } + + // For offset/high/low we need integer net ids to compute positions + final aInts = aBits.whereType().toList()..sort(); + final yInts = yBits.whereType().toList()..sort(); + + if (aInts.isNotEmpty && yInts.isNotEmpty) { + // Build index map from A net id -> position within A (0-based) + final aIndex = {}; + for (var i = 0; i < aInts.length; i++) { + aIndex[aInts[i]] = i; + } + + // Map each Y net id to its index in A; require that all Y ids exist + // within A to compute a contiguous OFFSET/HIGH/LOW. If not present, + // fall back to conservative defaults. + final mappedCandidates = yInts.map((yId) => aIndex[yId]).toList(); + final allMapped = mappedCandidates.every((e) => e != null); + final mappedIndices = + allMapped ? mappedCandidates.cast().toList() : []; + + if (allMapped && mappedIndices.isNotEmpty) { + mappedIndices.sort(); + final low = mappedIndices.first; + final high = mappedIndices.last; + parameters['OFFSET'] = low; + parameters['LOW'] = low; + parameters['HIGH'] = high; + parameters['Y_WIDTH'] = mappedIndices.length; + parameters['A_WIDTH'] = aInts.length; + } + // If connection-based mapping failed to determine offsets, try a + // fallback: parse output names for `_subset_HIGH_LOW` patterns which + // ROHD may emit for BusSubset outputs. This preserves previous + // behavior that relied on naming heuristics when structural mapping + // is not straightforward. + if ((parameters['LOW'] == null || + parameters['HIGH'] == null || + (parameters['LOW'] is int && + parameters['LOW'] == 0 && + parameters['HIGH'] is int && + parameters['HIGH'] == 0)) && + childModule.outputs.isNotEmpty) { + final re = RegExp(r'_subset_(\d+)_(\d+)'); + final match = childModule.outputs.keys + .map(re.firstMatch) + .firstWhere((m) => m != null, orElse: () => null); + if (match != null) { + final hi = int.parse(match.group(1)!); + final lo = int.parse(match.group(2)!); + final low = hi < lo ? hi : lo; + final high = hi < lo ? lo : hi; + parameters['LOW'] = low; + parameters['HIGH'] = high; + parameters['OFFSET'] = low; + parameters['Y_WIDTH'] = (high - low) + 1; + } + } + } + } + + // For $concat (concat/swizzle), derive input widths from mapped ports + if (prim.primitiveName == r'$concat') { + // Common placeholders A/B may represent inputs; if present, set widths + if (connMap.containsKey('A')) { + parameters['A_WIDTH'] = connMap['A']!.length; + } + if (connMap.containsKey('B')) { + parameters['B_WIDTH'] = connMap['B']!.length; + } + // Update Y width as sum if A/B provided + final aW = parameters['A_WIDTH']; + final bW = parameters['B_WIDTH']; + if (aW is int && bW is int) { + parameters['Y_WIDTH'] = aW + bW; + } + } + } + + /// Deterministically map ROHD port names to primitive port names. + /// + /// Returns a map where the key is the ROHD port name and the value is the + /// corresponding primitive port name. The mapping rules are: + /// 1. If the descriptor's `portMap` provides a literal ROHD name for a + /// primitive port and that ROHD port exists, use it. + /// 2. Group placeholder mappings (single-letter placeholders like 'A', 'B') + /// and map remaining ROHD ports in deterministic sorted order to the + /// placeholder-named primitive ports (sorted). + /// 3. Any remaining primitive ports are assigned positionally to remaining + /// ROHD ports in sorted order. + static Map mapRohdToPrimitivePorts(PrimitiveDescriptor prim, + Module childModule, Map portDirs) { + final rohdInputs = childModule.inputs.keys.toList()..sort(); + final rohdOutputs = childModule.outputs.keys.toList()..sort(); + final rohdInouts = childModule.inOuts.keys.toList()..sort(); + + // Normalize prim.portMap: it may be registered in either direction + // (primPort -> rohdName) or (rohdName -> primPort). Detect which form + // is used and build a `primToRohd` map. Build the set of primitive port + // names from both the explicit `portDirs` and any keys present in the + // descriptor's `portMap` so registrations may omit input entries and + // only declare outputs/inouts if desired. Missing directions default to + // 'input' during mapping below. + final primPortNames = {} + ..addAll(portDirs.keys) + ..addAll(prim.portMap.keys); + final rohdPortNames = childModule.ports.keys.toSet(); + + // Build mapping candidates: for each primitive port, collect either a + // literal mapping or a deterministic list of ROHD names matching a + // regex. We will consume these lists deterministically when assigning + // ROHD ports so regex matches are not accidentally reused or picked + // nondeterministically by different prim ports. + final primToRohdLists = >{}; + // Detect inverted maps (rohd->prim) where keys are ROHD names. + final anyKeyIsRohd = prim.portMap.keys.any(rohdPortNames.contains); + final anyKeyIsPrim = prim.portMap.keys.any(primPortNames.contains); + if (anyKeyIsRohd && !anyKeyIsPrim) { + // Invert rohd->prim into prim->rohd lists + for (final e in prim.portMap.entries) { + primToRohdLists[e.value] = [e.key]; + } + } else { + for (final e in prim.portMap.entries) { + final primPort = e.key; + final mapping = e.value; + if (mapping.startsWith('re:')) { + final pattern = RegExp(mapping.substring(3)); + // Collect all ROHD ports matching the regex and sort + // deterministically + final matches = rohdPortNames.where(pattern.hasMatch).toList() + ..sort(); + if (matches.isNotEmpty) { + primToRohdLists[primPort] = matches; + } + } else { + // Literal mapping or placeholder (like 'A'/'B'). Store the literal + // string so calling code can detect placeholders vs literal names. + primToRohdLists[primPort] = [mapping]; + } + } + } + + // Helper to map for a given direction. Treat prim ports missing from + // `portDirs` as inputs by default so registrations can declare only + // outputs/inouts when convenient. + Map doDirection(String direction, List rohdPorts) { + String getDir(String p) => portDirs[p] ?? 'input'; + // Primitive ports of this direction, sorted + final primPorts = + primPortNames.where((p) => getDir(p) == direction).toList()..sort(); + + final mapping = {}; // rohd -> prim + + // 1) Literal mappings from portMap. For regex mappings we collected + // candidate lists; pick the first unassigned ROHD match for each + // prim-port and mark it assigned so matches are not reused. + final assignedPrim = {}; + final assignedRohd = {}; + for (final primPort in primPorts) { + final candidates = primToRohdLists[primPort]; + if (candidates != null && candidates.isNotEmpty) { + // If any candidate is an actual ROHD port name, assign the first + // one that is not already assigned. + String? chosen; + for (final cand in candidates) { + if (rohdPorts.contains(cand) && !assignedRohd.contains(cand)) { + chosen = cand; + break; + } + } + if (chosen != null) { + mapping[chosen] = primPort; + assignedPrim.add(primPort); + assignedRohd.add(chosen); + continue; + } + // If candidates exist but none are actual ROHD names, fall + // through to placeholder handling below (mapping may be a + // placeholder like 'A' or 'B'). + } + // Also support the legacy case where primToRohdLists may be empty + // and prim.portMap contains a literal ROHD name. + final mappedLiteral = prim.portMap[primPort]; + if (mappedLiteral != null && rohdPorts.contains(mappedLiteral)) { + mapping[mappedLiteral] = primPort; + assignedPrim.add(primPort); + assignedRohd.add(mappedLiteral); + continue; + } + } + + // 2) Placeholder groups (e.g., 'A', 'B') + // Group prim ports by their placeholder value + final placeholderGroups = >{}; + for (final primPort in primPorts) { + if (assignedPrim.contains(primPort)) { + continue; + } + final mapped = prim.portMap[primPort]; + if (mapped != null && RegExp(r'^[A-Z][0-9]*$').hasMatch(mapped)) { + final key = mapped.replaceAll(RegExp('[0-9]+'), ''); + placeholderGroups.putIfAbsent(key, () => []).add(primPort); + assignedPrim.add(primPort); + } + } + // Sort each group's prim ports for deterministic assignment + for (final g in placeholderGroups.values) { + g.sort(); + } + + // Assign ROHD ports to placeholder prim ports in sorted order + var rohdIdx = 0; + for (final primList in placeholderGroups.values) { + for (final primPort in primList) { + while (rohdIdx < rohdPorts.length && + assignedRohd.contains(rohdPorts[rohdIdx])) { + rohdIdx++; + } + if (rohdIdx >= rohdPorts.length) { + break; + } + final rohdName = rohdPorts[rohdIdx++]; + mapping[rohdName] = primPort; + assignedRohd.add(rohdName); + } + } + + // 3) Positional mapping for any remaining prim ports + // Collect remaining prim ports not assigned + final remainingPrim = + primPorts.where((p) => !mapping.values.contains(p)).toList()..sort(); + // Collect remaining rohd ports + final remainingRohd = + rohdPorts.where((r) => !assignedRohd.contains(r)).toList()..sort(); + + // No primitive-specific positional heuristics; rely on descriptor + // mappings and deterministic regex consumption above. + + for (var i = 0; + i < remainingPrim.length && i < remainingRohd.length; + i++) { + mapping[remainingRohd[i]] = remainingPrim[i]; + } + + return mapping; + } + + final result = {} + ..addAll(doDirection('input', rohdInputs)) + ..addAll(doDirection('output', rohdOutputs)) + ..addAll(doDirection('inout', rohdInouts)); + // If the descriptor did not supply explicit `portDirs`, infer primitive + // port directions from the instantiation point (ROHD ports) so the + // caller can use instance-derived directions rather than requiring the + // descriptor to provide them. This mirrors how combinational/raw-port + // instances display correctly using their instantiation context. + if (prim.portDirs.isEmpty) { + // Build reverse map: primPort -> list of rohd ports mapped to it + final primToRohd = >{}; + for (final e in result.entries) { + primToRohd.putIfAbsent(e.value, () => []).add(e.key); + } + + String decideForPrim(String primPort) { + final rohdList = primToRohd[primPort] ?? const []; + return rohdList.any((r) => childModule.ports[r]?.isInOut ?? false) + ? 'inout' + : (rohdList.any((r) => childModule.ports[r]?.isOutput ?? false) + ? 'output' + : 'input'); + } + + // Populate missing entries in the provided portDirs map. + for (final primPort in primPortNames) { + portDirs.putIfAbsent( + primPort, + () => primPort == 'Y' + ? (childModule.outputs.isNotEmpty ? 'output' : 'input') + : decideForPrim(primPort)); + } + } + + return result; + } + + /// Build a primitive connection map (`primPort` -> bit-id list) using the + /// deterministic ROHD->primitive port mapping and the provided + /// `idsForRohd` lookup function which returns the bit ids for a ROHD + /// port name. This function also calls the safe finalizer to allow + /// parameter inference that depends on concrete connections. + static Map> buildPrimitiveConnections( + Module childModule, + PrimitiveDescriptor prim, + Map parameters, + Map portDirs, + List Function(String rohdName) idsForRohd) { + final connMap = >{}; + final rohdToPrim = mapRohdToPrimitivePorts(prim, childModule, portDirs); + + for (final entry in rohdToPrim.entries) { + final rohdName = entry.key; + final primPortName = entry.value; + final ids = idsForRohd(rohdName); + if (ids.isNotEmpty) { + connMap[primPortName] = ids; + } + } + + // Allow primitive logic to finalize parameters using the concrete + // connection ids we built. + finalizePrimitiveCell(childModule, prim, parameters, connMap); + + return connMap; + } + + /// Convenience wrapper used by the dumper when the lookup for ROHD port + /// ids needs to resolve ports via either a previously-produced + /// `SynthesisResult` for the child (preferred) or a child `ModuleMap` + /// (fallback). The [idsForChildLogic] callback should accept a `Logic` + /// and return the corresponding bit id list. The [childResultLookup] + /// callback, when provided, should return the `SynthesisResult` for a + /// given child module or null if not present. This allows using cached + /// synthesis outputs rather than rebuilding ModuleMaps. + static Map> + buildPrimitiveConnectionsWithChildLogicLookup( + Module childModule, + PrimitiveDescriptor prim, + Map parameters, + Map portDirs, + SynthesisResult? Function(Module) childResultLookup, + List Function(Logic) idsForChildLogic) { + // Adapter: convert rohdName -> idsForRohd by resolving the Logic + // either from the SchematicSynthesisResult (if available), the + // child ModuleMap contained within that result, or directly from + // the child module as a last resort. + List idsForRohd(String rohdName) { + final res = childResultLookup(childModule); + // If we have a SchematicSynthesisResult, try to use its port map. + if (res is SchematicSynthesisResult) { + final ports = res.ports; + if (ports.containsKey(rohdName)) { + // ports[rohdName]['bits'] is a List of bit ids + final bits = (ports[rohdName]! as Map)['bits']; + if (bits is List) { + return bits.cast(); + } + } + } + + // Fallback: try to obtain the logic from the child's ModuleMap if + // the result provides a way (many SchematicSynthesisResults do not + // expose ModuleMap directly), otherwise use the module's port + // reference and resolve via idsForChildLogic. + final logic = childModule.ports[rohdName]; + if (logic == null) { + return []; + } + return idsForChildLogic(logic); + } + + return buildPrimitiveConnections( + childModule, prim, parameters, portDirs, idsForRohd); + } +} /// Represents a primitive cell in the schematic JSON output. /// @@ -54,37 +589,38 @@ enum SchematicDefinitionGenerationType { /// Similar to [SystemVerilog] mixin for SystemVerilog synthesis, this mixin /// provides hooks for modules to customize their schematic representation. /// -/// ## Example +/// ## Architecture +/// +/// For simple primitives, prefer using [PrimitiveSchematic] mixin which handles +/// most common cases automatically. Use this mixin directly for: +/// - Complex primitives requiring custom cell emission (see +/// [emitSchematicCells]) +/// - Modules needing descriptor-based primitives (see [primitiveDescriptor]) +/// - Modules requiring custom definition generation +/// +/// Core ROHD modules (And2Gate, Or2Gate, etc.) are registered in +/// [CoreGatePrimitives] by type and do not need mixins added. +/// +/// ## Example: Descriptor-based primitive /// /// ```dart -/// class MyCustomPrimitive extends Module with Schematic { -/// MyCustomPrimitive(Logic a, Logic b) { +/// class MyPrimitive extends Module with Schematic { +/// MyPrimitive(Logic a, Logic b) { /// a = addInput('a', a); /// b = addInput('b', b); /// addOutput('y') <= a & b; /// } /// /// @override -/// SchematicDefinitionGenerationType get schematicDefinitionType => -/// SchematicDefinitionGenerationType.none; -/// -/// @override -/// SchematicCellDefinition? schematicCell( -/// String instanceType, -/// String instanceName, -/// Map ports, -/// ) { -/// return SchematicCellDefinition( -/// type: r'$and', -/// parameters: { -/// 'A_WIDTH': ports['a']!.width, -/// 'B_WIDTH': ports['b']!.width, -/// }, -/// portDirections: {'A': 'input', 'B': 'input', 'Y': 'output'}, -/// ); -/// } +/// PrimitiveDescriptor primitiveDescriptor() => const PrimitiveDescriptor( +/// primitiveName: r'$and', +/// portMap: {'a': 'A', 'b': 'B', 'y': 'Y'}, +/// portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, +/// ); /// } /// ``` +/// +/// For even simpler cases, use [PrimitiveSchematic] mixin instead. mixin Schematic on Module { /// Generates a custom schematic cell definition to be used when this module /// is instantiated as a child in another module's schematic. @@ -165,9 +701,81 @@ mixin Schematic on Module { /// Only used when [isSchematicPrimitive] is `true` or [schematicCell] /// returns `null` but the synthesizer determines this is a primitive. /// - /// By default, returns `null`, meaning the module's definition name is used. + /// By default, returns `null`, which defers to the descriptor or type + /// registry for the primitive name. String? get schematicPrimitiveName => null; + /// Optional: provide a [PrimitiveDescriptor] describing how this module + /// should be emitted as a primitive cell. If non-null, this descriptor + /// will be used instead of [CoreGatePrimitives] type-based lookup. + /// + /// Default: `null` (no module-specific descriptor). + @internal + PrimitiveDescriptor? primitiveDescriptor() => null; + + /// Optional hook for modules to directly emit one or more schematic cells + /// for their instantiation. This allows a module to control complex + /// expansions (for example, `Sequential` expanding to mux + dff cells) + /// without requiring external handlers. + /// + /// Returning `true` indicates the module emitted the necessary cells and + /// the caller should `continue` (no further primitive handling). The + /// default implementation returns `false`. + @internal + bool emitSchematicCells({ + required Map ports, + required Map> internalNetIds, + required List Function(Logic) idsForChildLogic, + required Map> cells, + required Map> syntheticNets, + required int Function() nextInternalNetIdGetter, + required void Function(int) nextInternalNetIdSetter, + }) => + false; + + /// Optional: allow a module to build the final primitive cell map for a + /// given [PrimitiveDescriptor]. If non-null is returned, it will be used + /// directly as the cell JSON (with keys `type`, `parameters`, + /// `port_directions`, `connections`, etc.). Return `null` to defer to + /// [PrimitiveHelper] for standard cell generation. + @internal + Map? schematicPrimitiveCell( + PrimitiveDescriptor prim, + List Function(Logic) idsForChildLogic, { + required Map> internalNetIds, + required Map> syntheticNets, + required int Function() nextInternalNetIdGetter, + required void Function(int) nextInternalNetIdSetter, + bool filterConstInputsToCombinational = false, + SynthesisResult? Function(Module)? lookupExistingResult, + }) => + null; + + /// Helper to determine whether a [Module] should be considered a + /// schematic primitive for purposes of emission and validation. + /// + /// This centralizes primitive detection logic. Checks module-provided + /// hooks first (e.g., `primitiveDescriptor()`, `isSchematicPrimitive`), + /// then consults [CoreGatePrimitives] for type-based registration. + static bool isPrimitiveModule(Module m) { + if (m is Schematic) { + if (m.primitiveDescriptor() != null || + m.isSchematicPrimitive || + m.schematicPrimitiveName != null || + m.schematicDefinitionType == SchematicDefinitionGenerationType.none) { + return true; + } + } + + // Check core gate registry + if (CoreGatePrimitives.instance.lookupByType(m) != null) { + return true; + } + + // All core ROHD modules are in CoreGatePrimitives; no fallback needed + return false; + } + /// Indicates that this module is only wires, no logic inside, which can be /// leveraged for pruning in schematic generation. @internal @@ -178,6 +786,31 @@ mixin Schematic on Module { /// as an inline primitive cell without generating a separate definition. /// /// This is the schematic equivalent of [InlineSystemVerilog]. +/// +/// Use this mixin when you have a simple module that should always be +/// represented as a specific primitive type with fixed parameters. +/// +/// ## Example +/// +/// ```dart +/// class MySimpleAnd extends Module with InlineSchematic { +/// MySimpleAnd(Logic a, Logic b) { +/// a = addInput('a', a); +/// b = addInput('b', b); +/// addOutput('y') <= a & b; +/// } +/// +/// @override +/// String get schematicPrimitiveName => r'$and'; +/// +/// @override +/// Map get schematicPortMap => { +/// 'a': 'A', +/// 'b': 'B', +/// 'y': 'Y', +/// }; +/// } +/// ``` mixin InlineSchematic on Module implements Schematic { /// The Yosys primitive type to use for this inline cell. /// @@ -234,3 +867,175 @@ mixin InlineSchematic on Module implements Schematic { @override bool get isSchematicWiresOnly => false; } + +/// A mixin for modules that can be represented as Yosys primitive cells using +/// a [PrimitiveDescriptor]. +/// +/// This mixin provides a default implementation of [schematicPrimitiveCell] +/// that builds the primitive cell JSON from the descriptor and module ports. +/// +/// Modules using this mixin should override [primitiveDescriptor] to provide +/// their descriptor. The mixin handles port mapping, parameter inference, and +/// connection building automatically. +/// +/// ## Example +/// +/// ```dart +/// class And2Gate extends Module with PrimitiveSchematic { +/// And2Gate(Logic a, Logic b) { +/// a = addInput('a', a, width: a.width); +/// b = addInput('b', b, width: b.width); +/// addOutput('y', width: a.width); +/// } +/// +/// @override +/// PrimitiveDescriptor primitiveDescriptor() => const PrimitiveDescriptor( +/// primitiveName: r'$and', +/// portMap: {'a': 'A', 'b': 'B', 'y': 'Y'}, +/// portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, +/// ); +/// } +/// ``` +mixin PrimitiveSchematic on Module implements Schematic { + @override + bool get isSchematicPrimitive => true; + + @override + SchematicDefinitionGenerationType get schematicDefinitionType => + SchematicDefinitionGenerationType.none; + + @override + Map? schematicDefinition(String definitionType) => {}; + + @override + @internal + Map? schematicPrimitiveCell( + PrimitiveDescriptor prim, + List Function(Logic) idsForChildLogic, { + required Map> internalNetIds, + required Map> syntheticNets, + required int Function() nextInternalNetIdGetter, + required void Function(int) nextInternalNetIdSetter, + bool filterConstInputsToCombinational = false, + SynthesisResult? Function(Module)? lookupExistingResult, + }) { + // Build primitive cell using the descriptor. + // This is a simplified version that handles common cases. + // Modules can override this for custom behavior. + + final cellType = prim.primitiveName; + final parameters = {...prim.defaultParams}; + final portDirs = {...prim.portDirs}; + final connections = >{}; + + // Access module's ports - 'this' is the Module since mixin is 'on Module' + final modulePorts = (this as Module).ports; + + // Build ROHD->primitive port mapping + final rohdToPrim = {}; + if (prim.useRawPortNames) { + // Use module's actual port names + for (final name in modulePorts.keys) { + rohdToPrim[name] = name; + } + } else { + // Use descriptor's portMap + for (final entry in prim.portMap.entries) { + final primPort = entry.key; + final rohdName = entry.value; + if (rohdName.startsWith('re:')) { + // Regex mapping - find matching ROHD port + final pattern = RegExp(rohdName.substring(3)); + final matches = modulePorts.keys.where(pattern.hasMatch).toList() + ..sort(); + if (matches.isNotEmpty) { + rohdToPrim[matches.first] = primPort; + } + } else if (modulePorts.containsKey(rohdName)) { + // Direct mapping + rohdToPrim[rohdName] = primPort; + } + } + + // Fill in unmapped ports with simple heuristics + final unmappedRohd = modulePorts.keys.toSet()..removeAll(rohdToPrim.keys); + final unmappedPrim = portDirs.keys.toSet()..removeAll(rohdToPrim.values); + + // Simple positional mapping for remaining ports + final unmappedRohdList = unmappedRohd.toList()..sort(); + final unmappedPrimList = unmappedPrim.toList()..sort(); + for (var i = 0; + i < unmappedRohdList.length && i < unmappedPrimList.length; + i++) { + rohdToPrim[unmappedRohdList[i]] = unmappedPrimList[i]; + } + } + + // Build connections using the mapping + for (final entry in rohdToPrim.entries) { + final rohdName = entry.key; + final primPort = entry.value; + final logic = modulePorts[rohdName]; + if (logic != null) { + final ids = idsForChildLogic(logic); + if (ids.isNotEmpty) { + connections[primPort] = ids; + } + } + } + + // Infer parameters from port widths if specified in descriptor + for (final entry in prim.paramFromPort.entries) { + final paramName = entry.key; + final primPort = entry.value; + if (paramName.endsWith('_WIDTH') && connections.containsKey(primPort)) { + parameters[paramName] = connections[primPort]!.length; + } + } + + // Ensure minimum parameter values + void ensureIntParam(String k, int defaultVal) { + final v = parameters[k]; + if (v is int) { + if (v <= 0) { + parameters[k] = defaultVal; + } + } else { + parameters[k] = defaultVal; + } + } + + ensureIntParam('A_WIDTH', 1); + ensureIntParam('B_WIDTH', 1); + ensureIntParam('Y_WIDTH', 1); + + // Infer port directions if not provided + if (prim.portDirs.isEmpty) { + for (final entry in rohdToPrim.entries) { + final rohdName = entry.key; + final primPort = entry.value; + final logic = modulePorts[rohdName]; + if (logic != null && !portDirs.containsKey(primPort)) { + portDirs[primPort] = logic.isInput + ? 'input' + : logic.isOutput + ? 'output' + : 'inout'; + } + } + } + + return { + 'hide_name': 0, + 'type': cellType, + 'parameters': parameters, + 'attributes': {}, + 'port_directions': portDirs, + 'connections': connections, + }; + } + + @internal + @override + bool get isSchematicWiresOnly => false; +} diff --git a/lib/src/synthesizers/schematic/schematic_primitives.dart b/lib/src/synthesizers/schematic/schematic_primitives.dart deleted file mode 100644 index e17039354..000000000 --- a/lib/src/synthesizers/schematic/schematic_primitives.dart +++ /dev/null @@ -1,856 +0,0 @@ -// Copyright (C) 2025 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// -// schematic_primitives.dart -// Primitive mapping helpers extracted from schematic_dumper for reuse. - -// 2025 December 14 -// Author: Desmond Kirkpatrick - -import 'package:rohd/rohd.dart'; -import 'package:rohd/src/synthesizers/schematic/module_utils.dart'; -import 'package:rohd/src/synthesizers/schematic/schematic_synthesis_result.dart'; - -/// Descriptor describing how a ROHD helper module maps to a Yosys -/// primitive type. -class PrimitiveDescriptor { - /// The Yosys primitive type name (e.g. "\$concat", "\$dff", "\$mux"). - final String primitiveName; - - /// Map from the ROHD module's port name to the primitive port name. - final Map portMap; - - /// Map of primitive parameter name -> ROHD port name or expression key. - final Map paramFromPort; - - /// Optional primitive port directions (primitive port -> - /// 'input'|'output'|'inout'). When provided in a descriptor, these directions - /// are used directly and the automatic direction inference is skipped for - /// those ports. - final Map portDirs; - - /// Default parameter values supplied by the descriptor (applied before - /// inference). Use this to move always-1 defaults into registration. - final Map defaultParams; - - /// When true, use the ROHD module's actual port names directly instead of - /// generating generic A/B/Y names. Useful for modules like Sequential that - /// have dynamic port names per instance. - final bool useRawPortNames; - - /// Creates a [PrimitiveDescriptor] for leaf schematic primitive mapping. - const PrimitiveDescriptor( - {required this.primitiveName, - this.portMap = const {}, - this.paramFromPort = const {}, - this.portDirs = const {}, - this.defaultParams = const {}, - this.useRawPortNames = false}); -} - -/// Singleton registry for primitive mappings used by the schematic dumper. -class Primitives { - Primitives._() { - _populateDefaults(); - } - - /// The singleton instance. - static final Primitives instance = Primitives._(); - - final Map _byDefinitionName = {}; - - /// Registers a [PrimitiveDescriptor] for a given ROHD module. - void register(String definitionName, PrimitiveDescriptor desc) { - _byDefinitionName[definitionName] = desc; - } - - /// Find a registered [PrimitiveDescriptor] by ROHD module definition name. - PrimitiveDescriptor? lookupByDefinitionName(String defName) => - _byDefinitionName[defName]; - - /// Lookup a [PrimitiveDescriptor] for a given [Module] by trying exact - /// definition name match first, then case-insensitive match, then - /// pattern match. - PrimitiveDescriptor? lookupForModule(Module m) { - final def = m.definitionName; - final nm = m.name; - - final p = lookupByDefinitionName(def); - if (p != null) { - return p; - } - - final defLower = def.toLowerCase(); - final nmLower = nm.toLowerCase(); - for (final entry in _byDefinitionName.entries) { - final keyLower = entry.key.toLowerCase(); - if (keyLower == defLower || keyLower == nmLower) { - return entry.value; - } - } - - for (final entry in _byDefinitionName.entries) { - final key = entry.key; - final pattern = RegExp( - '(^|[^A-Za-z0-9])${RegExp.escape(key)}(\$|[^A-Za-z0-9])', - caseSensitive: false); - if (pattern.hasMatch(def) || pattern.hasMatch(nm)) { - return entry.value; - } - } - - return null; - } - - /// Compute the primitive cell representation for a `Module` that maps to - /// a known primitive descriptor. Returns a map containing keys: - /// - 'type' -> String primitive type (e.g. r'$concat') - /// - 'parameters' -> `Map `of parameter values - /// - 'port_directions' -> `Map` mapping primitive port names - /// to directions expected by the loader ('input'/'output'/'inout') - Map computePrimitiveCell( - Module childModule, PrimitiveDescriptor prim) { - final cellType = prim.primitiveName; - final parameters = {}; - // Apply any descriptor-provided default parameters before inference. - // This lets registrations move always-1 (or other) defaults into the - // descriptor so they don't need to be inferred here. - if (prim.defaultParams.isNotEmpty) { - parameters.addAll(prim.defaultParams); - } - - void ensureIntParam(String k, int defaultVal) { - final v = parameters[k]; - if (v is int) { - if (v <= 0) { - parameters[k] = defaultVal; - } - } else { - parameters[k] = defaultVal; - } - } - - ensureIntParam('A_WIDTH', 1); - ensureIntParam('B_WIDTH', 1); - ensureIntParam('Y_WIDTH', 1); - if (parameters['OFFSET'] == null) { - parameters['OFFSET'] = 0; - } - - final ywVal = parameters['Y_WIDTH']; - if ((parameters['HIGH'] == null || parameters['LOW'] == null) && - ywVal is int) { - parameters['LOW'] = 0; - parameters['HIGH'] = (ywVal - 1) >= 0 ? (ywVal - 1) : 0; - } - - // Initialize `portDirs` from the descriptor. - final portDirs = {}..addAll(prim.portDirs); - - return { - 'type': cellType, - 'parameters': parameters, - 'port_directions': portDirs, - }; - } - - /// Finalize/adjust primitive parameters using the connection map built by - /// the dumper. This allows inference that depends on actual bit-id - /// connections (for example, determining slice offsets/high/low and - /// input widths) rather than only on port names or descriptor defaults. - /// - /// - [childModule] is the module instance for the primitive. - /// - [prim] is the primitive descriptor. - /// - [parameters] is the mutable parameters map produced by - /// `computePrimitiveCell` (will be modified in-place). - /// - [connMap] maps primitive port names (A/B/Y/etc) to lists of bit ids - /// (as produced by the dumper's connection resolution). Bit ids may be - /// integers (net ids) or string tokens for constants. - void finalizePrimitiveCell(Module childModule, PrimitiveDescriptor prim, - Map parameters, Map> connMap) { - // Apply simple paramFromPort mappings (e.g., A_WIDTH -> A) - prim.paramFromPort.entries - .where((e) => e.key.endsWith('_WIDTH')) - .forEach((e) { - final bits = connMap[e.value]; - if (bits != null) { - parameters[e.key] = bits.length; - } - }); - - // Specialized handling for $slice (BusSubset) primitives. Compute - // OFFSET/HIGH/LOW/Y_WIDTH/A_WIDTH when we have concrete connection ids - // for the source ('A') and the result ('Y'). - if (prim.primitiveName == r'$slice') { - final aBits = connMap['A'] ?? []; - final yBits = connMap['Y'] ?? []; - - // Populate widths if available - if (aBits.isNotEmpty) { - parameters['A_WIDTH'] = aBits.length; - } - if (yBits.isNotEmpty) { - parameters['Y_WIDTH'] = yBits.length; - } - - // For offset/high/low we need integer net ids to compute positions - final aInts = aBits.whereType().toList()..sort(); - final yInts = yBits.whereType().toList()..sort(); - - if (aInts.isNotEmpty && yInts.isNotEmpty) { - // Build index map from A net id -> position within A (0-based) - final aIndex = {}; - for (var i = 0; i < aInts.length; i++) { - aIndex[aInts[i]] = i; - } - - // Map each Y net id to its index in A; require that all Y ids exist - // within A to compute a contiguous OFFSET/HIGH/LOW. If not present, - // fall back to conservative defaults. - final mappedCandidates = yInts.map((yId) => aIndex[yId]).toList(); - final allMapped = mappedCandidates.every((e) => e != null); - final mappedIndices = - allMapped ? mappedCandidates.cast().toList() : []; - - if (allMapped && mappedIndices.isNotEmpty) { - mappedIndices.sort(); - final low = mappedIndices.first; - final high = mappedIndices.last; - parameters['OFFSET'] = low; - parameters['LOW'] = low; - parameters['HIGH'] = high; - parameters['Y_WIDTH'] = mappedIndices.length; - parameters['A_WIDTH'] = aInts.length; - } - // If connection-based mapping failed to determine offsets, try a - // fallback: parse output names for `_subset_HIGH_LOW` patterns which - // ROHD may emit for BusSubset outputs. This preserves previous - // behavior that relied on naming heuristics when structural mapping - // is not straightforward. - if ((parameters['LOW'] == null || - parameters['HIGH'] == null || - (parameters['LOW'] is int && - parameters['LOW'] == 0 && - parameters['HIGH'] is int && - parameters['HIGH'] == 0)) && - childModule.outputs.isNotEmpty) { - final re = RegExp(r'_subset_(\d+)_(\d+)'); - final match = childModule.outputs.keys - .map(re.firstMatch) - .firstWhere((m) => m != null, orElse: () => null); - if (match != null) { - final hi = int.parse(match.group(1)!); - final lo = int.parse(match.group(2)!); - final low = hi < lo ? hi : lo; - final high = hi < lo ? lo : hi; - parameters['LOW'] = low; - parameters['HIGH'] = high; - parameters['OFFSET'] = low; - parameters['Y_WIDTH'] = (high - low) + 1; - } - } - } - } - - // For $concat (concat/swizzle), derive input widths from mapped ports - if (prim.primitiveName == r'$concat') { - // Common placeholders A/B may represent inputs; if present, set widths - if (connMap.containsKey('A')) { - parameters['A_WIDTH'] = connMap['A']!.length; - } - if (connMap.containsKey('B')) { - parameters['B_WIDTH'] = connMap['B']!.length; - } - // Update Y width as sum if A/B provided - final aW = parameters['A_WIDTH']; - final bW = parameters['B_WIDTH']; - if (aW is int && bW is int) { - parameters['Y_WIDTH'] = aW + bW; - } - } - } - - /// Deterministically map ROHD port names to primitive port names. - /// - /// Returns a map where the key is the ROHD port name and the value is the - /// corresponding primitive port name. The mapping rules are: - /// 1. If the descriptor's `portMap` provides a literal ROHD name for a - /// primitive port and that ROHD port exists, use it. - /// 2. Group placeholder mappings (single-letter placeholders like 'A', 'B') - /// and map remaining ROHD ports in deterministic sorted order to the - /// placeholder-named primitive ports (sorted). - /// 3. Any remaining primitive ports are assigned positionally to remaining - /// ROHD ports in sorted order. - Map mapRohdToPrimitivePorts(PrimitiveDescriptor prim, - Module childModule, Map portDirs) { - final rohdInputs = childModule.inputs.keys.toList()..sort(); - final rohdOutputs = childModule.outputs.keys.toList()..sort(); - final rohdInouts = childModule.inOuts.keys.toList()..sort(); - - // Normalize prim.portMap: it may be registered in either direction - // (primPort -> rohdName) or (rohdName -> primPort). Detect which form - // is used and build a `primToRohd` map. Build the set of primitive port - // names from both the explicit `portDirs` and any keys present in the - // descriptor's `portMap` so registrations may omit input entries and - // only declare outputs/inouts if desired. Missing directions default to - // 'input' during mapping below. - final primPortNames = {} - ..addAll(portDirs.keys) - ..addAll(prim.portMap.keys); - final rohdPortNames = childModule.ports.keys.toSet(); - - // Build mapping candidates: for each primitive port, collect either a - // literal mapping or a deterministic list of ROHD names matching a - // regex. We will consume these lists deterministically when assigning - // ROHD ports so regex matches are not accidentally reused or picked - // nondeterministically by different prim ports. - final primToRohdLists = >{}; - // Detect inverted maps (rohd->prim) where keys are ROHD names. - final anyKeyIsRohd = prim.portMap.keys.any(rohdPortNames.contains); - final anyKeyIsPrim = prim.portMap.keys.any(primPortNames.contains); - if (anyKeyIsRohd && !anyKeyIsPrim) { - // Invert rohd->prim into prim->rohd lists - for (final e in prim.portMap.entries) { - primToRohdLists[e.value] = [e.key]; - } - } else { - for (final e in prim.portMap.entries) { - final primPort = e.key; - final mapping = e.value; - if (mapping.startsWith('re:')) { - final pattern = RegExp(mapping.substring(3)); - // Collect all ROHD ports matching the regex and sort - // deterministically - final matches = rohdPortNames.where(pattern.hasMatch).toList() - ..sort(); - if (matches.isNotEmpty) { - primToRohdLists[primPort] = matches; - } - } else { - // Literal mapping or placeholder (like 'A'/'B'). Store the literal - // string so calling code can detect placeholders vs literal names. - primToRohdLists[primPort] = [mapping]; - } - } - } - - // Helper to map for a given direction. Treat prim ports missing from - // `portDirs` as inputs by default so registrations can declare only - // outputs/inouts when convenient. - Map doDirection(String direction, List rohdPorts) { - String getDir(String p) => portDirs[p] ?? 'input'; - // Primitive ports of this direction, sorted - final primPorts = - primPortNames.where((p) => getDir(p) == direction).toList()..sort(); - - final mapping = {}; // rohd -> prim - - // 1) Literal mappings from portMap. For regex mappings we collected - // candidate lists; pick the first unassigned ROHD match for each - // prim-port and mark it assigned so matches are not reused. - final assignedPrim = {}; - final assignedRohd = {}; - for (final primPort in primPorts) { - final candidates = primToRohdLists[primPort]; - if (candidates != null && candidates.isNotEmpty) { - // If any candidate is an actual ROHD port name, assign the first - // one that is not already assigned. - String? chosen; - for (final cand in candidates) { - if (rohdPorts.contains(cand) && !assignedRohd.contains(cand)) { - chosen = cand; - break; - } - } - if (chosen != null) { - mapping[chosen] = primPort; - assignedPrim.add(primPort); - assignedRohd.add(chosen); - continue; - } - // If candidates exist but none are actual ROHD names, fall - // through to placeholder handling below (mapping may be a - // placeholder like 'A' or 'B'). - } - // Also support the legacy case where primToRohdLists may be empty - // and prim.portMap contains a literal ROHD name. - final mappedLiteral = prim.portMap[primPort]; - if (mappedLiteral != null && rohdPorts.contains(mappedLiteral)) { - mapping[mappedLiteral] = primPort; - assignedPrim.add(primPort); - assignedRohd.add(mappedLiteral); - continue; - } - } - - // 2) Placeholder groups (e.g., 'A', 'B') - // Group prim ports by their placeholder value - final placeholderGroups = >{}; - for (final primPort in primPorts) { - if (assignedPrim.contains(primPort)) { - continue; - } - final mapped = prim.portMap[primPort]; - if (mapped != null && RegExp(r'^[A-Z][0-9]*$').hasMatch(mapped)) { - final key = mapped.replaceAll(RegExp('[0-9]+'), ''); - placeholderGroups.putIfAbsent(key, () => []).add(primPort); - assignedPrim.add(primPort); - } - } - // Sort each group's prim ports for deterministic assignment - for (final g in placeholderGroups.values) { - g.sort(); - } - - // Assign ROHD ports to placeholder prim ports in sorted order - var rohdIdx = 0; - for (final primList in placeholderGroups.values) { - for (final primPort in primList) { - while (rohdIdx < rohdPorts.length && - assignedRohd.contains(rohdPorts[rohdIdx])) { - rohdIdx++; - } - if (rohdIdx >= rohdPorts.length) { - break; - } - final rohdName = rohdPorts[rohdIdx++]; - mapping[rohdName] = primPort; - assignedRohd.add(rohdName); - } - } - - // 3) Positional mapping for any remaining prim ports - // Collect remaining prim ports not assigned - final remainingPrim = - primPorts.where((p) => !mapping.values.contains(p)).toList()..sort(); - // Collect remaining rohd ports - final remainingRohd = - rohdPorts.where((r) => !assignedRohd.contains(r)).toList()..sort(); - - // No primitive-specific positional heuristics; rely on descriptor - // mappings and deterministic regex consumption above. - - for (var i = 0; - i < remainingPrim.length && i < remainingRohd.length; - i++) { - mapping[remainingRohd[i]] = remainingPrim[i]; - } - - return mapping; - } - - final result = {} - ..addAll(doDirection('input', rohdInputs)) - ..addAll(doDirection('output', rohdOutputs)) - ..addAll(doDirection('inout', rohdInouts)); - // If the descriptor did not supply explicit `portDirs`, infer primitive - // port directions from the instantiation point (ROHD ports) so the - // caller can use instance-derived directions rather than requiring the - // descriptor to provide them. This mirrors how combinational/raw-port - // instances display correctly using their instantiation context. - if (prim.portDirs.isEmpty) { - // Build reverse map: primPort -> list of rohd ports mapped to it - final primToRohd = >{}; - for (final e in result.entries) { - primToRohd.putIfAbsent(e.value, () => []).add(e.key); - } - - String decideForPrim(String primPort) { - final rohdList = primToRohd[primPort] ?? const []; - return rohdList.any((r) => childModule.ports[r]?.isInOut ?? false) - ? 'inout' - : (rohdList.any((r) => childModule.ports[r]?.isOutput ?? false) - ? 'output' - : 'input'); - } - - // Populate missing entries in the provided portDirs map. - for (final primPort in primPortNames) { - portDirs.putIfAbsent( - primPort, - () => primPort == 'Y' - ? (childModule.outputs.isNotEmpty ? 'output' : 'input') - : decideForPrim(primPort)); - } - } - - return result; - } - - /// Build a primitive connection map (`primPort` -> bit-id list) using the - /// deterministic ROHD->primitive port mapping and the provided - /// `idsForRohd` lookup function which returns the bit ids for a ROHD - /// port name. This function also calls the safe finalizer to allow - /// parameter inference that depends on concrete connections. - Map> buildPrimitiveConnections( - Module childModule, - PrimitiveDescriptor prim, - Map parameters, - Map portDirs, - List Function(String rohdName) idsForRohd) { - final connMap = >{}; - final rohdToPrim = mapRohdToPrimitivePorts(prim, childModule, portDirs); - - for (final entry in rohdToPrim.entries) { - final rohdName = entry.key; - final primPortName = entry.value; - final ids = idsForRohd(rohdName); - if (ids.isNotEmpty) { - connMap[primPortName] = ids; - } - } - - // Allow primitive logic to finalize parameters using the concrete - // connection ids we built. - finalizePrimitiveCell(childModule, prim, parameters, connMap); - - return connMap; - } - - /// Convenience wrapper used by the dumper when the lookup for ROHD port - /// ids needs to resolve ports via either a previously-produced - /// `SynthesisResult` for the child (preferred) or a child `ModuleMap` - /// (fallback). The [idsForChildLogic] callback should accept a `Logic` - /// and return the corresponding bit id list. The [childResultLookup] - /// callback, when provided, should return the `SynthesisResult` for a - /// given child module or null if not present. This allows using cached - /// synthesis outputs rather than rebuilding ModuleMaps. - Map> buildPrimitiveConnectionsWithChildLogicLookup( - Module childModule, - PrimitiveDescriptor prim, - Map parameters, - Map portDirs, - SynthesisResult? Function(Module) childResultLookup, - List Function(Logic) idsForChildLogic) { - // Adapter: convert rohdName -> idsForRohd by resolving the Logic - // either from the SchematicSynthesisResult (if available), the - // child ModuleMap contained within that result, or directly from - // the child module as a last resort. - List idsForRohd(String rohdName) { - final res = childResultLookup(childModule); - // If we have a SchematicSynthesisResult, try to use its port map. - if (res is SchematicSynthesisResult) { - final ports = res.ports; - if (ports.containsKey(rohdName)) { - // ports[rohdName]['bits'] is a List of bit ids - final bits = (ports[rohdName]! as Map)['bits']; - if (bits is List) { - return bits.cast(); - } - } - } - - // Fallback: try to obtain the logic from the child's ModuleMap if - // the result provides a way (many SchematicSynthesisResults do not - // expose ModuleMap directly), otherwise use the module's port - // reference and resolve via idsForChildLogic. - final logic = childModule.ports[rohdName]; - if (logic == null) { - return []; - } - return idsForChildLogic(logic); - } - - return buildPrimitiveConnections( - childModule, prim, parameters, portDirs, idsForRohd); - } - - void _populateDefaults() { - register( - 'Swizzle', - const PrimitiveDescriptor( - primitiveName: r'$concat', - // ROHD swizzles commonly name inputs like `in0_`, - // `in1_` and the output `swizzled` or `out`. Use regex - // mappings so the dumper can deterministically pick inputs and - // output without relying on positional heuristics. - portMap: { - 'A': r're:^in\d+_.+', - 'B': r're:^in\d+_.+', - 'Y': r're:^(?:swizzled$|out$)' - }, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, - )); - register( - 'BusSubset', - const PrimitiveDescriptor( - primitiveName: r'$slice', - // BusSubset (slice) instances often expose outputs with names - // containing `_subset_HIGH_LOW`. Inputs are typically `in...` or - // `A`. Prefer regex matches for both A (source bus) and Y - // (sliced output) so parameters can be extracted from names. - portMap: { - 'A': r're:^in\d*_.+|^in_.+|^A$', - 'Y': r're:.*_subset_\d+_\d+|^out$' - }, - paramFromPort: {'HIGH': 'A', 'LOW': 'A'}, - portDirs: {'A': 'input', 'Y': 'output'}, - )); - - // Comparison gates: ROHD uses in0_, in1_ for inputs. - register( - 'Equals', - const PrimitiveDescriptor( - primitiveName: r'$eq', - portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, - paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, - )); - register( - 'NotEquals', - const PrimitiveDescriptor( - primitiveName: r'$ne', - portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, - paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, - )); - register( - 'LessThan', - const PrimitiveDescriptor( - primitiveName: r'$lt', - portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, - paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, - )); - register( - 'LessThanOrEqual', - const PrimitiveDescriptor( - primitiveName: r'$le', - portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, - paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, - )); - register( - 'GreaterThan', - const PrimitiveDescriptor( - primitiveName: r'$gt', - portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, - paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, - )); - register( - 'GreaterThanOrEqual', - const PrimitiveDescriptor( - primitiveName: r'$ge', - portMap: {'A': 're:^_?in0_.+', 'B': 're:^_?in1_.+'}, - paramFromPort: {'A_WIDTH': 'A', 'B_WIDTH': 'B'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, - )); - - register( - 'lshift', - const PrimitiveDescriptor( - primitiveName: r'$shl', - portMap: {'A': 're:^in_.+', 'B': 're:^shiftAmount_.+'}, - paramFromPort: {'A_WIDTH': 'A'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); - register( - 'rshift', - const PrimitiveDescriptor( - primitiveName: r'$shr', - portMap: {'A': 're:^in_.+', 'B': 're:^shiftAmount_.+'}, - paramFromPort: {'A_WIDTH': 'A'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); - register( - 'ARShift', - const PrimitiveDescriptor( - primitiveName: r'$shiftx', - portMap: {'A': 'A', 'B': 'B', 'Y': 'Y'}, - paramFromPort: {'A_WIDTH': 'A'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'}, - )); - - register( - 'And2Gate', - const PrimitiveDescriptor( - primitiveName: r'$and', - portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); - register( - 'Or2Gate', - const PrimitiveDescriptor( - primitiveName: r'$or', - portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); - register( - 'Xor2Gate', - const PrimitiveDescriptor( - primitiveName: r'$xor', - portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); - register( - 'NotGate', - const PrimitiveDescriptor( - primitiveName: r'$not', - portMap: {'A': 're:^in_.+'}, - portDirs: {'A': 'input', 'Y': 'output'})); - - register( - 'AndUnary', - const PrimitiveDescriptor( - primitiveName: r'$logic_and', - portDirs: {'A': 'input', 'Y': 'output'})); - register( - 'OrUnary', - const PrimitiveDescriptor( - primitiveName: r'$logic_or', - portDirs: {'A': 'input', 'Y': 'output'})); - register( - 'XorUnary', - const PrimitiveDescriptor( - primitiveName: r'$xor', portDirs: {'A': 'input', 'Y': 'output'})); - - // Note: bitwise/logical gate descriptors are updated below with - // explicit `portDirs`; the earlier implicit registrations were - // redundant and have been removed to avoid confusion. - - // Update bitwise/logical gate descriptors to include directions. - register( - 'BitwiseAnd', - const PrimitiveDescriptor( - primitiveName: r'$and', - portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); - register( - 'BitwiseOr', - const PrimitiveDescriptor( - primitiveName: r'$or', - portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); - register( - 'BitwiseXor', - const PrimitiveDescriptor( - primitiveName: r'$xor', - portMap: {'A': 're:^in0_.+', 'B': 're:^in1_.+'}, - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); - register( - 'LogicNot', - const PrimitiveDescriptor( - primitiveName: r'$not', - portMap: {'A': 're:^in_.+'}, - portDirs: {'A': 'input', 'Y': 'output'})); - - register( - 'mux', - const PrimitiveDescriptor( - primitiveName: r'$mux', - // Use regex-based mappings to detect ROHD dynamic port names like - // control_, d0_, d1_, and out. Values prefixed - // with 're:' are treated as regular expressions against ROHD port - // names. - portMap: { - // Match common selector names: control_, sel_, - // s_, in0_/in1_ (sometimes select is named as in0/in1), or - // literal 'A'. - 'S': - r're:^(?:_?control_.+|_?sel_.+|_?s_.+|in0_.+|in1_.+|A$|.*_subset_\d+_\d+)', - // Data inputs: match d1_/d0_, or literal B/C/A depending on ROHD - 'A': r're:^(?:d1_.+|B$|d1$|d1_.+)', - 'B': r're:^(?:d0_.+|C$|d0$|d0_.+)', - 'Y': r're:^(?:out$|Y$)' - }, - // The WIDTH parameter should come from the data input (d1/d0), but - // we leave this as 'B' for compatibility — callers interpret this - // as the ROHD port name after mapping resolution. - paramFromPort: { - 'WIDTH': 'B' - })); - // Merged mux descriptor: provide port mappings, parameter source, - // and explicit port directions so fallback inference isn't required. - register( - 'mux', - const PrimitiveDescriptor(primitiveName: r'$mux', portMap: { - 'S': - r're:^(?:_?control_.+|_?sel_.+|_?s_.+|in0_.+|in1_.+|A$|.*_subset_\d+_\d+)', - 'A': r're:^(?:d1_.+|B$|d1$|d1_.+)', - 'B': r're:^(?:d0_.+|C$|d0$|d0_.+)', - 'Y': r're:^(?:out$|Y$)' - }, paramFromPort: { - 'WIDTH': 'B' - }, portDirs: { - 'S': 'input', - 'A': 'input', - 'B': 'input', - 'Y': 'output' - })); - - register( - 'mul', - const PrimitiveDescriptor( - primitiveName: r'$mul', - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); - - register( - 'AddSigned', - const PrimitiveDescriptor( - primitiveName: r'$add', - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); - register( - 'SubSigned', - const PrimitiveDescriptor( - primitiveName: r'$sub', - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); - register( - 'AddUnsigned', - const PrimitiveDescriptor( - primitiveName: r'$add', - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); - register( - 'SubUnsigned', - const PrimitiveDescriptor( - primitiveName: r'$sub', - portDirs: {'A': 'input', 'B': 'input', 'Y': 'output'})); - - register( - 'FlipFlop', - const PrimitiveDescriptor(primitiveName: r'$dff', portMap: { - 'd': 'D', - 'q': 'Q', - 'clk': 'CLK', - 'en': 'EN', - 'reset': 'SRST' - }, portDirs: { - 'd': 'input', - 'q': 'output', - 'clk': 'input', - 'en': 'input', - 'reset': 'input' - })); - - // Sequential is handled specially by SequentialHandler: - // - Simple Sequential (1 data input/output) → $dff primitive - // - Complex Sequential.multi → generates internal mux + dff structure - // Register it so that generatesDefinition() returns false (it's a leaf). - // The actual cell emission is done by SequentialHandler, not this - // descriptor. - register( - 'Sequential', - const PrimitiveDescriptor( - primitiveName: r'$sequential', useRawPortNames: true)); - - // Add and Combinational have dynamic port names per instance - register( - 'Add', - const PrimitiveDescriptor( - primitiveName: r'$add', useRawPortNames: true)); - register( - 'Combinational', - const PrimitiveDescriptor( - primitiveName: r'$combinational', useRawPortNames: true)); - - register( - 'AndUnary', - const PrimitiveDescriptor( - primitiveName: r'$logic_and', - portDirs: {'A': 'input', 'Y': 'output'})); - register( - 'OrUnary', - const PrimitiveDescriptor( - primitiveName: r'$logic_or', - portDirs: {'A': 'input', 'Y': 'output'})); - register( - 'XorUnary', - const PrimitiveDescriptor( - primitiveName: r'$xor', portDirs: {'A': 'input', 'Y': 'output'})); - } -} diff --git a/lib/src/synthesizers/schematic/schematic_synthesis_result.dart b/lib/src/synthesizers/schematic/schematic_synthesis_result.dart index a805cbdae..c0086b10b 100644 --- a/lib/src/synthesizers/schematic/schematic_synthesis_result.dart +++ b/lib/src/synthesizers/schematic/schematic_synthesis_result.dart @@ -551,31 +551,37 @@ class SchematicSynthesisResultBuilder { continue; } - // If schematicCell returns null but isSchematicPrimitive is true, - // fall through to primitive handling - if (childModule.isSchematicPrimitive) { - final prim = Primitives.instance.lookupForModule(childModule); - if (prim != null) { - _emitPrimitiveCell( - childModule: childModule, - cellKey: cellKey, - prim: prim, - cells: cells, - idsForChildLogic: idsForChildLogic, - constResult: constResult, - syntheticNets: syntheticNets, - nextInternalNetIdGetter: () => nextInternalNetId, - nextInternalNetIdSetter: (v) => nextInternalNetId = v, - internalNetIds: internalNetIds, - ); - continue; - } + // If schematicCell returns null, allow fallback primitive handling + // later in the emission flow. Modules that want primitive-like + // behavior should return a non-null `schematicCell` here. + } + + // Special handling for Sequential module without requiring mixin + if (childModule.runtimeType.toString() == 'Sequential') { + final emitted = SequentialSchematic.emitCells( + module: childModule, + ports: childModule.ports, + internalNetIds: internalNetIds, + idsForChildLogic: idsForChildLogic, + cells: cells, + syntheticNets: syntheticNets, + nextInternalNetIdGetter: () => nextInternalNetId, + nextInternalNetIdSetter: (v) => nextInternalNetId = v, + ); + if (emitted) { + continue; } } // Check if this is a primitive - if so, skip emitting a module instance // and instead emit the primitive cell directly - final prim = Primitives.instance.lookupForModule(childModule); + PrimitiveDescriptor? prim; + if (childModule is Schematic) { + prim = childModule.primitiveDescriptor() ?? + CoreGatePrimitives.instance.lookupByType(childModule); + } else { + prim = CoreGatePrimitives.instance.lookupByType(childModule); + } if (prim != null) { // Handle primitive cells _emitPrimitiveCell( @@ -963,20 +969,39 @@ class SchematicSynthesisResultBuilder { required void Function(int) nextInternalNetIdSetter, required Map> internalNetIds, }) { - // Handle Sequential modules - final seqHandler = SequentialHandler(); - final handled = seqHandler.handleSequential( - childModule: childModule, - ports: childModule.ports, - internalNetIds: internalNetIds, - idsForChildLogic: idsForChildLogic, - cells: cells, - syntheticNets: syntheticNets, - nextInternalNetIdGetter: nextInternalNetIdGetter, - nextInternalNetIdSetter: nextInternalNetIdSetter, - ); - if (handled) { - return; + // Give the module itself the first opportunity to emit schematic cells + // for its instantiation (modules may implement `emitSchematicCells`). + if (childModule is Schematic) { + final emitted = childModule.emitSchematicCells( + ports: childModule.ports, + internalNetIds: internalNetIds, + idsForChildLogic: idsForChildLogic, + cells: cells, + syntheticNets: syntheticNets, + nextInternalNetIdGetter: nextInternalNetIdGetter, + nextInternalNetIdSetter: nextInternalNetIdSetter, + ); + if (emitted) { + return; + } + } + + // Allow the module to provide the final primitive cell directly. + if (childModule is Schematic) { + final modulePrimCell = childModule.schematicPrimitiveCell( + prim, + idsForChildLogic, + internalNetIds: internalNetIds, + syntheticNets: syntheticNets, + nextInternalNetIdGetter: nextInternalNetIdGetter, + nextInternalNetIdSetter: nextInternalNetIdSetter, + filterConstInputsToCombinational: filterConstInputsToCombinational, + lookupExistingResult: lookupExistingResult, + ); + if (modulePrimCell != null) { + cells[cellKey] = modulePrimCell; + return; + } } if (prim.useRawPortNames) { @@ -1016,13 +1041,12 @@ class SchematicSynthesisResultBuilder { return; } - final primCell = - Primitives.instance.computePrimitiveCell(childModule, prim); + final primCell = PrimitiveHelper.computePrimitiveCell(childModule, prim); final portDirs = Map.from( (primCell['port_directions']! as Map).cast()); - final connMap = Primitives.instance - .buildPrimitiveConnectionsWithChildLogicLookup( + final connMap = + PrimitiveHelper.buildPrimitiveConnectionsWithChildLogicLookup( childModule, prim, (primCell['parameters']! as Map).cast(), diff --git a/lib/src/synthesizers/schematic/schematic_synthesizer.dart b/lib/src/synthesizers/schematic/schematic_synthesizer.dart index 8eba217ef..2fefb5e92 100644 --- a/lib/src/synthesizers/schematic/schematic_synthesizer.dart +++ b/lib/src/synthesizers/schematic/schematic_synthesizer.dart @@ -69,7 +69,8 @@ class SchematicSynthesizer extends Synthesizer { /// /// - [filterConstInputsToCombinational]: When true, filters out constant-only /// inputs to combinational primitives. - /// - [globalPortNames]: Port names on the top module to treat as global. + /// - [globalPortNames]: Port names on the top module to treat as global + /// (e.g., clock, reset). /// - [globalLogics]: Explicit [Logic] objects to treat as global (takes /// precedence over [globalPortNames]). SchematicSynthesizer({ @@ -121,6 +122,11 @@ class SchematicSynthesizer extends Synthesizer { @override bool generatesDefinition(Module module) { + // Sequential is handled as a primitive via SequentialSchematic + if (module.runtimeType.toString() == 'Sequential') { + return false; + } + // Check if module uses Schematic mixin and controls definition generation if (module is Schematic) { return module.schematicDefinitionType != @@ -128,7 +134,7 @@ class SchematicSynthesizer extends Synthesizer { } // Primitives don't generate separate definitions - they're inlined - final prim = Primitives.instance.lookupForModule(module); + final prim = CoreGatePrimitives.instance.lookupByType(module); return prim == null; } @@ -426,13 +432,9 @@ class SchematicSynthesizer extends Synthesizer { } } - // Primitive input driver checks + // Primitive input driver checks: use module-level indication first. for (final sub in module.subModules) { - var prim = Primitives.instance.lookupByDefinitionName(sub.definitionName); - if (prim == null && sub.subModules.isEmpty) { - prim = Primitives.instance.lookupForModule(sub); - } - if (prim == null) { + if (!Schematic.isPrimitiveModule(sub)) { continue; } for (final inLogic in sub.inputs.values) { diff --git a/lib/src/synthesizers/schematic/sequential_handler.dart b/lib/src/synthesizers/schematic/sequential_schematic.dart similarity index 83% rename from lib/src/synthesizers/schematic/sequential_handler.dart rename to lib/src/synthesizers/schematic/sequential_schematic.dart index df66dd3c7..a9241a86c 100644 --- a/lib/src/synthesizers/schematic/sequential_handler.dart +++ b/lib/src/synthesizers/schematic/sequential_schematic.dart @@ -1,27 +1,36 @@ // Copyright (C) 2025 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// sequential_handler.dart -// Class for handling [Sequential] child modules in the schematic -// dumper. This encapsulates simple-vs-complex mapping and synthetic net -// allocation for mux/dff generation. +// sequential_schematic.dart +// Schematic synthesis support for Sequential module. // -// 2025 December 16 +// 2025 December 20 // Author: Desmond Kirkpatrick import 'package:rohd/rohd.dart'; - -/// Handler to process [Sequential] child modules in the schematic -/// dumper. -class SequentialHandler { - /// Creates a [SequentialHandler]. - SequentialHandler(); - - /// Process a [Sequential]-type [Module] child, emitting cells and registering - /// any synthetic nets into [syntheticNets]. Returns true if the child was - /// processed (and the caller should `continue`), false otherwise. - bool handleSequential({ - required Module childModule, +import 'package:rohd/src/synthesizers/schematic/schematic_mixins.dart'; + +/// Schematic support for [Sequential] modules. +/// +/// This provides custom cell emission logic for Sequential blocks without +/// modifying the core Sequential class. The logic handles: +/// - Simple case: single trigger/input/output → emits $dff +/// - Complex case: multiple triggers/inputs/outputs → emits $mux + $dff +/// - Fallback: emits $sequential with raw port mapping +class SequentialSchematic { + SequentialSchematic._(); + + /// Descriptor for Sequential primitives. + static const PrimitiveDescriptor descriptor = PrimitiveDescriptor( + primitiveName: r'$sequential', + useRawPortNames: true, + ); + + /// Emit schematic cells for a Sequential module. + /// + /// Returns `true` if cells were emitted, `false` otherwise. + static bool emitCells({ + required Module module, required Map ports, required Map> internalNetIds, required List Function(Logic) idsForChildLogic, @@ -30,10 +39,6 @@ class SequentialHandler { required int Function() nextInternalNetIdGetter, required void Function(int) nextInternalNetIdSetter, }) { - if (childModule.definitionName != 'Sequential') { - return false; - } - final triggers = {}; final dataInputs = {}; final dataOutputs = {}; @@ -50,9 +55,7 @@ class SequentialHandler { } } - final cellKey = childModule.hasBuilt - ? childModule.uniqueInstanceName - : childModule.name; + final cellKey = module.hasBuilt ? module.uniqueInstanceName : module.name; // Simple case: 1 trigger, 1 data input, 1 output. if (triggers.length == 1 &&