code wiki / _hdl_build / nx_regalloc_calls.nx

nx_regalloc_calls.nx source

↩ module page · 142 lines · 6392 B

1// nx_regalloc_calls.nx -- the G1 register allocator's KEYSTONE capability: correct 2// handling of values LIVE ACROSS A CALL. This is the exact bug the bootstrap compiler 3// has (project-knowngood-compiler-regpressure-miscompile): a value held live across a 4// call is left in a CALLER-SAVED register and clobbered by the call, so a later read 5// gets garbage and a comparison flips. The base linear scan (nx_regalloc_linscan) models 6// live intervals but knows nothing about calls or the caller/callee-saved ABI split -- 7// so it can place a live-across-call value in a caller-saved register. That is the gap. 8// 9// This closes it and PROVES it three ways: 10// (1) ra_linscan_calls -- a vreg that spans a call may take only a CALLEE-saved reg, 11// else it SPILLS (a spilled value is in memory and survives any call). 12// (2) ra_validate_calls -- STATIC proof: no live-across-call vreg sits in a caller-saved reg. 13// (3) rc_interp_alloc -- EXECUTION model where a call POISONS caller-saved registers, 14// so a wrong allocation actually computes a wrong answer (the bug, reproduced). 15// The differential (call-unaware diverges / call-aware matches the reference) is the proof. 16// 17// x86-64 SysV ABI: caller-saved = rax,rcx,rdx,rsi,rdi,r8-r11 (clobbered by a call); 18// callee-saved = rbx,rbp,r12-r15 (survive a call). We model regs [0,ncaller) caller-saved, 19// [ncaller,nreg) callee-saved. license_tier: ORIGINAL 20 21import "nx_regalloc_interp.nx" // chains nx_regalloc_linscan + nx_syscalls; ri_get, ri_op_eval, RA_SPILL 22 23const ROP_CALL: i64 = 8 24const RC_POISON: i64 = 0 - 999999 // value a clobbered caller-saved register holds 25 26// does vreg v span a call? -- is there a call instruction c with v < c < ... and v still 27// used after c (last_use[v] > c)? such a v is live in a register across the call. 28func rc_spans_call(v: i64, last_use: *i64, is_call: *i64, n: i64) -> i64 { 29 var c: i64 = v + 1 30 while c < n { 31 if is_call[c] == 1 { if last_use[v] > c { return 1 } } 32 c = c + 1 33 } 34 return 0 35} 36 37// CALL-AWARE linear scan. caller-saved = [0,ncaller); callee-saved = [ncaller,nreg). 38// A vreg that spans a call may only take a callee-saved register; if none is free it 39// SPILLS (always call-safe). Non-spanning vregs prefer any free register. 40func ra_linscan_calls(n: i64, nreg: i64, ncaller: i64, last_use: *i64, is_call: *i64, alloc: *i64) -> i64 { 41 let reg_busy: *i64 = sys_mmap(8 * nreg) 42 let act_v: *i64 = sys_mmap(8 * (n + 1)) 43 let act_e: *i64 = sys_mmap(8 * (n + 1)) 44 var act_n: i64 = 0 45 var spills: i64 = 0 46 var i: i64 = 0 47 while i < n { 48 var k: i64 = 0 49 var w: i64 = 0 50 while k < act_n { 51 if act_e[k] < i { reg_busy[alloc[act_v[k]]] = 0 } 52 else { act_v[w] = act_v[k]; act_e[w] = act_e[k]; w = w + 1 } 53 k = k + 1 54 } 55 act_n = w 56 let spans: i64 = rc_spans_call(i, last_use, is_call, n) 57 var r: i64 = 0 - 1 58 if spans == 1 { 59 var j: i64 = ncaller 60 while j < nreg { if reg_busy[j] == 0 { r = j; j = nreg } else { j = j + 1 } } 61 } else { 62 var j: i64 = 0 63 while j < nreg { if reg_busy[j] == 0 { r = j; j = nreg } else { j = j + 1 } } 64 } 65 if r >= 0 { 66 alloc[i] = r; reg_busy[r] = 1 67 act_v[act_n] = i; act_e[act_n] = last_use[i]; act_n = act_n + 1 68 } else { 69 alloc[i] = RA_SPILL // spill -- in memory, survives any call 70 spills = spills + 1 71 } 72 i = i + 1 73 } 74 return spills 75} 76 77// STATIC soundness for calls: every vreg that spans a call must be SPILLED or callee-saved. 78// Returns 0 if valid, else 0-(v+1) for the first vreg in a caller-saved reg across a call. 79func ra_validate_calls(n: i64, last_use: *i64, is_call: *i64, alloc: *i64, ncaller: i64) -> i64 { 80 var v: i64 = 0 81 while v < n { 82 if rc_spans_call(v, last_use, is_call, n) == 1 { 83 let a: i64 = alloc[v] 84 if a >= 0 { if a < ncaller { return 0 - (v + 1) } } 85 } 86 v = v + 1 87 } 88 return 0 89} 90 91// REFERENCE interpreter with calls: one slot per vreg, a call yields its return value 92// (imm). No register reuse, so nothing can clobber -- always correct. 93func rc_interp_ref(n: i64, op: *i64, u0: *i64, u1: *i64, imm: *i64, is_call: *i64, 94 inputs: *i64, loa: i64, lob: i64, loc: i64) -> i64 { 95 let val: *i64 = sys_mmap(8 * n) 96 var i: i64 = 0 97 while i < n { 98 if is_call[i] == 1 { val[i] = imm[i] } 99 else { 100 if op[i] == ROP_INPUT { val[i] = inputs[imm[i]] } 101 else { 102 var a: i64 = 0 103 var b: i64 = 0 104 if u0[i] >= 0 { a = val[u0[i]] } 105 if u1[i] >= 0 { b = val[u1[i]] } 106 val[i] = ri_op_eval(op[i], a, b, imm[i]) 107 } 108 } 109 i = i + 1 110 } 111 return val[loa] * 31 + val[lob] * 7 + val[loc] 112} 113 114// ALLOCATED interpreter with calls: executing a CALL POISONS every caller-saved register. 115// A vreg that was left live in a caller-saved register across the call is now poison, so 116// the result diverges from the reference -- the miscompile, made observable. 117func rc_interp_alloc(n: i64, nreg: i64, ncaller: i64, op: *i64, u0: *i64, u1: *i64, imm: *i64, 118 is_call: *i64, alloc: *i64, inputs: *i64, loa: i64, lob: i64, loc: i64) -> i64 { 119 let regf: *i64 = sys_mmap(8 * nreg) 120 let spill: *i64 = sys_mmap(8 * n) 121 var i: i64 = 0 122 while i < n { 123 if is_call[i] == 1 { 124 var c: i64 = 0 125 while c < ncaller { regf[c] = RC_POISON; c = c + 1 } // the call clobbers caller-saved 126 if alloc[i] >= 0 { regf[alloc[i]] = imm[i] } else { spill[i] = imm[i] } 127 } else { 128 var v: i64 = 0 129 if op[i] == ROP_INPUT { v = inputs[imm[i]] } 130 else { 131 var a: i64 = 0 132 var b: i64 = 0 133 if u0[i] >= 0 { a = ri_get(u0[i], alloc, regf, spill) } 134 if u1[i] >= 0 { b = ri_get(u1[i], alloc, regf, spill) } 135 v = ri_op_eval(op[i], a, b, imm[i]) 136 } 137 if alloc[i] >= 0 { regf[alloc[i]] = v } else { spill[i] = v } 138 } 139 i = i + 1 140 } 141 return ri_get(loa, alloc, regf, spill) * 31 + ri_get(lob, alloc, regf, spill) * 7 + ri_get(loc, alloc, regf, spill) 142}