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,773 @@
1
+ """Sby formal solver adapter: run_formal behind the ToolAdapter port.
2
+
3
+ Protocol, verified against sby v0.70 (pinned oss-cad-suite,
4
+ bundled boolector engine): generate a `formal.sby` task from the
5
+ job (mode/depth/engines plus project sources, `prep -top`), run
6
+ `sby -f` under a timeout in its own process group, and parse the
7
+ machine-readable status file plus the DONE summary line
8
+ (PASS rc 0, FAIL rc 2, UNKNOWN rc 4, ERROR rc 16). Verdicts always
9
+ carry scope (proven/bounded/bounded-vacuous/unknown/timeout) with
10
+ assumptions and counterexamples — never a bare PASS.
11
+
12
+ Tool resolution: an explicit job `tool_dir` wins, else PATH. The
13
+ sby banner is version-asserted (an impostor binary refuses as
14
+ missing_dependency); where the suite lives on disk is PROV/B10
15
+ integration scope — this adapter only requires it resolvable.
16
+ """
17
+
18
+ import os
19
+ import re
20
+ import shutil
21
+ import signal
22
+ import subprocess
23
+ from pathlib import Path
24
+
25
+ from ...execution.errors import ExecutionError
26
+ from ...execution.runner import clean_stream, tool_roots
27
+ from ...interfaces import ToolAdapter
28
+ from ..errors import AdapterError
29
+ from .formal_edge import guard_vacuous_pass, property_sites
30
+
31
+ OPERATION = "run_formal"
32
+ _TIMEOUT_S = 300
33
+ _DEPTH = 20
34
+ _MODES = ("bmc", "prove", "cover")
35
+ _ENGINES = ["smtbmc boolector"]
36
+ _SBY_JOB = "formal"
37
+ _SBY_FILE = _SBY_JOB + ".sby"
38
+ _VERSION_PREFIX = "SBY "
39
+ _VERSION_TIMEOUT_S = 30
40
+ _MISSING_REMEDY = (
41
+ "PROV bundle: install the pinned oss-cad-suite tarball per "
42
+ "docs/provision-plan.md, or pass tool_dir."
43
+ )
44
+
45
+ _TOP_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_$]*")
46
+ _ENGINE_RE = re.compile(r"[A-Za-z0-9_][A-Za-z0-9_. \-]*")
47
+ _DONE_RE = re.compile(r"DONE \((PASS|FAIL|UNKNOWN|ERROR), rc=(\d+)\)")
48
+ _ENGINE_STATUS_RE = re.compile(
49
+ r"summary:\s+(engine_\d+)\s+\(([^)]+)\)\s+returned\s+(\S+)(?:\s+for\s+(\S+))?"
50
+ )
51
+ _ENGINE_NOSTATUS_RE = re.compile(r"summary:\s+(engine_\d+).*did not return a status")
52
+ _TRACE_RE = re.compile(
53
+ r"summary:\s+(counterexample trace(?:\s+\[([^\]]+)\])?|cover trace):\s+(\S+)"
54
+ )
55
+ _ASSERT_FAIL_RE = re.compile(
56
+ r"summary:\s+failed assertion\s+(\S+)\s+at\s+(\S+?)(?:\s+step\s+(\d+))?\s*$",
57
+ re.M,
58
+ )
59
+ _COVER_HIT_RE = re.compile(
60
+ r"summary:\s+reached cover statement\s+(\S+)\s+at\s+(\S+?)(?:\s+step\s+(\d+))?"
61
+ r"\s*$",
62
+ re.M,
63
+ )
64
+ _UNREACHED_HDR = "unreached cover statements:"
65
+ _UNREACHED_RE = re.compile(r"summary:\s+(\S+)\s+at\s+(\S+)\s*$")
66
+ _AT_RE = re.compile(r"([^:\s]+):(\d+)\.")
67
+ _YOSYS_ERR_RE = re.compile(r"(\S+?):(\d+):\s*ERROR:\s*(.+?)\s*$", re.M)
68
+ _BARE_ERR_RE = re.compile(r"ERROR:\s*(.+?)\s*$", re.M)
69
+ _PROP_RE = re.compile(r"\b(assert|assume|cover)\s*\(")
70
+
71
+
72
+ def _property_sites(sources, defines=None):
73
+ """Active assume/assert/cover sites by file and line (evidence-grade).
74
+
75
+ Ifdef-aware delegate to formal_edge.property_sites: define lines
76
+ never count and ifdef branches resolve against the active
77
+ defines (FORMAL assumed, matching sby read -formal). Unreadable
78
+ sources are skipped, never fatal — the sites annotate the
79
+ verdict, they are not the verdict.
80
+ """
81
+ return property_sites(sources, defines=defines)
82
+
83
+
84
+ def _split_at(token):
85
+ """(file, line) from an sby `file:line.col-span` token."""
86
+ match = _AT_RE.search(token or "")
87
+ if match is None:
88
+ return token, None
89
+ return match.group(1), int(match.group(2))
90
+
91
+
92
+ def _locate(name, by_basename):
93
+ """Staged path for an sby-reported basename, else the name as-is."""
94
+ return by_basename.get(Path(name).name, name) if name else None
95
+
96
+
97
+ def _read_status(workdir):
98
+ """(status, rc) from the sby status file, or (None, None)."""
99
+ try:
100
+ parts = (Path(workdir) / _SBY_JOB / "status").read_text().split()
101
+ except OSError:
102
+ return None, None
103
+ if len(parts) >= 2 and parts[0] in ("PASS", "FAIL", "UNKNOWN", "ERROR"):
104
+ try:
105
+ return parts[0], int(parts[1])
106
+ except ValueError:
107
+ return None, None
108
+ return None, None
109
+
110
+
111
+ def _summary_text(native_output):
112
+ """(text, source) for the sby summary regexes.
113
+
114
+ MIGRATE-STDIO-FILE: `<task>/logfile.txt` carries the complete
115
+ sby log including the summary lines, so it is the parse
116
+ channel; bare stdout survives only as a loud fallback for
117
+ natives recorded before the file channel existed (e.g. the
118
+ timeout path, where sby never flushes a logfile).
119
+
120
+ Args:
121
+ native_output: Native output with stdout plus the sby
122
+ workdir locating `<task>/logfile.txt`.
123
+
124
+ Returns:
125
+ (text, source) with source `logfile` or `stdout`.
126
+ """
127
+ candidate = Path(native_output.get("workdir") or ".") / _SBY_JOB / "logfile.txt"
128
+ try:
129
+ text = candidate.read_text(encoding="utf-8", errors="replace")
130
+ except OSError:
131
+ text = ""
132
+ if text.strip():
133
+ return text, "logfile"
134
+ return native_output.get("stdout", ""), "stdout"
135
+
136
+
137
+ def _engine_status(stdout):
138
+ """Per-engine `returned ...` notes plus no-status notes."""
139
+ notes = []
140
+ for match in _ENGINE_STATUS_RE.finditer(stdout):
141
+ engine, config, verdict, step = match.groups()
142
+ note = f"{engine} ({config}) returned {verdict}"
143
+ if step:
144
+ note += f" for {step}"
145
+ notes.append(note)
146
+ for match in _ENGINE_NOSTATUS_RE.finditer(stdout):
147
+ notes.append(f"{match.group(1)} did not return a status")
148
+ return notes
149
+
150
+
151
+ def _trace_reports(stdout):
152
+ """(kind, label, relpath) for each summary trace line."""
153
+ reports = []
154
+ for match in _TRACE_RE.finditer(stdout):
155
+ kind, label, relpath = match.groups()
156
+ reports.append(
157
+ (
158
+ "cover" if kind == "cover trace" else "counterexample",
159
+ label,
160
+ relpath,
161
+ )
162
+ )
163
+ return reports
164
+
165
+
166
+ def _failure_reports(stdout):
167
+ """Failure entries from failed-assertion and unreached-cover lines."""
168
+ failures = []
169
+ for match in _ASSERT_FAIL_RE.finditer(stdout):
170
+ name, token, step = match.groups()
171
+ file, line = _split_at(token)
172
+ failures.append(
173
+ {
174
+ "property": name,
175
+ "kind": "assert",
176
+ "file": file,
177
+ "line": line,
178
+ "step": int(step) if step is not None else None,
179
+ }
180
+ )
181
+ unreached = False
182
+ for line in stdout.splitlines():
183
+ if _UNREACHED_HDR in line:
184
+ unreached = True
185
+ continue
186
+ if not unreached or "summary:" not in line:
187
+ continue
188
+ match = _UNREACHED_RE.search(line)
189
+ if match is None:
190
+ unreached = False
191
+ continue
192
+ name, token = match.groups()
193
+ file, lineno = _split_at(token)
194
+ failures.append(
195
+ {
196
+ "property": name,
197
+ "kind": "cover",
198
+ "file": file,
199
+ "line": lineno,
200
+ "step": None,
201
+ }
202
+ )
203
+ return failures
204
+
205
+
206
+ def _cover_hits(stdout):
207
+ """Reached-cover entries from `reached cover statement` lines."""
208
+ hits = []
209
+ for match in _COVER_HIT_RE.finditer(stdout):
210
+ name, token, step = match.groups()
211
+ file, line = _split_at(token)
212
+ hits.append(
213
+ {
214
+ "property": name,
215
+ "file": file,
216
+ "line": line,
217
+ "step": int(step) if step is not None else None,
218
+ }
219
+ )
220
+ return hits
221
+
222
+
223
+ def _pair_traces(entries, traces, workdir, by_basename):
224
+ """Pair failure/hit entries with trace artifacts, in order.
225
+
226
+ Unpaired entries keep path None with present False; unpaired
227
+ traces surface with property None — nothing silently dropped.
228
+ """
229
+ paired = []
230
+ for index, entry in enumerate(entries):
231
+ path, present, label = None, False, None
232
+ if index < len(traces):
233
+ label, relpath = traces[index][1], traces[index][2]
234
+ candidate = Path(workdir) / relpath
235
+ path, present = str(candidate), candidate.is_file()
236
+ paired.append(
237
+ {
238
+ **entry,
239
+ "file": _locate(entry.get("file"), by_basename),
240
+ "path": path,
241
+ "present": present,
242
+ **({"label": label} if label else {}),
243
+ }
244
+ )
245
+ for label, relpath in [t[1:] for t in traces[len(entries):]]:
246
+ candidate = Path(workdir) / relpath
247
+ paired.append(
248
+ {
249
+ "property": None,
250
+ "path": str(candidate),
251
+ "present": candidate.is_file(),
252
+ **({"label": label} if label else {}),
253
+ }
254
+ )
255
+ return paired
256
+
257
+
258
+ class FormalSbyAdapter(ToolAdapter):
259
+ """run_formal via sby: bounded proof with honestly scoped verdicts.
260
+
261
+ Serves the run_formal operation with deck.tool `sby` (the only
262
+ tool). Generates a formal.sby task from the job's project
263
+ sources, runs it bounded and cancellable, and parses the sby
264
+ status plus the DONE summary into a scoped verdict: prove PASS
265
+ is proven, bmc/cover PASS is bounded to the depth, FAIL carries
266
+ counterexamples, inconclusive runs are unknown, wall-clock
267
+ exhaustion is timeout, and zero-property PASS is bounded-vacuous
268
+ FAIL — never a bare PASS.
269
+
270
+ Job keys:
271
+ sources (required): RTL source paths carrying the design
272
+ plus its assume/assert/cover properties.
273
+ top (required): Top module name for prep -top.
274
+ mode: bmc, prove, or cover (default bmc).
275
+ depth: Proof bound in steps (default 20).
276
+ engines: sby engine lines (default [smtbmc boolector]).
277
+ timeout_s: Proof budget in seconds (default 300).
278
+ tool_dir: Directory holding sby; PATH otherwise.
279
+ deck: Optional mapping; deck.tool must be sby here.
280
+ workdir: Run directory (default ".").
281
+
282
+ Args:
283
+ runner: Optional isolated runner; direct subprocess when None.
284
+
285
+ Caution: Overwrites formal.sby plus the formal/ output dir in
286
+ the workdir; exit zero is not PASS — the scope lives in
287
+ the measurements, and unknown/timeout fail loudly.
288
+ """
289
+
290
+ def __init__(self, runner=None):
291
+ self._runner = runner
292
+
293
+ def capabilities(self, target):
294
+ """Report the served operation, tool, and proof modes.
295
+
296
+ Args:
297
+ target: Target the capabilities are reported for
298
+ (accepted, unused — one proof task per job).
299
+
300
+ Returns:
301
+ Capability mapping with operations, tool, and modes.
302
+ """
303
+ return {"operations": [OPERATION], "tool": "sby", "modes": list(_MODES)}
304
+
305
+ def preflight(self, job):
306
+ """Check operation, deck, proof inputs, sources, and sby.
307
+
308
+ Cheap deterministic checks first, sby resolution and its
309
+ version probe last.
310
+
311
+ Args:
312
+ job: Solver job with operation run_formal plus
313
+ sources, top, mode, depth, and engines inputs.
314
+
315
+ Returns:
316
+ Checked mapping with the resolved sby path, banner,
317
+ sources, top, mode, depth, engines, and timeout_s.
318
+
319
+ Raises:
320
+ AdapterError: unsupported_operation for another
321
+ operation or a foreign deck.tool, invalid_command
322
+ for bad proof inputs or deck shape, invalid_reference
323
+ for missing sources, missing_dependency for an
324
+ unresolvable or impostor sby.
325
+ """
326
+ if job.get("operation") != OPERATION:
327
+ raise AdapterError(
328
+ "unsupported_operation",
329
+ f"Formal sby handles {OPERATION}, got {job.get('operation')}.",
330
+ )
331
+ deck = job.get("deck")
332
+ if deck is not None:
333
+ if not isinstance(deck, dict):
334
+ raise AdapterError(
335
+ "invalid_command", f"Bad deck: {deck!r}."
336
+ )
337
+ if deck.get("tool", "sby") != "sby":
338
+ raise AdapterError(
339
+ "unsupported_operation",
340
+ f"Formal sby handles deck.tool sby, got {deck.get('tool')}.",
341
+ )
342
+ timeout = job.get("timeout_s", _TIMEOUT_S)
343
+ if (
344
+ not isinstance(timeout, (int, float))
345
+ or isinstance(timeout, bool)
346
+ or not timeout > 0
347
+ ):
348
+ raise AdapterError("invalid_command", f"Bad timeout_s: {timeout!r}.")
349
+ mode = job.get("mode", "bmc")
350
+ if mode not in _MODES:
351
+ raise AdapterError("invalid_command", f"Bad mode: {mode!r}.")
352
+ depth = job.get("depth", _DEPTH)
353
+ if not isinstance(depth, int) or isinstance(depth, bool) or depth <= 0:
354
+ raise AdapterError("invalid_command", f"Bad depth: {depth!r}.")
355
+ top = job.get("top")
356
+ if not isinstance(top, str) or not _TOP_RE.fullmatch(top):
357
+ raise AdapterError("invalid_command", f"Bad top: {top!r}.")
358
+ sources = job.get("sources") or []
359
+ missing = [s for s in sources if not Path(s).is_file()]
360
+ if not sources or missing:
361
+ raise AdapterError(
362
+ "invalid_reference", f"Sources not found: {missing or sources}."
363
+ )
364
+ basenames = [Path(s).name for s in sources]
365
+ dupes = sorted({name for name in basenames if basenames.count(name) > 1})
366
+ if dupes:
367
+ raise AdapterError(
368
+ "invalid_command",
369
+ f"Duplicate source basenames collide in sby src/: {dupes}.",
370
+ )
371
+ engines = job.get("engines", list(_ENGINES))
372
+ if (
373
+ not isinstance(engines, list)
374
+ or not engines
375
+ or any(
376
+ not isinstance(e, str)
377
+ or not e
378
+ or e != e.strip()
379
+ or not _ENGINE_RE.fullmatch(e)
380
+ for e in engines
381
+ )
382
+ ):
383
+ raise AdapterError("invalid_command", f"Bad engines: {engines!r}.")
384
+ sby = self._resolve(job, "sby")
385
+ if self._runner is not None:
386
+ try:
387
+ self._runner.preflight()
388
+ except ExecutionError as exc:
389
+ raise AdapterError(exc.code, str(exc)) from exc
390
+ return {
391
+ "sby": sby,
392
+ "sby_version": self._probe_version(sby),
393
+ "sources": list(sources),
394
+ "top": top,
395
+ "mode": mode,
396
+ "depth": depth,
397
+ "engines": list(engines),
398
+ "timeout_s": timeout,
399
+ }
400
+
401
+ def execute(self, job):
402
+ """Generate the sby task and run it bounded and cancellable.
403
+
404
+ The sby run lives in its own process group under timeout_s;
405
+ expiry SIGKILLs the whole tree and reports a timeout native
406
+ block instead of hanging the engine.
407
+
408
+ Args:
409
+ job: Solver job with operation run_formal plus
410
+ sources, top, mode, depth, and engines inputs.
411
+
412
+ Returns:
413
+ Native output with exit_code, streams, sby status
414
+ inputs (workdir, sby_file), proof inputs echoed,
415
+ the sby banner, and the timed_out flag.
416
+
417
+ Raises:
418
+ AdapterError: invalid_reference for a missing or
419
+ unwritable workdir, runner_unavailable on a
420
+ runner failure, plus the preflight codes (execute
421
+ re-checks first).
422
+ """
423
+ checked = self.preflight(job)
424
+ workdir = job.get("workdir", ".")
425
+ if not Path(workdir).is_dir():
426
+ raise AdapterError(
427
+ "invalid_reference", f"Workdir not found: {workdir}."
428
+ )
429
+ sby_file = Path(workdir) / _SBY_FILE
430
+ try:
431
+ sby_file.write_text(
432
+ self._sby_text(
433
+ checked["sources"],
434
+ checked["top"],
435
+ checked["mode"],
436
+ checked["depth"],
437
+ checked["engines"],
438
+ ),
439
+ encoding="utf-8",
440
+ )
441
+ except OSError as exc:
442
+ raise AdapterError(
443
+ "invalid_reference", f"Cannot write sby task: {exc}."
444
+ ) from exc
445
+ timed_out, proc = self._run(
446
+ [checked["sby"], "-f", _SBY_FILE],
447
+ workdir,
448
+ checked["timeout_s"],
449
+ tool_roots([checked["sby"]]),
450
+ )
451
+ stdout = proc.stdout if proc is not None else ""
452
+ stderr = proc.stderr if proc is not None else ""
453
+ return {
454
+ "exit_code": proc.returncode if proc is not None else None,
455
+ "stdout": stdout,
456
+ "stderr": stderr,
457
+ "log": stdout,
458
+ "timed_out": timed_out,
459
+ "workdir": str(workdir),
460
+ "sby_file": str(sby_file),
461
+ "sources": checked["sources"],
462
+ "top": checked["top"],
463
+ "mode": checked["mode"],
464
+ "depth": checked["depth"],
465
+ "engines": checked["engines"],
466
+ "sby_version": checked["sby_version"],
467
+ "timeout_s": checked["timeout_s"],
468
+ }
469
+
470
+ def parse(self, native_output):
471
+ """Parse the sby status into a scoped verdict with evidence.
472
+
473
+ The status file wins, the DONE summary line is the fallback;
474
+ with neither the output refuses as runner_unavailable —
475
+ unparseable never parses as clean. Timeout natives report
476
+ scope timeout; FAIL/UNKNOWN/ERROR always carry error
477
+ findings, so PASS always means at least one property was
478
+ checked in some scope; zero-property sby PASS reports scope
479
+ bounded-vacuous with a formal_vacuous error (engine FAIL),
480
+ never a bare PASS.
481
+
482
+ Args:
483
+ native_output: Native output from execute with the
484
+ sby streams, workdir, proof inputs, and timed_out.
485
+
486
+ Returns:
487
+ {total, violations[], measurements, source} with
488
+ scope, sby_status, assumptions, properties,
489
+ failures, counterexamples, and cover witnesses;
490
+ source names the channel read (`logfile` for
491
+ formal/logfile.txt, `stdout` else).
492
+
493
+ Raises:
494
+ AdapterError: runner_unavailable when the output has
495
+ no sby status and no DONE summary line.
496
+ """
497
+ sources = native_output.get("sources") or []
498
+ assumptions, properties = _property_sites(sources)
499
+ by_basename = {Path(s).name: str(s) for s in sources}
500
+ _logtext, text_source = _summary_text(native_output)
501
+ measures = {
502
+ "mode": native_output.get("mode"),
503
+ "depth": native_output.get("depth"),
504
+ "top": native_output.get("top"),
505
+ "sby_version": native_output.get("sby_version"),
506
+ "timeout_s": native_output.get("timeout_s"),
507
+ "assumptions": assumptions,
508
+ "properties": properties,
509
+ }
510
+ if native_output.get("timed_out"):
511
+ timeout_s = native_output.get("timeout_s")
512
+ measures.update(
513
+ {
514
+ "scope": "timeout",
515
+ "sby_status": "timeout",
516
+ "sby_rc": None,
517
+ "engine_status": [],
518
+ "failures": [],
519
+ "counterexamples": [],
520
+ "cover_witnesses": [],
521
+ }
522
+ )
523
+ violation = {
524
+ "rule": "formal_timeout",
525
+ "severity": "error",
526
+ "message": f"sby timed out after {timeout_s}s.",
527
+ }
528
+ return {"total": 1, "violations": [violation],
529
+ "measurements": measures, "source": text_source}
530
+ workdir = native_output.get("workdir", ".")
531
+ stdout = _logtext
532
+ status, rc = _read_status(workdir)
533
+ if status is None:
534
+ done = _DONE_RE.search(stdout)
535
+ if done is None:
536
+ raise AdapterError(
537
+ "runner_unavailable",
538
+ "sby output has no status file and no DONE summary line.",
539
+ )
540
+ status, rc = done.group(1), int(done.group(2))
541
+ engine_notes = _engine_status(stdout)
542
+ failures = _failure_reports(stdout)
543
+ traces = _trace_reports(stdout)
544
+ cex_traces = [t for t in traces if t[0] == "counterexample"]
545
+ cover_traces = [t for t in traces if t[0] == "cover"]
546
+ counterexamples = _pair_traces(failures, cex_traces, workdir, by_basename)
547
+ witnesses = _pair_traces(
548
+ _cover_hits(stdout), cover_traces, workdir, by_basename
549
+ )
550
+ measures.update(
551
+ {
552
+ "sby_status": status,
553
+ "sby_rc": rc,
554
+ "engine_status": engine_notes,
555
+ "failures": [
556
+ {**f, "file": _locate(f["file"], by_basename)}
557
+ for f in failures
558
+ ],
559
+ "counterexamples": counterexamples,
560
+ "cover_witnesses": witnesses,
561
+ }
562
+ )
563
+ if status == "PASS":
564
+ vacuous = guard_vacuous_pass(
565
+ properties,
566
+ mode=native_output.get("mode"),
567
+ depth=native_output.get("depth"),
568
+ )
569
+ if vacuous is not None:
570
+ measures["scope"] = vacuous["scope"]
571
+ measures["vacuous"] = True
572
+ return {
573
+ "total": 1,
574
+ "violations": [vacuous["violation"]],
575
+ "measurements": measures,
576
+ "source": text_source,
577
+ }
578
+ scope = "proven" if native_output.get("mode") == "prove" else "bounded"
579
+ measures["scope"] = scope
580
+ return {"total": 0, "violations": [], "measurements": measures,
581
+ "source": text_source}
582
+ if status == "FAIL":
583
+ measures["scope"] = "bounded"
584
+ violations = []
585
+ for failure in failures:
586
+ rule = "assert_fail" if failure["kind"] == "assert" else "cover_missed"
587
+ message = f"property {failure['property']} failed"
588
+ if failure["step"] is not None:
589
+ message += f" at step {failure['step']}"
590
+ violation = {
591
+ "rule": rule,
592
+ "severity": "error",
593
+ "message": message,
594
+ }
595
+ located = _locate(failure["file"], by_basename)
596
+ if located is not None:
597
+ violation["file"] = located
598
+ if failure["line"] is not None:
599
+ violation["line"] = failure["line"]
600
+ violations.append(violation)
601
+ if not violations:
602
+ # Defensive: an sby FAIL must never parse as clean.
603
+ violations.append(
604
+ {
605
+ "rule": "assert_fail",
606
+ "severity": "error",
607
+ "message": f"sby reported FAIL (rc={rc}) with no "
608
+ "failed assertion.",
609
+ }
610
+ )
611
+ return {
612
+ "total": len(violations),
613
+ "violations": violations,
614
+ "measurements": measures,
615
+ "source": text_source,
616
+ }
617
+ if status == "UNKNOWN":
618
+ measures["scope"] = "unknown"
619
+ detail = "; ".join(engine_notes) if engine_notes else "no engine status"
620
+ violations = [
621
+ {
622
+ "rule": "formal_unknown",
623
+ "severity": "error",
624
+ "message": f"sby inconclusive (rc={rc}): {detail}.",
625
+ }
626
+ ]
627
+ for failure in failures:
628
+ violation = {
629
+ "rule": "assert_fail",
630
+ "severity": "error",
631
+ "message": f"property {failure['property']} unproven",
632
+ }
633
+ located = _locate(failure["file"], by_basename)
634
+ if located is not None:
635
+ violation["file"] = located
636
+ if failure["line"] is not None:
637
+ violation["line"] = failure["line"]
638
+ violations.append(violation)
639
+ return {
640
+ "total": len(violations),
641
+ "violations": violations,
642
+ "measurements": measures,
643
+ "source": text_source,
644
+ }
645
+ measures["scope"] = "unknown"
646
+ violation = self._error_violation(stdout, rc, by_basename)
647
+ measures["error"] = violation["message"]
648
+ return {"total": 1, "violations": [violation],
649
+ "measurements": measures, "source": text_source}
650
+
651
+ @staticmethod
652
+ def _sby_text(sources, top, mode, depth, engines):
653
+ """Render the formal.sby task for one proof job."""
654
+ basenames = " ".join(Path(s).name for s in sources)
655
+ lines = [
656
+ "# Generated by FormalSbyAdapter (run_formal); do not hand-edit.",
657
+ "[options]",
658
+ f"mode {mode}",
659
+ f"depth {depth}",
660
+ "",
661
+ "[engines]",
662
+ *engines,
663
+ "",
664
+ "[script]",
665
+ f"read -formal {basenames}",
666
+ f"prep -top {top}",
667
+ "",
668
+ "[files]",
669
+ *sources,
670
+ ]
671
+ return "\n".join(lines) + "\n"
672
+
673
+ @staticmethod
674
+ def _error_violation(stdout, rc, by_basename):
675
+ """The sby ERROR detail as one error violation, located when possible."""
676
+ match = _YOSYS_ERR_RE.search(stdout)
677
+ if match is not None:
678
+ file, line, message = match.groups()
679
+ return {
680
+ "rule": "formal_error",
681
+ "severity": "error",
682
+ "message": message,
683
+ "file": _locate(file, by_basename),
684
+ "line": int(line),
685
+ }
686
+ bare = _BARE_ERR_RE.search(stdout)
687
+ detail = bare.group(1) if bare else f"sby errored (rc={rc})"
688
+ return {"rule": "formal_error", "severity": "error", "message": detail}
689
+
690
+ @staticmethod
691
+ def _resolve(job, binary):
692
+ tool_dir = job.get("tool_dir")
693
+ if tool_dir:
694
+ candidate = Path(tool_dir) / binary
695
+ if candidate.is_file():
696
+ return str(candidate)
697
+ raise AdapterError(
698
+ "missing_dependency",
699
+ f"{binary} not found in {tool_dir} ({_MISSING_REMEDY}).",
700
+ )
701
+ found = shutil.which(binary)
702
+ if found is None:
703
+ raise AdapterError(
704
+ "missing_dependency",
705
+ f"{binary} not on PATH ({_MISSING_REMEDY}).",
706
+ )
707
+ return found
708
+
709
+ @staticmethod
710
+ def _probe_version(sby):
711
+ try:
712
+ proc = subprocess.run(
713
+ [sby, "--version"],
714
+ capture_output=True,
715
+ text=True,
716
+ timeout=_VERSION_TIMEOUT_S,
717
+ )
718
+ except (OSError, subprocess.TimeoutExpired) as exc:
719
+ raise AdapterError(
720
+ "missing_dependency",
721
+ f"sby version probe failed for {sby}: {exc} ({_MISSING_REMEDY}).",
722
+ ) from exc
723
+ banner = (proc.stdout or "").strip() or (proc.stderr or "").strip()
724
+ if proc.returncode != 0 or not banner.startswith(_VERSION_PREFIX):
725
+ raise AdapterError(
726
+ "missing_dependency",
727
+ f"sby at {sby} failed its version probe: {banner!r} "
728
+ f"({_MISSING_REMEDY}).",
729
+ )
730
+ return banner
731
+
732
+ def _run(self, argv, workdir, timeout_s, extra_binds=()):
733
+ """(timed_out, proc) for one sby run; expiry kills the tree."""
734
+ if self._runner is not None:
735
+ try:
736
+ proc = self._runner.run(argv, workdir, timeout_s, extra_binds)
737
+ except ExecutionError as exc:
738
+ if (
739
+ exc.code == "runner_unavailable"
740
+ and "timed out after" in str(exc)
741
+ ):
742
+ return True, None
743
+ raise AdapterError(exc.code, str(exc)) from exc
744
+ return False, proc
745
+ proc = subprocess.Popen(
746
+ argv,
747
+ stdout=subprocess.PIPE,
748
+ stderr=subprocess.PIPE,
749
+ cwd=workdir,
750
+ start_new_session=True,
751
+ )
752
+ try:
753
+ out_bytes, err_bytes = proc.communicate(timeout=timeout_s)
754
+ except subprocess.TimeoutExpired:
755
+ try:
756
+ os.killpg(proc.pid, signal.SIGKILL)
757
+ except (ProcessLookupError, PermissionError, OSError):
758
+ pass
759
+ try:
760
+ proc.kill()
761
+ except (ProcessLookupError, OSError):
762
+ pass
763
+ proc.wait()
764
+ return True, None
765
+ return (
766
+ False,
767
+ subprocess.CompletedProcess(
768
+ proc.args,
769
+ proc.returncode,
770
+ clean_stream(out_bytes),
771
+ clean_stream(err_bytes),
772
+ ),
773
+ )