|
28 | 28 | shapes (see the .wat header): large/small/negative/mixed consts, reuse ACROSS an |
29 | 29 | if/else (cache must reset), and a 12-live-local function that forces real spills. |
30 | 30 |
|
| 31 | +It also runs a DIRECT-PATH (`--relocatable`) gate on `const_cse_direct.wat` — |
| 32 | +the path gale's gust_mix regression lived on, where only the post-hoc pass |
| 33 | +runs — asserting CSE actually fires there, stays bit-identical to wasmtime, |
| 34 | +and never grows a function (non-vacuous: fails if the fixture stops firing). |
| 35 | +
|
31 | 36 | Run (needs wasmtime + unicorn + pyelftools): |
32 | 37 | SYNTH=./target/debug/synth python scripts/repro/const_cse_differential.py |
33 | 38 | Exits nonzero on any value mismatch OR any function that grew under const-CSE. |
|
49 | 54 | ) |
50 | 55 |
|
51 | 56 | WAT = Path(__file__).with_name("const_cse.wat") |
| 57 | +# Direct-path (`--relocatable`) trigger fixtures — shapes that make the DIRECT |
| 58 | +# selector emit the redundant const materialization `apply_const_cse` dedups, so |
| 59 | +# the direct-path gate actually exercises gale's path (single-param, reloc-free → |
| 60 | +# runnable under unicorn). See its header. |
| 61 | +WAT_DIRECT = Path(__file__).with_name("const_cse_direct.wat") |
| 62 | +DIRECT_EXPORTS = ["r1", "r2", "rneg"] |
52 | 63 | SYNTH = os.environ.get("SYNTH", "./target/release/synth") |
53 | 64 | CODE, STK, RET = 0x100000, 0x900000, 0x300000 |
54 | 65 | EXPORTS = ["large3", "small3", "neg", "mixed", "ctrl", "spill12"] |
55 | 66 | INPUTS = [5, 0, -3, 1000, -100000, 7] |
56 | 67 |
|
57 | 68 |
|
58 | | -def wasmtime_run(fn, arg): |
| 69 | +def wasmtime_run(fn, arg, wat=WAT): |
59 | 70 | engine = wasmtime.Engine() |
60 | | - module = wasmtime.Module.from_file(engine, str(WAT)) |
| 71 | + module = wasmtime.Module.from_file(engine, str(wat)) |
61 | 72 | store = wasmtime.Store(engine) |
62 | 73 | inst = wasmtime.Instance(store, module, []) |
63 | 74 | return inst.exports(store)[fn](store, arg) |
64 | 75 |
|
65 | 76 |
|
66 | | -def compile_optimized(out, cse_on, relocatable=False): |
| 77 | +def compile_optimized(out, cse_on, relocatable=False, wat=WAT): |
67 | 78 | env = {"PATH": "/usr/bin:/bin"} |
68 | 79 | if cse_on: |
69 | 80 | env["SYNTH_CONST_CSE"] = "1" |
70 | | - cmd = [SYNTH, "compile", str(WAT), "-o", out, "-b", "arm", |
| 81 | + cmd = [SYNTH, "compile", str(wat), "-o", out, "-b", "arm", |
71 | 82 | "--target", "cortex-m4", "--all-exports"] |
72 | 83 | if relocatable: |
73 | 84 | cmd.append("--relocatable") |
@@ -165,34 +176,59 @@ def main(): |
165 | 176 | print(f"OK {fn}: {o} -> {n} B ({tag})") |
166 | 177 | fails += grew |
167 | 178 |
|
168 | | - # --relocatable NO-REGRESSION GATE (#242). gale's gust_mix 90→92 B regression |
169 | | - # was on `--relocatable`, which routes through `select_direct()` — there the |
170 | | - # INLINE const cache never runs, so the post-hoc `apply_const_cse` acts ALONE. |
171 | | - # This is the precise path of the bug; the optimized-path gate above does not |
172 | | - # exercise it (inline aliasing dominates there). We compile the corpus on the |
173 | | - # direct path flag-on vs flag-off and assert no function grows. NOTE: post-hoc |
174 | | - # CSE is currently INERT on this corpus (the direct selector does not emit the |
175 | | - # redundant same-value-in-two-registers shape these arithmetic fixtures would |
176 | | - # need) — so this gate is, for now, a tripwire that will light up the moment a |
177 | | - # fixture that DOES trigger it (e.g. gale's gust_mix Q8 clamp mixer) is added. |
178 | | - print("\n--- per-function no-regression gate, --relocatable / direct path (#242) ---") |
179 | | - roff, ron = "/tmp/const_cse_reloc_off.o", "/tmp/const_cse_reloc_on.o" |
180 | | - compile_optimized(roff, cse_on=False, relocatable=True) |
181 | | - compile_optimized(ron, cse_on=True, relocatable=True) |
182 | | - _, _, _, roff_sizes = load(roff) |
183 | | - _, _, _, ron_sizes = load(ron) |
184 | | - rgrew, rfired = 0, 0 |
185 | | - for fn in sorted(set(roff_sizes) & set(ron_sizes)): |
186 | | - o, n = roff_sizes[fn], ron_sizes[fn] |
| 179 | + # --relocatable DIRECT-PATH GATE (#242) — the precise path of gale's gust_mix |
| 180 | + # 90→92 B regression. `--relocatable` routes through `select_direct()`, where |
| 181 | + # the INLINE const cache never runs, so the post-hoc `apply_const_cse` acts |
| 182 | + # ALONE; the optimized-path gate above does not exercise this (inline aliasing |
| 183 | + # dominates there). The main `const_cse.wat` corpus is INERT on this path (its |
| 184 | + # shapes don't make the direct selector emit the redundant same-value-in-two- |
| 185 | + # registers materialization apply_const_cse dedups), so it gave no positive |
| 186 | + # evidence. `const_cse_direct.wat` DOES trigger it. This gate is NON-VACUOUS: |
| 187 | + # it asserts (a) CSE actually fires on ≥1 function (else the gate has gone |
| 188 | + # blind — fail), (b) no function grows (the no-regression property on gale's |
| 189 | + # path), and (c) every result is bit-identical to wasmtime under unicorn |
| 190 | + # (correctness of post-hoc CSE on the direct selector's output). |
| 191 | + print("\n--- direct-path (--relocatable) gate, const_cse_direct.wat (#242) ---") |
| 192 | + doff, don = "/tmp/const_cse_direct_off.o", "/tmp/const_cse_direct_on.o" |
| 193 | + compile_optimized(doff, cse_on=False, relocatable=True, wat=WAT_DIRECT) |
| 194 | + compile_optimized(don, cse_on=True, relocatable=True, wat=WAT_DIRECT) |
| 195 | + d_off_code, d_off_base, _, d_off_sizes = load(doff) |
| 196 | + d_on_code, d_on_base, d_on_syms, d_on_sizes = load(don) |
| 197 | + dgrew, dfired = 0, 0 |
| 198 | + for fn in DIRECT_EXPORTS: |
| 199 | + o, n = d_off_sizes.get(fn), d_on_sizes.get(fn) |
| 200 | + if o is None or n is None: |
| 201 | + continue |
187 | 202 | if n > o: |
188 | | - rgrew += 1 |
| 203 | + dgrew += 1 |
189 | 204 | print(f"REGRESS {fn}: {o} -> {n} B (+{n - o}) — const-CSE grew a function") |
190 | 205 | elif n < o: |
191 | | - rfired += 1 |
192 | | - print(f"OK {fn}: {o} -> {n} B (-{o - n}) — CSE fired") |
193 | | - if rfired == 0: |
194 | | - print(f"(post-hoc CSE inert on this corpus — {len(roff_sizes)} fns, none changed)") |
195 | | - fails += rgrew |
| 206 | + dfired += 1 |
| 207 | + print(f"OK {fn}: {o} -> {n} B (-{o - n}) — CSE fired (direct path)") |
| 208 | + else: |
| 209 | + print(f"-- {fn}: {o} B (inert)") |
| 210 | + # correctness vs wasmtime on the CSE-on direct-path object (reloc-free → runs) |
| 211 | + for fn in DIRECT_EXPORTS: |
| 212 | + faddr = d_on_syms.get(fn) |
| 213 | + if faddr is None: |
| 214 | + continue |
| 215 | + for arg in INPUTS: |
| 216 | + exp = wasmtime_run(fn, arg, wat=WAT_DIRECT) |
| 217 | + got = unicorn_run(d_on_code, d_on_base, faddr, arg) |
| 218 | + if not (isinstance(got, int) and got == exp): |
| 219 | + fails += 1 |
| 220 | + print(f"FAIL {fn} cse-on(direct)({arg}) = {got} (wasmtime {exp})") |
| 221 | + fails += dgrew |
| 222 | + if dfired == 0: |
| 223 | + # Non-vacuity guard: if no function shrank, the fixture no longer triggers |
| 224 | + # post-hoc CSE on the direct path — the gate has gone blind. Fail loudly so |
| 225 | + # it is fixed rather than silently passing. |
| 226 | + fails += 1 |
| 227 | + print("FAIL: direct-path gate is VACUOUS — no function exercised post-hoc " |
| 228 | + "CSE (fixture no longer triggers it). Fix const_cse_direct.wat.") |
| 229 | + else: |
| 230 | + print(f"direct-path gate non-vacuous: CSE fired on {dfired} function(s), " |
| 231 | + f"all results match wasmtime, none grew") |
196 | 232 |
|
197 | 233 | print("\n--- instruction-count delta, optimized path (information) ---") |
198 | 234 | off_n, on_n = insn_count(off_code), insn_count(on_code) |
|
0 commit comments