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..aca8ba8a7 --- /dev/null +++ b/lib/src/synthesizers/schematic/constant_handler.dart @@ -0,0 +1,420 @@ +// 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/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) { + 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 + /// - [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 List childModules, + required List childResultsList, + 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 (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 + // so they do not become shared pattern-level $const cells; for + // other primitives we register const patterns as before. + 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 + // 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. + // 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 || childModules.contains(pm)); + }); + if (isTop && !hasScopeConsumer) { + return; + } + + final dsts = sig.dstConnections; + final anyToCombOrSeq = dsts.any((d) { + final pm = d.parentModule; + if (pm == null || !childModules.contains(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_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..a2f92519a --- /dev/null +++ b/lib/src/synthesizers/schematic/passthrough_handler.dart @@ -0,0 +1,111 @@ +// 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'; +import 'package:rohd/src/synthesizers/schematic/schematic_synthesis_result.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 List childModules, + required List childResultsList, + 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..fa8b7f089 --- /dev/null +++ b/lib/src/synthesizers/schematic/schematic.dart @@ -0,0 +1,12 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +export 'constant_handler.dart'; +export 'module_utils.dart'; +export 'passthrough_handler.dart'; +export 'schematic_gates.dart'; +export 'schematic_mixins.dart'; +export 'schematic_synthesis_result.dart'; +export 'schematic_synthesizer.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 new file mode 100644 index 000000000..61beafbd1 --- /dev/null +++ b/lib/src/synthesizers/schematic/schematic_mixins.dart @@ -0,0 +1,1041 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// schematic_mixins.dart +// Definition for Schematic Mixins for controlling schematic synthesis. +// +// 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. +/// +/// 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. +/// +/// ## 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 MyPrimitive extends Module with Schematic { +/// MyPrimitive(Logic a, Logic b) { +/// a = addInput('a', a); +/// b = addInput('b', b); +/// addOutput('y') <= a & b; +/// } +/// +/// @override +/// 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. + /// + /// 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`, 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 + 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]. +/// +/// 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. + /// + /// 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; +} + +/// 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_synthesis_result.dart b/lib/src/synthesizers/schematic/schematic_synthesis_result.dart new file mode 100644 index 000000000..c0086b10b --- /dev/null +++ b/lib/src/synthesizers/schematic/schematic_synthesis_result.dart @@ -0,0 +1,1086 @@ +// 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'; +// ModuleMap removed — migration to child SchematicSynthesisResult objects. + +/// 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; + + /// 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; + + /// The netnames map: net name → {bits, attributes}. + final Map netnames; + + /// 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(); + + /// Creates a [SchematicSynthesisResult] for [module]. + SchematicSynthesisResult( + 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, + '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]. +/// +/// Extracted from SchematicDumper.buildModuleEntryHierarchy to handle one +/// module level without recursion. +class SchematicSynthesisResultBuilder { + /// The module to synthesize. + final Module module; + + /// 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.getInstanceTypeOfModule, + this.resolvedGlobalLogics = const {}, + this.lookupExistingResult, + this.existingResults, + 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; + } + + // 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) { + 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 = >{}; + + // 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 (childGlobalSet.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, + childModules: childModules, + childResultsList: childResultsList, + 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, + childModules: childModules, + childResultsList: childResultsList, + 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 (portLogicsLocal.containsKey(logic) || + internalNetIds.containsKey(logic)) { + return; + } + intermediateLogics.add(logic); + for (final src in logic.srcConnections) { + collectIntermediates(src, visited); + } + } + + 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); + } + } + } + + for (final portLogic in portLogicsLocal.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 = [ + ...portLogicsLocal.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 = SchematicSynthesisResult.computeComponents( + allLogics.length, cellUnions); + + // Build root → canonical IDs mapping + final rootToIds = >{}; + + for (final portLogic in portLogicsLocal.keys) { + final idx = logicIndex[portLogic]; + if (idx == null) { + continue; + } + final root = cellRoots[idx]; + final ids = portLogicsLocal[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]] ?? + portLogicsLocal[l] ?? + internalNetIds[l] ?? + []; + } + return portLogicsLocal[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 portLogicsLocal.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 (var ci = 0; ci < childModules.length; ci++) { + final childModule = childModules[ci]; + + 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, 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 + 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( + childModule: childModule, + 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 = + SchematicSynthesisResult.computeComponents(signals.length, unions); + + final bitIdToLogic = {}; + 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) { + 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(portLogicsLocal[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() ?? + (portLogicsLocal[logic] ?? [])) + : (portLogicsLocal[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 = portLogicsLocal[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 = portLogicsLocal.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 = portLogicsLocal[inn] ?? []; + final outIds = internalNetIds[out] ?? portLogicsLocal[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 portLogicsLocal[elem] ?? internalLogicsFallback[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, + portLogics: portLogicsLocal, + globalLogics: reachableFromGlobals, + childResults: childResultsList, + ); + } + + /// Emits a primitive cell into [cells]. + void _emitPrimitiveCell({ + required Module childModule, + 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, + }) { + // 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) { + 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 = PrimitiveHelper.computePrimitiveCell(childModule, prim); + final portDirs = Map.from( + (primCell['port_directions']! as Map).cast()); + + final connMap = + PrimitiveHelper.buildPrimitiveConnectionsWithChildLogicLookup( + childModule, + prim, + (primCell['parameters']! as Map).cast(), + portDirs, + lookupExistingResult ?? ((Module _) => null), + 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..2fefb5e92 --- /dev/null +++ b/lib/src/synthesizers/schematic/schematic_synthesizer.dart @@ -0,0 +1,535 @@ +// 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 'dart:convert'; + +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 = {}; + + /// 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 + /// inputs to combinational primitives. + /// - [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({ + this.filterConstInputsToCombinational = false, + this.globalPortNames = const [], + this.globalLogics, + }); + + @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 = {}; + + // 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.', + ); + } + } + + // ModuleMap-based helpers removed — Schematic synthesis prefers + // child SchematicSynthesisResult objects and builder-local computations. + + @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 != + SchematicDefinitionGenerationType.none; + } + + // Primitives don't generate separate definitions - they're inlined + final prim = CoreGatePrimitives.instance.lookupByType(module); + return prim == null; + } + + @override + SynthesisResult synthesize( + 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, + ); + + // 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); + } + } + } + } + + 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: use module-level indication first. + for (final sub in module.subModules) { + if (!Schematic.isPrimitiveModule(sub)) { + 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); + } + + /// 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/schematic/sequential_schematic.dart b/lib/src/synthesizers/schematic/sequential_schematic.dart new file mode 100644 index 000000000..a9241a86c --- /dev/null +++ b/lib/src/synthesizers/schematic/sequential_schematic.dart @@ -0,0 +1,202 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// sequential_schematic.dart +// Schematic synthesis support for Sequential module. +// +// 2025 December 20 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +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, + required Map> cells, + required Map> syntheticNets, + required int Function() nextInternalNetIdGetter, + required void Function(int) nextInternalNetIdSetter, + }) { + 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 = module.hasBuilt ? module.uniqueInstanceName : module.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..0af75e7c4 --- /dev/null +++ b/lib/src/synthesizers/schematic/yosys/_yosys_loader_runner.mjs @@ -0,0 +1,114 @@ +// 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'; +import { fileURLToPath } from 'url'; + +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); + } + // 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); + 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; + } + } + + 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; + 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..39608abaf --- /dev/null +++ b/lib/src/synthesizers/schematic/yosys/yosys_loader_helper.dart @@ -0,0 +1,12 @@ +// 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 + +// 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..665040d48 --- /dev/null +++ b/lib/src/synthesizers/schematic/yosys/yosys_loader_web.dart @@ -0,0 +1,247 @@ +// 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. + +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/lib/src/synthesizers/synth_builder.dart b/lib/src/synthesizers/synth_builder.dart index 54e312ab3..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); @@ -56,6 +59,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]; @@ -101,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 b70c9338e..d13e6b819 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,12 +12,29 @@ 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); /// 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 new file mode 100644 index 000000000..06c84c5ed --- /dev/null +++ b/test/schematic_example_test.dart @@ -0,0 +1,257 @@ +// 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: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() { + // 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); + + // 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(outPath); + await file.create(recursive: true); + await file.writeAsString(json); + } + return runYosysLoaderFromString(json); + } + + 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(); + counter.generateSynth(); + + final r = + await convertTestWriteSchematic(counter, 'build/Counter.rohd.json'); + + expect( + r.success, + isTrue, + reason: 'loader should load Counter from string: ${r.error ?? r}', + ); + }); + + group('SynthBuilder schematic generation for examples', () { + 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 r = await convertTestWriteSchematic( + counter, 'build/Counter.synth.rohd.json'); + 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); + + final r = await convertTestWriteSchematic( + fir, 'build/FirFilter.synth.rohd.json'); + 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); + + final r = await convertTestWriteSchematic( + la, 'build/LogicArrayExample.synth.rohd.json'); + 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); + + final r = await convertTestWriteSchematic( + oven, 'build/OvenModule.synth.rohd.json'); + 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); + + final r = await convertTestWriteSchematic( + tree, 'build/TreeOfTwoInputModules.synth.rohd.json'); + expect(r.success, isTrue, + reason: 'loader should load TreeOfTwoInputModules synth: ' + '${r.error ?? r}'); + }); + }); + + 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'; + final rStr = await convertTestWriteSchematic(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 { + expect( + rStr.success, + isTrue, + reason: + 'loader should load FirFilter from string: ${rStr.error ?? rStr}', + ); + } + }); + + 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'; + final rStr = await convertTestWriteSchematic(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 { + expect( + rStr.success, + isTrue, + reason: 'loader should load LogicArrayExample from string: ' + '${rStr.error ?? rStr}', + ); + } + }); + + 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'; + final rStr = await convertTestWriteSchematic(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 { + expect( + rStr.success, + isTrue, + reason: + 'loader should load OvenModule from string: ${rStr.error ?? rStr}', + ); + } + }); + + 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'; + 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 { + expect(rStr.success, isTrue, + reason: 'loader should load TreeOfTwoInputModules from string: ' + '${rStr.error ?? rStr}'); + } + }); +} 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); + }); + }); }