code wiki / (root) / nx_ir_validate.nx

nx_ir_validate.nx source

↩ module page · 271 lines · 10488 B

1// ir_validate.nx -- structural invariants on the in-memory IR. 2// 3// Mirrors what nxc2/ir_validate.c does for the C anchor, ported to 4// run inside nxc.elf so the sovereign pipeline can self-check. 5// Catches the class of bug that bit us 2026-04-25: 6// * BR / BR_COND op_X references a block id past f.n_blocks 7// (the dangling-label landmine). 8// * BR / BR_COND op_X disagrees with bb.succ0 / bb.succ1 9// (the stale-pointer landmine). 10// * A block listed in f.blocks has a different .id than its 11// position (the compaction-skew landmine). 12// 13// Designed to run AFTER each opt pass. When an invariant breaks, 14// the validator prints {function name, pass label, block id, 15// invariant code, instr op} to stderr and returns the count of 16// violations. Caller decides whether to abort or continue. 17// 18// Design rule: validator reads f only -- no writes. Safe to 19// invoke between any two passes without altering pipeline behavior. 20 21// nx_safety_envelope: 22// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 23// sil_target: SIL1 24// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 25// verdict: NOT_YET_EVALUATED 26 27import "nx_types.nx" 28import "nx_syscalls.nx" 29import "nx_ir.nx" 30 31// ----- one-char + decimal helpers (local; no log.nx dep) ----- 32 33func iv_putc(c: i64) -> i64 { 34 let buf: *u8 = sys_mmap(8) 35 buf[0] = c 36 sys_write(2, buf, 1) 37 return 0 38} 39 40func iv_puts(s: *u8) -> i64 { 41 var n: i64 = 0 42 while s[n] != 0 { n = n + 1 } 43 sys_write(2, s, n) 44 return 0 45} 46 47func iv_putd(v: i64) -> i64 { 48 let buf: *u8 = sys_mmap(32) 49 var n: i64 = v 50 if n < 0 { 51 iv_putc(0x2D) // '-' 52 n = 0 - n 53 } 54 var k: i64 = 0 55 if n == 0 { buf[k] = 0x30; k = 1 } 56 while n > 0 { 57 buf[k] = 0x30 + (n - (n / 10) * 10) 58 n = n / 10 59 k = k + 1 60 } 61 var j: i64 = k - 1 62 while j >= 0 { 63 sys_write(2, (((buf as i64) + j) as *u8), 1) 64 j = j - 1 65 } 66 return 0 67} 68 69// Print one violation header: "ir_validate[<pass>] <fn>: ". fn name 70// is read from f.name_start (stored as *u8 pointer per ir.nx 71// convention). 72func iv_violation(f: *Function, pass_label: *u8) -> i64 { 73 iv_puts("ir_validate[" as *u8) 74 iv_puts(pass_label) 75 iv_puts("] " as *u8) 76 let fname: *u8 = f.name_start as *u8 77 var ni: i64 = 0 78 while fname[ni] != 0 { ni = ni + 1 } 79 sys_write(2, fname, ni) 80 iv_puts(": " as *u8) 81 return 0 82} 83 84// ----- the validator ----- 85 86// Returns the number of violations found. Caller picks abort vs 87// continue. Pass `pass_label` is a short c-string identifying which 88// opt pass just ran (e.g. "post-sweep", "post-sccp", "post-parse"). 89func ir_validate_function(f: *Function, pass_label: *u8) -> i64 { 90 var violations: i64 = 0 91 let n: i64 = f.n_blocks 92 if n == 0 { return 0 } 93 94 var bi: i64 = 0 95 while bi < n { 96 let bb: *BasicBlock = block_at(f, bi) 97 98 // V1: block at slot bi must have .id == bi (compaction 99 // invariant -- post-SWEEP2 should hold). 100 if bb.id != bi { 101 iv_violation(f, pass_label) 102 iv_puts("V1 bb at slot " as *u8); iv_putd(bi) 103 iv_puts(" has .id=" as *u8); iv_putd(bb.id) 104 iv_putc(0x0A) 105 violations = violations + 1 106 } 107 108 // V2 + V3: walk this block's instructions; any BR/BR_COND 109 // operand id must be in range, AND any non-zero succ pointer 110 // must match the BR/BR_COND it corresponds to. 111 var inst: *Instr = bb.head 112 var saw_br: i64 = 0 113 var saw_br_cond: i64 = 0 114 var br_target: i64 = -1 115 var br_cond_t: i64 = -1 116 var br_cond_f: i64 = -1 117 while inst != (0 as *Instr) { 118 if inst.op == OP_BR { 119 if saw_br == 0 { br_target = inst.op0 } 120 saw_br = saw_br + 1 121 if inst.op0 < 0 { 122 iv_violation(f, pass_label) 123 iv_puts("V2 bb" as *u8); iv_putd(bi) 124 iv_puts(" OP_BR target=" as *u8); iv_putd(inst.op0) 125 iv_puts(" (negative)" as *u8); iv_putc(0x0A) 126 violations = violations + 1 127 } 128 if inst.op0 >= n { 129 iv_violation(f, pass_label) 130 iv_puts("V2 bb" as *u8); iv_putd(bi) 131 iv_puts(" OP_BR target=" as *u8); iv_putd(inst.op0) 132 iv_puts(" out of range (n_blocks=" as *u8); iv_putd(n) 133 iv_puts(")" as *u8); iv_putc(0x0A) 134 violations = violations + 1 135 } 136 } 137 if inst.op == OP_BR_COND { 138 if saw_br_cond == 0 { 139 br_cond_t = inst.op1 140 br_cond_f = inst.op2 141 } 142 saw_br_cond = saw_br_cond + 1 143 if inst.op1 < 0 { 144 iv_violation(f, pass_label) 145 iv_puts("V2 bb" as *u8); iv_putd(bi) 146 iv_puts(" OP_BR_COND on_true=" as *u8); iv_putd(inst.op1) 147 iv_puts(" (negative)" as *u8); iv_putc(0x0A) 148 violations = violations + 1 149 } 150 if inst.op1 >= n { 151 iv_violation(f, pass_label) 152 iv_puts("V2 bb" as *u8); iv_putd(bi) 153 iv_puts(" OP_BR_COND on_true=" as *u8); iv_putd(inst.op1) 154 iv_puts(" out of range (n_blocks=" as *u8); iv_putd(n) 155 iv_puts(")" as *u8); iv_putc(0x0A) 156 violations = violations + 1 157 } 158 if inst.op2 < 0 { 159 iv_violation(f, pass_label) 160 iv_puts("V2 bb" as *u8); iv_putd(bi) 161 iv_puts(" OP_BR_COND on_false=" as *u8); iv_putd(inst.op2) 162 iv_puts(" (negative)" as *u8); iv_putc(0x0A) 163 violations = violations + 1 164 } 165 if inst.op2 >= n { 166 iv_violation(f, pass_label) 167 iv_puts("V2 bb" as *u8); iv_putd(bi) 168 iv_puts(" OP_BR_COND on_false=" as *u8); iv_putd(inst.op2) 169 iv_puts(" out of range (n_blocks=" as *u8); iv_putd(n) 170 iv_puts(")" as *u8); iv_putc(0x0A) 171 violations = violations + 1 172 } 173 } 174 inst = inst.next 175 } 176 177 // V3: succ pointer must agree with the BR/BR_COND op id (if 178 // either side is set). Tonight's bug: succ stale, op fresh. 179 // We don't insist succ is non-null -- some passes legitimately 180 // clear it -- but if it IS non-null it must agree. 181 if saw_br_cond > 0 { 182 if bb.succ0 != (0 as *BasicBlock) { 183 if bb.succ0.id != br_cond_t { 184 if br_cond_t >= 0 { 185 if br_cond_t < n { 186 iv_violation(f, pass_label) 187 iv_puts("V3 bb" as *u8); iv_putd(bi) 188 iv_puts(" succ0.id=" as *u8); iv_putd(bb.succ0.id) 189 iv_puts(" but BR_COND on_true=" as *u8); iv_putd(br_cond_t) 190 iv_putc(0x0A) 191 violations = violations + 1 192 } 193 } 194 } 195 } 196 if bb.succ1 != (0 as *BasicBlock) { 197 if bb.succ1.id != br_cond_f { 198 if br_cond_f >= 0 { 199 if br_cond_f < n { 200 iv_violation(f, pass_label) 201 iv_puts("V3 bb" as *u8); iv_putd(bi) 202 iv_puts(" succ1.id=" as *u8); iv_putd(bb.succ1.id) 203 iv_puts(" but BR_COND on_false=" as *u8); iv_putd(br_cond_f) 204 iv_putc(0x0A) 205 violations = violations + 1 206 } 207 } 208 } 209 } 210 } 211 if saw_br > 0 { 212 if saw_br_cond == 0 { 213 if bb.succ0 != (0 as *BasicBlock) { 214 if bb.succ0.id != br_target { 215 if br_target >= 0 { 216 if br_target < n { 217 iv_violation(f, pass_label) 218 iv_puts("V3 bb" as *u8); iv_putd(bi) 219 iv_puts(" succ0.id=" as *u8); iv_putd(bb.succ0.id) 220 iv_puts(" but BR target=" as *u8); iv_putd(br_target) 221 iv_putc(0x0A) 222 violations = violations + 1 223 } 224 } 225 } 226 } 227 } 228 } 229 230 // V4: every block must END in a terminator (OP_RETURN / OP_BR / 231 // OP_BR_COND / OP_TAIL_CALL). Backends emit blocks in creation 232 // order and never use intentional fallthrough; an open block 233 // falls through into whatever block was created next, which is 234 // NOT its CFG successor. Caught live 2026-06-09: parse_function 235 // left the final loop-exit block of nx_ttt_evaluate open, so the 236 // board-full DRAW store fell through into the cell-empty ONGOING 237 // store -- draw detection silently clobbered on x86 AND RISC-V. 238 let tail: *Instr = bb.tail 239 var v4_terminated: i64 = 0 240 if tail != (0 as *Instr) { 241 if tail.op == OP_RETURN { v4_terminated = 1 } 242 if tail.op == OP_BR { v4_terminated = 1 } 243 if tail.op == OP_BR_COND { v4_terminated = 1 } 244 if tail.op == OP_TAIL_CALL { v4_terminated = 1 } 245 } 246 if v4_terminated == 0 { 247 iv_violation(f, pass_label) 248 iv_puts("V4 bb" as *u8); iv_putd(bi) 249 iv_puts(" has no terminator (falls through to wrong block)" as *u8) 250 iv_putc(0x0A) 251 violations = violations + 1 252 } 253 254 bi = bi + 1 255 } 256 257 return violations 258} 259 260// Validate every function in a Module. Returns total violations. 261func ir_validate_module(m: *Module, pass_label: *u8) -> i64 { 262 var total: i64 = 0 263 var i: i64 = 0 264 while i < m.n_functions { 265 let base: i64 = m.functions as i64 266 let f: *Function = (base + i * 176) as *Function 267 total = total + ir_validate_function(f, pass_label) 268 i = i + 1 269 } 270 return total 271}