-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathverify_vafcodegen.py
More file actions
322 lines (288 loc) · 17 KB
/
Copy pathverify_vafcodegen.py
File metadata and controls
322 lines (288 loc) · 17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
#!/usr/bin/env python3
"""verify_vafcodegen.py -- Enhancements 286-293: eight openvaf-r optimizer/code-generator
defects found by a targeted robustness campaign against the committed compiler.
Five of the eight aborted the compile outright; two produced LLVM IR that the module
verifier rejects; one produced a wrong memory offset. What made the last three durable is
that every check that would have caught them -- the MIR verifier and the LLVM module
verifier -- sits behind a `debug_assert!`, so a release build carried the malformed
function or the bad offset forward without a word:
[286] constfold.va -- folding `5/0` (or `5%0`, `i32::MIN/-1`, an out-of-range shift)
EVALUATED the operation inside the compiler, so openvaf-r died
with an internal error. A runtime zero divisor was always
accepted, so a literal one must be too.
[287] orphanblock.va -- a noise operator in an `if` CONDITION lets the optimizer fold
the branch; folding it orphaned a block, but the sweep never
re-ran, leaving a phi edge naming a value reachable only
through the deleted edge -- a broken-SSA function.
[288] hypotclog2.va -- `hypot` declared with ONE parameter but called with two.
[289] hypotclog2.va -- `llvm.ctlz` needs its type suffix (`llvm.ctlz.i32`); it backs
`$clog2`.
[290] tempacstim.va -- `$temperature` as an operator argument took a struct-GEP handed
the FIELD type instead of the instance struct, so the offset
came out as a flat `5*sizeof(double)` rather than
`offsetof(instance, temperature)`. The shipped compiler died
with SIGSEGV (exit 139) optimizing this model.
[291] casemax.va -- `max`/`min`/`abs` in a `case` DEFAULT arm left the case's
fall-through block unsealed ("block N is not sealed").
[292] ssprune.va -- small-signal pruning indexed a map with a key its own replay
never inserted ("no entry found for key").
[295] paramslots.va -- every parameter must read back through its OWN eval-output
slot, per instance: the guard for the E-290 CLASS, a
reader/writer slot mismatch. Mutation-tested by making
`nth_opvar_ptr` read a different slot than eval wrote, which
makes a parameter read its neighbour's value.
[294] staleuse.va -- rewriting a `Branch` (one value operand) into a `Jump` (none) by
overwriting the instruction left the condition's entry in the use
list, naming an operand the instruction no longer has.
[293] seconderiv.va -- one analog operator nested DIRECTLY inside another
(`ddt(ddt(x))`): the inner operator's result was deleted while a
later linear contribution still named it OUTSIDE the data-flow
graph, where the rewrite could not reach it.
Where a fix changes a NUMBER, this checks the number against closed form rather than
against the old binary. Two of them (288/289, 287) do not change any number on this
platform -- invalid IR that LLVM happened to lower as intended, and a malformed function
the release build tolerated -- so for those the assertions below are forward regression
guards, and the authoritative check is that an assertions-enabled compiler now accepts
the module.
"""
import os
import re
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
from _setup import VAF as OPENVAF, NG as NGSPICE # noqa: E402
from _setup import check_both_solvers as _check_both_solvers # noqa: E402
_check_both_solvers(__file__) # verify under BOTH KLU and Sparse solvers
checks = passed = 0
def check(label, ok, detail=""):
global checks, passed
checks += 1
passed += bool(ok)
print(f" {'PASS' if ok else 'FAIL'} {label}" + (f" [{detail}]" if detail else ""))
def compile_va_raw(name):
"""Compile <name>.va; return (returncode, combined output) verbatim.
Enhancement-333 needs to assert a specific NON-zero exit (a clean compile
error, 65) and inspect the diagnostic text, which `compile_va` collapses to a
pass/fail verdict.
"""
osdi = os.path.join(HERE, name.replace(".va", ".osdi"))
try:
r = subprocess.run([OPENVAF, os.path.join(HERE, name), "-o", osdi],
capture_output=True, text=True, timeout=90, errors="replace")
except subprocess.TimeoutExpired:
return "HANG", ""
return r.returncode, (r.stdout or "") + (r.stderr or "")
def compile_va(name):
"""Compile <name>.va next to this script; return (ok, verdict)."""
osdi = os.path.join(HERE, name.replace(".va", ".osdi"))
try:
r = subprocess.run([OPENVAF, os.path.join(HERE, name), "-o", osdi],
capture_output=True, text=True, timeout=90, errors="replace")
except subprocess.TimeoutExpired:
return False, "HANG"
out = ((r.stdout or "") + (r.stderr or "")).lower()
if r.returncode is not None and r.returncode < 0:
return False, f"CRASH (signal {-r.returncode})"
if r.returncode == 139:
return False, "SIGSEGV"
if "panicked at" in out or "has crashed" in out or r.returncode == 101:
return False, "ICE"
if r.returncode != 0:
return False, f"exit {r.returncode}"
return os.path.exists(osdi), "compiled"
def ngspice(deck, name):
"""Run a deck (first line is the TITLE) from HERE; return stdout+stderr."""
path = os.path.join(HERE, name)
with open(path, "w") as fh:
fh.write(deck)
try:
r = subprocess.run([NGSPICE, "-b", name], cwd=HERE, capture_output=True,
text=True, timeout=120, errors="replace")
except subprocess.TimeoutExpired:
return "[TIMEOUT]"
return (r.stdout or "") + (r.stderr or "")
def value(out, vec):
m = re.search(rf"^{re.escape(vec)}\s*=\s*([-\d.eE+]+)", out, re.M)
return float(m.group(1)) if m else None
print("Enhancements 286-293: openvaf-r optimizer / code-generator defects")
# ---------------------------------------------------------------- [286]
print("\n[286] constant-folding an integer div/rem by zero killed the compiler")
ok, verdict = compile_va("constfold.va")
check("`i32::MIN/-1`, `1<<40`, wrapping add and a localparam zero divisor compile "
"(were an internal error)", ok, verdict)
# ---------------------------------------------------------------- [333]
# E-286 also ACCEPTED a literal `5/0`, leaving an `sdiv x, 0` in the IR. LLVM
# treats that as undefined behaviour and lowers it to a trap, so the compiled
# .osdi killed the host simulator with SIGTRAP and no diagnostic. It is now a
# clean compile error -- which must still not be an internal error/panic.
print("\n[333] a LITERAL zero divisor is rejected instead of trapping the simulator")
rc, out = compile_va_raw("constfold_divzero.va")
check("`5/0` / `5%0` are a clean compile error, not a crash and not accepted",
rc == 65, f"rc={rc}")
check("the diagnostic names the defect and both operators",
"division by zero" in out and "remainder by zero" in out,
(out.strip().splitlines() or ["no output"])[0][:70])
# ---------------------------------------------------------------- [287]
print("\n[287] a folded-away branch orphaned a block, leaving a stale phi edge")
ok, verdict = compile_va("orphanblock.va")
check("noise in an `if` condition + branch-dependent variables compile", ok, verdict)
if ok:
out = ngspice("* E-287 orphaned block\nv1 a 0 dc 1\nn1 a 0 om\n.model om orphanblock()\n"
".control\npre_osdi orphanblock.osdi\nop\nprint i(v1)\n.endc\n.end\n",
"_ob.cir")
i = value(out, "i(v1)")
# the model is a 1 kOhm resistor plus a term that is 0 in the large-signal domain
check("and simulate: I == V/1k", i is not None and abs(i - (-1e-3)) < 1e-9,
f"i(v1)={i}")
# ---------------------------------------------------------------- [288]/[289]
print("\n[288]/[289] `hypot` arity and `llvm.ctlz` mangling -- invalid LLVM IR")
ok, verdict = compile_va("hypotclog2.va")
check("model using runtime-argument `hypot` and `$clog2` compiles", ok, verdict)
if ok:
out = ngspice("* E-288/289 hypot + clog2\nv1 a 0 dc 0\nn1 a 0 hm\n"
".model hm hypotclog2(px=3 py=4 pn=100)\n"
".control\npre_osdi hypotclog2.osdi\nop\nprint i(v1)\n.endc\n.end\n",
"_hc.cir")
i = value(out, "i(v1)")
# hypot(3,4) = 5 exactly; $clog2(100) = 7 -> 12
check("hypot(3,4) + $clog2(100) == 12 exactly",
i is not None and abs(i - (-12.0)) < 1e-12, f"i(v1)={i} expect=-12")
# ---------------------------------------------------------------- [290]
print("\n[290] `$temperature` as an operator argument used the wrong struct-GEP type")
ok, verdict = compile_va("tempacstim.va")
check("ac_stim(\"ac\", $temperature, 0) compiles (shipped died with SIGSEGV)",
ok, verdict)
if ok:
out = ngspice("* E-290 ac_stim magnitude from $temperature\nNDUT out nm\nR1 out 0 1\n"
".model nm tempacstim\n.control\npre_osdi tempacstim.osdi\n"
"ac lin 1 1k 1k\nprint mag(v(out))\n.endc\n.end\n", "_ta.cir")
v = value(out, "mag(v(out))")
# AC magnitude is $temperature itself: nominal 300.15 K across a 1 ohm load
check("and reads back the nominal temperature, 300.15 K",
v is not None and abs(v - 300.15) < 1e-3, f"mag(v(out))={v} expect=300.15")
# ---------------------------------------------------------------- [291]
print("\n[291] `max`/`min`/`abs` in a `case` default arm left a block unsealed")
ok, verdict = compile_va("casemax.va")
check("case with `max` in its default arm compiles", ok, verdict)
if ok:
for vbias, expect, which in ((2.0, 7.0, "default arm -> max(3,7)"),
(5.0, 11.0, "item arm -> 11")):
out = ngspice(f"* E-291 case default + max, V={vbias}\nv1 a 0 dc {vbias}\n"
f"n1 a 0 cm\n.model cm casemax()\n.control\npre_osdi casemax.osdi\n"
f"op\nprint i(v1)\n.endc\n.end\n", "_cm.cir")
i = value(out, "i(v1)")
check(f"V={vbias}: {which}", i is not None and abs(i - (-expect)) < 1e-12,
f"i(v1)={i} expect={-expect}")
# ---------------------------------------------------------------- [292]
print("\n[292] small-signal pruning indexed a key its own replay never inserted")
ok, verdict = compile_va("ssprune.va")
check("noise routed through idt into nested laplace_nd coefficients compiles",
ok, verdict)
# ---------------------------------------------------------------- [293]
print("\n[293] one analog operator nested directly inside another")
ok, verdict = compile_va("seconderiv.va")
check("`ddt(ddt(V))` compiles", ok, verdict)
if ok:
# In AC a ddt is j*omega, so a second derivative is (j*omega)^2 = -omega^2:
# |I| must track omega^2 exactly, over decades.
out = ngspice("* E-293 second time derivative in AC\nv1 a 0 dc 0 ac 1\nn1 a 0 dm\n"
".model dm seconderiv()\n.control\npre_osdi seconderiv.osdi\n"
"ac dec 1 1 1000\nprint mag(i(v1))\n.endc\n.end\n", "_sd.cir")
rows = re.findall(r"^\d+\s+([\d.eE+-]+)\s+([\d.eE+-]+)", out, re.M)
good = len(rows) == 4
detail = []
for f_s, mag_s in rows:
f, mag = float(f_s), float(mag_s)
w2 = (2.0 * 3.141592653589793 * f) ** 2
rel = abs(mag - w2) / w2
good = good and rel < 1e-6
detail.append(f"{f:g}Hz:{rel:.1e}")
check("|I| == omega^2 across 1 Hz .. 1 kHz", good, " ".join(detail) or "no rows")
# and the formulation that ALREADY compiled (a scaled inner ddt) must agree
out = ngspice("* E-293 nested vs scaled-nested second derivative\n"
"v1 a 0 dc 0 ac 1\nn1 a 0 dm\n.model dm seconderiv()\n"
"v2 c 0 dc 0 ac 1\nn2 c 0 dx\n.model dx seconderiv2x()\n"
".control\npre_osdi seconderiv.osdi\nac lin 1 1 1\n"
"print mag(i(v1)) mag(i(v2))\n.endc\n.end\n", "_sd2.cir")
a, b = value(out, "mag(i(v1))"), value(out, "mag(i(v2))")
check("`ddt(2*ddt(V))` (the path that already worked) is exactly 2x",
a and b and abs(b - 2.0 * a) < 1e-9 * max(1.0, abs(b)),
f"nested={a} scaled={b}")
# Transient: chained ddt is unusable under the DEFAULT trapezoidal integration
# (a non-decaying Nyquist-rate ring -- trapezoidal is A-stable but not L-stable) and
# fine under Gear, which is L-stable. This guards the workaround the docs recommend.
out = ngspice("* E-293 second derivative in transient under Gear\n"
".options method=gear\nv1 a 0 dc 0 sin(0 1 1)\nn1 a 0 dm\n"
".model dm seconderiv()\n.control\npre_osdi seconderiv.osdi\n"
"tran 100u 0.63\nmeas tran ii find i(v1) at=0.625\n"
"meas tran vv find v(a) at=0.625\n.endc\n.end\n", "_sg.cir")
ii, vv = value(out, "ii"), value(out, "vv")
w2 = (2.0 * 3.141592653589793) ** 2
rel = abs(ii / vv - w2) / w2 if (ii is not None and vv) else None
check("transient under `.options method=gear` matches omega^2 (the documented "
"workaround)", rel is not None and rel < 0.01,
f"i/v={ii / vv:.5f} expect={w2:.5f} rel={rel:.2e}" if rel is not None else "no data")
# ---------------------------------------------------------------- [294]
print("\n[294] Branch->Jump rewrite left the condition in the use list")
ok, verdict = compile_va("staleuse.va")
check("a `$fatal` arm guarded by a parameter compare compiles", ok, verdict)
if ok:
out = ngspice("* E-294 stale use after branch-to-jump rewrite\nv1 a 0 dc 1\n"
"n1 a 0 sm\n.model sm staleuse(p=1)\n.control\npre_osdi staleuse.osdi\n"
"op\nprint i(v1)\n.endc\n.end\n", "_su.cir")
i = value(out, "i(v1)")
check("and simulates: I == V/1k", i is not None and abs(i - (-1e-3)) < 1e-9,
f"i(v1)={i}")
# ---------------------------------------------------------------- [295]
# Regression guard for the Enhancement-290 CLASS: a wrong struct-GEP offset. With one
# or two parameters a bad offset can land on the right bytes by luck, so this model
# interleaves model/instance parameters of different types with distinct non-round
# values, and mirrors each through its own operating-point variable.
print("\n[295] every parameter reads back through its own slot, per instance")
DEFAULTS = {"mp0": 3.25, "mp1": 117.5, "mp2": 0.008125, "mp3": 940.75, "mp4": 2.5e-3,
"ip0": 7, "ip1": 4093, "ip2": 19, "ip3": 8191,
"mq0": 1.5e-6, "mq1": 4.75e-4, "mq2": 6.125e-5, "mq3": 9.5e-3}
MODEL_P = [n for n in DEFAULTS if not n.startswith("ip")]
INST_P = [n for n in DEFAULTS if n.startswith("ip")]
# two distinct override sets, so a cross-instance leak shows up as a wrong number
SET_A = {"mp0": 1.75, "mp1": 33.25, "mp2": 0.5625, "mp3": 12.125, "mp4": 7.5e-3,
"ip0": 23, "ip1": 1021, "ip2": 61, "ip3": 3079,
"mq0": 8.5e-6, "mq1": 2.25e-4, "mq2": 3.375e-5, "mq3": 1.5e-3}
SET_B = {k: (v * 2 if isinstance(v, float) else v + 5) for k, v in SET_A.items()}
ok, verdict = compile_va("paramslots.va")
check("a model with 13 interleaved model/instance parameters compiles", ok, verdict)
if ok:
def mcard(tag, d):
return f".model {tag} paramslots(" + " ".join(f"{n}={d[n]}" for n in MODEL_P) + ")"
def icard(inst, tag, d):
return f"{inst} a 0 {tag} " + " ".join(f"{n}={d[n]}" for n in INST_P)
prints = "\n".join(f"print @{i}[ov_{n}]" for i in ("n1", "n2", "n3")
for n in DEFAULTS)
out = ngspice("* E-295 parameter slot readback\nv1 a 0 dc 1\n"
+ icard("n1", "pmA", SET_A) + "\n"
+ icard("n2", "pmB", SET_B) + "\n"
+ "n3 a 0 pmA\n"
+ mcard("pmA", SET_A) + "\n" + mcard("pmB", SET_B) + "\n"
+ ".control\npre_osdi paramslots.osdi\nop\n" + prints
+ "\n.endc\n.end\n", "_ps.cir")
# n1/n2 override everything; n3 shares model card A but leaves the INSTANCE
# parameters at their declaration defaults -- the resolution order, in one deck.
want = {"n1": SET_A, "n2": SET_B,
"n3": {**{n: SET_A[n] for n in MODEL_P},
**{n: DEFAULTS[n] for n in INST_P}}}
bad, seen = [], 0
for inst in ("n1", "n2", "n3"):
for n in DEFAULTS:
g = value(out, f"@{inst}[ov_{n}]")
e = float(want[inst][n])
seen += 1
if g is None or abs(g - e) > max(1e-12, abs(e) * 1e-6):
bad.append(f"{inst}.{n}={g} want {e}")
check(f"{seen} (instance, parameter) readbacks: defaults, model card, instance "
f"line, and no cross-instance bleed", not bad,
f"{len(bad)} wrong" + (f" e.g. {bad[0]}" if bad else ""))
print(f"\n{passed}/{checks} checks passed")
if passed == checks:
print("ALL PASS")
raise SystemExit(0 if passed == checks else 1)