rocket-engine 0.0.2b1__py3-none-any.whl

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 (241) hide show
  1. rocket_engine/__init__.py +8 -0
  2. rocket_engine/__main__.py +12 -0
  3. rocket_engine/adapters/README.md +54 -0
  4. rocket_engine/adapters/__init__.py +7 -0
  5. rocket_engine/adapters/api.py +39 -0
  6. rocket_engine/adapters/base.py +46 -0
  7. rocket_engine/adapters/cace/__init__.py +163 -0
  8. rocket_engine/adapters/circulax/README.md +10 -0
  9. rocket_engine/adapters/circulax/__init__.py +112 -0
  10. rocket_engine/adapters/circulax/_driver.py +190 -0
  11. rocket_engine/adapters/circulax/adapter.py +538 -0
  12. rocket_engine/adapters/circulax/results.py +291 -0
  13. rocket_engine/adapters/cli/README.md +34 -0
  14. rocket_engine/adapters/cli/__init__.py +305 -0
  15. rocket_engine/adapters/connectivity/__init__.py +196 -0
  16. rocket_engine/adapters/connectivity/results.py +214 -0
  17. rocket_engine/adapters/cosim/README.md +9 -0
  18. rocket_engine/adapters/cosim/__init__.py +1390 -0
  19. rocket_engine/adapters/cosim/results.py +214 -0
  20. rocket_engine/adapters/devsim/__init__.py +171 -0
  21. rocket_engine/adapters/digital_flow/README.md +9 -0
  22. rocket_engine/adapters/digital_flow/__init__.py +1121 -0
  23. rocket_engine/adapters/digital_flow/results.py +227 -0
  24. rocket_engine/adapters/digital_flow/state.py +462 -0
  25. rocket_engine/adapters/drc/README.klayout.md +7 -0
  26. rocket_engine/adapters/drc/README.md +7 -0
  27. rocket_engine/adapters/drc/__init__.py +1 -0
  28. rocket_engine/adapters/drc/klayout.py +952 -0
  29. rocket_engine/adapters/drc/magic.py +287 -0
  30. rocket_engine/adapters/drc/results.py +184 -0
  31. rocket_engine/adapters/drc/results_kl.py +188 -0
  32. rocket_engine/adapters/elmer/__init__.py +163 -0
  33. rocket_engine/adapters/envelope.py +120 -0
  34. rocket_engine/adapters/equivalence/__init__.py +156 -0
  35. rocket_engine/adapters/errors.py +181 -0
  36. rocket_engine/adapters/extract_magic/README.md +15 -0
  37. rocket_engine/adapters/extract_magic/__init__.py +425 -0
  38. rocket_engine/adapters/extract_magic/results.py +209 -0
  39. rocket_engine/adapters/formal_sby/README.md +7 -0
  40. rocket_engine/adapters/formal_sby/__init__.py +773 -0
  41. rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
  42. rocket_engine/adapters/formal_sby/results.py +185 -0
  43. rocket_engine/adapters/gdsfactory/__init__.py +194 -0
  44. rocket_engine/adapters/gdsfactory/_driver.py +459 -0
  45. rocket_engine/adapters/gdsfactory/adapter.py +609 -0
  46. rocket_engine/adapters/gdsfactory/results.py +181 -0
  47. rocket_engine/adapters/hdl_sim/README.md +7 -0
  48. rocket_engine/adapters/hdl_sim/__init__.py +369 -0
  49. rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
  50. rocket_engine/adapters/hdl_sim/results.py +178 -0
  51. rocket_engine/adapters/hdl_sim/verilator.py +167 -0
  52. rocket_engine/adapters/http/README.md +15 -0
  53. rocket_engine/adapters/http/__init__.py +458 -0
  54. rocket_engine/adapters/http/events.py +63 -0
  55. rocket_engine/adapters/ipc/README.md +9 -0
  56. rocket_engine/adapters/ipc/__init__.py +111 -0
  57. rocket_engine/adapters/lvs_netgen/README.md +20 -0
  58. rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
  59. rocket_engine/adapters/lvs_netgen/results.py +168 -0
  60. rocket_engine/adapters/mcp/README.md +19 -0
  61. rocket_engine/adapters/mcp/__init__.py +85 -0
  62. rocket_engine/adapters/mcp/api-schema.json +6107 -0
  63. rocket_engine/adapters/mcp/pull.py +246 -0
  64. rocket_engine/adapters/mcp/server.py +794 -0
  65. rocket_engine/adapters/meep/__init__.py +167 -0
  66. rocket_engine/adapters/meow/README.md +7 -0
  67. rocket_engine/adapters/meow/__init__.py +190 -0
  68. rocket_engine/adapters/meow/_driver.py +227 -0
  69. rocket_engine/adapters/meow/_envelope.py +108 -0
  70. rocket_engine/adapters/meow/adapter.py +491 -0
  71. rocket_engine/adapters/meow/results.py +218 -0
  72. rocket_engine/adapters/mutation/__init__.py +192 -0
  73. rocket_engine/adapters/ngspice/README.md +28 -0
  74. rocket_engine/adapters/ngspice/__init__.py +627 -0
  75. rocket_engine/adapters/ngspice/results.py +215 -0
  76. rocket_engine/adapters/postlayout/README.md +28 -0
  77. rocket_engine/adapters/postlayout/__init__.py +17 -0
  78. rocket_engine/adapters/postlayout/adapter.py +1062 -0
  79. rocket_engine/adapters/postlayout/results.py +330 -0
  80. rocket_engine/adapters/postlayout/translate.py +71 -0
  81. rocket_engine/adapters/profile.py +220 -0
  82. rocket_engine/adapters/sax/README.md +23 -0
  83. rocket_engine/adapters/sax/__init__.py +516 -0
  84. rocket_engine/adapters/sax/_driver.py +148 -0
  85. rocket_engine/adapters/sax/results.py +291 -0
  86. rocket_engine/adapters/stdio_console.py +1151 -0
  87. rocket_engine/adapters/syntax/README.md +5 -0
  88. rocket_engine/adapters/syntax/__init__.py +366 -0
  89. rocket_engine/adapters/syntax/results.py +181 -0
  90. rocket_engine/adapters/synth_yosys/README.md +7 -0
  91. rocket_engine/adapters/synth_yosys/__init__.py +655 -0
  92. rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
  93. rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
  94. rocket_engine/adapters/synth_yosys/results.py +173 -0
  95. rocket_engine/adapters/timing_sta/README.md +9 -0
  96. rocket_engine/adapters/timing_sta/__init__.py +726 -0
  97. rocket_engine/adapters/view_sync/__init__.py +185 -0
  98. rocket_engine/adapters/view_sync/results.py +189 -0
  99. rocket_engine/adapters/vlsir/__init__.py +142 -0
  100. rocket_engine/adapters/xschem/__init__.py +440 -0
  101. rocket_engine/adapters/xschem/deck.py +266 -0
  102. rocket_engine/clients/__init__.py +5 -0
  103. rocket_engine/clients/runtime.py +255 -0
  104. rocket_engine/composition.py +2279 -0
  105. rocket_engine/execution/README.md +30 -0
  106. rocket_engine/execution/__init__.py +150 -0
  107. rocket_engine/execution/apig/__init__.py +1 -0
  108. rocket_engine/execution/apig/pool.py +463 -0
  109. rocket_engine/execution/apig/submit.py +124 -0
  110. rocket_engine/execution/block_runs_scope.py +479 -0
  111. rocket_engine/execution/coalesce.py +414 -0
  112. rocket_engine/execution/console/__init__.py +1 -0
  113. rocket_engine/execution/console/apply_session.py +318 -0
  114. rocket_engine/execution/console/lifecycle.py +265 -0
  115. rocket_engine/execution/dispatch.py +205 -0
  116. rocket_engine/execution/errors.py +31 -0
  117. rocket_engine/execution/ports.py +797 -0
  118. rocket_engine/execution/pyexec/__init__.py +1 -0
  119. rocket_engine/execution/pyexec/apply_context.py +547 -0
  120. rocket_engine/execution/pyexec/approvals.py +173 -0
  121. rocket_engine/execution/pyexec/runner.py +404 -0
  122. rocket_engine/execution/pyexec/support.py +464 -0
  123. rocket_engine/execution/python_exec.py +897 -0
  124. rocket_engine/execution/queue.py +85 -0
  125. rocket_engine/execution/replay_safe.py +601 -0
  126. rocket_engine/execution/routing/__init__.py +1 -0
  127. rocket_engine/execution/routing/calls.py +364 -0
  128. rocket_engine/execution/routing/namespace.py +785 -0
  129. rocket_engine/execution/routing/ports.py +102 -0
  130. rocket_engine/execution/routing/tools.py +346 -0
  131. rocket_engine/execution/runner.py +311 -0
  132. rocket_engine/execution/runtime.py +1149 -0
  133. rocket_engine/execution/service.py +279 -0
  134. rocket_engine/execution/worker.py +464 -0
  135. rocket_engine/interfaces/README.md +26 -0
  136. rocket_engine/interfaces/__init__.py +58 -0
  137. rocket_engine/interfaces/apig_assets.py +407 -0
  138. rocket_engine/interfaces/celllink.py +461 -0
  139. rocket_engine/interfaces/documents.py +92 -0
  140. rocket_engine/interfaces/envelopes.py +131 -0
  141. rocket_engine/interfaces/errors.py +16 -0
  142. rocket_engine/interfaces/layout.py +410 -0
  143. rocket_engine/interfaces/layout_schema.yaml +278 -0
  144. rocket_engine/interfaces/paths.py +223 -0
  145. rocket_engine/interfaces/pdks.py +550 -0
  146. rocket_engine/interfaces/ports.py +72 -0
  147. rocket_engine/interfaces/references.py +115 -0
  148. rocket_engine/interfaces/refs.py +85 -0
  149. rocket_engine/interfaces/runparams.py +304 -0
  150. rocket_engine/interfaces/setup.py +552 -0
  151. rocket_engine/interfaces/setup_catalogue.yaml +243 -0
  152. rocket_engine/interfaces/setup_probes.py +66 -0
  153. rocket_engine/interfaces/vocabulary.py +58 -0
  154. rocket_engine/interfaces/yaml_dump.py +31 -0
  155. rocket_engine/library/README.md +22 -0
  156. rocket_engine/library/__init__.py +13 -0
  157. rocket_engine/library/apig_catalog.py +371 -0
  158. rocket_engine/library/catalog.py +185 -0
  159. rocket_engine/library/errors.py +16 -0
  160. rocket_engine/mutate/__init__.py +776 -0
  161. rocket_engine/mutate/operators.py +700 -0
  162. rocket_engine/ops/__init__.py +1 -0
  163. rocket_engine/ops/deferred.py +279 -0
  164. rocket_engine/ops/editors.py +404 -0
  165. rocket_engine/ops/evidence.py +194 -0
  166. rocket_engine/ops/gates.py +262 -0
  167. rocket_engine/ops/ports.py +76 -0
  168. rocket_engine/ops/queries.py +417 -0
  169. rocket_engine/ops/runs.py +553 -0
  170. rocket_engine/ops/submit.py +614 -0
  171. rocket_engine/project/README.md +27 -0
  172. rocket_engine/project/__init__.py +6 -0
  173. rocket_engine/project/canvas/__init__.py +55 -0
  174. rocket_engine/project/canvas/compile.py +109 -0
  175. rocket_engine/project/canvas/integrate.py +48 -0
  176. rocket_engine/project/canvas/schema.py +241 -0
  177. rocket_engine/project/canvas/templates.py +46 -0
  178. rocket_engine/project/canvas/validate.py +603 -0
  179. rocket_engine/project/edit/__init__.py +5 -0
  180. rocket_engine/project/edit/cas.py +87 -0
  181. rocket_engine/project/edit/transactions.py +282 -0
  182. rocket_engine/project/errors.py +16 -0
  183. rocket_engine/project/frontend13.py +685 -0
  184. rocket_engine/project/geometry/__init__.py +23 -0
  185. rocket_engine/project/geometry/ops.py +451 -0
  186. rocket_engine/project/geometry/snap.py +103 -0
  187. rocket_engine/project/ip/__init__.py +1 -0
  188. rocket_engine/project/ip/blocks.py +195 -0
  189. rocket_engine/project/ip/entry.schema.json +78 -0
  190. rocket_engine/project/ip/lifecycle.py +402 -0
  191. rocket_engine/project/ip/verbs.py +123 -0
  192. rocket_engine/project/layers/__init__.py +6 -0
  193. rocket_engine/project/layers/bindings.py +115 -0
  194. rocket_engine/project/layers/catalog.py +228 -0
  195. rocket_engine/project/layers/presets.py +43 -0
  196. rocket_engine/project/ledger/__init__.py +1 -0
  197. rocket_engine/project/ledger/docs.py +167 -0
  198. rocket_engine/project/ledger/entities.py +176 -0
  199. rocket_engine/project/ledger/events.py +29 -0
  200. rocket_engine/project/ledger/runs.py +257 -0
  201. rocket_engine/project/ledger/shared.py +174 -0
  202. rocket_engine/project/ledger/snapshots.py +385 -0
  203. rocket_engine/project/ledger/wires.py +552 -0
  204. rocket_engine/project/process/__init__.py +14 -0
  205. rocket_engine/project/process/model.py +193 -0
  206. rocket_engine/project/process/recipes.py +121 -0
  207. rocket_engine/project/schematic/__init__.py +1 -0
  208. rocket_engine/project/schematic/hierarchy.py +312 -0
  209. rocket_engine/project/schematic/instances.py +393 -0
  210. rocket_engine/project/schematic/sheets.py +198 -0
  211. rocket_engine/project/service.py +2941 -0
  212. rocket_engine/project/symbols/__init__.py +1 -0
  213. rocket_engine/project/symbols/cells.py +324 -0
  214. rocket_engine/project/symbols/linkage.py +356 -0
  215. rocket_engine/project/symbols/symbols.py +352 -0
  216. rocket_engine/project/templates/__init__.py +303 -0
  217. rocket_engine/project/templates/errors.py +25 -0
  218. rocket_engine/project/testbench/__init__.py +49 -0
  219. rocket_engine/project/testbench/blocks.py +619 -0
  220. rocket_engine/project/testbench/templates.py +134 -0
  221. rocket_engine/stores/README.md +12 -0
  222. rocket_engine/stores/__init__.py +6 -0
  223. rocket_engine/stores/ecs.py +1508 -0
  224. rocket_engine/stores/layers.py +1023 -0
  225. rocket_engine/stores/settings.py +1188 -0
  226. rocket_engine/stores/wireplace.py +1324 -0
  227. rocket_engine/workflows/README.md +18 -0
  228. rocket_engine/workflows/__init__.py +53 -0
  229. rocket_engine/workflows/capability_plan.py +102 -0
  230. rocket_engine/workflows/completion.py +228 -0
  231. rocket_engine/workflows/connectgraph.py +734 -0
  232. rocket_engine/workflows/connectgraph_emit.py +336 -0
  233. rocket_engine/workflows/gates.py +144 -0
  234. rocket_engine/workflows/photonic_wire.py +76 -0
  235. rocket_engine/workflows/stages.py +324 -0
  236. rocket_engine/workflows/viewsync.py +399 -0
  237. rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
  238. rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
  239. rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
  240. rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
  241. rocket_engine-0.0.2b1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,655 @@
