code wiki / _hdl_build / nx_regalloc_interp.nx
nx_regalloc_interp.nx source
↩ module page · 85 lines · 3435 B
1// nx_regalloc_interp.nx -- EXECUTION-equivalence verifier for register allocation.
2//
3// The static soundness checker (nx_regalloc_linscan: no two live vregs share a
4// register) is necessary but the absorption gate wants more: PROVE that code run
5// THROUGH the allocation (values living in physical registers + spill slots)
6// computes the IDENTICAL result to the reference (one slot per value). Two
7// interpreters, compared 1:1 over many inputs -- if they ever diverge, the
8// allocation miscompiled (the g1 failure, caught before it can ship).
9//
10// The IR carries real opcodes so the interpretation is meaningful (it computes the
11// race kernel's actual arithmetic). live-out values (loop-carried) are kept live to
12// the block end so they remain readable -- exactly how a real backend treats them.
13
14import "nx_regalloc_linscan.nx" // chains in nx_syscalls + the allocator (single import path)
15
16const ROP_INPUT: i64 = 0 // imm = input index
17const ROP_MUL_IMM: i64 = 1
18const ROP_ADD_IMM: i64 = 2
19const ROP_SHR_IMM: i64 = 3
20const ROP_XOR: i64 = 4
21const ROP_AND_IMM: i64 = 5
22const ROP_ADD: i64 = 6
23const ROP_LT_IMM: i64 = 7
24
25const RI_SPILL: i64 = 0 - 1
26
27func ri_op_eval(op: i64, a: i64, b: i64, imm: i64) -> i64 {
28 if op == ROP_MUL_IMM { return a * imm }
29 if op == ROP_ADD_IMM { return a + imm }
30 if op == ROP_SHR_IMM { return a >> imm }
31 if op == ROP_XOR { return a ^ b }
32 if op == ROP_AND_IMM { return a & imm }
33 if op == ROP_ADD { return a + b }
34 if op == ROP_LT_IMM { if a < imm { return 1 } return 0 }
35 return 0
36}
37
38// where vreg v lives under this allocation (a register, or its spill slot).
39func ri_get(v: i64, alloc: *i64, regf: *i64, spill: *i64) -> i64 {
40 if alloc[v] >= 0 { return regf[alloc[v]] }
41 return spill[v]
42}
43
44// REFERENCE: one slot per vreg (always correct). Returns a checksum of the three
45// live-outs.
46func ri_interp_ref(n: i64, op: *i64, u0: *i64, u1: *i64, imm: *i64, inputs: *i64,
47 loa: i64, lob: i64, loc: i64) -> i64 {
48 let val: *i64 = sys_mmap(8 * n)
49 var i: i64 = 0
50 while i < n {
51 if op[i] == ROP_INPUT { val[i] = inputs[imm[i]] }
52 else {
53 var a: i64 = 0
54 var b: i64 = 0
55 if u0[i] >= 0 { a = val[u0[i]] }
56 if u1[i] >= 0 { b = val[u1[i]] }
57 val[i] = ri_op_eval(op[i], a, b, imm[i])
58 }
59 i = i + 1
60 }
61 return val[loa] * 31 + val[lob] * 7 + val[loc]
62}
63
64// ALLOCATED: values live in regf[alloc[v]] or spill[v]. Same checksum -- must match
65// the reference iff the allocation is execution-correct.
66func ri_interp_alloc(n: i64, nreg: i64, op: *i64, u0: *i64, u1: *i64, imm: *i64,
67 alloc: *i64, inputs: *i64, loa: i64, lob: i64, loc: i64) -> i64 {
68 let regf: *i64 = sys_mmap(8 * nreg)
69 let spill: *i64 = sys_mmap(8 * n)
70 var i: i64 = 0
71 while i < n {
72 var v: i64 = 0
73 if op[i] == ROP_INPUT { v = inputs[imm[i]] }
74 else {
75 var a: i64 = 0
76 var b: i64 = 0
77 if u0[i] >= 0 { a = ri_get(u0[i], alloc, regf, spill) }
78 if u1[i] >= 0 { b = ri_get(u1[i], alloc, regf, spill) }
79 v = ri_op_eval(op[i], a, b, imm[i])
80 }
81 if alloc[i] >= 0 { regf[alloc[i]] = v } else { spill[i] = v }
82 i = i + 1
83 }
84 return ri_get(loa, alloc, regf, spill) * 31 + ri_get(lob, alloc, regf, spill) * 7 + ri_get(loc, alloc, regf, spill)
85}