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}