code wiki / _hdl_build / nx_regalloc_calls_test.nx

nx_regalloc_calls_test.nx source

↩ module page · 110 lines · 5970 B

1// nx_regalloc_calls_test.nx -- PROVE the G1 allocator's live-across-call capability, with 2// the bug reproduced under control. One kernel where v0,v1,v2 are all live across a call: 3// CALL-UNAWARE alloc (base ra_linscan): places a spanning vreg in a caller-saved reg 4// -> ra_validate_calls flags it (STATIC) AND the execution model diverges (DYNAMIC). 5// CALL-AWARE alloc (ra_linscan_calls): spanning vregs are callee-saved or spilled 6// -> validate passes AND execution matches the reference on every input. 7// Known answer: fixed sound + naive unsound + fixed==ref for all inputs -> exit 0. 8 9import "nx_regalloc_calls.nx" 10 11func rt_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 12func rt_num(v: i64) -> i64 { 13 let b: *u8 = sys_mmap(28); var m: i64 = v; if m < 0 { m = 0 - m } 14 let t: *u8 = sys_mmap(28); var k: i64 = 0 15 if m == 0 { t[0] = 48; k = 1 } 16 while m > 0 { t[k] = 48 + (m % 10); m = m / 10; k = k + 1 } 17 if v < 0 { rt_puts("-" as *u8) } 18 var i: i64 = 0; while i < k { b[i] = t[k - 1 - i]; i = i + 1 } 19 sys_write(1, b, k); return 0 20} 21func rt_eq(a: i64, b: i64) -> i64 { if a == b { return 1 } return 0 } 22func tally(r: *i64, n: i64) -> i64 { 23 var ec: i64 = 0; var i: i64 = 0 24 while i < n { if r[i] != 1 { if ec == 0 { ec = i + 1 } } i = i + 1 } 25 return ec 26} 27 28// build the kernel: v0,v1 inputs; v2=3*v0; v3=CALL; v4=v2+v3; v5=v0+v4; v6=v1^v5. 29// v0,v1,v2 are defined before the call (i3) and used after -> they SPAN it. 30func build_kernel(op: *i64, u0: *i64, u1: *i64, imm: *i64, isc: *i64) -> i64 { 31 op[0]=ROP_INPUT; u0[0]=0-1; u1[0]=0-1; imm[0]=0; isc[0]=0 32 op[1]=ROP_INPUT; u0[1]=0-1; u1[1]=0-1; imm[1]=1; isc[1]=0 33 op[2]=ROP_MUL_IMM; u0[2]=0; u1[2]=0-1; imm[2]=3; isc[2]=0 34 op[3]=ROP_CALL; u0[3]=0-1; u1[3]=0-1; imm[3]=100; isc[3]=1 35 op[4]=ROP_ADD; u0[4]=2; u1[4]=3; imm[4]=0; isc[4]=0 36 op[5]=ROP_ADD; u0[5]=0; u1[5]=4; imm[5]=0; isc[5]=0 37 op[6]=ROP_XOR; u0[6]=1; u1[6]=5; imm[6]=0; isc[6]=0 38 return 7 39} 40 41// run T trials over varying inputs; counts[0]=fixed mismatches, counts[1]=naive mismatches. 42func run_trials(n: i64, nreg: i64, ncaller: i64, op: *i64, u0: *i64, u1: *i64, imm: *i64, isc: *i64, 43 a_fix: *i64, a_naive: *i64, counts: *i64) -> i64 { 44 counts[0] = 0; counts[1] = 0 45 let inp: *i64 = sys_mmap(8 * 4) as *i64 46 var t: i64 = 0 47 while t < 24 { 48 inp[0] = t * 7 + 1 49 inp[1] = t * 5 + 3 50 let ref: i64 = rc_interp_ref(n, op, u0, u1, imm, isc, inp, 4, 5, 6) 51 let fix: i64 = rc_interp_alloc(n, nreg, ncaller, op, u0, u1, imm, isc, a_fix, inp, 4, 5, 6) 52 let naive: i64 = rc_interp_alloc(n, nreg, ncaller, op, u0, u1, imm, isc, a_naive, inp, 4, 5, 6) 53 if fix != ref { counts[0] = counts[0] + 1 } 54 if naive != ref { counts[1] = counts[1] + 1 } 55 t = t + 1 56 } 57 return 0 58} 59 60func main() -> i64 { 61 rt_puts("=== G1 register allocator: LIVE-ACROSS-CALL capability, proven by execution ===\n" as *u8) 62 let op: *i64 = sys_mmap(8 * 16) as *i64 63 let u0: *i64 = sys_mmap(8 * 16) as *i64 64 let u1: *i64 = sys_mmap(8 * 16) as *i64 65 let imm: *i64 = sys_mmap(8 * 16) as *i64 66 let isc: *i64 = sys_mmap(8 * 16) as *i64 67 let lu: *i64 = sys_mmap(8 * 16) as *i64 68 let a_fix: *i64 = sys_mmap(8 * 16) as *i64 69 let a_naive: *i64 = sys_mmap(8 * 16) as *i64 70 let counts: *i64 = sys_mmap(8 * 4) as *i64 71 let r: *i64 = sys_mmap(8 * 8) as *i64 72 73 let n: i64 = build_kernel(op, u0, u1, imm, isc) 74 ra_compute_last_use(n, u0, u1, lu) 75 lu[4] = n - 1; lu[5] = n - 1; lu[6] = n - 1 // live-outs stay live to block end (read by the checksum) 76 let nreg: i64 = 3 77 let ncaller: i64 = 2 // regs 0,1 caller-saved; reg 2 callee-saved 78 79 // allocate both ways 80 ra_linscan(n, nreg, lu, a_naive) // call-UNAWARE (the base allocator = the gap) 81 ra_linscan_calls(n, nreg, ncaller, lu, isc, a_fix) // call-AWARE (the fix) 82 83 // static soundness 84 let vn: i64 = ra_validate_calls(n, lu, isc, a_naive, ncaller) 85 let vf: i64 = ra_validate_calls(n, lu, isc, a_fix, ncaller) 86 rt_puts(" static : call-unaware sound? " as *u8); if vn == 0 { rt_puts("yes (missed it!)" as *u8) } else { rt_puts("NO -- caller-saved reg held live across a call at v" as *u8); rt_num((0 - vn) - 1) } 87 rt_puts("\n static : call-aware sound? " as *u8); if vf == 0 { rt_puts("YES" as *u8) } else { rt_puts("no (violation v" as *u8); rt_num((0 - vf) - 1); rt_puts(")" as *u8) }; rt_puts("\n" as *u8) 88 89 // dynamic differential 90 run_trials(n, nreg, ncaller, op, u0, u1, imm, isc, a_fix, a_naive, counts) 91 rt_puts(" exec : over 24 inputs -- call-aware mismatches=" as *u8); rt_num(counts[0]) 92 rt_puts(" call-unaware mismatches=" as *u8); rt_num(counts[1]); rt_puts("\n" as *u8) 93 94 // verdicts (memory-backed) 95 r[0] = rt_eq(vf, 0) // fix is statically sound 96 r[1] = 0; if vn != 0 { r[1] = 1 } // base allocator is statically UNSOUND for calls 97 r[2] = rt_eq(counts[0], 0) // fix matches reference on every input 98 r[3] = 0; if counts[1] > 0 { r[3] = 1 } // base allocator DIVERGES -- bug reproduced 99 100 rt_puts("----------------------------------------------------------------\n" as *u8) 101 rt_puts(" The G1 allocator spills/callee-saves anything that spans a call -- the live-across-call\n" as *u8) 102 rt_puts(" miscompile is fixed at the SOURCE (allocation policy) and proven by execution.\n" as *u8) 103 104 rt_puts(" verdicts: r0(fix-sound)=" as *u8); rt_num(r[0]); rt_puts(" r1(naive-unsound)=" as *u8); rt_num(r[1]) 105 rt_puts(" r2(fix-matches)=" as *u8); rt_num(r[2]); rt_puts(" r3(naive-diverges)=" as *u8); rt_num(r[3]); rt_puts("\n" as *u8) 106 let ec: i64 = tally(r, 4) 107 rt_puts(" gate ec : " as *u8); rt_num(ec); rt_puts("\n" as *u8) 108 sys_exit(ec) 109 return ec 110}