1
+ """Yosys synth solver adapter: run_synth behind the ToolAdapter port.
2
+
3
+ Protocol, verified against Yosys 0.70+219 (oss-cad-suite): write a
4
+ synth script (`read_verilog` the sources, `hierarchy -check -top`,
5
+ `synth -top`, then `stat` plus `write_verilog`), run it under a
6
+ timeout, and parse the log plus the stat report. With a `liberty`
7
+ input the flow maps to standard cells instead (`dfflibmap` +
8
+ `abc -liberty`, `stat -liberty`): evidence is the mapped netlist
9
+ artifact, per-family cell counts, and the chip area. `hierarchy
10
+ -check` fails loudly (nonzero exit plus `ERROR:`) on a missing top,
11
+ and broken RTL fails the same way; warnings (`Warning:`, including
12
+ `Latch inferred` and `has no driver`) parse as findings that never
13
+ fail the verdict on their own. ABC's own `ABC: Warning:` chatter is
14
+ tool noise, never a design finding.
15
+
16
+ Tool resolution: an explicit job `tool_dir` wins, else PATH, and the
17
+ resolved binary must answer `yosys -V` with a parseable `Yosys
18
+ major.minor` banner — ambient installs are kept and
19
+ version-asserted, never reinstalled. Where the tools live on disk is
20
+ B10 integration scope.
21
+ """
22
+
23
+ import json
24
+ import re
25
+ import shutil
26
+ import subprocess
27
+ from pathlib import Path
28
+
29
+ from ...execution.errors import ExecutionError
30
+ from ...execution.runner import tool_roots
31
+ from ...interfaces import ToolAdapter
32
+ from ..errors import AdapterError
33
+
34
+ OPERATION = "run_synth"
35
+ _TOOL = "yosys"
36
+ _TIMEOUT_S = 120
37
+ _VERSION_TIMEOUT_S = 30
38
+
39
+ _SCRIPT_NAME = "synth.ys"
40
+ _LOG_NAME = "synth.log"
41
+ _STAT_NAME = "synth_stat.txt"
42
+ _NETLIST_NAME = "synth_netlist.v"
43
+ _NETLIST_JSON_NAME = "synth_netlist.json"
44
+
45
+ _TOP_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
46
+ # Verilog `define names, with an optional =value (no whitespace or
47
+ # shell metacharacters; argv is never shelled, belt and suspenders).
48
+ _DEFINE_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(=[A-Za-z0-9_.+-]+)?")
49
+ _VERSION_RE = re.compile(r"Yosys (\d+)\.(\d+)")
50
+ _ERROR_RE = re.compile(r"^(?:(.+?):(\d+): )?ERROR: (.*)$", re.M)
51
+ _WARNING_RE = re.compile(r"^(?:(.+?):(\d+): )?Warning: (.*)$", re.M)
52
+ _CELLS_RE = re.compile(r"^\s*(\d+)(?:\s+[\d.Ee+-]+)?\s+cells\s*$")
53
+ _CELL_LINE_RE = re.compile(r"^\s*(\d+)(?:\s+([\d.Ee+-]+))?\s+(\S+)\s*$")
54
+ _WIRES_RE = re.compile(r"^\s*(\d+)(?:\s+(?:[\d.Ee+-]+|-))?\s+wires\s*$")
55
+ _SUBMODULES_RE = re.compile(r"^\s*\d+\s+submodules\s*$")
56
+ _AREA_TOP_RE = re.compile(r"Chip area for top module '[^']*':\s*([\d.Ee+-]+)")
57
+ _AREA_MODULE_RE = re.compile(r"Chip area for module '[^']*':\s*([\d.Ee+-]+)")
58
+
59
+
60
+ def _parse_errors(text):
61
+ """Collect synth_error violations from yosys ERROR lines.
62
+
63
+ Args:
64
+ text: Combined yosys log/stdout/stderr text.
65
+
66
+ Returns:
67
+ Error violations with file/line when the log carries them.
68
+ """
69
+ violations = []
70
+ for match in _ERROR_RE.finditer(text):
71
+ file, line, message = match.groups()
72
+ violation = {
73
+ "rule": "synth_error",
74
+ "severity": "error",
75
+ "message": message,
76
+ }
77
+ if file is not None:
78
+ violation["file"] = file
79
+ violation["line"] = int(line)
80
+ violations.append(violation)
81
+ return violations
82
+
83
+
84
+ def _warning_rule(message):
85
+ """Classify one yosys warning into its finding rule.
86
+
87
+ Args:
88
+ message: Warning body after the `Warning:` prefix.
89
+
90
+ Returns:
91
+ Latch, undriven, or the generic synth_warning rule name.
92
+ """
93
+ if "Latch inferred" in message:
94
+ return "latch"
95
+ if "has no driver" in message:
96
+ return "undriven"
97
+ return "synth_warning"
98
+
99
+
100
+ def _parse_warnings(text):
101
+ """Collect warning findings from yosys Warning lines.
102
+
103
+ ABC's own `ABC: Warning:` lines are optimizer chatter, and the
104
+ trailing `Warnings: N unique` line is a summary — neither is a
105
+ design finding, so both are skipped.
106
+
107
+ Args:
108
+ text: Combined yosys log/stdout/stderr text.
109
+
110
+ Returns:
111
+ Warning violations with file/line when the log carries them.
112
+ """
113
+ violations = []
114
+ for match in _WARNING_RE.finditer(text):
115
+ start = match.start()
116
+ line_start = text.rfind("\n", 0, start) + 1
117
+ prefix = text[line_start:start]
118
+ if "ABC:" in prefix:
119
+ continue
120
+ file, line, message = match.groups()
121
+ violation = {
122
+ "rule": _warning_rule(message),
123
+ "severity": "warning",
124
+ "message": message,
125
+ }
126
+ if file is not None:
127
+ violation["file"] = file
128
+ violation["line"] = int(line)
129
+ violations.append(violation)
130
+ return violations
131
+
132
+
133
+ def _parse_stat(text):
134
+ """Aggregate cell counts, wires, and area over stat sections.
135
+
136
+ Each `=== module ===` section reports local counts excluding
137
+ submodules, so summing across sections is the design total. The
138
+ `Chip area for top module` line wins when present; otherwise the
139
+ per-module areas sum (same total, verified on SERV).
140
+
141
+ Args:
142
+ text: Stat report text from `tee -o stat`.
143
+
144
+ Returns:
145
+ Mapping with cells (type -> count), cell_count, wire_count,
146
+ area (None without a liberty-mapped stat), and
147
+ saw_cells (whether any cells block parsed).
148
+ """
149
+ cells = {}
150
+ cell_count = 0
151
+ wire_count = 0
152
+ saw_cells = False
153
+ in_cells = False
154
+ for line in text.splitlines():
155
+ if _WIRES_RE.match(line):
156
+ wire_count += int(_WIRES_RE.match(line).group(1))
157
+ in_cells = False
158
+ continue
159
+ if _CELLS_RE.match(line):
160
+ saw_cells = True
161
+ in_cells = True
162
+ continue
163
+ if in_cells:
164
+ if not line.strip() or _SUBMODULES_RE.match(line):
165
+ in_cells = False
166
+ continue
167
+ entry = _CELL_LINE_RE.match(line)
168
+ if entry is not None:
169
+ count, _area, cell_type = entry.groups()
170
+ cells[cell_type] = cells.get(cell_type, 0) + int(count)
171
+ cell_count += int(count)
172
+ top_area = _AREA_TOP_RE.search(text)
173
+ if top_area is not None:
174
+ area = float(top_area.group(1))
175
+ else:
176
+ module_areas = _AREA_MODULE_RE.findall(text)
177
+ area = sum(float(a) for a in module_areas) if module_areas else None
178
+ return {
179
+ "cells": cells,
180
+ "cell_count": cell_count,
181
+ "wire_count": wire_count,
182
+ "area": area,
183
+ "saw_cells": saw_cells,
184
+ }
185
+
186
+
187
+ def _parse_netlist_json(payload):
188
+ """Aggregate cell counts and wires over a write_json netlist.
189
+
190
+ Blessed structured escape (ESCAPE-HARDEN): yosys `write_json`
191
+ carries the same design the scraped `stat` text summarizes.
192
+ Per-module netnames count as wires; per-module cells count
193
+ except cells whose type names another module (submodule
194
+ instances, never leaf cells) — this reproduces the stat text
195
+ local-count sums byte-identically (live-verified on yosys
196
+ 0.70+219, including a hierarchical design). The JSON carries
197
+ no mapped area, so area still comes from the stat text.
198
+
199
+ Args:
200
+ payload: Parsed `write_json` mapping, or None when yosys
201
+ wrote no JSON report.
202
+
203
+ Returns:
204
+ Mapping with cells (type -> count), cell_count,
205
+ wire_count, and saw_cells, or None when the payload
206
+ carries no module table.
207
+ """
208
+ if not isinstance(payload, dict):
209
+ return None
210
+ modules = payload.get("modules")
211
+ if not isinstance(modules, dict) or not modules:
212
+ return None
213
+ names = set(modules)
214
+ cells = {}
215
+ cell_count = 0
216
+ wire_count = 0
217
+ for module in modules.values():
218
+ if not isinstance(module, dict):
219
+ continue
220
+ netnames = module.get("netnames")
221
+ if isinstance(netnames, dict):
222
+ wire_count += len(netnames)
223
+ for cell in (module.get("cells") or {}).values():
224
+ cell_type = cell.get("type") if isinstance(cell, dict) else None
225
+ if not isinstance(cell_type, str) or cell_type in names:
226
+ continue
227
+ cells[cell_type] = cells.get(cell_type, 0) + 1
228
+ cell_count += 1
229
+ return {
230
+ "cells": cells,
231
+ "cell_count": cell_count,
232
+ "wire_count": wire_count,
233
+ "saw_cells": True,
234
+ }
235
+
236
+
237
+ def _quote(path):
238
+ """Quote one path for the yosys script language.
239
+
240
+ Args:
241
+ path: Filesystem path to embed in the script.
242
+
243
+ Returns:
244
+ Double-quoted path with backslashes and quotes escaped.
245
+ """
246
+ return '"' + str(path).replace("\\", "\\\\").replace('"', '\\"') + '"'
247
+
248
+
249
+ class YosysSynthAdapter(ToolAdapter):
250
+ """run_synth via Yosys: RTL to mapped netlist plus evidence.
251
+
252
+ Serves the run_synth operation with deck.tool `yosys` (the only
253
+ tool). Synthesizes the sources for the named top with `synth
254
+ -top`; with a `liberty` input it maps to standard cells
255
+ (`dfflibmap` + `abc -liberty`) and reports the chip area.
256
+ Evidence is the mapped netlist artifact, per-family cell counts,
257
+ the area estimate when mapped, and warnings as findings.
258
+
259
+ Job keys:
260
+ sources (required): Verilog source paths under synth.
261
+ top (required): Top module name (plain identifier).
262
+ defines: Verilog `define list, optional =value each.
263
+ liberty: Liberty (.lib) path for standard-cell mapping;
264
+ generic synth without it.
265
+ timeout_s: Synth budget in seconds (default 120).
266
+ tool_dir: Directory holding yosys; PATH otherwise.
267
+ deck: Optional mapping; deck.tool must be yosys here.
268
+ workdir: Run directory (default ".").
269
+
270
+ Args:
271
+ runner: Optional isolated runner; direct subprocess when None.
272
+
273
+ Caution: Broken RTL and a missing top fail loudly (nonzero exit
274
+ plus synth_error findings) — a failed run never parses as
275
+ clean, and warnings alone never fail the verdict.
276
+ """
277
+
278
+ def __init__(self, runner=None):
279
+ self._runner = runner
280
+
281
+ def capabilities(self, target):
282
+ """Report the served operation, tool, and mapping modes.
283
+
284
+ Args:
285
+ target: Target the capabilities are reported for
286
+ (accepted, unused — one top per job).
287
+
288
+ Returns:
289
+ Capability mapping with operations, tool, defines, and
290
+ liberty support.
291
+ """
292
+ return {
293
+ "operations": [OPERATION],
294
+ "tool": _TOOL,
295
+ "defines": True,
296
+ "liberty": True,
297
+ }
298
+
299
+ def preflight(self, job):
300
+ """Check operation, top, sources, defines, liberty, yosys.
301
+
302
+ Cheap deterministic checks first, tool resolution last; the
303
+ resolved binary must answer its version probe.
304
+
305
+ Args:
306
+ job: Solver job with operation run_synth plus sources,
307
+ top, and optional defines/liberty/timeout inputs.
308
+
309
+ Returns:
310
+ Checked mapping with the resolved yosys path and
311
+ version, sources, top, defines, liberty, timeout_s.
312
+
313
+ Raises:
314
+ AdapterError: unsupported_operation for another
315
+ operation, invalid_command for a bad top, timeout,
316
+ or defines, invalid_reference for missing sources
317
+ or liberty, missing_dependency for an unresolvable
318
+ or unassertable yosys.
319
+ """
320
+ # Cheap deterministic checks first: operation, top, timeout,
321
+ # sources, defines, liberty. Tool resolution last, since it
322
+ # depends on the machine.
323
+ if job.get("operation") != OPERATION:
324
+ raise AdapterError(
325
+ "unsupported_operation",
326
+ f"Yosys synth handles {OPERATION}, got {job.get('operation')}.",
327
+ )
328
+ top = job.get("top")
329
+ if not isinstance(top, str) or not _TOP_RE.fullmatch(top):
330
+ raise AdapterError("invalid_command", f"Bad top: {top!r}.")
331
+ timeout = job.get("timeout_s", _TIMEOUT_S)
332
+ if (
333
+ not isinstance(timeout, (int, float))
334
+ or isinstance(timeout, bool)
335
+ or not timeout > 0
336
+ ):
337
+ raise AdapterError("invalid_command", f"Bad timeout_s: {timeout!r}.")
338
+ sources = job.get("sources") or []
339
+ missing = [s for s in sources if not Path(s).is_file()]
340
+ if not sources or missing:
341
+ raise AdapterError(
342
+ "invalid_reference", f"Sources not found: {missing or sources}."
343
+ )
344
+ defines = job.get("defines") or []
345
+ if not isinstance(defines, list) or any(
346
+ not isinstance(d, str) or not _DEFINE_RE.fullmatch(d) for d in defines
347
+ ):
348
+ raise AdapterError("invalid_command", f"Bad defines: {defines!r}.")
349
+ liberty = job.get("liberty")
350
+ if liberty is not None and not Path(liberty).is_file():
351
+ raise AdapterError("invalid_reference", f"Liberty not found: {liberty}.")
352
+ yosys = self._resolve(job, _TOOL)
353
+ checked = {
354
+ "yosys": yosys,
355
+ "yosys_version": self._version(yosys),
356
+ "sources": list(sources),
357
+ "top": top,
358
+ "defines": list(defines),
359
+ "liberty": liberty,
360
+ "timeout_s": timeout,
361
+ }
362
+ if self._runner is not None:
363
+ try:
364
+ self._runner.preflight()
365
+ except ExecutionError as exc:
366
+ raise AdapterError(exc.code, str(exc)) from exc
367
+ return checked
368
+
369
+ def execute(self, job):
370
+ """Synthesize the sources for the top with yosys.
371
+
372
+ Writes the synth script into the workdir, runs it under the
373
+ timeout, and returns the log, stat report, JSON netlist, and
374
+ netlist path. A failed run still returns its log — the
375
+ verdict comes from parse, never the exit code alone.
376
+
377
+ Args:
378
+ job: Solver job with operation run_synth plus sources,
379
+ top, and optional defines/liberty/timeout inputs.
380
+
381
+ Returns:
382
+ Native output with exit_code, stdout/stderr, log and
383
+ stat text, the parsed `write_json` netlist under
384
+ `netlist_json` (None when yosys wrote none), netlist
385
+ path, top, mapped flag, and the yosys version.
386
+
387
+ Raises:
388
+ AdapterError: invalid_reference for a missing workdir,
389
+ runner_unavailable on a synth timeout, plus the
390
+ preflight codes (execute re-checks first).
391
+ """
392
+ checked = self.preflight(job)
393
+ workdir = job.get("workdir", ".")
394
+ if not Path(workdir).is_dir():
395
+ raise AdapterError(
396
+ "invalid_reference", f"Workdir not found: {workdir}."
397
+ )
398
+ script = self._script(checked)
399
+ (Path(workdir) / _SCRIPT_NAME).write_text(script)
400
+ roots = tool_roots([checked["yosys"]])
401
+ proc = self._run(
402
+ [checked["yosys"], "-q", "-s", _SCRIPT_NAME, "-l", _LOG_NAME],
403
+ workdir,
404
+ checked["timeout_s"],
405
+ roots,
406
+ )
407
+ log_path = Path(workdir) / _LOG_NAME
408
+ stat_path = Path(workdir) / _STAT_NAME
409
+ netlist_json_path = Path(workdir) / _NETLIST_JSON_NAME
410
+ netlist_json = None
411
+ if netlist_json_path.is_file():
412
+ try:
413
+ parsed_json = json.loads(netlist_json_path.read_text())
414
+ if isinstance(parsed_json, dict):
415
+ netlist_json = parsed_json
416
+ except (OSError, ValueError):
417
+ netlist_json = None
418
+ return {
419
+ "exit_code": proc.returncode,
420
+ "stdout": proc.stdout,
421
+ "stderr": proc.stderr,
422
+ "log": log_path.read_text() if log_path.is_file() else "",
423
+ "stat": stat_path.read_text() if stat_path.is_file() else "",
424
+ "netlist_json": netlist_json,
425
+ "netlist": str(Path(workdir) / _NETLIST_NAME),
426
+ "top": checked["top"],
427
+ "mapped": checked["liberty"] is not None,
428
+ "yosys_version": checked["yosys_version"],
429
+ }
430
+
431
+ def parse(self, native_output):
432
+ """Parse errors, warnings, stat counts, and area evidence.
433
+
434
+ Cell counts come from the `write_json` netlist first
435
+ (byte-identical to the stat text; the mapped area still
436
+ comes from the stat text since JSON carries no area key),
437
+ falling back to the scraped stat text for older natives.
438
+ Failed runs surface their ERROR lines (synthesizing one when
439
+ the log carries none, so failure never parses as clean);
440
+ warnings become findings that never fail alone; a clean run
441
+ reports cell counts plus the mapped area.
442
+
443
+ Args:
444
+ native_output: Native output with exit_code, log and
445
+ stat text, the optional parsed `netlist_json`, and
446
+ the netlist path.
447
+
448
+ Returns:
449
+ {total, violations[], measurements[], source} plus
450
+ cells, cell_count, wire_count, area, mapped,
451
+ netlist, top, and yosys_version evidence; source
452
+ is `log` or `stdio_fallback`.
453
+
454
+ Raises:
455
+ AdapterError: runner_unavailable when the output has no
456
+ exit code, or a clean run left no stat verdict or
457
+ netlist — unparseable never parses as clean.
458
+ """
459
+ exit_code = native_output.get("exit_code")
460
+ if exit_code is None:
461
+ raise AdapterError(
462
+ "runner_unavailable", "Native output has no exit code."
463
+ )
464
+ # MIGRATE-STDIO-FILE: synth.log (via -l) is the parse
465
+ # channel; with -q the stdout join could double-report
466
+ # lines present in both streams, so stdio survives only
467
+ # as a loud fallback for natives recorded before the
468
+ # file channel existed.
469
+ log = native_output.get("log") or ""
470
+ if log.strip():
471
+ text, source = log, "log"
472
+ else:
473
+ text = "\n".join(
474
+ native_output.get(key, "") or ""
475
+ for key in ("stdout", "stderr")
476
+ )
477
+ source = "stdio_fallback"
478
+ errors = _parse_errors(text)
479
+ warnings = _parse_warnings(text)
480
+ stat_text = _parse_stat(native_output.get("stat") or "")
481
+ stat_json = _parse_netlist_json(native_output.get("netlist_json"))
482
+ if stat_json is not None:
483
+ # Structured counts win; the mapped area has no JSON
484
+ # key, so it still comes from the stat text.
485
+ stat = {**stat_json, "area": stat_text["area"]}
486
+ else:
487
+ stat = stat_text
488
+ netlist = native_output.get("netlist")
489
+ if exit_code != 0:
490
+ if not errors:
491
+ # Defensive: a failed run must never parse as clean.
492
+ errors = [
493
+ {
494
+ "rule": "synth_error",
495
+ "severity": "error",
496
+ "message": f"yosys exited {exit_code}",
497
+ }
498
+ ]
499
+ violations = errors + warnings
500
+ return {
501
+ "total": len(violations),
502
+ "violations": violations,
503
+ "measurements": [],
504
+ "cells": stat["cells"],
505
+ "cell_count": stat["cell_count"],
506
+ "wire_count": stat["wire_count"],
507
+ "area": None,
508
+ "mapped": bool(native_output.get("mapped")),
509
+ "netlist": netlist,
510
+ "top": native_output.get("top"),
511
+ "yosys_version": native_output.get("yosys_version"),
512
+ "source": source,
513
+ }
514
+ if not stat["saw_cells"]:
515
+ raise AdapterError(
516
+ "runner_unavailable", "yosys output has no stat verdict."
517
+ )
518
+ if netlist is None or not Path(netlist).is_file():
519
+ raise AdapterError(
520
+ "runner_unavailable", "yosys run left no netlist artifact."
521
+ )
522
+ violations = errors + warnings
523
+ measurements = [
524
+ {"name": "cell_count", "value": stat["cell_count"], "unit": "cells"},
525
+ {"name": "wire_count", "value": stat["wire_count"], "unit": "wires"},
526
+ ]
527
+ if stat["area"] is not None:
528
+ measurements.append(
529
+ {"name": "area", "value": stat["area"], "unit": "um^2"}
530
+ )
531
+ return {
532
+ "total": len(violations),
533
+ "violations": violations,
534
+ "measurements": measurements,
535
+ "cells": stat["cells"],
536
+ "cell_count": stat["cell_count"],
537
+ "wire_count": stat["wire_count"],
538
+ "area": stat["area"],
539
+ "mapped": bool(native_output.get("mapped")),
540
+ "netlist": netlist,
541
+ "top": native_output.get("top"),
542
+ "yosys_version": native_output.get("yosys_version"),
543
+ "source": source,
544
+ }
545
+
546
+ @staticmethod
547
+ def _script(checked):
548
+ """Render the yosys synth script for the checked job.
549
+
550
+ Args:
551
+ checked: Preflight mapping with sources, top, defines,
552
+ and the optional liberty path.
553
+
554
+ Returns:
555
+ Yosys script text (read, check, synth, map, stat, dump
556
+ plus the `write_json` structured netlist).
557
+ """
558
+ lines = [
559
+ "read_verilog"
560
+ + "".join(f" -D{d}" for d in checked["defines"])
561
+ + "".join(f" {_quote(s)}" for s in checked["sources"]),
562
+ f"hierarchy -check -top {checked['top']}",
563
+ f"synth -top {checked['top']}",
564
+ ]
565
+ if checked["liberty"] is not None:
566
+ quoted = _quote(checked["liberty"])
567
+ lines.extend(
568
+ [
569
+ f"dfflibmap -liberty {quoted}",
570
+ f"abc -liberty {quoted}",
571
+ "clean",
572
+ f"tee -o {_STAT_NAME} stat -liberty {quoted}",
573
+ ]
574
+ )
575
+ else:
576
+ lines.append(f"tee -o {_STAT_NAME} stat")
577
+ lines.append(f"write_verilog {_NETLIST_NAME}")
578
+ lines.append(f"write_json {_NETLIST_JSON_NAME}")
579
+ return "\n".join(lines) + "\n"
580
+
581
+ @staticmethod
582
+ def _resolve(job, binary):
583
+ tool_dir = job.get("tool_dir")
584
+ if tool_dir:
585
+ candidate = Path(tool_dir) / binary
586
+ if candidate.is_file():
587
+ return str(candidate)
588
+ raise AdapterError(
589
+ "missing_dependency",
590
+ f"{binary} not found in {tool_dir} — install the pinned "
591
+ "oss-cad-suite bundle (see docs/provision-plan.md).",
592
+ )
593
+ found = shutil.which(binary)
594
+ if found is None:
595
+ raise AdapterError(
596
+ "missing_dependency",
597
+ f"{binary} not on PATH — install the pinned oss-cad-suite "
598
+ "bundle (see docs/provision-plan.md) or pass tool_dir.",
599
+ )
600
+ return found
601
+
602
+ @staticmethod
603
+ def _version(yosys):
604
+ """Assert the binary answers with a parseable yosys banner.
605
+
606
+ Args:
607
+ yosys: Resolved yosys binary path.
608
+
609
+ Returns:
610
+ Full `yosys -V` banner line (kept as evidence).
611
+
612
+ Raises:
613
+ AdapterError: missing_dependency when the probe fails,
614
+ times out, or answers no yosys banner.
615
+ """
616
+ try:
617
+ proc = subprocess.run(
618
+ [yosys, "-V"],
619
+ capture_output=True,
620
+ text=True,
621
+ timeout=_VERSION_TIMEOUT_S,
622
+ )
623
+ except subprocess.TimeoutExpired as exc:
624
+ raise AdapterError(
625
+ "missing_dependency",
626
+ f"yosys version probe timed out at {yosys}.",
627
+ ) from exc
628
+ banner = (proc.stdout + "\n" + proc.stderr).strip().splitlines()
629
+ versioned = next((line for line in banner if _VERSION_RE.search(line)), None)
630
+ if proc.returncode != 0 or versioned is None:
631
+ raise AdapterError(
632
+ "missing_dependency",
633
+ f"yosys at {yosys} answered no yosys banner — install the "
634
+ "pinned oss-cad-suite bundle (see docs/provision-plan.md).",
635
+ )
636
+ return versioned.strip()
637
+
638
+ def _run(self, argv, workdir, timeout_s, extra_binds=()):
639
+ if self._runner is not None:
640
+ try:
641
+ return self._runner.run(argv, workdir, timeout_s, extra_binds)
642
+ except ExecutionError as exc:
643
+ raise AdapterError(exc.code, str(exc)) from exc
644
+ try:
645
+ return subprocess.run(
646
+ argv,
647
+ capture_output=True,
648
+ text=True,
649
+ timeout=timeout_s,
650
+ cwd=workdir,
651
+ )
652
+ except subprocess.TimeoutExpired as exc:
653
+ raise AdapterError(
654
+ "runner_unavailable", f"synth timed out after {timeout_s}s."
655
+ ) from exc