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.
- rocket_engine/__init__.py +8 -0
- rocket_engine/__main__.py +12 -0
- rocket_engine/adapters/README.md +54 -0
- rocket_engine/adapters/__init__.py +7 -0
- rocket_engine/adapters/api.py +39 -0
- rocket_engine/adapters/base.py +46 -0
- rocket_engine/adapters/cace/__init__.py +163 -0
- rocket_engine/adapters/circulax/README.md +10 -0
- rocket_engine/adapters/circulax/__init__.py +112 -0
- rocket_engine/adapters/circulax/_driver.py +190 -0
- rocket_engine/adapters/circulax/adapter.py +538 -0
- rocket_engine/adapters/circulax/results.py +291 -0
- rocket_engine/adapters/cli/README.md +34 -0
- rocket_engine/adapters/cli/__init__.py +305 -0
- rocket_engine/adapters/connectivity/__init__.py +196 -0
- rocket_engine/adapters/connectivity/results.py +214 -0
- rocket_engine/adapters/cosim/README.md +9 -0
- rocket_engine/adapters/cosim/__init__.py +1390 -0
- rocket_engine/adapters/cosim/results.py +214 -0
- rocket_engine/adapters/devsim/__init__.py +171 -0
- rocket_engine/adapters/digital_flow/README.md +9 -0
- rocket_engine/adapters/digital_flow/__init__.py +1121 -0
- rocket_engine/adapters/digital_flow/results.py +227 -0
- rocket_engine/adapters/digital_flow/state.py +462 -0
- rocket_engine/adapters/drc/README.klayout.md +7 -0
- rocket_engine/adapters/drc/README.md +7 -0
- rocket_engine/adapters/drc/__init__.py +1 -0
- rocket_engine/adapters/drc/klayout.py +952 -0
- rocket_engine/adapters/drc/magic.py +287 -0
- rocket_engine/adapters/drc/results.py +184 -0
- rocket_engine/adapters/drc/results_kl.py +188 -0
- rocket_engine/adapters/elmer/__init__.py +163 -0
- rocket_engine/adapters/envelope.py +120 -0
- rocket_engine/adapters/equivalence/__init__.py +156 -0
- rocket_engine/adapters/errors.py +181 -0
- rocket_engine/adapters/extract_magic/README.md +15 -0
- rocket_engine/adapters/extract_magic/__init__.py +425 -0
- rocket_engine/adapters/extract_magic/results.py +209 -0
- rocket_engine/adapters/formal_sby/README.md +7 -0
- rocket_engine/adapters/formal_sby/__init__.py +773 -0
- rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
- rocket_engine/adapters/formal_sby/results.py +185 -0
- rocket_engine/adapters/gdsfactory/__init__.py +194 -0
- rocket_engine/adapters/gdsfactory/_driver.py +459 -0
- rocket_engine/adapters/gdsfactory/adapter.py +609 -0
- rocket_engine/adapters/gdsfactory/results.py +181 -0
- rocket_engine/adapters/hdl_sim/README.md +7 -0
- rocket_engine/adapters/hdl_sim/__init__.py +369 -0
- rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
- rocket_engine/adapters/hdl_sim/results.py +178 -0
- rocket_engine/adapters/hdl_sim/verilator.py +167 -0
- rocket_engine/adapters/http/README.md +15 -0
- rocket_engine/adapters/http/__init__.py +458 -0
- rocket_engine/adapters/http/events.py +63 -0
- rocket_engine/adapters/ipc/README.md +9 -0
- rocket_engine/adapters/ipc/__init__.py +111 -0
- rocket_engine/adapters/lvs_netgen/README.md +20 -0
- rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
- rocket_engine/adapters/lvs_netgen/results.py +168 -0
- rocket_engine/adapters/mcp/README.md +19 -0
- rocket_engine/adapters/mcp/__init__.py +85 -0
- rocket_engine/adapters/mcp/api-schema.json +6107 -0
- rocket_engine/adapters/mcp/pull.py +246 -0
- rocket_engine/adapters/mcp/server.py +794 -0
- rocket_engine/adapters/meep/__init__.py +167 -0
- rocket_engine/adapters/meow/README.md +7 -0
- rocket_engine/adapters/meow/__init__.py +190 -0
- rocket_engine/adapters/meow/_driver.py +227 -0
- rocket_engine/adapters/meow/_envelope.py +108 -0
- rocket_engine/adapters/meow/adapter.py +491 -0
- rocket_engine/adapters/meow/results.py +218 -0
- rocket_engine/adapters/mutation/__init__.py +192 -0
- rocket_engine/adapters/ngspice/README.md +28 -0
- rocket_engine/adapters/ngspice/__init__.py +627 -0
- rocket_engine/adapters/ngspice/results.py +215 -0
- rocket_engine/adapters/postlayout/README.md +28 -0
- rocket_engine/adapters/postlayout/__init__.py +17 -0
- rocket_engine/adapters/postlayout/adapter.py +1062 -0
- rocket_engine/adapters/postlayout/results.py +330 -0
- rocket_engine/adapters/postlayout/translate.py +71 -0
- rocket_engine/adapters/profile.py +220 -0
- rocket_engine/adapters/sax/README.md +23 -0
- rocket_engine/adapters/sax/__init__.py +516 -0
- rocket_engine/adapters/sax/_driver.py +148 -0
- rocket_engine/adapters/sax/results.py +291 -0
- rocket_engine/adapters/stdio_console.py +1151 -0
- rocket_engine/adapters/syntax/README.md +5 -0
- rocket_engine/adapters/syntax/__init__.py +366 -0
- rocket_engine/adapters/syntax/results.py +181 -0
- rocket_engine/adapters/synth_yosys/README.md +7 -0
- rocket_engine/adapters/synth_yosys/__init__.py +655 -0
- rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
- rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
- rocket_engine/adapters/synth_yosys/results.py +173 -0
- rocket_engine/adapters/timing_sta/README.md +9 -0
- rocket_engine/adapters/timing_sta/__init__.py +726 -0
- rocket_engine/adapters/view_sync/__init__.py +185 -0
- rocket_engine/adapters/view_sync/results.py +189 -0
- rocket_engine/adapters/vlsir/__init__.py +142 -0
- rocket_engine/adapters/xschem/__init__.py +440 -0
- rocket_engine/adapters/xschem/deck.py +266 -0
- rocket_engine/clients/__init__.py +5 -0
- rocket_engine/clients/runtime.py +255 -0
- rocket_engine/composition.py +2279 -0
- rocket_engine/execution/README.md +30 -0
- rocket_engine/execution/__init__.py +150 -0
- rocket_engine/execution/apig/__init__.py +1 -0
- rocket_engine/execution/apig/pool.py +463 -0
- rocket_engine/execution/apig/submit.py +124 -0
- rocket_engine/execution/block_runs_scope.py +479 -0
- rocket_engine/execution/coalesce.py +414 -0
- rocket_engine/execution/console/__init__.py +1 -0
- rocket_engine/execution/console/apply_session.py +318 -0
- rocket_engine/execution/console/lifecycle.py +265 -0
- rocket_engine/execution/dispatch.py +205 -0
- rocket_engine/execution/errors.py +31 -0
- rocket_engine/execution/ports.py +797 -0
- rocket_engine/execution/pyexec/__init__.py +1 -0
- rocket_engine/execution/pyexec/apply_context.py +547 -0
- rocket_engine/execution/pyexec/approvals.py +173 -0
- rocket_engine/execution/pyexec/runner.py +404 -0
- rocket_engine/execution/pyexec/support.py +464 -0
- rocket_engine/execution/python_exec.py +897 -0
- rocket_engine/execution/queue.py +85 -0
- rocket_engine/execution/replay_safe.py +601 -0
- rocket_engine/execution/routing/__init__.py +1 -0
- rocket_engine/execution/routing/calls.py +364 -0
- rocket_engine/execution/routing/namespace.py +785 -0
- rocket_engine/execution/routing/ports.py +102 -0
- rocket_engine/execution/routing/tools.py +346 -0
- rocket_engine/execution/runner.py +311 -0
- rocket_engine/execution/runtime.py +1149 -0
- rocket_engine/execution/service.py +279 -0
- rocket_engine/execution/worker.py +464 -0
- rocket_engine/interfaces/README.md +26 -0
- rocket_engine/interfaces/__init__.py +58 -0
- rocket_engine/interfaces/apig_assets.py +407 -0
- rocket_engine/interfaces/celllink.py +461 -0
- rocket_engine/interfaces/documents.py +92 -0
- rocket_engine/interfaces/envelopes.py +131 -0
- rocket_engine/interfaces/errors.py +16 -0
- rocket_engine/interfaces/layout.py +410 -0
- rocket_engine/interfaces/layout_schema.yaml +278 -0
- rocket_engine/interfaces/paths.py +223 -0
- rocket_engine/interfaces/pdks.py +550 -0
- rocket_engine/interfaces/ports.py +72 -0
- rocket_engine/interfaces/references.py +115 -0
- rocket_engine/interfaces/refs.py +85 -0
- rocket_engine/interfaces/runparams.py +304 -0
- rocket_engine/interfaces/setup.py +552 -0
- rocket_engine/interfaces/setup_catalogue.yaml +243 -0
- rocket_engine/interfaces/setup_probes.py +66 -0
- rocket_engine/interfaces/vocabulary.py +58 -0
- rocket_engine/interfaces/yaml_dump.py +31 -0
- rocket_engine/library/README.md +22 -0
- rocket_engine/library/__init__.py +13 -0
- rocket_engine/library/apig_catalog.py +371 -0
- rocket_engine/library/catalog.py +185 -0
- rocket_engine/library/errors.py +16 -0
- rocket_engine/mutate/__init__.py +776 -0
- rocket_engine/mutate/operators.py +700 -0
- rocket_engine/ops/__init__.py +1 -0
- rocket_engine/ops/deferred.py +279 -0
- rocket_engine/ops/editors.py +404 -0
- rocket_engine/ops/evidence.py +194 -0
- rocket_engine/ops/gates.py +262 -0
- rocket_engine/ops/ports.py +76 -0
- rocket_engine/ops/queries.py +417 -0
- rocket_engine/ops/runs.py +553 -0
- rocket_engine/ops/submit.py +614 -0
- rocket_engine/project/README.md +27 -0
- rocket_engine/project/__init__.py +6 -0
- rocket_engine/project/canvas/__init__.py +55 -0
- rocket_engine/project/canvas/compile.py +109 -0
- rocket_engine/project/canvas/integrate.py +48 -0
- rocket_engine/project/canvas/schema.py +241 -0
- rocket_engine/project/canvas/templates.py +46 -0
- rocket_engine/project/canvas/validate.py +603 -0
- rocket_engine/project/edit/__init__.py +5 -0
- rocket_engine/project/edit/cas.py +87 -0
- rocket_engine/project/edit/transactions.py +282 -0
- rocket_engine/project/errors.py +16 -0
- rocket_engine/project/frontend13.py +685 -0
- rocket_engine/project/geometry/__init__.py +23 -0
- rocket_engine/project/geometry/ops.py +451 -0
- rocket_engine/project/geometry/snap.py +103 -0
- rocket_engine/project/ip/__init__.py +1 -0
- rocket_engine/project/ip/blocks.py +195 -0
- rocket_engine/project/ip/entry.schema.json +78 -0
- rocket_engine/project/ip/lifecycle.py +402 -0
- rocket_engine/project/ip/verbs.py +123 -0
- rocket_engine/project/layers/__init__.py +6 -0
- rocket_engine/project/layers/bindings.py +115 -0
- rocket_engine/project/layers/catalog.py +228 -0
- rocket_engine/project/layers/presets.py +43 -0
- rocket_engine/project/ledger/__init__.py +1 -0
- rocket_engine/project/ledger/docs.py +167 -0
- rocket_engine/project/ledger/entities.py +176 -0
- rocket_engine/project/ledger/events.py +29 -0
- rocket_engine/project/ledger/runs.py +257 -0
- rocket_engine/project/ledger/shared.py +174 -0
- rocket_engine/project/ledger/snapshots.py +385 -0
- rocket_engine/project/ledger/wires.py +552 -0
- rocket_engine/project/process/__init__.py +14 -0
- rocket_engine/project/process/model.py +193 -0
- rocket_engine/project/process/recipes.py +121 -0
- rocket_engine/project/schematic/__init__.py +1 -0
- rocket_engine/project/schematic/hierarchy.py +312 -0
- rocket_engine/project/schematic/instances.py +393 -0
- rocket_engine/project/schematic/sheets.py +198 -0
- rocket_engine/project/service.py +2941 -0
- rocket_engine/project/symbols/__init__.py +1 -0
- rocket_engine/project/symbols/cells.py +324 -0
- rocket_engine/project/symbols/linkage.py +356 -0
- rocket_engine/project/symbols/symbols.py +352 -0
- rocket_engine/project/templates/__init__.py +303 -0
- rocket_engine/project/templates/errors.py +25 -0
- rocket_engine/project/testbench/__init__.py +49 -0
- rocket_engine/project/testbench/blocks.py +619 -0
- rocket_engine/project/testbench/templates.py +134 -0
- rocket_engine/stores/README.md +12 -0
- rocket_engine/stores/__init__.py +6 -0
- rocket_engine/stores/ecs.py +1508 -0
- rocket_engine/stores/layers.py +1023 -0
- rocket_engine/stores/settings.py +1188 -0
- rocket_engine/stores/wireplace.py +1324 -0
- rocket_engine/workflows/README.md +18 -0
- rocket_engine/workflows/__init__.py +53 -0
- rocket_engine/workflows/capability_plan.py +102 -0
- rocket_engine/workflows/completion.py +228 -0
- rocket_engine/workflows/connectgraph.py +734 -0
- rocket_engine/workflows/connectgraph_emit.py +336 -0
- rocket_engine/workflows/gates.py +144 -0
- rocket_engine/workflows/photonic_wire.py +76 -0
- rocket_engine/workflows/stages.py +324 -0
- rocket_engine/workflows/viewsync.py +399 -0
- rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
- rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
- rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
- rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|