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,181 @@
1
+ """Gdsfactory v1 normalizer: v0 parse output to a ResultBundle.
2
+
3
+ Maps the ``generate_views`` parse shape (``measurements[]`` with
4
+ per-cell polygon/port/area counts plus ``mappings[]`` and
5
+ ``violations[]``) onto a typed cell mappings table, a bound
6
+ view, PASS measurements, and findings. Pure function of the
7
+ parsed output: no I/O, no tool calls, deterministic ids derived
8
+ from the run id. Artifact paths stay in the table as evidence;
9
+ no bundle artifact is fabricated without a recorded digest.
10
+ """
11
+
12
+ TASK = "generate_views"
13
+
14
+ _DIAG_CAP = 240
15
+
16
+ _SEVERITY = {"error": "error", "warning": "warning"}
17
+
18
+
19
+ def _cap(text):
20
+ """Cap one diagnostic string at the bundle prose limit.
21
+
22
+ Args:
23
+ text: Raw diagnostic text, possibly unbounded tool prose.
24
+
25
+ Returns:
26
+ The text unchanged when short, else truncated with an
27
+ ellipsis marker.
28
+ """
29
+ text = text if isinstance(text, str) else str(text)
30
+ if len(text) <= _DIAG_CAP:
31
+ return text
32
+ return text[: _DIAG_CAP - 3] + "..."
33
+
34
+
35
+ def _quantity(value, unit):
36
+ """Build one v1 Quantity, mapping unitless to dimensionless.
37
+
38
+ Args:
39
+ value: Numeric value.
40
+ unit: Unit string, possibly empty for unitless measures.
41
+
42
+ Returns:
43
+ Quantity mapping with an explicit unit.
44
+ """
45
+ return {"value": float(value), "unit": unit if unit else "1"}
46
+
47
+
48
+ def _provenance(project_id, run_id, tools, provenance):
49
+ """Build ResultProvenance, defaulting to a placeholder closure.
50
+
51
+ Args:
52
+ project_id: Project the run belongs to.
53
+ run_id: Run the bundle describes.
54
+ tools: Tool ingredient mappings (id, version, digest?).
55
+ provenance: Full provenance mapping when the caller has
56
+ one; a deterministic placeholder otherwise.
57
+
58
+ Returns:
59
+ Provenance mapping valid against the v1 schema.
60
+ """
61
+ if provenance is not None:
62
+ return provenance
63
+ ref = {"scope": "project", "id": project_id, "revision": "r1"}
64
+ return {
65
+ "snapshot": dict(ref),
66
+ "input_lock": dict(ref),
67
+ "closure_hash": "0" * 64,
68
+ "tools": [
69
+ {
70
+ "id": item["id"],
71
+ "version": item.get("version", ""),
72
+ "digest": item.get("digest", "0" * 64),
73
+ }
74
+ for item in tools
75
+ ],
76
+ "parents": [],
77
+ }
78
+
79
+
80
+ def normalize(parsed, *, run_id, project_id="project", target=None,
81
+ tools=(), provenance=None):
82
+ """Normalize one gdsfactory views parse output to a ResultBundle.
83
+
84
+ Cell mappings become a table dataset (component, ports,
85
+ polygons, bbox area) plus a bound table view; per-cell
86
+ counts ride PASS measurements; violations become findings
87
+ with capped messages.
88
+
89
+ Args:
90
+ parsed: v0 parse mapping with ``measurements[]``,
91
+ ``mappings[]``, and ``violations[]``.
92
+ run_id: Run the bundle describes (anchors all ids).
93
+ project_id: Project the run belongs to.
94
+ target: v1 target mapping; project kind by default.
95
+ tools: Tool ingredient mappings for provenance.
96
+ provenance: Full provenance mapping when the caller has
97
+ one; a deterministic placeholder otherwise.
98
+
99
+ Returns:
100
+ Schema-valid ResultBundle mapping.
101
+ """
102
+ measurements_in = parsed.get("measurements") or []
103
+ mappings = parsed.get("mappings") or []
104
+ violations = parsed.get("violations") or []
105
+ table_id = f"{run_id}:dataset:cells"
106
+ datasets = [
107
+ {
108
+ "id": table_id,
109
+ "kind": "table",
110
+ "row_count": len(mappings),
111
+ "columns": [
112
+ {
113
+ "id": f"{table_id}:component",
114
+ "name": "component",
115
+ "dtype": "utf8",
116
+ "unit": "1",
117
+ "data": [m.get("component", "") for m in mappings],
118
+ },
119
+ {
120
+ "id": f"{table_id}:ports",
121
+ "name": "ports",
122
+ "dtype": "float64",
123
+ "unit": "1",
124
+ "data": [float(len(m.get("ports") or [])) for m in mappings],
125
+ },
126
+ {
127
+ "id": f"{table_id}:polygons",
128
+ "name": "polygons",
129
+ "dtype": "float64",
130
+ "unit": "1",
131
+ "data": [float(m.get("n_polygons", 0)) for m in mappings],
132
+ },
133
+ ],
134
+ "conditions": [],
135
+ }
136
+ ]
137
+ views = [
138
+ {
139
+ "id": f"{run_id}:view:cells",
140
+ "kind": "table",
141
+ "dataset_id": table_id,
142
+ "name": "cell mappings",
143
+ }
144
+ ]
145
+ measurements = [
146
+ {
147
+ "name": item["name"],
148
+ "value": _quantity(item["value"], item.get("unit", "")),
149
+ "status": "PASS",
150
+ }
151
+ for item in measurements_in
152
+ ]
153
+ findings = [
154
+ {
155
+ "id": f"{run_id}:finding:{index}",
156
+ "severity": _SEVERITY.get(v.get("severity"), "info"),
157
+ "code": v.get("rule", "views.failed"),
158
+ "message": _cap(v.get("message", "")),
159
+ }
160
+ for index, v in enumerate(violations)
161
+ ]
162
+ return {
163
+ "schema": "rocketeda/results/v1",
164
+ "ref": {"scope": "project", "id": run_id, "revision": "r1"},
165
+ "run_id": run_id,
166
+ "task": TASK,
167
+ "target": dict(target)
168
+ if target is not None
169
+ else {"project_id": project_id, "kind": "project"},
170
+ "state": "complete",
171
+ "freshness": "current",
172
+ "convergence": "not_applicable",
173
+ "provenance": _provenance(project_id, run_id, tools, provenance),
174
+ "datasets": datasets,
175
+ "views": views,
176
+ "measurements": measurements,
177
+ "findings": findings,
178
+ "artifacts": [],
179
+ "logs": [],
180
+ "gaps": [],
181
+ }
@@ -0,0 +1,7 @@
1
+ # Icarus HDL simulation adapter
2
+
3
+ HdlSimAdapter serves run_hdl_test. It compiles project testbench and design sources with iverilog, runs them with vvp under a timeout, and parses compile diagnostics, FATAL, and ERROR output into normalized findings.
4
+
5
+ The adapter is registered in Engine as the icarus solver. Tool resolution uses PATH or an explicit job tool_dir. When a BwrapRunner is supplied, compile and simulation processes run inside the configured isolation boundary.
6
+
7
+ Some HDL failures can exit with status zero. Verdicts come from parsed diagnostics and simulation output, never the exit code alone.
@@ -0,0 +1,369 @@
1
+ """Icarus HDL sim solver adapter: run_hdl_test behind the ToolAdapter port.
2
+
3
+ Protocol, verified against iverilog 14.0 + vvp: compile the sources
4
+ (exit 2 + `file:line: msg` diagnostics on failure), run the sim under
5
+ a timeout, and parse `FATAL:`/`ERROR:` lines for the verdict. `$error`
6
+ exits 0, so the verdict always comes from parsed output, never from
7
+ the exit code alone. Hangs raise instead of hanging the engine.
8
+
9
+ Tool resolution: an explicit job `tool_dir` wins, else PATH. Where
10
+ the tools live on disk is B10 integration scope — this adapter only
11
+ requires them resolvable, and refuses loudly when they are not.
12
+ """
13
+
14
+ import re
15
+ import shutil
16
+ import subprocess
17
+ from pathlib import Path
18
+
19
+ from ...execution.errors import ExecutionError
20
+ from ...execution.runner import tool_roots
21
+ from ...interfaces import ToolAdapter
22
+ from ..errors import AdapterError
23
+
24
+ OPERATION = "run_hdl_test"
25
+ _TIMEOUT_S = 120
26
+ _LOG_NAME = "sim.log"
27
+
28
+ _COMPILE_RE = re.compile(r"^([^:\s][^:]*):(\d+):\s*(.+)$", re.M)
29
+ _SIM_RE = re.compile(r"^(FATAL|ERROR):\s*(\S+):(\d+):\s*(.*)$")
30
+ _TIME_RE = re.compile(r"Time:\s*(\d+)")
31
+ # Verilog `define names, with an optional =value (no whitespace or
32
+ # shell metacharacters; argv is never shelled, belt and suspenders).
33
+ _DEFINE_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(=[A-Za-z0-9_.+-]+)?")
34
+
35
+
36
+ def _parse_compile(text):
37
+ violations = []
38
+ for match in _COMPILE_RE.finditer(text):
39
+ file, line, message = match.group(1), match.group(2), match.group(3)
40
+ warning = message.lower().startswith("warning")
41
+ violations.append(
42
+ {
43
+ "rule": "compile",
44
+ "severity": "warning" if warning else "error",
45
+ "message": message,
46
+ "file": file,
47
+ "line": int(line),
48
+ }
49
+ )
50
+ return violations
51
+
52
+
53
+ def _block_text(block):
54
+ """(text, source) for one compile/run block, log-first.
55
+
56
+ MIGRATE-STDIO-FILE: the `vvp -l sim.log` file is the parse
57
+ channel; stdio survives only as a loud fallback for natives
58
+ recorded before the file channel existed.
59
+
60
+ Args:
61
+ block: Native block with optional `log` plus
62
+ stdout/stderr.
63
+
64
+ Returns:
65
+ (text, source) with source `log` or `stdio_fallback`.
66
+ """
67
+ log = (block or {}).get("log") or ""
68
+ if log.strip():
69
+ return log, "log"
70
+ return (
71
+ (block or {}).get("stdout", "") + "\n" + (block or {}).get("stderr", ""),
72
+ "stdio_fallback",
73
+ )
74
+
75
+
76
+ def _parse_sim(text):
77
+ violations = []
78
+ lines = text.splitlines()
79
+ for index, line in enumerate(lines):
80
+ match = _SIM_RE.match(line)
81
+ if match is None:
82
+ continue
83
+ kind, file, lineno, message = match.groups()
84
+ violation = {
85
+ "rule": "assert_fatal" if kind == "FATAL" else "assert_error",
86
+ "severity": "error",
87
+ "message": message,
88
+ "file": file,
89
+ "line": int(lineno),
90
+ }
91
+ if index + 1 < len(lines):
92
+ timed = _TIME_RE.search(lines[index + 1])
93
+ if timed:
94
+ violation["sim_time"] = int(timed.group(1))
95
+ violations.append(violation)
96
+ return violations
97
+
98
+
99
+ class HdlSimAdapter(ToolAdapter):
100
+ """run_hdl_test via Icarus: compile plus timed Verilog sim.
101
+
102
+ Serves the run_hdl_test operation with deck.tool `icarus` (the
103
+ only tool). Compiles the sources with iverilog, runs the sim
104
+ under vvp with a timeout, and parses compile diagnostics plus
105
+ FATAL/ERROR assertion lines.
106
+
107
+ Job keys:
108
+ sources (required): Verilog source paths, bench included.
109
+ defines: Verilog `define list, optional =value each.
110
+ timeout_s: Sim budget in seconds (default 120).
111
+ tool_dir: Directory holding iverilog/vvp; PATH otherwise.
112
+ deck: Optional mapping; deck.tool must be icarus here.
113
+ workdir: Run directory (default ".").
114
+
115
+ Args:
116
+ runner: Optional isolated runner; direct subprocess when None.
117
+
118
+ Caution: $error exits 0 and hangs raise instead of hanging the
119
+ engine — the verdict always comes from parsed output, never
120
+ the exit code alone.
121
+ """
122
+
123
+ def __init__(self, runner=None):
124
+ self._runner = runner
125
+
126
+ def capabilities(self, target):
127
+ """Report the served operation, tool, sim, and defines.
128
+
129
+ Args:
130
+ target: Target the capabilities are reported for
131
+ (accepted, unused — one bench per job).
132
+
133
+ Returns:
134
+ Capability mapping with operations, tool, sim, and
135
+ defines.
136
+ """
137
+ return {"operations": [OPERATION], "tool": "icarus", "sim": "vvp", "defines": True}
138
+
139
+ def preflight(self, job):
140
+ """Check operation, timeout, sources, defines, and tools.
141
+
142
+ Cheap deterministic checks first, tool resolution last.
143
+
144
+ Args:
145
+ job: Solver job with operation run_hdl_test plus
146
+ sources, defines, and timeout inputs.
147
+
148
+ Returns:
149
+ Checked mapping with resolved iverilog/vvp paths,
150
+ sources, timeout_s, and defines.
151
+
152
+ Raises:
153
+ AdapterError: unsupported_operation for another
154
+ operation, invalid_command for a bad timeout or
155
+ defines, invalid_reference for missing sources,
156
+ missing_dependency for unresolvable tools.
157
+ """
158
+ # Cheap deterministic checks first: operation, timeout, sources.
159
+ # Tool resolution last, since it depends on the machine.
160
+ if job.get("operation") != OPERATION:
161
+ raise AdapterError(
162
+ "unsupported_operation",
163
+ f"HDL sim handles {OPERATION}, got {job.get('operation')}.",
164
+ )
165
+ timeout = job.get("timeout_s", _TIMEOUT_S)
166
+ if (
167
+ not isinstance(timeout, (int, float))
168
+ or isinstance(timeout, bool)
169
+ or not timeout > 0
170
+ ):
171
+ raise AdapterError("invalid_command", f"Bad timeout_s: {timeout!r}.")
172
+ sources = job.get("sources") or []
173
+ missing = [s for s in sources if not Path(s).is_file()]
174
+ if not sources or missing:
175
+ raise AdapterError(
176
+ "invalid_reference", f"Sources not found: {missing or sources}."
177
+ )
178
+ defines = job.get("defines") or []
179
+ if not isinstance(defines, list) or any(
180
+ not isinstance(d, str) or not _DEFINE_RE.fullmatch(d) for d in defines
181
+ ):
182
+ raise AdapterError("invalid_command", f"Bad defines: {defines!r}.")
183
+ checked = {
184
+ "iverilog": self._resolve(job, "iverilog"),
185
+ "vvp": self._resolve(job, "vvp"),
186
+ "sources": list(sources),
187
+ "timeout_s": timeout,
188
+ "defines": list(defines),
189
+ }
190
+ if self._runner is not None:
191
+ try:
192
+ self._runner.preflight()
193
+ except ExecutionError as exc:
194
+ raise AdapterError(exc.code, str(exc)) from exc
195
+ return checked
196
+
197
+ def execute(self, job):
198
+ """Compile with iverilog, then run the sim under vvp.
199
+
200
+ A failed compile returns its block with no run; the verdict
201
+ still comes from parse, never the exit code alone. The sim
202
+ runs with `vvp -l sim.log` so assertion lines land in a
203
+ file first (MIGRATE-STDIO-FILE; live-verified
204
+ byte-identical to stdio on vvp 14.0).
205
+
206
+ Args:
207
+ job: Solver job with operation run_hdl_test plus
208
+ sources, defines, and timeout inputs.
209
+
210
+ Returns:
211
+ Native output with exit_code plus compile and run
212
+ blocks (run is None when the build failed; the
213
+ run block carries the vvp log text under `log`).
214
+
215
+ Raises:
216
+ AdapterError: invalid_reference for a missing workdir,
217
+ runner_unavailable on a sim timeout, plus the
218
+ preflight codes (execute re-checks first).
219
+ """
220
+ checked = self.preflight(job)
221
+ workdir = job.get("workdir", ".")
222
+ if not Path(workdir).is_dir():
223
+ raise AdapterError(
224
+ "invalid_reference", f"Workdir not found: {workdir}."
225
+ )
226
+ roots = tool_roots([checked["iverilog"], checked["vvp"]])
227
+ build = self._run(
228
+ [
229
+ checked["iverilog"],
230
+ "-o",
231
+ "sim.vvp",
232
+ *[f"-D{d}" for d in checked["defines"]],
233
+ *checked["sources"],
234
+ ],
235
+ workdir,
236
+ checked["timeout_s"],
237
+ roots,
238
+ )
239
+ if build.returncode != 0:
240
+ return {
241
+ "exit_code": build.returncode,
242
+ "compile": _block(build),
243
+ "run": None,
244
+ }
245
+ run = self._run(
246
+ [checked["vvp"], "-l", _LOG_NAME, "sim.vvp"],
247
+ workdir,
248
+ checked["timeout_s"],
249
+ roots,
250
+ )
251
+ log_path = Path(workdir) / _LOG_NAME
252
+ run_block = _block(run)
253
+ run_block["log"] = (
254
+ log_path.read_text(encoding="utf-8", errors="replace")
255
+ if log_path.is_file()
256
+ else ""
257
+ )
258
+ return {
259
+ "exit_code": run.returncode,
260
+ "compile": _block(build),
261
+ "run": run_block,
262
+ }
263
+
264
+ def parse(self, native_output):
265
+ """Parse compile diagnostics plus sim assertion lines.
266
+
267
+ Failed compiles and crashed sims with nothing parseable
268
+ synthesize error violations rather than going clean. The
269
+ decisive block reads from the vvp log file, falling back
270
+ to stdio only for older natives (loudly marked).
271
+
272
+ Args:
273
+ native_output: Native output with compile and run blocks.
274
+
275
+ Returns:
276
+ {total, violations[], source} with file/line
277
+ locations and sim_time where the sim reports it;
278
+ source is `log` or `stdio_fallback`.
279
+
280
+ Raises:
281
+ AdapterError: runner_unavailable when a compiled run
282
+ has no run block.
283
+ """
284
+ compile_block = native_output.get("compile") or {}
285
+ compile_text, compile_source = _block_text(compile_block)
286
+ diags = _parse_compile(compile_text)
287
+ if compile_block.get("exit_code", 0) != 0:
288
+ if not diags:
289
+ # Defensive: a failed compile must never parse as clean.
290
+ diags = [
291
+ {
292
+ "rule": "compile",
293
+ "severity": "error",
294
+ "message": f"iverilog exited {compile_block.get('exit_code')}",
295
+ }
296
+ ]
297
+ return {
298
+ "total": len(diags),
299
+ "violations": diags,
300
+ "source": compile_source,
301
+ }
302
+ run_block = native_output.get("run")
303
+ if run_block is None:
304
+ raise AdapterError(
305
+ "runner_unavailable", "Native output has no run block."
306
+ )
307
+ run_text, run_source = _block_text(run_block)
308
+ sim = _parse_sim(run_text)
309
+ if run_block.get("exit_code", 0) != 0 and not sim:
310
+ # Defensive: crashes print nothing parseable; the nonzero
311
+ # exit is the only signal, and it must fail loudly.
312
+ sim = [
313
+ {
314
+ "rule": "sim_fail",
315
+ "severity": "error",
316
+ "message": f"vvp exited {run_block.get('exit_code')}",
317
+ }
318
+ ]
319
+ violations = diags + sim
320
+ return {
321
+ "total": len(violations),
322
+ "violations": violations,
323
+ "source": run_source,
324
+ }
325
+
326
+ @staticmethod
327
+ def _resolve(job, binary):
328
+ tool_dir = job.get("tool_dir")
329
+ if tool_dir:
330
+ candidate = Path(tool_dir) / binary
331
+ if candidate.is_file():
332
+ return str(candidate)
333
+ raise AdapterError(
334
+ "missing_dependency", f"{binary} not found in {tool_dir}."
335
+ )
336
+ found = shutil.which(binary)
337
+ if found is None:
338
+ raise AdapterError(
339
+ "missing_dependency",
340
+ f"{binary} not on PATH (pass tool_dir or integrate via B10).",
341
+ )
342
+ return found
343
+
344
+ def _run(self, argv, workdir, timeout_s, extra_binds=()):
345
+ if self._runner is not None:
346
+ try:
347
+ return self._runner.run(argv, workdir, timeout_s, extra_binds)
348
+ except ExecutionError as exc:
349
+ raise AdapterError(exc.code, str(exc)) from exc
350
+ try:
351
+ return subprocess.run(
352
+ argv,
353
+ capture_output=True,
354
+ text=True,
355
+ timeout=timeout_s,
356
+ cwd=workdir,
357
+ )
358
+ except subprocess.TimeoutExpired as exc:
359
+ raise AdapterError(
360
+ "runner_unavailable", f"sim timed out after {timeout_s}s."
361
+ ) from exc
362
+
363
+
364
+ def _block(proc):
365
+ return {
366
+ "exit_code": proc.returncode,
367
+ "stdout": proc.stdout,
368
+ "stderr": proc.stderr,
369
+ }