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,726 @@
1
+ """OpenSTA timing adapter: run_timing behind the ToolAdapter port.
2
+
3
+ Protocol, best-knowledge OpenSTA (live proof deferred: `sta` is not
4
+ provisioned in this sandbox, so the report flags every unverified
5
+ command-line assumption for the gate host): write a timing script
6
+ (`read_liberty` the libs, `read_verilog` the mapped netlist files,
7
+ `link_design` the top, `read_spef` when a top-level SPEF is given,
8
+ `read_sdc` the constraints), then marker-delimited report sections
9
+ (`report_checks -path_delay min_max` for setup+hold critical paths,
10
+ `report_wns`/`report_tns` per corner for WNS/TNS, bare
11
+ `report_check_types` for slew/load violators, `check_setup -verbose`
12
+ unconstrained endpoints) closed by a trailing `exit` so `sta` can
13
+ never linger at its interactive prompt. Parse splits the log on the
14
+ markers: per-path slack records with MET/VIOLATED status, the four
15
+ WNS/TNS margins, constraint violators, and check_setup evidence.
16
+ Unconstrained endpoints are findings (rule
17
+ `unconstrained_endpoint`) plus raw `check_setup` text that is always
18
+ attached — listed, never silent, never assumed.
19
+
20
+ Tool resolution: an explicit job `tool_dir` wins, then
21
+ `$OPENSTA_PREFIX/bin` (the PROV consumer contract), else PATH, and
22
+ the resolved binary must answer its version probe with an `OpenSTA`
23
+ banner — ambient installs are kept and version-asserted, never
24
+ reinstalled. Where the tools live on disk is B10 integration scope.
25
+ """
26
+
27
+ import os
28
+ import re
29
+ import shutil
30
+ import subprocess
31
+ from pathlib import Path
32
+
33
+ from ...execution.errors import ExecutionError
34
+ from ...execution.runner import tool_roots
35
+ from ...interfaces import ToolAdapter
36
+ from ..errors import AdapterError
37
+
38
+ OPERATION = "run_timing"
39
+ _TOOL = "sta"
40
+ _TIMEOUT_S = 300
41
+ _VERSION_TIMEOUT_S = 30
42
+
43
+ _SCRIPT_NAME = "timing.tcl"
44
+ _LOG_NAME = "timing.log"
45
+
46
+ _WNS_TNS_RE = re.compile(
47
+ r"^(?:wns|tns)\s+(?:max|min)\s+"
48
+ r"([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)\s*$",
49
+ re.M,
50
+ )
51
+ _TOP_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
52
+ _VERSION_RE = re.compile(r"OpenSTA", re.IGNORECASE)
53
+ # Real `sta -version` prints a bare number (observed: "2.7.0");
54
+ # the OpenSTA banner only appears on startup. Accept either —
55
+ # the binary path is already the pinned provisioned one.
56
+ _BARE_VERSION_RE = re.compile(r"^\d+\.\d+(?:\.\d+)?$")
57
+ _ERROR_RE = re.compile(r"^(?:Error|ERROR):\s*(.*)$", re.M)
58
+ _WARNING_RE = re.compile(r"^Warning:\s*(.*)$", re.M)
59
+ _MARKER_RE = re.compile(r"^===== TIMING_STA ([A-Z_]+) =====\s*$", re.M)
60
+ _MARGIN_RE = re.compile(
61
+ r"^\s*(?:wns|tns)\s+([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)",
62
+ re.M,
63
+ )
64
+ _STARTPOINT_RE = re.compile(r"^Startpoint:\s*(\S+)", re.M)
65
+ _ENDPOINT_RE = re.compile(r"^Endpoint:\s*(\S+)", re.M)
66
+ _GROUP_RE = re.compile(r"^Path Group:\s*(.+?)\s*$", re.M)
67
+ _TYPE_RE = re.compile(r"^Path Type:\s*(max|min)\s*$", re.M)
68
+ _SLACK_RE = re.compile(
69
+ r"^\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)"
70
+ r"\s+slack\s+\((MET|VIOLATED)\)",
71
+ re.M,
72
+ )
73
+
74
+
75
+ def _parse_errors(text):
76
+ """Collect sta_error violations from sta Error lines.
77
+
78
+ Args:
79
+ text: Combined sta log/stdout/stderr text.
80
+
81
+ Returns:
82
+ Error violations in log order.
83
+ """
84
+ return [
85
+ {"rule": "sta_error", "severity": "error", "message": message}
86
+ for message in _ERROR_RE.findall(text)
87
+ ]
88
+
89
+
90
+ def _parse_warnings(text):
91
+ """Collect warning findings, flagging unconstrained endpoints.
92
+
93
+ Warnings whose message names something unconstrained become
94
+ `unconstrained_endpoint` findings; every other warning is a
95
+ generic `sta_warning`. Neither fails the verdict on its own.
96
+
97
+ Args:
98
+ text: Combined sta log/stdout/stderr text.
99
+
100
+ Returns:
101
+ Warning violations in log order.
102
+ """
103
+ violations = []
104
+ for message in _WARNING_RE.findall(text):
105
+ rule = (
106
+ "unconstrained_endpoint"
107
+ if "unconstrained" in message.lower()
108
+ else "sta_warning"
109
+ )
110
+ violations.append(
111
+ {"rule": rule, "severity": "warning", "message": message}
112
+ )
113
+ return violations
114
+
115
+
116
+ def _parse_sections(text):
117
+ """Split the log on TIMING_STA markers into {name: body}.
118
+
119
+ Args:
120
+ text: Combined sta log/stdout/stderr text.
121
+
122
+ Returns:
123
+ Mapping of section name to section body (possibly empty).
124
+ """
125
+ sections = {}
126
+ marks = list(_MARKER_RE.finditer(text))
127
+ for index, mark in enumerate(marks):
128
+ start = mark.end()
129
+ end = marks[index + 1].start() if index + 1 < len(marks) else len(text)
130
+ sections[mark.group(1)] = text[start:end]
131
+ return sections
132
+
133
+
134
+ def _parse_paths(text):
135
+ """Parse critical paths from the report_checks section.
136
+
137
+ Each `Startpoint:` chunk yields startpoint, endpoint, path group,
138
+ path type (max = setup, min = hold), slack in ns, and the
139
+ violated flag. Chunks without a slack line are skipped: an empty
140
+ or pathless section parses as no paths, never as an error.
141
+
142
+ Args:
143
+ text: REPORT_CHECKS section body.
144
+
145
+ Returns:
146
+ Path records in report order.
147
+ """
148
+ paths = []
149
+ starts = list(_STARTPOINT_RE.finditer(text))
150
+ for index, start in enumerate(starts):
151
+ end = starts[index + 1].start() if index + 1 < len(starts) else len(text)
152
+ chunk = text[start.start() : end]
153
+ slack = _SLACK_RE.search(chunk)
154
+ if slack is None:
155
+ continue
156
+ endpoint = _ENDPOINT_RE.search(chunk)
157
+ group = _GROUP_RE.search(chunk)
158
+ path_type = _TYPE_RE.search(chunk)
159
+ value = float(slack.group(1))
160
+ paths.append(
161
+ {
162
+ "startpoint": start.group(1),
163
+ "endpoint": endpoint.group(1) if endpoint else "",
164
+ "group": group.group(1) if group else "",
165
+ "type": path_type.group(1) if path_type else "",
166
+ "slack": value,
167
+ "violated": slack.group(2) == "VIOLATED" or value < 0,
168
+ }
169
+ )
170
+ return paths
171
+
172
+
173
+ def _parse_margin(text):
174
+ """Read the first wns/tns value from one margin section.
175
+
176
+ Real `report_wns`/`report_tns` print `wns max <v>` lines
177
+ (observed); the slack-parenthetical form is kept as a
178
+ fallback for path-row text.
179
+
180
+ Args:
181
+ text: One WNS_*/TNS_* section body.
182
+
183
+ Returns:
184
+ Margin in ns, or None when the section carries no value.
185
+ """
186
+ match = _WNS_TNS_RE.search(text)
187
+ if match is None:
188
+ match = _MARGIN_RE.search(text)
189
+ return float(match.group(1)) if match is not None else None
190
+
191
+
192
+ def _parse_constraint_violations(text):
193
+ """Collect constraint_violation errors from VIOLATED lines.
194
+
195
+ Args:
196
+ text: CONSTRAINTS section body.
197
+
198
+ Returns:
199
+ Error violations, one per VIOLATED line.
200
+ """
201
+ return [
202
+ {
203
+ "rule": "constraint_violation",
204
+ "severity": "error",
205
+ "message": line.strip(),
206
+ }
207
+ for line in text.splitlines()
208
+ if "VIOLATED" in line
209
+ ]
210
+
211
+
212
+ def _quote(path):
213
+ """Brace-quote one path for the sta TCL script.
214
+
215
+ Args:
216
+ path: Filesystem path to embed in the script.
217
+
218
+ Returns:
219
+ Brace-quoted path (no TCL substitution inside braces).
220
+ """
221
+ return "{" + str(path) + "}"
222
+
223
+
224
+ class OpenStaTimingAdapter(ToolAdapter):
225
+ """run_timing via OpenSTA: top-level STA recheck plus evidence.
226
+
227
+ Serves the run_timing operation with deck.tool `opensta` (the
228
+ only tool). Reads the timing libs, the mapped netlist files, the
229
+ optional top-level SPEF, and the SDC constraints, then reports
230
+ setup/hold WNS/TNS margins, critical paths, slew/load
231
+ violators, and unconstrained endpoints. Evidence is the four
232
+ margins, the critical-path records, the unconstrained-endpoint
233
+ findings with the raw check_setup text, and the sta version.
234
+
235
+ Job keys:
236
+ netlist (required): Mapped Verilog netlist paths, non-empty
237
+ list, every file must exist.
238
+ libs (required): Timing liberty (.lib) paths, non-empty
239
+ list, every file must exist.
240
+ sdc (required): SDC constraints path, must exist. Passed to
241
+ sta opaquely — generated clocks and boundary
242
+ constraints flow through untouched.
243
+ spef: Top-level SPEF path for extracted-RC annotation;
244
+ no parasitics without it.
245
+ top (required): Top module name (plain identifier).
246
+ timeout_s: STA budget in seconds (default 300).
247
+ tool_dir: Directory holding sta; $OPENSTA_PREFIX/bin then
248
+ PATH otherwise.
249
+ deck: Optional mapping; deck.tool must be opensta here.
250
+ workdir: Run directory (default ".").
251
+
252
+ Args:
253
+ runner: Optional isolated runner; direct subprocess when None.
254
+
255
+ Caution: Exit zero is not PASS — sta keeps going past command
256
+ errors, so any `Error:` line fails the run however it
257
+ exited, and a clean run without all four WNS/TNS margins
258
+ is runner_unavailable, never a clean verdict.
259
+ """
260
+
261
+ def __init__(self, runner=None):
262
+ self._runner = runner
263
+
264
+ def capabilities(self, target):
265
+ """Report the served operation, tool, and SPEF support.
266
+
267
+ Args:
268
+ target: Target the capabilities are reported for
269
+ (accepted, unused — one top per job).
270
+
271
+ Returns:
272
+ Capability mapping with operations, tool, and spef
273
+ support.
274
+ """
275
+ return {
276
+ "operations": [OPERATION],
277
+ "tool": "opensta",
278
+ "spef": True,
279
+ }
280
+
281
+ def preflight(self, job):
282
+ """Check operation, top, inputs, timeout, and sta.
283
+
284
+ Cheap deterministic checks first, tool resolution last; the
285
+ resolved binary must answer its version probe with an
286
+ OpenSTA banner.
287
+
288
+ Args:
289
+ job: Solver job with operation run_timing plus netlist,
290
+ libs, sdc, and optional spef/top/timeout inputs.
291
+
292
+ Returns:
293
+ Checked mapping with the resolved sta path and
294
+ version, netlist, libs, sdc, spef, top, timeout_s.
295
+
296
+ Raises:
297
+ AdapterError: unsupported_operation for another
298
+ operation, invalid_command for a bad top or
299
+ timeout, invalid_reference for missing or
300
+ non-list netlist/libs, a missing sdc/spef, or a
301
+ path unsafe for the TCL script, missing_dependency
302
+ for an unresolvable or unassertable sta.
303
+ """
304
+ # Cheap deterministic checks first: operation, top, timeout,
305
+ # netlist, libs, sdc, spef. Tool resolution last, since it
306
+ # depends on the machine.
307
+ if job.get("operation") != OPERATION:
308
+ raise AdapterError(
309
+ "unsupported_operation",
310
+ f"OpenSTA timing handles {OPERATION}, got {job.get('operation')}.",
311
+ )
312
+ top = job.get("top")
313
+ if not isinstance(top, str) or not _TOP_RE.fullmatch(top):
314
+ raise AdapterError("invalid_command", f"Bad top: {top!r}.")
315
+ timeout = job.get("timeout_s", _TIMEOUT_S)
316
+ if (
317
+ not isinstance(timeout, (int, float))
318
+ or isinstance(timeout, bool)
319
+ or not timeout > 0
320
+ ):
321
+ raise AdapterError("invalid_command", f"Bad timeout_s: {timeout!r}.")
322
+ netlist = self._checked_paths(job.get("netlist"), "netlist")
323
+ libs = self._checked_paths(job.get("libs"), "libs")
324
+ sdc = job.get("sdc")
325
+ if not isinstance(sdc, (str, Path)) or not Path(sdc).is_file():
326
+ raise AdapterError("invalid_reference", f"SDC not found: {sdc}.")
327
+ spef = job.get("spef")
328
+ if spef is not None and (
329
+ not isinstance(spef, (str, Path)) or not Path(spef).is_file()
330
+ ):
331
+ raise AdapterError("invalid_reference", f"SPEF not found: {spef}.")
332
+ for path in [sdc] + ([spef] if spef is not None else []):
333
+ self._checked_scriptable(str(path))
334
+ sta = self._resolve(job, _TOOL)
335
+ checked = {
336
+ "sta": sta,
337
+ "sta_version": self._version(sta),
338
+ "netlist": netlist,
339
+ "libs": libs,
340
+ "sdc": str(sdc),
341
+ "spef": str(spef) if spef is not None else None,
342
+ "top": top,
343
+ "timeout_s": timeout,
344
+ }
345
+ if self._runner is not None:
346
+ try:
347
+ self._runner.preflight()
348
+ except ExecutionError as exc:
349
+ raise AdapterError(exc.code, str(exc)) from exc
350
+ return checked
351
+
352
+ def execute(self, job):
353
+ """Run the timing script under sta with the job timeout.
354
+
355
+ Writes timing.tcl into the workdir, runs it (stdin closed
356
+ so sta can never linger at its prompt), and returns the
357
+ captured log plus the run inputs. A failed run still
358
+ returns its log — the verdict comes from parse, never the
359
+ exit code alone.
360
+
361
+ Args:
362
+ job: Solver job with operation run_timing plus netlist,
363
+ libs, sdc, and optional spef/top/timeout inputs.
364
+
365
+ Returns:
366
+ Native output with exit_code, stdout/stderr, log,
367
+ top, netlist, libs, sdc, spef, and the sta version.
368
+
369
+ Raises:
370
+ AdapterError: invalid_reference for a missing workdir,
371
+ runner_unavailable on an STA timeout, plus the
372
+ preflight codes (execute re-checks first).
373
+ """
374
+ checked = self.preflight(job)
375
+ workdir = job.get("workdir", ".")
376
+ if not Path(workdir).is_dir():
377
+ raise AdapterError(
378
+ "invalid_reference", f"Workdir not found: {workdir}."
379
+ )
380
+ (Path(workdir) / _SCRIPT_NAME).write_text(self._script(checked))
381
+ roots = tool_roots([checked["sta"]])
382
+ proc = self._run(
383
+ [checked["sta"], _SCRIPT_NAME],
384
+ workdir,
385
+ checked["timeout_s"],
386
+ roots,
387
+ )
388
+ log = proc.stdout or ""
389
+ (Path(workdir) / _LOG_NAME).write_text(log)
390
+ return {
391
+ "exit_code": proc.returncode,
392
+ "stdout": proc.stdout,
393
+ "stderr": proc.stderr,
394
+ "log": log,
395
+ "top": checked["top"],
396
+ "netlist": list(checked["netlist"]),
397
+ "libs": list(checked["libs"]),
398
+ "sdc": checked["sdc"],
399
+ "spef": checked["spef"],
400
+ "sta_version": checked["sta_version"],
401
+ }
402
+
403
+ def parse(self, native_output):
404
+ """Parse margins, paths, violations, and endpoint evidence.
405
+
406
+ Failed runs (nonzero exit or any `Error:` line) surface
407
+ their errors plus warnings with no measurements — a failed
408
+ run never parses as clean. Clean runs require all four
409
+ WNS/TNS margins; violated paths become setup/hold error
410
+ findings, constraint VIOLATED lines become errors, and
411
+ warnings (including unconstrained endpoints) never fail
412
+ alone. Raw check_setup text always rides along so missing
413
+ constraints stay visible however the parse lands.
414
+
415
+ Args:
416
+ native_output: Native output with exit_code and log
417
+ text.
418
+
419
+ Returns:
420
+ {total, violations[], measurements[]} plus
421
+ critical_paths, unconstrained_endpoints, the four
422
+ margins, check_setup/constraints raw text, top,
423
+ netlist, libs, sdc, spef, and sta_version evidence.
424
+
425
+ Raises:
426
+ AdapterError: runner_unavailable when the output has no
427
+ exit code, or a clean run left no parseable
428
+ WNS/TNS verdict — unparseable never parses as
429
+ clean.
430
+ """
431
+ exit_code = native_output.get("exit_code")
432
+ if exit_code is None:
433
+ raise AdapterError(
434
+ "runner_unavailable", "Native output has no exit code."
435
+ )
436
+ text = "\n".join(
437
+ native_output.get(key, "") or ""
438
+ for key in ("log", "stdout", "stderr")
439
+ )
440
+ errors = _parse_errors(text)
441
+ warnings = _parse_warnings(text)
442
+ sections = _parse_sections(text)
443
+ paths = _parse_paths(sections.get("REPORT_CHECKS", ""))
444
+ unconstrained = [
445
+ v["message"] for v in warnings if v["rule"] == "unconstrained_endpoint"
446
+ ]
447
+ evidence = {
448
+ "critical_paths": paths,
449
+ "unconstrained_endpoints": unconstrained,
450
+ "check_setup": sections.get("CHECK_SETUP", ""),
451
+ "constraints": sections.get("CONSTRAINTS", ""),
452
+ "top": native_output.get("top"),
453
+ "netlist": native_output.get("netlist"),
454
+ "libs": native_output.get("libs"),
455
+ "sdc": native_output.get("sdc"),
456
+ "spef": native_output.get("spef"),
457
+ "sta_version": native_output.get("sta_version"),
458
+ }
459
+ if exit_code != 0 or errors:
460
+ if exit_code != 0 and not errors:
461
+ # Defensive: a failed run must never parse as clean.
462
+ errors = [
463
+ {
464
+ "rule": "sta_error",
465
+ "severity": "error",
466
+ "message": f"sta exited {exit_code}",
467
+ }
468
+ ]
469
+ violations = errors + warnings
470
+ return {
471
+ "total": len(violations),
472
+ "violations": violations,
473
+ "measurements": [],
474
+ **evidence,
475
+ }
476
+ margins = {
477
+ "wns_setup": _parse_margin(sections.get("WNS_MAX", "")),
478
+ "tns_setup": _parse_margin(sections.get("TNS_MAX", "")),
479
+ "wns_hold": _parse_margin(sections.get("WNS_MIN", "")),
480
+ "tns_hold": _parse_margin(sections.get("TNS_MIN", "")),
481
+ }
482
+ missing = sorted(name for name, value in margins.items() if value is None)
483
+ if missing:
484
+ raise AdapterError(
485
+ "runner_unavailable",
486
+ f"sta output has no parseable verdict: {', '.join(missing)}.",
487
+ )
488
+ violations = warnings + _parse_constraint_violations(
489
+ sections.get("CONSTRAINTS", "")
490
+ )
491
+ for path in paths:
492
+ if not path["violated"]:
493
+ continue
494
+ rule = "setup_violation" if path["type"] == "max" else "hold_violation"
495
+ violations.append(
496
+ {
497
+ "rule": rule,
498
+ "severity": "error",
499
+ "message": (
500
+ f"{path['type']} path {path['startpoint']} -> "
501
+ f"{path['endpoint']} slack {path['slack']} ns"
502
+ + (f" ({path['group']})" if path["group"] else "")
503
+ ),
504
+ }
505
+ )
506
+ violated = sum(1 for path in paths if path["violated"])
507
+ measurements = [
508
+ {"name": name, "value": value, "unit": "ns"}
509
+ for name, value in margins.items()
510
+ ]
511
+ measurements.extend(
512
+ [
513
+ {
514
+ "name": "critical_path_count",
515
+ "value": len(paths),
516
+ "unit": "count",
517
+ },
518
+ {
519
+ "name": "violated_path_count",
520
+ "value": violated,
521
+ "unit": "count",
522
+ },
523
+ {
524
+ "name": "unconstrained_count",
525
+ "value": len(unconstrained),
526
+ "unit": "count",
527
+ },
528
+ ]
529
+ )
530
+ return {
531
+ "total": len(violations),
532
+ "violations": violations,
533
+ "measurements": measurements,
534
+ **margins,
535
+ **evidence,
536
+ }
537
+
538
+ @staticmethod
539
+ def _checked_paths(value, key):
540
+ """Require a non-empty list of existing, scriptable paths.
541
+
542
+ Args:
543
+ value: Job value for the key, expected to be a list of
544
+ paths.
545
+ key: Job key name for error messages.
546
+
547
+ Returns:
548
+ The paths as plain strings.
549
+
550
+ Raises:
551
+ AdapterError: invalid_reference when the value is not a
552
+ non-empty list, a path is missing, or a path is
553
+ unsafe for the TCL script.
554
+ """
555
+ if not isinstance(value, list) or not value:
556
+ raise AdapterError(
557
+ "invalid_reference", f"{key} must be a non-empty list: {value!r}."
558
+ )
559
+ missing = [
560
+ item
561
+ for item in value
562
+ if not isinstance(item, (str, Path)) or not Path(item).is_file()
563
+ ]
564
+ if missing:
565
+ raise AdapterError("invalid_reference", f"{key} not found: {missing}.")
566
+ for item in value:
567
+ OpenStaTimingAdapter._checked_scriptable(str(item))
568
+ return [str(item) for item in value]
569
+
570
+ @staticmethod
571
+ def _checked_scriptable(path):
572
+ """Refuse paths that brace-quoting cannot carry safely.
573
+
574
+ Args:
575
+ path: Filesystem path destined for the TCL script.
576
+
577
+ Raises:
578
+ AdapterError: invalid_reference when the path holds
579
+ braces or newlines (TCL injection surface).
580
+ """
581
+ if any(char in path for char in ("{", "}", "\n", "\r")):
582
+ raise AdapterError(
583
+ "invalid_reference", f"Path unsafe for the TCL script: {path}."
584
+ )
585
+
586
+ @staticmethod
587
+ def _script(checked):
588
+ """Render the sta timing script for the checked job.
589
+
590
+ Args:
591
+ checked: Preflight mapping with netlist, libs, sdc,
592
+ spef, and top.
593
+
594
+ Returns:
595
+ TCL script text (read, link, constrain, report, exit).
596
+ """
597
+ lines = [f"read_liberty {_quote(lib)}" for lib in checked["libs"]]
598
+ lines += [f"read_verilog {_quote(net)}" for net in checked["netlist"]]
599
+ lines.append(f"link_design {checked['top']}")
600
+ if checked["spef"] is not None:
601
+ lines.append(f"read_spef {_quote(checked['spef'])}")
602
+ lines.append(f"read_sdc {_quote(checked['sdc'])}")
603
+ reports = [
604
+ (
605
+ "REPORT_CHECKS",
606
+ "report_checks -path_delay min_max -format full_clock_expanded "
607
+ "-fields {slew cap} -group_count 50",
608
+ ),
609
+ ("WNS_MAX", "report_wns -max"),
610
+ ("TNS_MAX", "report_tns -max"),
611
+ ("WNS_MIN", "report_wns -min"),
612
+ ("TNS_MIN", "report_tns -min"),
613
+ # report_check_types (bare): slew/load/width tables whose
614
+ # violated rows carry the VIOLATED marker the parse keys
615
+ # on. Pinned OpenSTA has no report_constraint command.
616
+ ("CONSTRAINTS", "report_check_types"),
617
+ # -verbose names the offending pins on the lines after
618
+ # each warning; bare check_setup only counts them.
619
+ ("CHECK_SETUP", "check_setup -verbose"),
620
+ ]
621
+ for name, command in reports:
622
+ lines.append(f"puts {{===== TIMING_STA {name} =====}}")
623
+ lines.append(command)
624
+ lines.append("exit")
625
+ return "\n".join(lines) + "\n"
626
+
627
+ @staticmethod
628
+ def _resolve(job, binary):
629
+ tool_dir = job.get("tool_dir")
630
+ if tool_dir:
631
+ candidate = Path(tool_dir) / binary
632
+ if candidate.is_file():
633
+ return str(candidate)
634
+ raise AdapterError(
635
+ "missing_dependency",
636
+ f"{binary} not found in {tool_dir} — install the pinned "
637
+ "OpenSTA build via scripts/provision_toolchain.sh --install "
638
+ "(PROV phase 1; see docs/work/PROV/report.md).",
639
+ )
640
+ prefix = os.environ.get("OPENSTA_PREFIX")
641
+ if prefix:
642
+ candidate = Path(prefix) / "bin" / binary
643
+ if candidate.is_file():
644
+ return str(candidate)
645
+ raise AdapterError(
646
+ "missing_dependency",
647
+ f"{binary} not found under $OPENSTA_PREFIX={prefix} — rebuild "
648
+ "the pinned OpenSTA build via "
649
+ "scripts/provision_toolchain.sh --install (PROV phase 1; "
650
+ "see docs/work/PROV/report.md).",
651
+ )
652
+ found = shutil.which(binary)
653
+ if found is None:
654
+ raise AdapterError(
655
+ "missing_dependency",
656
+ f"{binary} not on PATH and $OPENSTA_PREFIX unset — install "
657
+ "the pinned OpenSTA build via "
658
+ "scripts/provision_toolchain.sh --install (PROV phase 1; "
659
+ "see docs/work/PROV/report.md) or pass tool_dir.",
660
+ )
661
+ return found
662
+
663
+ @staticmethod
664
+ def _version(sta):
665
+ """Assert the binary answers with a parseable OpenSTA banner.
666
+
667
+ Probes `-version` first, then `--version` (same fallback the
668
+ PROV provisioner uses).
669
+
670
+ Args:
671
+ sta: Resolved sta binary path.
672
+
673
+ Returns:
674
+ OpenSTA banner line (kept as evidence).
675
+
676
+ Raises:
677
+ AdapterError: missing_dependency when the probes fail,
678
+ time out, or answer no OpenSTA banner.
679
+ """
680
+ for flag in ("-version", "--version"):
681
+ try:
682
+ proc = subprocess.run(
683
+ [sta, flag],
684
+ capture_output=True,
685
+ text=True,
686
+ timeout=_VERSION_TIMEOUT_S,
687
+ )
688
+ except subprocess.TimeoutExpired as exc:
689
+ raise AdapterError(
690
+ "missing_dependency",
691
+ f"sta version probe timed out at {sta}.",
692
+ ) from exc
693
+ banner = (proc.stdout + "\n" + proc.stderr).strip().splitlines()
694
+ versioned = next(
695
+ (line for line in banner
696
+ if _VERSION_RE.search(line)
697
+ or _BARE_VERSION_RE.search(line.strip())), None
698
+ )
699
+ if proc.returncode == 0 and versioned is not None:
700
+ return versioned.strip()
701
+ raise AdapterError(
702
+ "missing_dependency",
703
+ f"sta at {sta} answered no OpenSTA banner — install the "
704
+ "pinned OpenSTA build via scripts/provision_toolchain.sh "
705
+ "--install (PROV phase 1; see docs/work/PROV/report.md).",
706
+ )
707
+
708
+ def _run(self, argv, workdir, timeout_s, extra_binds=()):
709
+ if self._runner is not None:
710
+ try:
711
+ return self._runner.run(argv, workdir, timeout_s, extra_binds)
712
+ except ExecutionError as exc:
713
+ raise AdapterError(exc.code, str(exc)) from exc
714
+ try:
715
+ return subprocess.run(
716
+ argv,
717
+ capture_output=True,
718
+ text=True,
719
+ timeout=timeout_s,
720
+ cwd=workdir,
721
+ stdin=subprocess.DEVNULL,
722
+ )
723
+ except subprocess.TimeoutExpired as exc:
724
+ raise AdapterError(
725
+ "runner_unavailable", f"timing timed out after {timeout_s}s."
726
+ ) from exc