PySHDL 0.2.4__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {pyshdl-0.2.4 → pyshdl-0.3.0}/PKG-INFO +1 -1
  2. {pyshdl-0.2.4 → pyshdl-0.3.0}/pyproject.toml +1 -1
  3. pyshdl-0.3.0/src/SHDL/bus_compiler/__init__.py +10 -0
  4. pyshdl-0.3.0/src/SHDL/bus_compiler/analyzer.py +382 -0
  5. pyshdl-0.3.0/src/SHDL/bus_compiler/codegen.py +576 -0
  6. pyshdl-0.3.0/src/SHDL/bus_compiler/compiler.py +63 -0
  7. pyshdl-0.3.0/src/SHDL/bus_compiler/graph.py +143 -0
  8. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/driver/circuit.py +72 -45
  9. {pyshdl-0.2.4 → pyshdl-0.3.0}/uv.lock +1 -1
  10. {pyshdl-0.2.4 → pyshdl-0.3.0}/.github/workflows/deploy-docs.yml +0 -0
  11. {pyshdl-0.2.4 → pyshdl-0.3.0}/.github/workflows/publish.yml +0 -0
  12. {pyshdl-0.2.4 → pyshdl-0.3.0}/.github/workflows/test.yml +0 -0
  13. {pyshdl-0.2.4 → pyshdl-0.3.0}/.gitignore +0 -0
  14. {pyshdl-0.2.4 → pyshdl-0.3.0}/LICENSE +0 -0
  15. {pyshdl-0.2.4 → pyshdl-0.3.0}/README.md +0 -0
  16. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/README.md +0 -0
  17. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/architecture/_category_.json +0 -0
  18. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/architecture/base-shdl.md +0 -0
  19. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/architecture/compiler-internals.md +0 -0
  20. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/architecture/flattening-pipeline.md +0 -0
  21. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/architecture/overview.md +0 -0
  22. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/architecture/pyshdl-internals.md +0 -0
  23. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/debugger/_category_.json +0 -0
  24. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/debugger/breakpoints.md +0 -0
  25. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/debugger/commands.md +0 -0
  26. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/debugger/common-problems.md +0 -0
  27. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/debugger/debug-build.md +0 -0
  28. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/debugger/getting-started.md +0 -0
  29. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/debugger/hierarchy.md +0 -0
  30. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/debugger/inspection.md +0 -0
  31. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/debugger/overview.md +0 -0
  32. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/debugger/python-api.md +0 -0
  33. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/debugger/scripting.md +0 -0
  34. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/debugger/waveforms.md +0 -0
  35. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/examples/8-bit-adder.md +0 -0
  36. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/examples/_category_.json +0 -0
  37. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/examples/comparator.md +0 -0
  38. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/examples/decoder.md +0 -0
  39. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/examples/full-adder.md +0 -0
  40. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/examples/half-adder.md +0 -0
  41. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/examples/multiplexer.md +0 -0
  42. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/examples/register.md +0 -0
  43. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/getting-started/_category_.json +0 -0
  44. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/getting-started/first-circuit.md +0 -0
  45. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/getting-started/installation.md +0 -0
  46. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/getting-started/using-pyshdl.md +0 -0
  47. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/intro.md +0 -0
  48. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/language-reference/_category_.json +0 -0
  49. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/language-reference/components.md +0 -0
  50. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/language-reference/connections.md +0 -0
  51. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/language-reference/constants.md +0 -0
  52. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/language-reference/errors.md +0 -0
  53. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/language-reference/generators.md +0 -0
  54. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/language-reference/imports.md +0 -0
  55. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/language-reference/lexical-elements.md +0 -0
  56. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/language-reference/overview.md +0 -0
  57. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/language-reference/signals.md +0 -0
  58. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docs/language-reference/standard-gates.md +0 -0
  59. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/docusaurus.config.ts +0 -0
  60. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/package.json +0 -0
  61. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/sidebars.ts +0 -0
  62. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/src/components/HomepageFeatures/index.tsx +0 -0
  63. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/src/components/HomepageFeatures/styles.module.css +0 -0
  64. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/src/css/custom.css +0 -0
  65. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/src/pages/index.module.css +0 -0
  66. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/src/pages/index.tsx +0 -0
  67. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/src/pages/markdown-page.md +0 -0
  68. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/static/.nojekyll +0 -0
  69. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/static/img/docusaurus-social-card.jpg +0 -0
  70. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/static/img/docusaurus.png +0 -0
  71. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/static/img/favicon.ico +0 -0
  72. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/static/img/halfAdder.png +0 -0
  73. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/static/img/logo.svg +0 -0
  74. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/static/img/undraw_docusaurus_mountain.svg +0 -0
  75. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/static/img/undraw_docusaurus_react.svg +0 -0
  76. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/static/img/undraw_docusaurus_tree.svg +0 -0
  77. {pyshdl-0.2.4 → pyshdl-0.3.0}/docs/tsconfig.json +0 -0
  78. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/add100.shdl +0 -0
  79. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/addSub16.shdl +0 -0
  80. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/adder16.shdl +0 -0
  81. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/alu.shdl +0 -0
  82. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/and16inputs.shdl +0 -0
  83. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/bitwise_and16.shdl +0 -0
  84. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/bitwise_not16.shdl +0 -0
  85. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/bitwise_or16.shdl +0 -0
  86. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/bitwise_xor16.shdl +0 -0
  87. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/clock.shdl +0 -0
  88. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/demux.shdl +0 -0
  89. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/flagsZN.shdl +0 -0
  90. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/fullAdder.shdl +0 -0
  91. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/mux2.shdl +0 -0
  92. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/mux2_16.shdl +0 -0
  93. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/mux8.shdl +0 -0
  94. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/mux8_16.shdl +0 -0
  95. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/negate16.shdl +0 -0
  96. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/or16inputs.shdl +0 -0
  97. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/reg16.shdl +0 -0
  98. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/SHDL_components/shift1.shdl +0 -0
  99. {pyshdl-0.2.4 → pyshdl-0.3.0}/examples/interacting.py +0 -0
  100. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDB/__init__.py +0 -0
  101. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/__init__.py +0 -0
  102. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/compiler/__init__.py +0 -0
  103. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/compiler/analyzer.py +0 -0
  104. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/compiler/ast.py +0 -0
  105. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/compiler/cli.py +0 -0
  106. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/compiler/codegen.py +0 -0
  107. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/compiler/compiler.py +0 -0
  108. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/compiler/debug_codegen.py +0 -0
  109. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/compiler/debug_info_gen.py +0 -0
  110. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/compiler/lexer.py +0 -0
  111. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/compiler/parser.py +0 -0
  112. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/debugger/__init__.py +0 -0
  113. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/debugger/circuit.py +0 -0
  114. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/debugger/cli.py +0 -0
  115. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/debugger/controller.py +0 -0
  116. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/debugger/debuginfo.py +0 -0
  117. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/debugger/sourcemap.py +0 -0
  118. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/debugger/symbols.py +0 -0
  119. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/driver/__init__.py +0 -0
  120. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/driver/exceptions.py +0 -0
  121. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/errors.py +0 -0
  122. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/flattener/__init__.py +0 -0
  123. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/flattener/ast.py +0 -0
  124. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/flattener/flattener.py +0 -0
  125. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/flattener/lexer.py +0 -0
  126. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/flattener/parser.py +0 -0
  127. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/flattener/tokens.py +0 -0
  128. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/py.typed +0 -0
  129. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/semantic/__init__.py +0 -0
  130. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/semantic/analyzer.py +0 -0
  131. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/semantic/connection.py +0 -0
  132. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/semantic/resolver.py +0 -0
  133. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/semantic/type_check.py +0 -0
  134. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/semantic/warnings.py +0 -0
  135. {pyshdl-0.2.4 → pyshdl-0.3.0}/src/SHDL/source_map.py +0 -0
  136. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/README.md +0 -0
  137. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/TEST_REPORT.md +0 -0
  138. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/circuits/test_adder4.shdl +0 -0
  139. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/circuits/test_adder8.shdl +0 -0
  140. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/circuits/test_bitwise.shdl +0 -0
  141. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/circuits/test_comparator.shdl +0 -0
  142. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/circuits/test_constants.shdl +0 -0
  143. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/circuits/test_decoder.shdl +0 -0
  144. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/circuits/test_gates.shdl +0 -0
  145. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/circuits/test_generators.shdl +0 -0
  146. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/circuits/test_half_full_adder.shdl +0 -0
  147. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/circuits/test_mux.shdl +0 -0
  148. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/conftest.py +0 -0
  149. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/test_debugger.py +0 -0
  150. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/test_errors.py +0 -0
  151. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/test_shdl.py +0 -0
  152. {pyshdl-0.2.4 → pyshdl-0.3.0}/tests/test_shdl_comprehensive.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySHDL
