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