3
- Version: 0.2.4
3
+ Version: 0.3.0
4
4
  Summary: SHDL (Simple Hardware Description Language) is a minimal HDL designed for creating digital circuits and easily simulating them. It compiles directly to C for maximum performance and portability. PySHDL is the Python interface for SHDL.
5
5
  Project-URL: Homepage, https://github.com/rafa-rrayes/SHDL
6
6
  Project-URL: Repository, https://github.com/rafa-rrayes/SHDL
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "PySHDL"
3
- version = "0.2.4"
3
+ version = "0.3.0"
4
4
  description = "SHDL (Simple Hardware Description Language) is a minimal HDL designed for creating digital circuits and easily simulating them. It compiles directly to C for maximum performance and portability. PySHDL is the Python interface for SHDL."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -0,0 +1,10 @@
1
+ """
2
+ Signal-Width-Aware Code Generator for SHDL
3
+
4
+ Packs gates that process corresponding bits of the same bus together,
5
+ enabling word-level C operations instead of individual bit extractions.
6
+ """
7
+
8
+ from .compiler import BusCompiler
9
+
10
+ __all__ = ["BusCompiler"]
@@ -0,0 +1,382 @@
1
+ """
2
+ Bus Pattern Detection and Topological Sorting.
3
+
4
+ Uses partition refinement to group gates that process corresponding bits
5
+ of the same bus, enabling word-width C operations.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from collections import defaultdict
10
+
11
+ from .graph import ConnectionGraph, GateNode, WireRef, OutputSink
12
+
13
+
14
+ @dataclass
15
+ class BusSource:
16
+ """Describes how a bus group input is sourced."""
17
+ kind: str # port_aligned, port_broadcast, bus_group, constant, mixed
18
+ ref: str = "" # port name or bus group name
19
+ broadcast_bit: int = 0 # for broadcast: which bit (1-based)
20
+ per_bit: list = field(default_factory=list) # for mixed: list of WireRef per gate
21
+ shift: int = 0 # for port_aligned: right shift amount (0-based)
22
+
23
+
24
+ @dataclass
25
+ class BusGroup:
26
+ """A group of gates that can be evaluated as a word-width operation."""
27
+ name: str
28
+ primitive: str
29
+ width: int
30
+ gates: list[GateNode] # ordered by bit position
31
+ bit_indices: list[int] # the bit index each gate corresponds to
32
+ input_sources: dict[str, BusSource] = field(default_factory=dict)
33
+ is_feedback: bool = False
34
+ scc_id: int = -1 # which SCC this group belongs to (-1 = none)
35
+
36
+
37
+ @dataclass
38
+ class AnalysisResult:
39
+ bus_groups: list[BusGroup] # topologically sorted
40
+ singleton_gates: list[GateNode] # gates not in any bus group
41
+ output_sinks: list[OutputSink]
42
+ input_ports: dict[str, int]
43
+ output_ports: dict[str, int]
44
+ # Maps gate instance name -> (group_name, position_in_group)
45
+ gate_to_group: dict[str, tuple[str, int]] = field(default_factory=dict)
46
+
47
+
48
+ class BusAnalyzer:
49
+ """Analyzes a ConnectionGraph to detect bus-width operation patterns."""
50
+
51
+ def __init__(self, graph: ConnectionGraph):
52
+ self.graph = graph
53
+
54
+ def analyze(self) -> AnalysisResult:
55
+ # Filter out VCC/GND pseudo-gates
56
+ all_gates = [
57
+ g for g in self.graph.gates.values()
58
+ if g.primitive not in ("__VCC__", "__GND__")
59
+ ]
60
+
61
+ # Step 1: Partition refinement to find bus groups
62
+ group_of = self._partition_refinement(all_gates)
63
+
64
+ # Step 2: Assign bit positions
65
+ positions = self._assign_positions(all_gates)
66
+
67
+ # Step 3: Build BusGroup objects from partition results
68
+ groups_by_id: dict[str, list[GateNode]] = defaultdict(list)
69
+ for gate in all_gates:
70
+ groups_by_id[group_of[gate.name]].append(gate)
71
+
72
+ bus_groups: list[BusGroup] = []
73
+ grouped_gates: set[str] = set()
74
+ gate_group_map: dict[str, str] = {} # gate_name -> group_name
75
+ group_counter = 0
76
+
77
+ for gid, gates in groups_by_id.items():
78
+ if len(gates) < 2:
79
+ continue # singleton
80
+
81
+ # Sort by bit position
82
+ gate_bits = []
83
+ for gate in gates:
84
+ bit = positions.get(gate.name, 1)
85
+ gate_bits.append((bit, gate))
86
+ gate_bits.sort(key=lambda x: x[0])
87
+
88
+ # Deduplicate bit positions
89
+ seen_bits: set[int] = set()
90
+ deduped = []
91
+ for bit, gate in gate_bits:
92
+ if bit not in seen_bits:
93
+ seen_bits.add(bit)
94
+ deduped.append((bit, gate))
95
+ gate_bits = deduped
96
+
97
+ if len(gate_bits) < 2:
98
+ continue
99
+
100
+ group_counter += 1
101
+ prim = gates[0].primitive
102
+ name = f"bus_{prim.lower()}_{group_counter}"
103
+
104
+ group = BusGroup(
105
+ name=name,
106
+ primitive=prim,
107
+ width=len(gate_bits),
108
+ gates=[g for _, g in gate_bits],
109
+ bit_indices=[b for b, _ in gate_bits],
110
+ )
111
+ bus_groups.append(group)
112
+
113
+ for gate in group.gates:
114
+ grouped_gates.add(gate.name)
115
+ gate_group_map[gate.name] = name
116
+
117
+ # Singletons
118
+ singletons = [g for g in all_gates if g.name not in grouped_gates]
119
+
120
+ # Step 4: Classify sources for each bus group
121
+ for group in bus_groups:
122
+ self._classify_sources(group, gate_group_map)
123
+
124
+ # Step 5: SCC detection
125
+ scc_map = self._detect_feedback(bus_groups)
126
+
127
+ # Step 6: Topological sort
128
+ sorted_groups = self._topological_sort(bus_groups, scc_map)
129
+
130
+ # Build gate_to_group map
131
+ gate_to_group = {}
132
+ for group in sorted_groups:
133
+ for i, gate in enumerate(group.gates):
134
+ gate_to_group[gate.name] = (group.name, i)
135
+
136
+ return AnalysisResult(
137
+ bus_groups=sorted_groups,
138
+ singleton_gates=singletons,
139
+ output_sinks=self.graph.output_sinks,
140
+ input_ports=self.graph.input_ports,
141
+ output_ports=self.graph.output_ports,
142
+ gate_to_group=gate_to_group,
143
+ )
144
+
145
+ def _partition_refinement(self, all_gates: list[GateNode]) -> dict[str, str]:
146
+ """Global partition refinement using group IDs instead of primitive types."""
147
+ # Round 0: group by primitive type
148
+ group_of: dict[str, str] = {}
149
+ for gate in all_gates:
150
+ group_of[gate.name] = gate.primitive
151
+
152
+ for _ in range(100): # safety bound
153
+ fingerprints: dict[str, tuple] = {}
154
+ for gate in all_gates:
155
+ fp = [gate.primitive]
156
+ for port in sorted(gate.inputs.keys()):
157
+ wire = gate.inputs[port]
158
+ if wire.kind == "port_input":
159
+ fp.append(("port", wire.name))
160
+ elif wire.kind == "gate_output":
161
+ fp.append(("gate", group_of.get(wire.name, "?")))
162
+ elif wire.kind == "constant":
163
+ fp.append(("const", wire.name))
164
+ fingerprints[gate.name] = tuple(fp)
165
+
166
+ new_group_of: dict[str, str] = {}
167
+ fp_to_id: dict[tuple, str] = {}
168
+ for gate in all_gates:
169
+ fp = fingerprints[gate.name]
170
+ if fp not in fp_to_id:
171
+ fp_to_id[fp] = f"G{len(fp_to_id)}"
172
+ new_group_of[gate.name] = fp_to_id[fp]
173
+
174
+ if new_group_of == group_of:
175
+ break # converged
176
+ group_of = new_group_of
177
+
178
+ return group_of
179
+
180
+ def _assign_positions(self, all_gates: list[GateNode]) -> dict[str, int]:
181
+ """Iterative bit position assignment with cycle safety.
182
+
183
+ Only uses multi-bit port inputs for position info (single-bit ports
184
+ like 'clk' are broadcasts and don't carry position).
185
+ """
186
+ positions: dict[str, int] = {}
187
+
188
+ # Direct: gates with multi-bit port_input wires get position from bit_index
189
+ for gate in all_gates:
190
+ for wire in gate.inputs.values():
191
+ if wire.kind == "port_input" and self.graph.input_ports.get(wire.name, 1) > 1:
192
+ positions[gate.name] = wire.bit_index
193
+ break
194
+
195
+ # Propagate: inherit from already-assigned source gates
196
+ changed = True
197
+ while changed:
198
+ changed = False
199
+ for gate in all_gates:
200
+ if gate.name in positions:
201
+ continue
202
+ for wire in gate.inputs.values():
203
+ if wire.kind == "gate_output" and wire.name in positions:
204
+ positions[gate.name] = positions[wire.name]
205
+ changed = True
206
+ break
207
+
208
+ # Fallback: gates with only single-bit port inputs get position 1
209
+ for gate in all_gates:
210
+ if gate.name not in positions:
211
+ for wire in gate.inputs.values():
212
+ if wire.kind == "port_input":
213
+ positions[gate.name] = wire.bit_index
214
+ break
215
+
216
+ return positions
217
+
218
+ def _classify_sources(self, group: BusGroup, gate_group_map: dict[str, str]):
219
+ """Classify each input port source for a bus group."""
220
+ input_port_names = ["A", "B"] if group.primitive != "NOT" else ["A"]
221
+
222
+ for port_name in input_port_names:
223
+ wires = []
224
+ for gate in group.gates:
225
+ wire = gate.inputs.get(port_name)
226
+ wires.append(wire)
227
+
228
+ if not wires or wires[0] is None:
229
+ continue
230
+
231
+ source = self._classify_wire_list(wires, group.bit_indices, gate_group_map)
232
+ group.input_sources[port_name] = source
233
+
234
+ def _classify_wire_list(
235
+ self, wires: list[WireRef], bit_indices: list[int],
236
+ gate_group_map: dict[str, str]
237
+ ) -> BusSource:
238
+ """Classify a list of wires (one per gate in the group)."""
239
+ if not wires or wires[0] is None:
240
+ return BusSource(kind="mixed", per_bit=wires)
241
+
242
+ # Check: all constant?
243
+ if all(w and w.kind == "constant" for w in wires):
244
+ val = wires[0].name
245
+ if all(w.name == val for w in wires):
246
+ return BusSource(kind="constant", ref=val)
247
+
248
+ # Check: all from same port?
249
+ if all(w and w.kind == "port_input" for w in wires):
250
+ port_name = wires[0].name
251
+ if all(w.name == port_name for w in wires):
252
+ if all(w.bit_index == bi for w, bi in zip(wires, bit_indices)):
253
+ # Check contiguity: bit_indices must be consecutive
254
+ contiguous = all(
255
+ bit_indices[i+1] == bit_indices[i] + 1
256
+ for i in range(len(bit_indices) - 1)
257
+ )
258
+ if contiguous:
259
+ shift = bit_indices[0] - 1 # 1-based to 0-based
260
+ return BusSource(kind="port_aligned", ref=port_name, shift=shift)
261
+ # Non-contiguous: fall through to mixed
262
+ if all(w.bit_index == wires[0].bit_index for w in wires):
263
+ return BusSource(kind="port_broadcast", ref=port_name,
264
+ broadcast_bit=wires[0].bit_index)
265
+
266
+ # Check: all from gate outputs in the same group?
267
+ if all(w and w.kind == "gate_output" for w in wires):
268
+ src_groups = set()
269
+ for w in wires:
270
+ grp = gate_group_map.get(w.name)
271
+ if grp:
272
+ src_groups.add(grp)
273
+ else:
274
+ src_groups.add(None)
275
+
276
+ if len(src_groups) == 1 and None not in src_groups:
277
+ group_name = src_groups.pop()
278
+ return BusSource(kind="bus_group", ref=group_name)
279
+
280
+ # Fallback: mixed
281
+ return BusSource(kind="mixed", per_bit=list(wires))
282
+
283
+ def _detect_feedback(self, groups: list[BusGroup]) -> dict[str, int]:
284
+ """Detect feedback loops (SCCs). Returns gate_group_name -> scc_id mapping."""
285
+ group_map = {g.name: g for g in groups}
286
+ adj: dict[str, set[str]] = defaultdict(set)
287
+
288
+ for group in groups:
289
+ for source in group.input_sources.values():
290
+ if source.kind == "bus_group" and source.ref in group_map:
291
+ adj[source.ref].add(group.name)
292
+
293
+ # Tarjan's SCC
294
+ index_counter = [0]
295
+ stack = []
296
+ on_stack = set()
297
+ indices = {}
298
+ lowlinks = {}
299
+ sccs: list[list[str]] = []
300
+
301
+ def strongconnect(v):
302
+ indices[v] = index_counter[0]
303
+ lowlinks[v] = index_counter[0]
304
+ index_counter[0] += 1
305
+ stack.append(v)
306
+ on_stack.add(v)
307
+
308
+ for w in adj.get(v, set()):
309
+ if w not in indices:
310
+ strongconnect(w)
311
+ lowlinks[v] = min(lowlinks[v], lowlinks[w])
312
+ elif w in on_stack:
313
+ lowlinks[v] = min(lowlinks[v], indices[w])
314
+
315
+ if lowlinks[v] == indices[v]:
316
+ scc = []
317
+ while True:
318
+ w = stack.pop()
319
+ on_stack.discard(w)
320
+ scc.append(w)
321
+ if w == v:
322
+ break
323
+ sccs.append(scc)
324
+
325
+ for name in group_map:
326
+ if name not in indices:
327
+ strongconnect(name)
328
+
329
+ # Build SCC map and mark feedback groups
330
+ scc_map: dict[str, int] = {}
331
+ for scc_id, scc in enumerate(sccs):
332
+ if len(scc) > 1:
333
+ for name in scc:
334
+ if name in group_map:
335
+ group_map[name].is_feedback = True
336
+ group_map[name].scc_id = scc_id
337
+ scc_map[name] = scc_id
338
+
339
+ return scc_map
340
+
341
+ def _topological_sort(
342
+ self, groups: list[BusGroup], scc_map: dict[str, int]
343
+ ) -> list[BusGroup]:
344
+ """Topologically sort bus groups, removing SCC back-edges."""
345
+ from collections import deque
346
+
347
+ group_map = {g.name: g for g in groups}
348
+
349
+ # Build forward deps and reverse adjacency
350
+ deps: dict[str, set[str]] = {g.name: set() for g in groups}
351
+ rdeps: dict[str, set[str]] = {g.name: set() for g in groups}
352
+ for group in groups:
353
+ for source in group.input_sources.values():
354
+ if source.kind == "bus_group" and source.ref in group_map:
355
+ src_name = source.ref
356
+ if (group.name in scc_map and src_name in scc_map
357
+ and scc_map[group.name] == scc_map[src_name]):
358
+ continue
359
+ deps[group.name].add(src_name)
360
+ rdeps[src_name].add(group.name)
361
+
362
+ # Kahn's algorithm with reverse adjacency
363
+ in_degree = {g.name: len(deps[g.name]) for g in groups}
364
+ queue = deque(name for name, deg in in_degree.items() if deg == 0)
365
+ result = []
366
+
367
+ while queue:
368
+ name = queue.popleft()
369
+ result.append(group_map[name])
370
+ for dependent in rdeps.get(name, set()):
371
+ in_degree[dependent] -= 1
372
+ if in_degree[dependent] == 0:
373
+ queue.append(dependent)
374
+
375
+ # Add any remaining
376
+ if len(result) < len(groups):
377
+ visited = {g.name for g in result}
378
+ for g in groups:
379
+ if g.name not in visited:
380
+ result.append(g)
381
+
382
+ return result