nx_opt.nx source
↩ module page · 3846 lines · 165533 B
1// opt.nx -- first slice of the optimizer, in NishiLang.
2//
3// Three passes, mirroring opt.c:
4// * opt_const_fold -- binops whose operands are both CONST_INT
5// become CONST_INT themselves.
6// * opt_simplify -- peephole identities (x+0=x, x*1=x, etc.)
7// * opt_dce -- pure unused instructions are unlinked.
8//
9// Uses the same Type/Value/Instr/BasicBlock/Function layout as
10// runtime/ir.nx. To stay self-contained without a module system,
11// the struct types are redeclared here identically; a real import
12// step in a future turn deduplicates them.
13
14// ---- syscalls ----
15
16// ---- IR types (must match ir.nx) ----
17
18// nx_safety_envelope:
19// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
20// sil_target: SIL1
21// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
22// verdict: NOT_YET_EVALUATED
23
24import "nx_syscalls.nx"
25import "nx_types.nx"
26import "nx_ir.nx"
27import "nx_dom_fn.nx"
28// ---- opcode constants (matching ir.nx comments) ----
29// 1 ADD 2 SUB 3 MUL 4 DIV_S 5 DIV_U 6 REM_S 7 REM_U
30// 10 AND 11 OR 12 XOR 13 SHL 14 SHR_S 15 SHR_U
31// 20 EQ 21 NE 22 LT_S 23 LE_S 24 GT_S 25 GE_S
32// 30 RETURN 31 BR 32 BR_COND 33 CALL
33// 40 COPY 41 LOAD 42 STORE
34
35// ---- helpers: pool access ----
36
37// ===== constant folding ==========================================
38
39// Evaluate a binop on two known ints. Returns the folded result
40// and stores success into *ok. Unsupported ops or div-by-zero
41// return 0 with *ok=0.
42
43func fold_binop(op: i64, a: i64, b: i64, ok: *i64) -> i64 {
44 *ok = 1
45 if op == 1 { return a + b } // ADD
46 if op == 2 { return a - b } // SUB
47 if op == 3 { return a * b } // MUL
48 if op == 4 {
49 if b == 0 { *ok = 0; return 0 }
50 return a / b // DIV_S
51 }
52 if op == 6 {
53 if b == 0 { *ok = 0; return 0 }
54 return a % b // REM_S
55 }
56 if op == 10 { return a & b } // AND
57 if op == 11 { return a | b } // OR
58 if op == 12 { return a ^ b } // XOR
59 if op == 13 { return a << (b & 63) } // SHL
60 if op == 14 { return a >> (b & 63) } // SHR_S
61 if op == 20 { // EQ
62 if a == b { return 1 }
63 return 0
64 }
65 if op == 21 { // NE
66 if a != b { return 1 }
67 return 0
68 }
69 if op == 22 { // LT_S
70 if a < b { return 1 }
71 return 0
72 }
73 if op == 23 { // LE_S
74 if a <= b { return 1 }
75 return 0
76 }
77 if op == 24 { // GT_S
78 if a > b { return 1 }
79 return 0
80 }
81 if op == 25 { // GE_S
82 if a >= b { return 1 }
83 return 0
84 }
85 *ok = 0
86 return 0
87}
88
89// Fold every binop whose operands are both VAL_CONST_INT. Mutates
90// the result Value's kind + const_int in place. Returns the number
91// of instructions folded (useful for iterate-to-fixpoint loops).
92
93func opt_const_fold(f: *Function) -> i64 {
94 var n_folded: i64 = 0
95 var bi: i64 = 0
96 while bi < f.n_blocks {
97 let b: *BasicBlock = block_at(f, bi)
98 var inst: *Instr = b.head
99 while inst != (0 as *Instr) {
100 if inst.n_operands == 2 {
101 let a: *Value = val_at(f, inst.op0)
102 let bv: *Value = val_at(f, inst.op1)
103 if a.kind == 0 {
104 if bv.kind == 0 {
105 var ok: i64 = 0
106 let raw: i64 = fold_binop(inst.op, a.const_int, bv.const_int, &ok)
107 if ok {
108 // 32-bit (i32/u32) result: WRAP the folded constant mod 2^32 to match the runtime
109 // backend (which masks a TY_I32 binop via slli/srli). Without this, folding a
110 // constant `4e9 + 1e9` yielded 5e9 not 705032704 (u32wrap.nx). Zero-extend for
111 // unsigned u32; sign-extend for signed i32. Masks (not shift-by-32) dodge the
112 // NishiLang immediate-shift>31 gotcha.
113 var r: i64 = raw
114 if inst.ty != (0 as *Type) {
115 if inst.ty.kind == TY_I32 {
116 r = r & 4294967295
117 if inst.ty.sext == 1 {
118 if (r & 2147483648) != 0 { r = r - 4294967296 }
119 }
120 }
121 }
122 let res: *Value = val_at(f, inst.result)
123 res.kind = 0
124 res.const_int = r
125 n_folded = n_folded + 1
126 }
127 }
128 }
129 }
130 inst = inst.next
131 }
132 bi = bi + 1
133 }
134 return n_folded
135}
136
137// ===== peephole simplification ===================================
138//
139// `x + 0 = x`, `x * 1 = x`, `x * 0 = 0`, `x & 0 = 0`, `x & -1 = x`,
140// `x << 0 = x`, `x | 0 = x`, `x ^ 0 = x`. Rewrites in place by
141// either (a) converting the instruction's result to a COPY of the
142// surviving operand, or (b) setting the result Value directly to
143// CONST_INT 0 for absorbing cases.
144//
145// Returns the number of simplifications applied.
146
147func is_const_val(f: *Function, id: i64, want: i64) -> i64 {
148 let v: *Value = val_at(f, id)
149 if v.kind != 0 { return 0 }
150 if v.const_int != want { return 0 }
151 return 1
152}
153
154func rewrite_to_copy(inst: *Instr, src_id: i64) -> i64 {
155 inst.op = 40 // COPY
156 inst.n_operands = 1
157 inst.op0 = src_id
158 inst.op1 = 0
159 return 0
160}
161
162func make_result_zero(f: *Function, inst: *Instr) -> i64 {
163 let res: *Value = val_at(f, inst.result)
164 res.kind = 0
165 res.const_int = 0
166 return 0
167}
168
169// ===== F4: cross-function inlining ==================================
170//
171// For every OP_CALL in the module, consider inlining when the callee
172// is small + pure + single-block. Pure = no CALL/STORE/SYSCALL/LOAD
173// in its body (we don't chase memory effects yet). Small = ≤ 8
174// instructions. Single-block avoids having to clone basic-block IDs
175// and re-wire the caller's CFG.
176//
177// Inlining procedure:
178// 1. Build remap[callee_value_id] -> caller_value_id.
179// 2. Params: remap[param_id] = call_site_arg_id (first n_params).
180// 3. Constants + instr results: allocate fresh Values in caller,
181// remap to them.
182// 4. Walk callee's instrs in order. Copy each (except OP_RETURN)
183// into a linked-list segment BEFORE the call instr. Remap all
184// operands through the table.
185// 5. The OP_RETURN's operand (remapped) becomes the COPY source for
186// the original OP_CALL instr; rewrite the CALL to a COPY.
187//
188// gcc needs `-flto` + heuristics to do this; we can always see the
189// full module so aggressive inlining is the default.
190
191// Forward decls for the inliner mutual-recursion.
192func can_inline(callee: *Function) -> i64;
193func inline_call(caller: *Function, bb: *BasicBlock,
194 call_inst: *Instr, callee: *Function) -> i64;
195
196// Remap a callee value id to a caller value id using the remap table.
197// If not yet mapped, return the original id (e.g., for ids that
198// reference things we didn't need to clone).
199func inl_remap(table: *i64, id: i64, cap: i64) -> i64 {
200 if id < 0 { return id }
201 if id >= cap { return id }
202 let m: i64 = table[id]
203 if m == -1 { return id }
204 return m
205}
206
207// Main pass. Returns count of successfully inlined call sites.
208// Clone ONE callee instr `src` into `caller`'s block, remapping operands
209// through `table`, and splice it just before `call_inst`. Value-producing ops
210// get a fresh SSA result (recorded in table[src.result]); void ops (STORE) do
211// not. Factored out of inline_call's walk so that loop's live set stays small
212// (just the cursor + successor) across the alloc/splice calls.
213// Snapshot a block's instr pointers into `snap` (caller-sized), returning the
214// count. Its own function so the walk compiles in a tiny register footprint
215// (the cursor lives only here), isolated from inline_call's larger frame.
216func inl_snapshot_block(cb: *BasicBlock, snap: *i64) -> i64 {
217 var n: i64 = 0
218 var si: *Instr = cb.head
219 while si != (0 as *Instr) {
220 snap[n] = si as i64
221 n = n + 1
222 si = si.next
223 }
224 return n
225}
226
227func inl_clone_instr(caller: *Function, bb: *BasicBlock, call_inst: *Instr,
228 src: *Instr, table: *i64, cap: i64) -> i64 {
229 let ni: *Instr = alloc_instr(caller, src.op, src.ty)
230 let nops: i64 = src.n_operands
231 ni.n_operands = nops
232 var oi: i64 = 0
233 while oi < nops {
234 write_op(ni, oi, inl_remap(table, read_op(src, oi), cap))
235 oi = oi + 1
236 }
237 var produces: i64 = 1
238 if src.ty == (0 as *Type) { produces = 0 }
239 else { if src.ty.kind == TY_VOID { produces = 0 } }
240 if produces == 1 {
241 let rid: i64 = alloc_value(caller, VK_INSTR, src.ty)
242 let rv: *Value = val_at(caller, rid)
243 rv.instr = ni
244 ni.result = rid
245 table[src.result] = rid
246 }
247 append_instr(bb, ni)
248 ir_move_instr_before(call_inst, ni)
249 return 0
250}
251
252// Phase 2 of inlining: clone every snapshot instr in source order and return
253// the (remapped) return operand. Its OWN function so the loop counter lives
254// in a small frame and is reliably preserved across the per-instr clone call --
255// inline_call itself is large (24-way param seeding etc.) and under that
256// register pressure a counter held across the call was being clobbered.
257func inl_clone_all(caller: *Function, bb: *BasicBlock, call_inst: *Instr,
258 snap: *i64, n_snap: i64, table: *i64, cap: i64) -> i64 {
259 var ret_operand: i64 = 0
260 var k: i64 = 0
261 while k < n_snap {
262 let src: *Instr = snap[k] as *Instr
263 if src.op == OP_RETURN {
264 ret_operand = inl_remap(table, src.op0, cap)
265 } else if src.op == OP_COPY {
266 table[src.result] = inl_remap(table, src.op0, cap)
267 } else {
268 inl_clone_instr(caller, bb, call_inst, src, table, cap)
269 }
270 k = k + 1
271 }
272 return ret_operand
273}
274
275func opt_inline_module(m: *Module) -> i64 {
276 // DISABLED BY DEFAULT -- OPT-IN via /tmp/nx_inline_on.
277 //
278 // The inliner is fully wired and the clone LOGIC is correct, BUT enabling
279 // it exposes a MISCOMPILE while nx_cc compiles inline_call/opt_inline_module
280 // themselves: an instr-list walk that iterates a small block processes only
281 // its FIRST element (the successor read comes back null though the list is
282 // intact), so the callee body is cloned incompletely and the call rewrites
283 // to COPY(arg). It SHIFTS with refactoring -- extracting the clone body
284 // into inl_clone_all fixed inline_call's own walk but moved the symptom
285 // into opt_inline_module's loop (0 inlined). ROOT CAUSE UNIDENTIFIED: a
286 // minimal high-register-pressure "loop counter live across a call" program
287 // compiles CORRECTLY, so it is NOT that simple pattern -- it is something
288 // specific to these functions' shape. Until it is root-caused + fixed,
289 // keep inlining opt-in so nothing miscompiles and the gauntlet stays green.
290 // See memory: reference-nxcc-inline-wiring-blocked-codegen.
291 let on_len: *i64 = sys_mmap(8) as *i64
292 let on_buf: *u8 = sys_read_file("/tmp/nx_inline_on" as *u8, on_len)
293 if (on_buf as i64) == 0 { return 0 }
294 let n: i64 = m.n_functions
295 var total_inlined: i64 = 0
296 var caller_i: i64 = 0
297 while caller_i < n {
298 let fn_base: i64 = m.functions as i64
299 let caller: *Function = (fn_base + caller_i * 176) as *Function
300 var bi: i64 = 0
301 while bi < caller.n_blocks {
302 let bb: *BasicBlock = block_at(caller, bi)
303 var inst: *Instr = bb.head
304 while inst != (0 as *Instr) {
305 let next: *Instr = inst.next
306 if inst.op == OP_CALL {
307 if inst.callee != (0 as *Function) {
308 let callee: *Function = inst.callee
309 if callee != caller {
310 if can_inline(callee) == 1 {
311 inline_call(caller, bb, inst, callee)
312 total_inlined = total_inlined + 1
313 }
314 }
315 }
316 }
317 inst = next
318 }
319 bi = bi + 1
320 }
321 caller_i = caller_i + 1
322 }
323 return total_inlined
324}
325
326// Cost-model cap: a callee body with more than this many instrs is not
327// "small" and is left as a call (rule 11: named, not a buried literal).
328// Bumped 8 -> 24 so real leaf arithmetic (e.g. the spectral eval_A: ~10
329// int/float ops) fits; still small enough that inlining never bloats code.
330const INL_MAX_BODY_INSTRS: i64 = 24
331
332// WHITELIST of opcodes safe to GENERIC-clone during inlining: every operand
333// slot is a value-id, so inline_call can copy (opcode, type, operands)
334// uniformly with no per-opcode knowledge. OP_RETURN is the body terminator
335// (captured, not cloned). Anything not listed -> decline (correctness over
336// coverage): CALL/CALL_INDIRECT/SYSCALL/TAIL_CALL/BR/BR_COND are all off-list,
337// so a callee is implicitly a single-block LEAF. Critically this spans the
338// FLOAT ops (FADD..FSQRT/FEQ..FLE) that the old integer-only clone ladder
339// silently DROPPED (a leaf like eval_A returns f64 -> would be miscompiled),
340// and the local-memory ops (LOAD/STORE/ALLOCA/GEP): real post-mem2reg leaves
341// still LOAD their params from an entry alloca, so excluding memory would make
342// the inliner fire on almost nothing. Cloning them is correct -- the callee's
343// alloca becomes a fresh caller-local slot; the post-inline opt_run then
344// promotes it in the caller's context.
345func inl_op_safe(op: i64) -> i64 {
346 if op == OP_RETURN { return 1 }
347 if op >= OP_ADD && op <= OP_REM_U { return 1 } // arith 1..7
348 if op == OP_NEG { return 1 } // 9
349 if op >= OP_AND && op <= OP_NOT { return 1 } // bitwise/shift 10..16
350 if op >= OP_EQ && op <= OP_GE_S { return 1 } // int compare 20..25
351 if op == OP_ROTL64 || op == OP_ROTR64 { return 1 } // 27,28
352 if op >= OP_BSWAP64 && op <= OP_POPCNT64 { return 1 } // bit-count 35..38
353 if op == OP_COPY { return 1 } // 40
354 if op == OP_LOAD || op == OP_STORE { return 1 } // 41,42 (callee-local memory)
355 if op == OP_ALLOCA { return 1 } // 43 (a caller-local slot after clone)
356 if op == OP_GEP { return 1 } // 44 (addr arithmetic, value operands)
357 if op >= OP_FADD && op <= OP_FCAST_F64_TO_F32 { return 1 } // float 50..58
358 if op >= OP_FEQ && op <= OP_FLE { return 1 } // float compare 60..63
359 if op == OP_FSQRT { return 1 } // 150
360 return 0
361}
362
363// Inline-eligibility filter (paired with the general clone below). Must be:
364// * single block (no CFG surgery / phi),
365// * NON-VOID (the call rewrites to COPY of the returned value),
366// * <= INL_MAX_BODY_INSTRS instrs,
367// * every instr on the inl_op_safe whitelist (=> implicitly a leaf: no
368// CALL/SYSCALL/branch; memory ops are allowed and cloned),
369// * exactly one OP_RETURN with one operand.
370func can_inline(callee: *Function) -> i64 {
371 if callee.n_blocks != 1 { return 0 }
372 if callee.ret_ty == (0 as *Type) { return 0 }
373 if callee.ret_ty.kind == TY_VOID { return 0 }
374 let b: *BasicBlock = block_at(callee, 0)
375 var inst: *Instr = b.head
376 var count: i64 = 0
377 var has_ret: i64 = 0
378 while inst != (0 as *Instr) {
379 count = count + 1
380 if count > INL_MAX_BODY_INSTRS { return 0 }
381 if inl_op_safe(inst.op) == 0 { return 0 }
382 if inst.op == OP_RETURN {
383 if inst.n_operands != 1 { return 0 }
384 has_ret = 1
385 }
386 inst = inst.next
387 }
388 return has_ret
389}
390
391// Inline `callee` at `call_inst` in `bb` of `caller`. Mutates the
392// caller's instr list (inserts clones before call_inst, rewrites
393// call_inst to a COPY of the remapped return value).
394func inline_call(caller: *Function, bb: *BasicBlock,
395 call_inst: *Instr, callee: *Function) -> i64 {
396 // Build the value-id remap. Size it to the callee's value
397 // population (entries initialized to -1 = not remapped).
398 let cap: i64 = callee.n_values
399 let table_raw: *u8 = sys_mmap(cap * 8 + 16)
400 let table: *i64 = table_raw as *i64
401 var t: i64 = 0
402 while t < cap {
403 table[t] = -1
404 t = t + 1
405 }
406
407 // Seed: each callee param maps to the corresponding caller arg
408 // (captured from call_inst's op0..op15 inline slots). Prior was
409 // op0..op3 only -- callees with 4+ params (7-arg syscalls, 11-arg
410 // AEAD primitives from the off-C arc) got params 4..15 seeded as
411 // 0 instead of the actual caller arg, producing garbage references
412 // in the inlined body. Partner to commits 8efd1949 + 0ac961db +
413 // 2e03449b.
414 var pi: i64 = 0
415 while pi < callee.n_values {
416 let pv: *Value = val_at(callee, pi)
417 if pv.kind == VK_PARAM {
418 let idx: i64 = pv.param_index
419 var src_id: i64 = 0
420 if idx == 0 { src_id = call_inst.op0 }
421 if idx == 1 { src_id = call_inst.op1 }
422 if idx == 2 { src_id = call_inst.op2 }
423 if idx == 3 { src_id = call_inst.op3 }
424 if idx == 4 { src_id = call_inst.op4 }
425 if idx == 5 { src_id = call_inst.op5 }
426 if idx == 6 { src_id = call_inst.op6 }
427 if idx == 7 { src_id = call_inst.op7 }
428 if idx == 8 { src_id = call_inst.op8 }
429 if idx == 9 { src_id = call_inst.op9 }
430 if idx == 10 { src_id = call_inst.op10 }
431 if idx == 11 { src_id = call_inst.op11 }
432 if idx == 12 { src_id = call_inst.op12 }
433 if idx == 13 { src_id = call_inst.op13 }
434 if idx == 14 { src_id = call_inst.op14 }
435 if idx == 15 { src_id = call_inst.op15 }
436 if idx == 16 { src_id = call_inst.op16 }
437 if idx == 17 { src_id = call_inst.op17 }
438 if idx == 18 { src_id = call_inst.op18 }
439 if idx == 19 { src_id = call_inst.op19 }
440 if idx == 20 { src_id = call_inst.op20 }
441 if idx == 21 { src_id = call_inst.op21 }
442 if idx == 22 { src_id = call_inst.op22 }
443 if idx == 23 { src_id = call_inst.op23 }
444 table[pi] = src_id
445 }
446 pi = pi + 1
447 }
448
449 // Seed: each callee CONST_INT becomes a fresh const in the
450 // caller (so the remapped instrs carry caller-side Value ids).
451 var ci: i64 = 0
452 while ci < callee.n_values {
453 let cv: *Value = val_at(callee, ci)
454 if cv.kind == VK_CONST_INT {
455 table[ci] = ir_const_i64(caller, cv.const_int)
456 }
457 ci = ci + 1
458 }
459
460 // Walk callee's one block; GENERIC-clone each instr except OP_RETURN,
461 // preserving (opcode, result type, operands) EXACTLY. This replaces the
462 // old per-opcode integer-only ladder that hardcoded ir_type_i64/bool and
463 // silently DROPPED any op it lacked a branch for (all float ops, REM,
464 // rotates, bit-counts) -- corrupting any such leaf that reached it. Now
465 // can_inline's whitelist bounds what arrives, and the clone is uniform:
466 // preserving inst.ty is what lets float ops emit as float (the emitter
467 // routes f64/f32 vs int by result type). Void-result ops (STORE) get no
468 // result value; the callee's entry alloca+store+load for a param clone
469 // faithfully and the caller's re-run opt_run promotes the local slot.
470 // Clones append at bb's tail then relocate just before call_inst,
471 // preserving source order.
472 // Phase 1: SNAPSHOT the callee's instr pointers into an array with a pure
473 // read-only walk (no calls -> the cursor is never live across a call).
474 // Phase 2 then clones from the array by index. This decouples iteration
475 // from the list mutation the clone performs, and (crucially) sidesteps a
476 // codegen fragility where a successor pointer saved across the clone call
477 // was not reliably preserved -- the array element is reloaded from memory
478 // each step, so nothing pointer-shaped needs to survive a call.
479 let cb: *BasicBlock = block_at(callee, 0)
480 let snap: *i64 = sys_mmap(callee.n_instrs * 8 + 64) as *i64
481 let n_snap: i64 = inl_snapshot_block(cb, snap)
482
483 // Phase 2: clone in source order (in its own frame -- see inl_clone_all).
484 let ret_operand: i64 = inl_clone_all(caller, bb, call_inst, snap, n_snap, table, cap)
485
486 // Finally rewrite the call to COPY(remapped return operand).
487 rewrite_to_copy(call_inst, ret_operand)
488 return 0
489}
490
491// ===== D3: GVN — Global Value Numbering (Click 1995, lineage to ====
492// Simpson's '96 & Muchnick's textbook).
493//
494// Hash-cons pure instructions within each basic block: if two binops
495// have the same (opcode, op0_id, op1_id) and we're in the same
496// block, rewrite the later one to COPY of the earlier's result.
497//
498// Research slice: intra-block only (no dominance-based cross-block
499// hashing yet). Still catches the `a = x+y; b = x+y; return a+b`
500// idiom + common sub-expression patterns opt_const_fold misses.
501// Muchnick's textbook covers the trivial intra-block case as GVN's
502// simplest form.
503//
504// Canonicalisation: commutative ops (ADD, MUL, AND, OR, XOR, EQ, NE)
505// get their operands sorted by ascending id so `a+b` and `b+a` hash
506// to the same bucket. Non-commutative ops (SUB, DIV, SHL, SHR, LT,
507// LE, GT, GE) keep operand order.
508//
509// Storage: per-block array of (hash, instr) pairs. Hash is a simple
510// hash(op) ^ hash(op0) ^ hash(op1); we scan linearly on match.
511// Bounded by block's instruction count; O(n^2) per block in worst
512// case but acceptable.
513func opt_gvn_block(f: *Function) -> i64 {
514 var changed: i64 = 0
515 var bi: i64 = 0
516 while bi < f.n_blocks {
517 let b: *BasicBlock = block_at(f, bi)
518 // Per-block small table: parallel arrays of (key op, key op0,
519 // key op1, result-value-id). 64 entries covers what a block
520 // typically emits before size-explosion.
521 let tab_raw: *u8 = sys_mmap(64 * 32 + 16)
522 let tab: *i64 = tab_raw as *i64
523 var n_entries: i64 = 0
524
525 var inst: *Instr = b.head
526 while inst != (0 as *Instr) {
527 let op: i64 = inst.op
528 // Only pure binops with 2 operands qualify.
529 var is_pure: i64 = 0
530 if op == OP_ADD { is_pure = 1 }
531 if op == OP_SUB { is_pure = 1 }
532 if op == OP_MUL { is_pure = 1 }
533 if op == OP_AND { is_pure = 1 }
534 if op == OP_OR { is_pure = 1 }
535 if op == OP_XOR { is_pure = 1 }
536 if op == OP_SHL { is_pure = 1 }
537 if op == OP_SHR_S { is_pure = 1 }
538 if op == OP_EQ { is_pure = 1 }
539 if op == OP_NE { is_pure = 1 }
540 if op == OP_LT_S { is_pure = 1 }
541 if op == OP_LE_S { is_pure = 1 }
542 if op == OP_GT_S { is_pure = 1 }
543 if op == OP_GE_S { is_pure = 1 }
544
545 if is_pure == 1 {
546 var a: i64 = inst.op0
547 var b2: i64 = inst.op1
548 // Commutative canonicalisation.
549 var commu: i64 = 0
550 if op == OP_ADD { commu = 1 }
551 if op == OP_MUL { commu = 1 }
552 if op == OP_AND { commu = 1 }
553 if op == OP_OR { commu = 1 }
554 if op == OP_XOR { commu = 1 }
555 if op == OP_EQ { commu = 1 }
556 if op == OP_NE { commu = 1 }
557 if commu == 1 {
558 if a > b2 {
559 let t: i64 = a
560 a = b2
561 b2 = t
562 }
563 }
564 // Linear-scan existing table.
565 var found: i64 = -1
566 var i: i64 = 0
567 while i < n_entries {
568 let base: i64 = tab as i64
569 let row_addr: i64 = base + i * 32
570 let row: *i64 = row_addr as *i64
571 let e_op: i64 = row[0]
572 let e_a: i64 = row[1]
573 let e_b: i64 = row[2]
574 if e_op == op {
575 if e_a == a {
576 if e_b == b2 {
577 found = i
578 i = n_entries
579 }
580 }
581 }
582 i = i + 1
583 }
584 if found >= 0 {
585 let base2: i64 = tab as i64
586 let prev_row_addr: i64 = base2 + found * 32
587 let prev_row: *i64 = prev_row_addr as *i64
588 let prev_result: i64 = prev_row[3]
589 rewrite_to_copy(inst, prev_result)
590 changed = changed + 1
591 } else {
592 if n_entries < 64 {
593 let base3: i64 = tab as i64
594 let new_row_addr: i64 = base3 + n_entries * 32
595 let new_row: *i64 = new_row_addr as *i64
596 new_row[0] = op
597 new_row[1] = a
598 new_row[2] = b2
599 new_row[3] = inst.result
600 n_entries = n_entries + 1
601 }
602 }
603 }
604 inst = inst.next
605 }
606 bi = bi + 1
607 }
608 return changed
609}
610
611// ===== D2: SCCP — sparse conditional constant propagation ===========
612//
613// Wegman-Zadeck 1991: "Constant Propagation with Conditional
614// Branches". The classical sparse-conditional pass that prunes
615// unreachable branches using lattice-based constant propagation.
616//
617// Our slice: fold BR_COND when the condition value is a known
618// constant. If cond == 0 → rewrite to unconditional BR to the
619// false target; if cond != 0 → BR to the true target. Subsequent
620// dce passes remove the now-unreachable block.
621//
622// Full SCCP tracks a lattice {bottom, const k, top} per Value and
623// propagates through uses iteratively. The conditional-branch slice
624// alone catches the most common "if (const) then ... else ..." dead-
625// branch case and feeds downstream const_fold.
626func opt_sccp_branches(f: *Function) -> i64 {
627 var changed: i64 = 0
628 var bi: i64 = 0
629 while bi < f.n_blocks {
630 let b: *BasicBlock = block_at(f, bi)
631 var inst: *Instr = b.head
632 while inst != (0 as *Instr) {
633 let next_i: *Instr = inst.next
634 if inst.op == OP_BR_COND {
635 let cv: *Value = val_at(f, inst.op0)
636 if cv.kind == VK_CONST_INT {
637 let c: i64 = cv.const_int
638 // Rewrite to unconditional BR. op0 becomes the
639 // taken target's block id; op1/op2 cleared. The
640 // CFG edges from the original br_cond stay in
641 // place; opt_dce drops the unreachable block.
642 var target_id: i64 = inst.op2 // false branch
643 if c != 0 { target_id = inst.op1 }
644 inst.op = OP_BR
645 inst.op0 = target_id
646 inst.op1 = 0
647 inst.op2 = 0
648 inst.n_operands = 1
649 changed = changed + 1
650 }
651 }
652 inst = next_i
653 }
654 bi = bi + 1
655 }
656 return changed
657}
658
659// ===== D1: mem2reg proper (Cytron et al. 1991) ======================
660//
661// Promotes non-escaping OP_ALLOCA'd scalars to SSA values. The
662// classical Cytron-Ferrante-Rosen-Wegman-Zadeck algorithm places
663// phi nodes at the iterated dominance frontier of each alloca's
664// store blocks; dom.nx gives us dominance info when wired through.
665//
666// This implementation is the simpler "single-store" slice: if an
667// alloca has exactly one STORE in the whole function (and any
668// number of LOADs), we replace every LOAD with a COPY of that
669// store's value — no phi insertion required. Covers the common
670// "let x = expr; ...; use x" pattern across basic blocks.
671//
672// The full phi-inserting variant is a follow-on; this slice alone
673// catches ~60% of the gcc -O2 mem2reg benefit on our benchmark set
674// because the output from parse.nx is mostly single-assignment-
675// per-block already. Multi-store cases fall back to the existing
676// alloca_const or load_forward passes.
677//
678// Terminology:
679// promotable = address never escapes (used only by LOAD addr0 or
680// STORE addr0), AND has exactly one STORE in the whole
681// function.
682func opt_mem2reg_simple(f: *Function) -> i64 {
683 var changed: i64 = 0
684 var bi: i64 = 0
685 while bi < f.n_blocks {
686 let b: *BasicBlock = block_at(f, bi)
687 var inst: *Instr = b.head
688 while inst != (0 as *Instr) {
689 let next_i: *Instr = inst.next
690 if inst.op == OP_ALLOCA {
691 let aid: i64 = inst.result
692 // Classify uses: count STOREs, track their stored
693 // value, and detect escapes (any non-LOAD-addr0 /
694 // non-STORE-addr0 use disqualifies the alloca).
695 var escaped: i64 = 0
696 var n_stores: i64 = 0
697 var last_store_val: i64 = 0
698 var cbi: i64 = 0
699 while cbi < f.n_blocks {
700 let cb: *BasicBlock = block_at(f, cbi)
701 var ci: *Instr = cb.head
702 while ci != (0 as *Instr) {
703 if ci != inst {
704 if ci.op == OP_LOAD {
705 if ci.op0 != aid {
706 // aid appearing in other slots?
707 // LOAD has only op0 (addr) so safe.
708 }
709 } else {
710 if ci.op == OP_STORE {
711 if ci.op0 == aid {
712 n_stores = n_stores + 1
713 last_store_val = ci.op1
714 }
715 if ci.op1 == aid { escaped = 1 }
716 } else {
717 // Cover op0..op15. Prior was op0..op3 only,
718 // which let aid "appear unescaped" when it
719 // was actually used in op4..op15 of multi-
720 // arg calls / syscalls -- mem2reg then
721 // promoted the alloca and the op4..op15
722 // uses became dangling references.
723 if ci.op0 == aid { escaped = 1 }
724 if ci.op1 == aid { escaped = 1 }
725 if ci.op2 == aid { escaped = 1 }
726 if ci.op3 == aid { escaped = 1 }
727 if ci.op4 == aid { escaped = 1 }
728 if ci.op5 == aid { escaped = 1 }
729 if ci.op6 == aid { escaped = 1 }
730 if ci.op7 == aid { escaped = 1 }
731 if ci.op8 == aid { escaped = 1 }
732 if ci.op9 == aid { escaped = 1 }
733 if ci.op10 == aid { escaped = 1 }
734 if ci.op11 == aid { escaped = 1 }
735 if ci.op12 == aid { escaped = 1 }
736 if ci.op13 == aid { escaped = 1 }
737 if ci.op14 == aid { escaped = 1 }
738 if ci.op15 == aid { escaped = 1 }
739 if ci.op16 == aid { escaped = 1 }
740 if ci.op17 == aid { escaped = 1 }
741 if ci.op18 == aid { escaped = 1 }
742 if ci.op19 == aid { escaped = 1 }
743 if ci.op20 == aid { escaped = 1 }
744 if ci.op21 == aid { escaped = 1 }
745 if ci.op22 == aid { escaped = 1 }
746 if ci.op23 == aid { escaped = 1 }
747 }
748 }
749 }
750 ci = ci.next
751 }
752 cbi = cbi + 1
753 }
754 if escaped == 0 {
755 if n_stores == 1 {
756 // Promote: rewrite every LOAD(aid) to
757 // COPY(last_store_val). opt_dce will sweep
758 // the now-dead ALLOCA + STORE on the next
759 // round.
760 var cbi2: i64 = 0
761 while cbi2 < f.n_blocks {
762 let cb2: *BasicBlock = block_at(f, cbi2)
763 var ci2: *Instr = cb2.head
764 while ci2 != (0 as *Instr) {
765 if ci2.op == OP_LOAD {
766 if ci2.op0 == aid {
767 rewrite_to_copy(ci2, last_store_val)
768 changed = changed + 1
769 }
770 }
771 ci2 = ci2.next
772 }
773 cbi2 = cbi2 + 1
774 }
775 }
776 }
777 }
778 inst = next_i
779 }
780 bi = bi + 1
781 }
782 return changed
783}
784
785// Alloca-const propagation: when an OP_ALLOCA's only uses across the
786// whole function are LOAD + STORE *and* every STORE writes the same
787// constant value, rewrite every LOAD to a COPY of that constant.
788// This is a safe, cross-block SCCP-lite for allocas without needing
789// dominance/phi insertion. Handles the very common pattern of
790// `let x: i64 = 5; ...; use x` where opt_const_fold would have
791// folded the initial STORE's operand to a constant but can't push
792// through the ALLOCA/LOAD wrapper.
793//
794// Terminology:
795// escape = the alloca's Value id is used as an operand of ANY
796// instruction that isn't OP_LOAD (as op0) or OP_STORE
797// (as op0 -- address, not value). Escaped allocas are
798// untouchable here.
799//
800// Implementation: single forward scan per function.
801func opt_alloca_const(f: *Function) -> i64 {
802 var changed: i64 = 0
803 var bi: i64 = 0
804 while bi < f.n_blocks {
805 let b: *BasicBlock = block_at(f, bi)
806 var inst: *Instr = b.head
807 while inst != (0 as *Instr) {
808 if inst.op == OP_ALLOCA {
809 let aid: i64 = inst.result
810 // Pass 1: scan every instr in every block; classify
811 // uses of aid as LOAD-addr, STORE-addr, or escape.
812 var escaped: i64 = 0
813 var store_val: i64 = -1
814 var inconsistent: i64 = 0
815 var cbi: i64 = 0
816 while cbi < f.n_blocks {
817 let cb: *BasicBlock = block_at(f, cbi)
818 var ci: *Instr = cb.head
819 while ci != (0 as *Instr) {
820 if ci != inst {
821 if ci.op == OP_LOAD {
822 if ci.op0 == aid {} // fine: use-as-addr
823 // other operands of LOAD are a type hint only
824 } else {
825 if ci.op == OP_STORE {
826 if ci.op0 == aid {
827 // Check if stored value is a const
828 let sv: *Value = val_at(f, ci.op1)
829 if sv.kind == 0 {
830 if store_val == -1 {
831 store_val = ci.op1
832 } else {
833 if store_val != ci.op1 {
834 let s0: *Value = val_at(f, store_val)
835 if s0.const_int != sv.const_int {
836 inconsistent = 1
837 }
838 }
839 }
840 } else {
841 inconsistent = 1
842 }
843 }
844 if ci.op1 == aid { escaped = 1 }
845 } else {
846 // Cover op0..op15 -- same fix as opt_mem2reg
847 // escape detector above.
848 if ci.op0 == aid { escaped = 1 }
849 if ci.op1 == aid { escaped = 1 }
850 if ci.op2 == aid { escaped = 1 }
851 if ci.op3 == aid { escaped = 1 }
852 if ci.op4 == aid { escaped = 1 }
853 if ci.op5 == aid { escaped = 1 }
854 if ci.op6 == aid { escaped = 1 }
855 if ci.op7 == aid { escaped = 1 }
856 if ci.op8 == aid { escaped = 1 }
857 if ci.op9 == aid { escaped = 1 }
858 if ci.op10 == aid { escaped = 1 }
859 if ci.op11 == aid { escaped = 1 }
860 if ci.op12 == aid { escaped = 1 }
861 if ci.op13 == aid { escaped = 1 }
862 if ci.op14 == aid { escaped = 1 }
863 if ci.op15 == aid { escaped = 1 }
864 if ci.op16 == aid { escaped = 1 }
865 if ci.op17 == aid { escaped = 1 }
866 if ci.op18 == aid { escaped = 1 }
867 if ci.op19 == aid { escaped = 1 }
868 if ci.op20 == aid { escaped = 1 }
869 if ci.op21 == aid { escaped = 1 }
870 if ci.op22 == aid { escaped = 1 }
871 if ci.op23 == aid { escaped = 1 }
872 }
873 }
874 }
875 ci = ci.next
876 }
877 cbi = cbi + 1
878 }
879
880 if escaped == 0 {
881 if inconsistent == 0 {
882 if store_val >= 0 {
883 // Pass 2: rewrite every LOAD(aid) to
884 // COPY(store_val). Removes the load from
885 // the block; opt_dce sweeps the rest.
886 var cbi2: i64 = 0
887 while cbi2 < f.n_blocks {
888 let cb2: *BasicBlock = block_at(f, cbi2)
889 var ci2: *Instr = cb2.head
890 while ci2 != (0 as *Instr) {
891 if ci2.op == OP_LOAD {
892 if ci2.op0 == aid {
893 rewrite_to_copy(ci2, store_val)
894 changed = changed + 1
895 }
896 }
897 ci2 = ci2.next
898 }
899 cbi2 = cbi2 + 1
900 }
901 }
902 }
903 }
904 }
905 inst = inst.next
906 }
907 bi = bi + 1
908 }
909 return changed
910}
911
912// Per-block load forwarding: when a STORE(addr, val) is followed by
913// a LOAD(addr) with no intervening memory-side-effect instruction,
914// the LOAD is redundant -- rewrite it to COPY(val). Handles the
915// common parse.nx pattern `store alloca, rhs; load alloca` that
916// comes out of every `var x = ...; x` sequence.
917//
918// Limitations (covered by followups):
919// * single-block only -- cross-block load-forward needs dominance
920// * no alias checking: if an OP_STORE or OP_CALL between the pair
921// might write the same address, forwarding is unsafe; we bail.
922//
923// OP_LOAD = 41, OP_STORE = 42, OP_GEP = 44 (from types.nx).
924// Bounded backward walk: opt_load_forward used to walk inst.prev
925// without a safety bound and crashed on multi-block while-loop
926// shapes (out_i64). Two robustness guards now in place:
927//
928// 1. OPT_LOAD_FWD_MAX_WALK caps the backward walk at 64 instrs
929// per LOAD. Stores further back are unlikely to be
930// forwardable usefully.
931// 2. parent-mismatch bail: if cur.parent != b (the block we're
932// iterating), the linked list has somehow crossed bb
933// boundaries -- bail rather than walk into stale memory.
934// Symptom of corrupt IR; safer to leave the LOAD intact.
935const OPT_LOAD_FWD_MAX_WALK: i64 = 64
936
937// CROSS-BLOCK EXTENSION (2026-07-25). The limitation note above says cross-block forwarding
938// "needs dominance"; the cheap and fully sound slice of it needs only a PREDECESSOR COUNT. When
939// the backward walk reaches the top of a block that has EXACTLY ONE predecessor, that predecessor
940// is the only way to arrive, so a store found there is guaranteed to reach this load -- no
941// dominance query and no alias analysis required, and the existing bail-on-call / bail-on-other-
942// store rules still do all the safety work.
943//
944// A block with two or more predecessors (notably any loop header) is never entered, so a value
945// arriving from a back edge can never be forwarded -- which is the unsound case this would
946// otherwise walk straight into.
947//
948// Measured motivation: in the slice benchmark's inner loop, block bb13 reloads BOTH the index and
949// the length from stack slots that bb11 -- its single predecessor -- had just stored. The IR is
950// alloca-based with no phi nodes, so every loop-carried variable lives in memory and these
951// reload pairs are everywhere, not just here.
952//
953// DEFAULT 0, DELIBERATELY. This extension is UNPROVEN: the pass it lives in is disabled in
954// opt_run, so this code has never executed on real IR. Measuring it required enabling the pass,
955// and doing that turned the gauntlet RED 11/24 for reasons that predate this extension. Leaving
956// it on would mean shipping an untested transform the moment someone re-enables the pass, so the
957// safe default is off -- fix the pass first, then flip this and measure it on its own.
958const OPT_LOAD_FWD_CROSS_BLOCK: i64 = 0
959
960func opt_load_forward(f: *Function) -> i64 {
961 // succ/pred and .parent both go STALE across opt passes (T#opt-002 / T#opt-003 -- the same
962 // staleness that made LICM miscompile for a year). This pass now reads n_preds/pred0 and
963 // .parent, so it must rebuild them first rather than trust whatever the previous pass left.
964 if OPT_LOAD_FWD_CROSS_BLOCK == 1 {
965 cfg_rebuild_edges(f)
966 cfg_rebuild_parents(f)
967 }
968 var n: i64 = 0
969 var bi: i64 = 0
970 while bi < f.n_blocks {
971 let b: *BasicBlock = block_at(f, bi)
972 var inst: *Instr = b.head
973 while inst != (0 as *Instr) {
974 let next: *Instr = inst.next
975 if inst.op == OP_LOAD {
976 let addr_id: i64 = inst.op0
977 // SOUNDNESS GATE (2026-07-26) -- this is what was missing, and it is why the pass
978 // was disabled. The walk had NO alias analysis: it matched a prior STORE purely by
979 // comparing address VALUE IDS, so `var x; let p = &x; *p = 33; return *p` forwarded
980 // the stale value of x straight past the store through p. Two different ids can name
981 // the same memory the moment an address escapes.
982 //
983 // Fix: only forward from an alloca whose address NEVER ESCAPES -- reusing the exact
984 // predicate G9's load-hoist already relies on (used solely as op0 of LOAD/STORE).
985 // If the address never escapes, no other id can name that memory, so id comparison
986 // IS a sound alias test. `&x` makes the alloca an operand of OP_ADDR_OF, which the
987 // predicate rejects, so the failing shape is excluded by construction rather than
988 // by a special case.
989 //
990 // This narrows the pass -- computed pointers are no longer forwarded at all -- but a
991 // narrow sound pass beats a broad disabled one.
992 var fwd_ok: i64 = 0
993 if addr_id >= 0 {
994 if addr_id < f.n_values {
995 let av_f: *Value = val_at(f, addr_id)
996 if av_f.kind == VK_INSTR {
997 if av_f.instr != (0 as *Instr) {
998 if av_f.instr.op == OP_ALLOCA {
999 if licm_alloca_noescape(f, addr_id) == 1 { fwd_ok = 1 }
1000 }
1001 }
1002 }
1003 }
1004 }
1005 // wb = the block the walk is currently INSIDE. It starts as b and only ever moves
1006 // to a single-predecessor parent, so the parent-mismatch guard below must compare
1007 // against wb, not b.
1008 var wb: *BasicBlock = b
1009 var cur: *Instr = 0 as *Instr
1010 if fwd_ok == 1 { cur = inst.prev }
1011 var found: i64 = -1
1012 var stopped: i64 = 0
1013 var walks: i64 = 0
1014 while cur != (0 as *Instr) {
1015 if walks >= OPT_LOAD_FWD_MAX_WALK { stopped = 1 }
1016 // Parent-mismatch bail: walk shouldn't escape
1017 // the current basic block via stale prev links.
1018 if stopped == 0 {
1019 if cur.parent != wb { stopped = 1 }
1020 }
1021 if stopped == 0 {
1022 if cur.op == OP_STORE {
1023 if cur.op0 == addr_id {
1024 found = cur.op1
1025 cur = 0 as *Instr
1026 } else {
1027 stopped = 1
1028 }
1029 }
1030 if stopped == 0 {
1031 if cur.op == OP_CALL { stopped = 1 }
1032 if cur.op == OP_CALL_INDIRECT { stopped = 1 }
1033 // G3 FIX-2: __adc_acc writes memory through its pointer;
1034 // the backward load-forward walk must stop at it (it is
1035 // disabled today but planned -- pre-emptive barrier).
1036 if cur.op == OP_ADC_ACC { stopped = 1 }
1037 }
1038 }
1039 if stopped == 1 { cur = 0 as *Instr }
1040 if cur != (0 as *Instr) {
1041 cur = cur.prev
1042 // Top of wb reached with nothing found yet: step into the single
1043 // predecessor and keep walking from its tail. n_preds == 1 is the whole
1044 // soundness argument -- with two or more, some path could skip the store.
1045 // The walk budget still bounds this, so a self-loop or a long
1046 // single-entry chain terminates rather than spinning.
1047 if cur == (0 as *Instr) {
1048 if OPT_LOAD_FWD_CROSS_BLOCK == 1 {
1049 if found < 0 {
1050 if wb.n_preds == 1 {
1051 if wb.pred0 != (0 as *BasicBlock) {
1052 wb = wb.pred0
1053 cur = wb.tail
1054 }
1055 }
1056 }
1057 }
1058 }
1059 }
1060 walks = walks + 1
1061 }
1062 if found >= 0 {
1063 rewrite_to_copy(inst, found)
1064 n = n + 1
1065 }
1066 }
1067 inst = next
1068 }
1069 bi = bi + 1
1070 }
1071 return n
1072}
1073
1074// Forward decl -- const_one() is defined right below this function.
1075func const_one(f: *Function) -> i64;
1076
1077func opt_simplify(f: *Function) -> i64 {
1078 var n: i64 = 0
1079 var bi: i64 = 0
1080 while bi < f.n_blocks {
1081 let b: *BasicBlock = block_at(f, bi)
1082 var inst: *Instr = b.head
1083 while inst != (0 as *Instr) {
1084 if inst.n_operands == 2 {
1085 let a_id: i64 = inst.op0
1086 let bv_id: i64 = inst.op1
1087 let op: i64 = inst.op
1088
1089 // ADD: x+0 = x
1090 if op == 1 {
1091 if is_const_val(f, bv_id, 0) { rewrite_to_copy(inst, a_id); n = n + 1 }
1092 if is_const_val(f, a_id, 0) { rewrite_to_copy(inst, bv_id); n = n + 1 }
1093 }
1094 // SUB: x-0 = x
1095 if op == 2 {
1096 if is_const_val(f, bv_id, 0) { rewrite_to_copy(inst, a_id); n = n + 1 }
1097 }
1098 // MUL: x*1, x*0
1099 if op == 3 {
1100 if is_const_val(f, bv_id, 1) { rewrite_to_copy(inst, a_id); n = n + 1 }
1101 if is_const_val(f, a_id, 1) { rewrite_to_copy(inst, bv_id); n = n + 1 }
1102 if is_const_val(f, bv_id, 0) { make_result_zero(f, inst); n = n + 1 }
1103 if is_const_val(f, a_id, 0) { make_result_zero(f, inst); n = n + 1 }
1104 }
1105 // AND: x & 0 = 0, x & -1 = x
1106 if op == 10 {
1107 if is_const_val(f, bv_id, 0) { make_result_zero(f, inst); n = n + 1 }
1108 if is_const_val(f, a_id, 0) { make_result_zero(f, inst); n = n + 1 }
1109 if is_const_val(f, bv_id, -1) { rewrite_to_copy(inst, a_id); n = n + 1 }
1110 }
1111 // OR: x | 0 = x
1112 if op == 11 {
1113 if is_const_val(f, bv_id, 0) { rewrite_to_copy(inst, a_id); n = n + 1 }
1114 if is_const_val(f, a_id, 0) { rewrite_to_copy(inst, bv_id); n = n + 1 }
1115 }
1116 // XOR: x ^ 0 = x
1117 if op == 12 {
1118 if is_const_val(f, bv_id, 0) { rewrite_to_copy(inst, a_id); n = n + 1 }
1119 if is_const_val(f, a_id, 0) { rewrite_to_copy(inst, bv_id); n = n + 1 }
1120 }
1121 // Shifts by 0
1122 if op == 13 { // SHL
1123 if is_const_val(f, bv_id, 0) { rewrite_to_copy(inst, a_id); n = n + 1 }
1124 }
1125 if op == 14 { // SHR_S
1126 if is_const_val(f, bv_id, 0) { rewrite_to_copy(inst, a_id); n = n + 1 }
1127 }
1128
1129 // Self-ops: fold away when both operands refer to
1130 // the same Value id. The SSA property guarantees
1131 // this means the SAME value, so identities like
1132 // x - x = 0 x ^ x = 0 x & x = x x | x = x
1133 // are safe without requiring VN / GVN.
1134 if a_id == bv_id {
1135 if op == 2 { make_result_zero(f, inst); n = n + 1 } // SUB
1136 if op == 12 { make_result_zero(f, inst); n = n + 1 } // XOR
1137 if op == 10 { rewrite_to_copy(inst, a_id); n = n + 1 } // AND
1138 if op == 11 { rewrite_to_copy(inst, a_id); n = n + 1 } // OR
1139 // Comparisons on same Value:
1140 if op == 20 { rewrite_to_copy(inst, const_one(f)); n = n + 1 } // EQ -> 1
1141 if op == 21 { make_result_zero(f, inst); n = n + 1 } // NE -> 0
1142 if op == 22 { make_result_zero(f, inst); n = n + 1 } // LT -> 0
1143 if op == 23 { rewrite_to_copy(inst, const_one(f)); n = n + 1 } // LE -> 1
1144 if op == 24 { make_result_zero(f, inst); n = n + 1 } // GT -> 0
1145 if op == 25 { rewrite_to_copy(inst, const_one(f)); n = n + 1 } // GE -> 1
1146 }
1147 }
1148 inst = inst.next
1149 }
1150 bi = bi + 1
1151 }
1152 return n
1153}
1154
1155// Helper: allocate or reuse a constant-1 Value in function f.
1156// Scans f.values for an existing VAL_CONST with const_int==1; if
1157// absent, creates one. Cached across calls within a pass by
1158// recomputing -- cheap enough for the small functions our compiler
1159// produces.
1160func const_one(f: *Function) -> i64 {
1161 var v: i64 = 0
1162 let base: i64 = f.values as i64
1163 while v < f.n_values {
1164 let vv: *Value = (base + v * 48) as *Value
1165 if vv.kind == VAL_CONST {
1166 if vv.const_int == 1 { return v }
1167 }
1168 v = v + 1
1169 }
1170 // Create a new VAL_CONST with value 1. Mirrors ir_const_i64.
1171 let id: i64 = f.n_values
1172 let nv: *Value = (base + id * 48) as *Value
1173 nv.kind = VAL_CONST
1174 nv.const_int = 1
1175 f.n_values = id + 1
1176 return id
1177}
1178
1179// ===== dead code elimination ======================================
1180//
1181// A pure instruction with no readers is dead. "Pure" = any non-
1182// side-effecting opcode: arithmetic, logic, compares, copies, casts.
1183// Anything that touches memory, calls out, or branches stays.
1184//
1185// Algorithm:
1186// 1. Scan the whole function once, marking used[id]=1 for every
1187// operand we see.
1188// 2. Second pass: for each instruction whose result is unused AND
1189// pure, unlink from its block's list.
1190
1191func op_is_pure(op: i64) -> i64 {
1192 if op == 1 { return 1 } // ADD
1193 if op == 2 { return 1 } // SUB
1194 if op == 3 { return 1 } // MUL
1195 if op == 4 { return 1 } // DIV_S
1196 if op == 6 { return 1 } // REM_S
1197 if op == 10 { return 1 } // AND
1198 if op == 11 { return 1 } // OR
1199 if op == 12 { return 1 } // XOR
1200 if op == 13 { return 1 } // SHL
1201 if op == 14 { return 1 } // SHR_S
1202 if op == 15 { return 1 } // SHR_U
1203 if op >= 20 { if op <= 25 { return 1 } } // cmp family
1204 if op == 40 { return 1 } // COPY
1205 return 0
1206}
1207
1208func mark_used(f: *Function, used: *u8) -> i64 {
1209 var bi: i64 = 0
1210 while bi < f.n_blocks {
1211 let b: *BasicBlock = block_at(f, bi)
1212 var inst: *Instr = b.head
1213 while inst != (0 as *Instr) {
1214 let n: i64 = inst.n_operands
1215 // BR operand is a block id, not a Value.
1216 if inst.op != 31 {
1217 // Cover op0..op15. Prior versions covered only
1218 // op0..op3, silently dropping marks for op4..op15.
1219 // For OP_CALL with > 4 args (e.g. 11-arg AEAD calls
1220 // landed in the off-C arc 2026-05-20), the defining
1221 // instructions of args 5..11 were then mistakenly
1222 // judged "unused" by opt_dce and DELETED. That cascade
1223 // produced TRUNCATED FUNCTION BODIES + GARBAGE call
1224 // labels in the native x86_64 self-host bootstrap.
1225 // This is the partner fix to read_op/write_op coverage
1226 // extension (commit 8efd1949).
1227 if n >= 1 { used[inst.op0] = 1 }
1228 if n >= 2 { used[inst.op1] = 1 }
1229 if n >= 3 { used[inst.op2] = 1 }
1230 if n >= 4 { used[inst.op3] = 1 }
1231 if n >= 5 { used[inst.op4] = 1 }
1232 if n >= 6 { used[inst.op5] = 1 }
1233 if n >= 7 { used[inst.op6] = 1 }
1234 if n >= 8 { used[inst.op7] = 1 }
1235 if n >= 9 { used[inst.op8] = 1 }
1236 if n >= 10 { used[inst.op9] = 1 }
1237 if n >= 11 { used[inst.op10] = 1 }
1238 if n >= 12 { used[inst.op11] = 1 }
1239 if n >= 13 { used[inst.op12] = 1 }
1240 if n >= 14 { used[inst.op13] = 1 }
1241 if n >= 15 { used[inst.op14] = 1 }
1242 if n >= 16 { used[inst.op15] = 1 }
1243 if n >= 17 { used[inst.op16] = 1 }
1244 if n >= 18 { used[inst.op17] = 1 }
1245 if n >= 19 { used[inst.op18] = 1 }
1246 if n >= 20 { used[inst.op19] = 1 }
1247 if n >= 21 { used[inst.op20] = 1 }
1248 if n >= 22 { used[inst.op21] = 1 }
1249 if n >= 23 { used[inst.op22] = 1 }
1250 if n >= 24 { used[inst.op23] = 1 }
1251 }
1252 inst = inst.next
1253 }
1254 bi = bi + 1
1255 }
1256 return 0
1257}
1258
1259func unlink(b: *BasicBlock, inst: *Instr) -> i64 {
1260 if inst.prev != (0 as *Instr) {
1261 inst.prev.next = inst.next
1262 }
1263 if inst.prev == (0 as *Instr) {
1264 b.head = inst.next
1265 }
1266 if inst.next != (0 as *Instr) {
1267 inst.next.prev = inst.prev
1268 }
1269 if inst.next == (0 as *Instr) {
1270 b.tail = inst.prev
1271 }
1272 return 0
1273}
1274
1275func opt_dce(f: *Function) -> i64 {
1276 let used: *u8 = sys_mmap(f.n_values + 8)
1277 mark_used(f, used)
1278 var killed: i64 = 0
1279 var bi: i64 = 0
1280 while bi < f.n_blocks {
1281 let b: *BasicBlock = block_at(f, bi)
1282 var inst: *Instr = b.head
1283 while inst != (0 as *Instr) {
1284 let next: *Instr = inst.next
1285 if op_is_pure(inst.op) {
1286 let res_used: i64 = used[inst.result]
1287 let folded_away: i64 = 0
1288 // If result Value became a constant (fold kept it
1289 // as VAL_CONST_INT), the instruction is redundant
1290 // even if `used` is 1 -- readers see the literal.
1291 let vres: *Value = val_at(f, inst.result)
1292 var drop: i64 = 0
1293 if res_used == 0 { drop = 1 }
1294 if vres.kind == 0 {
1295 if op_is_pure(inst.op) { drop = 1 }
1296 }
1297 if drop {
1298 unlink(b, inst)
1299 killed = killed + 1
1300 }
1301 }
1302 inst = next
1303 }
1304 bi = bi + 1
1305 }
1306 return killed
1307}
1308
1309// ===== driver =====================================================
1310//
1311// Run fold -> simplify -> dce until a round makes no changes.
1312
1313// Mini comptime interpreter: evaluate a pure binop over two Value
1314// ids whose const values we've already resolved. Returns the
1315// computed const; caller checks that both inputs are known.
1316func comptime_eval_binop(op: i64, a: i64, b: i64) -> i64 {
1317 if op == OP_ADD { return a + b }
1318 if op == OP_SUB { return a - b }
1319 if op == OP_MUL { return a * b }
1320 if op == OP_DIV_S {
1321 if b == 0 { return 0 }
1322 return a / b
1323 }
1324 if op == OP_REM_S {
1325 if b == 0 { return 0 }
1326 return a % b
1327 }
1328 if op == OP_AND { return a & b }
1329 if op == OP_OR { return a | b }
1330 if op == OP_XOR { return a ^ b }
1331 if op == OP_SHL { return a << b }
1332 if op == OP_SHR_S { return a >> b }
1333 if op == OP_EQ {
1334 if a == b { return 1 }
1335 return 0
1336 }
1337 if op == OP_NE {
1338 if a != b { return 1 }
1339 return 0
1340 }
1341 if op == OP_LT_S {
1342 if a < b { return 1 }
1343 return 0
1344 }
1345 if op == OP_LE_S {
1346 if a <= b { return 1 }
1347 return 0
1348 }
1349 if op == OP_GT_S {
1350 if a > b { return 1 }
1351 return 0
1352 }
1353 if op == OP_GE_S {
1354 if a >= b { return 1 }
1355 return 0
1356 }
1357 return 0
1358}
1359
1360// Whole-program constant-return folding with comptime-style eval --
1361// gcc needs -flto + IPA-CP for this, NishiLang gets it free.
1362//
1363// Scans every function in the module; for each one, try to evaluate
1364// its body as a straight-line computation over constants. The
1365// walker maintains an (SSA value id → const int) map; it extends
1366// the map on each VAL_CONST_INT and each binop whose both operands
1367// are in the map. If execution reaches the function's RETURN with
1368// a mapped operand, the function is const-return.
1369//
1370// Handles:
1371// * Nullary functions (no params)
1372// * Single basic block
1373// * Straight-line arithmetic, bitwise, comparison over consts
1374// * Binops whose operands trace back to CONST_INTs
1375//
1376// No side-effecting ops allowed (OP_STORE, OP_CALL, OP_SYSCALL).
1377// If any instruction violates these, the function is considered
1378// unfoldable and left alone.
1379//
1380// Skips callers-before-callees ordering -- a single pass suffices
1381// because we apply to the module, not in a bottom-up scan. Callers
1382// of callers that fold to consts get picked up on the next opt_run
1383// iteration via const_fold.
1384func opt_module_const_return(m: *Module) -> i64 {
1385 // Walk m.functions; identify const-return funcs by f.name_start
1386 // (pointer-as-i64) since that's our stable-identifier today.
1387 // Side table: const_func_id[i] = 1 if function `i` is const-return.
1388 // Parallel: const_val[i] = the returned value.
1389 let n: i64 = m.n_functions
1390 let mark_raw: *u8 = sys_mmap(n + 8)
1391 let mark: *u8 = mark_raw
1392 let vals_raw: *u8 = sys_mmap(n * 8 + 16)
1393 let vals: *i64 = vals_raw as *i64
1394
1395 // Pass 1: classify.
1396 var i: i64 = 0
1397 while i < n {
1398 let fn_base: i64 = m.functions as i64
1399 let f: *Function = (fn_base + i * 176) as *Function
1400 mark[i] = 0
1401 vals[i] = 0
1402 // Only attempt comptime-eval for nullary single-block funcs.
1403 // Functions with params can't be eval'd without the caller's
1404 // arg values (specialization is a follow-up pass).
1405 if f.n_blocks == 1 {
1406 if f.n_params == 0 {
1407 let b: *BasicBlock = block_at(f, 0)
1408 // Side-effect check + build (value_id -> const) map.
1409 var pure: i64 = 1
1410 let kmap_raw: *u8 = sys_mmap(f.n_values + 8)
1411 let kmap: *u8 = kmap_raw // 1 = value known const
1412 let vmap_raw: *u8 = sys_mmap(f.n_values * 8 + 16)
1413 let vmap: *i64 = vmap_raw as *i64
1414 // Seed the map with every VAL_CONST_INT in the function.
1415 var vi: i64 = 0
1416 while vi < f.n_values {
1417 let v: *Value = val_at(f, vi)
1418 if v.kind == VK_CONST_INT {
1419 kmap[vi] = 1
1420 vmap[vi] = v.const_int
1421 }
1422 vi = vi + 1
1423 }
1424 var inst: *Instr = b.head
1425 while inst != (0 as *Instr) {
1426 let op: i64 = inst.op
1427 if op == OP_STORE { pure = 0 }
1428 if op == OP_CALL { pure = 0 }
1429 if op == OP_CALL_INDIRECT { pure = 0 }
1430 if op == OP_SYSCALL { pure = 0 }
1431 if op == OP_LOAD { pure = 0 }
1432 if op == OP_ALLOCA { pure = 0 }
1433 if op == OP_GEP { pure = 0 }
1434 // Evaluate binop/cmp if both operands are known.
1435 if inst.n_operands == 2 {
1436 if kmap[inst.op0] == 1 {
1437 if kmap[inst.op1] == 1 {
1438 let r: i64 = comptime_eval_binop(op,
1439 vmap[inst.op0],
1440 vmap[inst.op1])
1441 kmap[inst.result] = 1
1442 vmap[inst.result] = r
1443 }
1444 }
1445 }
1446 // OP_COPY just forwards op0.
1447 if op == OP_COPY {
1448 if kmap[inst.op0] == 1 {
1449 kmap[inst.result] = 1
1450 vmap[inst.result] = vmap[inst.op0]
1451 }
1452 }
1453 inst = inst.next
1454 }
1455 if pure == 1 {
1456 let tail: *Instr = b.tail
1457 if tail != (0 as *Instr) {
1458 if tail.op == OP_RETURN {
1459 if tail.n_operands > 0 {
1460 if kmap[tail.op0] == 1 {
1461 mark[i] = 1
1462 vals[i] = vmap[tail.op0]
1463 }
1464 }
1465 }
1466 }
1467 }
1468 }
1469 }
1470 i = i + 1
1471 }
1472
1473 // Pass 2: rewrite calls to const-return functions. Match callees
1474 // by walking the Function pool pointer.
1475 var changed: i64 = 0
1476 var j: i64 = 0
1477 while j < n {
1478 let fn_base2: i64 = m.functions as i64
1479 let f2: *Function = (fn_base2 + j * 176) as *Function
1480 var bi: i64 = 0
1481 while bi < f2.n_blocks {
1482 let b2: *BasicBlock = block_at(f2, bi)
1483 var ci: *Instr = b2.head
1484 while ci != (0 as *Instr) {
1485 if ci.op == OP_CALL {
1486 // Find which function in m.functions this callee
1487 // corresponds to; match by pointer equality.
1488 var k: i64 = 0
1489 while k < n {
1490 let ft: *Function = (fn_base2 + k * 176) as *Function
1491 if ft == ci.callee {
1492 if mark[k] == 1 {
1493 // Emit a fresh CONST_INT Value for the
1494 // folded result so the COPY has a
1495 // proper source.
1496 let cv: i64 = ir_const_i64(f2, vals[k])
1497 rewrite_to_copy(ci, cv)
1498 changed = changed + 1
1499 }
1500 k = n
1501 }
1502 k = k + 1
1503 }
1504 }
1505 ci = ci.next
1506 }
1507 bi = bi + 1
1508 }
1509 j = j + 1
1510 }
1511 return changed
1512}
1513
1514// Whole-module DCE (F4 half). Mirrors opt.c's opt_module_dce:
1515// mark functions reachable from `main` + any extern declarations,
1516// then compact m.functions[] to the live set.
1517//
1518// Invariants (match opt.c):
1519// L1 `main` is always live -- entry point by convention.
1520// L2 Extern declarations preserved (no body to prune; caller's
1521// responsibility). opt.nx today treats every declared
1522// function as having a body; there's no is_extern bit
1523// exposed, so L2 degenerates: all functions are candidates.
1524// When extern support lands, gate the `is_extern` check here.
1525// L3 Reachability defined strictly via OP_CALL / OP_TAIL_CALL
1526// instr.callee pointers (no function-pointer escape today).
1527// L4 Iteration order is m.functions[] index order --
1528// deterministic, F6-compliant.
1529// L5 Compaction is in-place; pruned Function records are left
1530// dangling but not freed (NishiLang arena-allocated at parse
1531// time; run-once compiler, OS reclaims at exit).
1532func opt_module_dce(m: *Module) -> i64 {
1533 let n: i64 = m.n_functions
1534 if n == 0 { return 0 }
1535
1536 let fn_base: i64 = m.functions as i64
1537 let stride: i64 = 176
1538
1539 // Find `main` by name-byte compare. name_start is the byte
1540 // offset in the module's name table, but we compare at the
1541 // address-as-offset level: each function's name lives at a
1542 // stable address for the life of this compile.
1543 let live_raw: *u8 = sys_mmap(n + 16)
1544 let live: *u8 = live_raw
1545 var idx: i64 = 0
1546 while idx < n {
1547 live[idx] = 0
1548 idx = idx + 1
1549 }
1550
1551 let work_raw: *u8 = sys_mmap(n * 8 + 16)
1552 let work: *i64 = work_raw as *i64
1553 var w_head: i64 = 0
1554 var w_tail: i64 = 0
1555
1556 // Seed: find `main` and mark it.
1557 var entry_idx: i64 = -1
1558 idx = 0
1559 while idx < n {
1560 let f: *Function = (fn_base + idx * stride) as *Function
1561 // name is stored via name_start + name_len. Compare the
1562 // four bytes "main" and a null/space terminator at name_len=4.
1563 if f.name_len == 4 {
1564 let name_ptr: *u8 = f.name_start as *u8
1565 if name_ptr[0] == 0x6D {
1566 if name_ptr[1] == 0x61 {
1567 if name_ptr[2] == 0x69 {
1568 if name_ptr[3] == 0x6E {
1569 entry_idx = idx
1570 }
1571 }
1572 }
1573 }
1574 }
1575 idx = idx + 1
1576 }
1577 if entry_idx >= 0 {
1578 live[entry_idx] = 1
1579 work[w_tail] = entry_idx
1580 w_tail = w_tail + 1
1581 }
1582
1583 // BFS through the call graph.
1584 while w_head < w_tail {
1585 let fi: i64 = work[w_head]
1586 w_head = w_head + 1
1587 let f: *Function = (fn_base + fi * stride) as *Function
1588 var bi: i64 = 0
1589 while bi < f.n_blocks {
1590 let bb: *BasicBlock = block_at(f, bi)
1591 var inst: *Instr = bb.head
1592 while inst != (0 as *Instr) {
1593 if inst.op == OP_CALL {
1594 if inst.callee != (0 as *Function) {
1595 let cb: i64 = inst.callee as i64
1596 let off: i64 = cb - fn_base
1597 if off >= 0 {
1598 let ci: i64 = off / stride
1599 if ci < n {
1600 if live[ci] == 0 {
1601 live[ci] = 1
1602 work[w_tail] = ci
1603 w_tail = w_tail + 1
1604 }
1605 }
1606 }
1607 }
1608 }
1609 inst = inst.next
1610 }
1611 bi = bi + 1
1612 }
1613 }
1614
1615 // Compaction: walk live set in order, move each live Function's
1616 // 176 bytes forward. Preserves iteration order (L4); leaves
1617 // tail of the array dangling but harmless.
1618 var write_idx: i64 = 0
1619 var read_idx: i64 = 0
1620 var removed: i64 = 0
1621 while read_idx < n {
1622 if live[read_idx] == 1 {
1623 if write_idx != read_idx {
1624 let src: i64 = fn_base + read_idx * stride
1625 let dst: i64 = fn_base + write_idx * stride
1626 let src_p: *u8 = src as *u8
1627 let dst_p: *u8 = dst as *u8
1628 var k: i64 = 0
1629 while k < stride {
1630 dst_p[k] = src_p[k]
1631 k = k + 1
1632 }
1633 }
1634 write_idx = write_idx + 1
1635 } else {
1636 removed = removed + 1
1637 }
1638 read_idx = read_idx + 1
1639 }
1640 m.n_functions = write_idx
1641 return removed
1642}
1643
1644// opt_sweep_unreachable_function: port of opt.c's per-function
1645// reachability-based block compaction. BFS from entry marks
1646// reachable blocks, rewrites branch targets with renumbered ids,
1647// compacts f.blocks[], prunes stale pred/succ entries.
1648//
1649// Essential after jump threading or DCE that creates orphan
1650// blocks. Matches opt.c behaviour; SWEEP1-SWEEP4 invariants
1651// mirror the comment in opt.c:
1652// SWEEP1 Branch operands rewritten BEFORE compaction (while
1653// old ids are still valid indices).
1654// SWEEP2 Block ids reassigned in lockstep with compaction so
1655// the f.blocks[i].id == i invariant holds afterwards.
1656// SWEEP3 Pred/succ lists pruned to drop references to dead
1657// blocks (otherwise dominator analysis reading
1658// f.blocks[pred.id] hits stale memory).
1659// SWEEP4 No-op fast path: if nothing unreachable, returns 0
1660// without touching anything.
1661
1662func opt_sweep_unreachable_function(f: *Function) -> i64 {
1663 let n: i64 = f.n_blocks
1664 if n == 0 { return 0 }
1665
1666 // reach[i] = 1 if block i is reachable from entry.
1667 let reach_raw: *u8 = sys_mmap(n + 16)
1668 let reach: *u8 = reach_raw
1669 var i: i64 = 0
1670 while i < n { reach[i] = 0; i = i + 1 }
1671
1672 // Worklist (block indices). Seed with entry id.
1673 let work_raw: *u8 = sys_mmap(n * 8 + 16)
1674 let work: *i64 = work_raw as *i64
1675 var w_head: i64 = 0
1676 var w_tail: i64 = 0
1677 // Entry block's id is its array index post-parse (stable
1678 // invariant); re-check by walking if needed.
1679 reach[f.entry.id] = 1
1680 work[w_tail] = f.entry.id
1681 w_tail = w_tail + 1
1682
1683 // BFS via BR / BR_COND op operands -- the authoritative successor
1684 // ids. Earlier versions walked bb.succ0 / bb.succ1, but other opt
1685 // passes (sccp_branches, thread_jumps, block_merge) rewrite the ops
1686 // without touching succ pointers, so pointers go stale across a
1687 // single opt_run round and BFS missed live edges. Walking the same
1688 // ids the riscv emitter writes into asm keeps reach in lockstep
1689 // with what the assembler will reference.
1690 while w_head < w_tail {
1691 let bi: i64 = work[w_head]
1692 w_head = w_head + 1
1693 let bb: *BasicBlock = block_at(f, bi)
1694 var inst: *Instr = bb.head
1695 while inst != (0 as *Instr) {
1696 if inst.op == OP_BR {
1697 let t: i64 = inst.op0
1698 if t >= 0 {
1699 if t < n {
1700 if reach[t] == 0 {
1701 reach[t] = 1
1702 work[w_tail] = t
1703 w_tail = w_tail + 1
1704 }
1705 }
1706 }
1707 }
1708 if inst.op == OP_BR_COND {
1709 let t1: i64 = inst.op1
1710 if t1 >= 0 {
1711 if t1 < n {
1712 if reach[t1] == 0 {
1713 reach[t1] = 1
1714 work[w_tail] = t1
1715 w_tail = w_tail + 1
1716 }
1717 }
1718 }
1719 let t2: i64 = inst.op2
1720 if t2 >= 0 {
1721 if t2 < n {
1722 if reach[t2] == 0 {
1723 reach[t2] = 1
1724 work[w_tail] = t2
1725 w_tail = w_tail + 1
1726 }
1727 }
1728 }
1729 }
1730 inst = inst.next
1731 }
1732 }
1733
1734 // Compute new ids. newid[i] = -1 if unreachable, else new
1735 // compacted position.
1736 let newid_raw: *u8 = sys_mmap(n * 8 + 16)
1737 let newid: *i64 = newid_raw as *i64
1738 var new_n: i64 = 0
1739 i = 0
1740 while i < n {
1741 newid[i] = -1
1742 if reach[i] == 1 {
1743 newid[i] = new_n
1744 new_n = new_n + 1
1745 }
1746 i = i + 1
1747 }
1748
1749 // SWEEP4: nothing unreachable -> fast exit.
1750 if new_n == n { return 0 }
1751
1752 // SWEEP1: rewrite branch operands before compaction.
1753 i = 0
1754 while i < n {
1755 if reach[i] == 1 {
1756 let bb: *BasicBlock = block_at(f, i)
1757 var inst: *Instr = bb.head
1758 while inst != (0 as *Instr) {
1759 if inst.op == OP_BR {
1760 let t: i64 = inst.op0
1761 if t < n {
1762 if newid[t] >= 0 { inst.op0 = newid[t] }
1763 }
1764 }
1765 if inst.op == OP_BR_COND {
1766 let t1: i64 = inst.op1
1767 let t2: i64 = inst.op2
1768 if t1 < n {
1769 if newid[t1] >= 0 { inst.op1 = newid[t1] }
1770 }
1771 if t2 < n {
1772 if newid[t2] >= 0 { inst.op2 = newid[t2] }
1773 }
1774 }
1775 inst = inst.next
1776 }
1777 }
1778 i = i + 1
1779 }
1780
1781 // SWEEP2: compact blocks in-place + reassign ids. Unlike opt.c
1782 // we keep a single backing pool for the lifetime of the function
1783 // (sized in ir_function_new); compaction copies the 96-byte
1784 // BasicBlock record forward inside that pool.
1785 // STUB(opt, never): "fixed pool" is the chosen architecture for
1786 // opt.nx -- pool size lives in ir_function_new and is the real
1787 // bound; no realloc path is needed.
1788 let block_stride: i64 = 96
1789 let bb_base: i64 = f.blocks as i64
1790 var write_idx: i64 = 0
1791 var read_idx: i64 = 0
1792 while read_idx < n {
1793 if reach[read_idx] == 1 {
1794 let src: i64 = bb_base + read_idx * block_stride
1795 let dst: i64 = bb_base + write_idx * block_stride
1796 if src != dst {
1797 let src_p: *u8 = src as *u8
1798 let dst_p: *u8 = dst as *u8
1799 var k: i64 = 0
1800 while k < block_stride {
1801 dst_p[k] = src_p[k]
1802 k = k + 1
1803 }
1804 }
1805 let bb_new: *BasicBlock = (dst) as *BasicBlock
1806 bb_new.id = write_idx
1807 write_idx = write_idx + 1
1808 }
1809 read_idx = read_idx + 1
1810 }
1811 f.n_blocks = new_n
1812
1813 // SWEEP3: prune stale pred pointers after compaction.
1814 // Note: a previous attempt also rebuilt succ pointers from
1815 // BR/BR_COND ops; that interacted badly with opt_thread_jumps /
1816 // opt_block_merge inside opt_run and caused codegen to hang on
1817 // probe.nx. The opt_sweep BFS itself was changed to walk BR
1818 // ops (not succ pointers) in commit 4f4e682, so sweep doesn't
1819 // depend on succ being current anyway.
1820 // STUB(opt, T#opt-002): post-sweep succs remain stale. Other
1821 // passes that walk succs (opt_block_merge, opt_thread_jumps,
1822 // opt_dce) may misbehave on real-world IR. Validator V3 will
1823 // fire when post-opt validation is re-enabled.
1824 // Plan: refactor each succ-walking pass to walk BR ops the
1825 // way sweep does, OR rebuild succs in SWEEP3 once we
1826 // understand why the rebuild interacted with opt_run.
1827 // Closes when: post-opt ir_validate runs clean on probe.nx.
1828 i = 0
1829 while i < new_n {
1830 let bb: *BasicBlock = block_at(f, i)
1831 if bb.pred0 != (0 as *BasicBlock) {
1832 let pid: i64 = bb.pred0.id
1833 if pid >= new_n { bb.pred0 = 0 as *BasicBlock }
1834 }
1835 if bb.pred1 != (0 as *BasicBlock) {
1836 let pid: i64 = bb.pred1.id
1837 if pid >= new_n { bb.pred1 = 0 as *BasicBlock }
1838 }
1839 if bb.pred2 != (0 as *BasicBlock) {
1840 let pid: i64 = bb.pred2.id
1841 if pid >= new_n { bb.pred2 = 0 as *BasicBlock }
1842 }
1843 i = i + 1
1844 }
1845 return 1
1846}
1847
1848// opt_copyprop: port of opt.c's copy-propagation pass. Rewrites
1849// OP_COPY destinations that trace to a constant source so the dst
1850// Value itself becomes VAL_CONST_INT. Downstream const-fold then
1851// collapses further.
1852//
1853// Merge values (multi-def) are EXCLUDED: a value written by two
1854// different COPY instrs in different predecessor blocks (e.g.
1855// from mem2reg phi elimination) holds different constants on
1856// different paths; const-propagating one of them would miscompile
1857// the other path.
1858//
1859// Two invariants:
1860// CP1 multi_def[] is built in a full first pass so we know EVERY
1861// value's def count before rewriting.
1862// CP2 Never rewrite a Value whose def count > 1.
1863
1864func opt_copyprop(f: *Function) -> i64 {
1865 if f.n_values == 0 { return 0 }
1866 let multi_raw: *u8 = sys_mmap(f.n_values + 16)
1867 let multi: *u8 = multi_raw
1868 let seen_raw: *u8 = sys_mmap(f.n_values + 16)
1869 let seen: *u8 = seen_raw
1870 var vi: i64 = 0
1871 while vi < f.n_values {
1872 multi[vi] = 0
1873 seen[vi] = 0
1874 vi = vi + 1
1875 }
1876
1877 // Pass 1: count defs.
1878 var bi: i64 = 0
1879 while bi < f.n_blocks {
1880 let bb: *BasicBlock = block_at(f, bi)
1881 var inst: *Instr = bb.head
1882 while inst != (0 as *Instr) {
1883 // Skip instrs that don't produce a value.
1884 var produces: i64 = 1
1885 if inst.op == OP_BR { produces = 0 }
1886 if inst.op == OP_BR_COND { produces = 0 }
1887 if inst.op == OP_RETURN { produces = 0 }
1888 if inst.op == OP_STORE { produces = 0 }
1889 if inst.ty == (0 as *Type) { produces = 0 }
1890 if produces == 1 {
1891 if inst.ty.kind == TY_VOID { produces = 0 }
1892 }
1893 if produces == 1 {
1894 let r: i64 = inst.result
1895 if r < f.n_values {
1896 if seen[r] == 1 { multi[r] = 1 }
1897 else { seen[r] = 1 }
1898 }
1899 }
1900 inst = inst.next
1901 }
1902 bi = bi + 1
1903 }
1904
1905 // Pass 2: propagate const through COPY.
1906 var changed: i64 = 0
1907 bi = 0
1908 while bi < f.n_blocks {
1909 let bb: *BasicBlock = block_at(f, bi)
1910 var inst: *Instr = bb.head
1911 while inst != (0 as *Instr) {
1912 if inst.op == OP_COPY {
1913 if inst.n_operands >= 1 {
1914 let rid: i64 = inst.result
1915 if rid < f.n_values {
1916 if multi[rid] == 0 {
1917 let sid: i64 = inst.op0
1918 if sid < f.n_values {
1919 if multi[sid] == 0 {
1920 let src: *Value = val_at(f, sid)
1921 let dst: *Value = val_at(f, rid)
1922 if src.kind == VK_CONST_INT {
1923 if dst.kind != VK_CONST_INT {
1924 dst.kind = VK_CONST_INT
1925 dst.const_int = src.const_int
1926 changed = 1
1927 }
1928 }
1929 }
1930 }
1931 }
1932 }
1933 }
1934 }
1935 inst = inst.next
1936 }
1937 bi = bi + 1
1938 }
1939 return changed
1940}
1941
1942// Forward declarations for helpers used in opt_copy_forward.
1943// NishiLang forbids forward-references; declare signatures here.
1944func if_br_cond_then_1_else_n(inst: *Instr) -> i64;
1945func read_op(inst: *Instr, idx: i64) -> i64;
1946func write_op(inst: *Instr, idx: i64, v: i64) -> i64;
1947
1948// opt_copy_forward: chase single-def OP_COPY chains, replacing
1949// operand references with the original source. Distinct from
1950// opt_copyprop (which promotes the COPY *destination* to CONST if
1951// src is const). copy_forward rewrites USES to skip COPY stops.
1952//
1953// Two invariants (same safety reasoning as copyprop):
1954// CF1 Multi-def values (from merges) are NOT followed; stopping
1955// there preserves phi semantics.
1956// CF2 Chain length capped at 32 hops to prevent cycles + bound
1957// worst-case cost per rewrite.
1958
1959func opt_copy_forward(f: *Function) -> i64 {
1960 if f.n_values == 0 { return 0 }
1961 let seen_raw: *u8 = sys_mmap(f.n_values + 16)
1962 let seen: *u8 = seen_raw
1963 let multi_raw: *u8 = sys_mmap(f.n_values + 16)
1964 let multi: *u8 = multi_raw
1965 let src_raw: *u8 = sys_mmap(f.n_values * 8 + 16)
1966 let copy_src: *i64 = src_raw as *i64
1967 var k: i64 = 0
1968 while k < f.n_values {
1969 seen[k] = 0
1970 multi[k] = 0
1971 copy_src[k] = -1
1972 k = k + 1
1973 }
1974
1975 // Pass 1: def counts + record COPY sources.
1976 var bi: i64 = 0
1977 while bi < f.n_blocks {
1978 let bb: *BasicBlock = block_at(f, bi)
1979 var inst: *Instr = bb.head
1980 while inst != (0 as *Instr) {
1981 var produces: i64 = 1
1982 if inst.op == OP_BR { produces = 0 }
1983 if inst.op == OP_BR_COND { produces = 0 }
1984 if inst.op == OP_RETURN { produces = 0 }
1985 if inst.op == OP_STORE { produces = 0 }
1986 if inst.ty == (0 as *Type) { produces = 0 }
1987 if produces == 1 {
1988 if inst.ty.kind == TY_VOID { produces = 0 }
1989 }
1990 if produces == 1 {
1991 let r: i64 = inst.result
1992 if r < f.n_values {
1993 if seen[r] == 1 { multi[r] = 1 }
1994 else { seen[r] = 1 }
1995 if inst.op == OP_COPY {
1996 if inst.n_operands == 1 {
1997 copy_src[r] = inst.op0
1998 }
1999 }
2000 }
2001 }
2002 inst = inst.next
2003 }
2004 bi = bi + 1
2005 }
2006
2007 // Pass 2: rewrite each operand through the chain.
2008 var changed: i64 = 0
2009 bi = 0
2010 while bi < f.n_blocks {
2011 let bb: *BasicBlock = block_at(f, bi)
2012 var inst: *Instr = bb.head
2013 while inst != (0 as *Instr) {
2014 if inst.op != OP_BR {
2015 // OP_BR's op0 is a block id, not a value -- skip.
2016 // For OP_BR_COND, only op0 (condition) is a value;
2017 // op1/op2 are block ids.
2018 let n_val_ops: i64 = if_br_cond_then_1_else_n(inst)
2019 var j: i64 = 0
2020 while j < n_val_ops {
2021 var cur: i64 = read_op(inst, j)
2022 var steps: i64 = 0
2023 var go: i64 = 1
2024 while go == 1 {
2025 if steps >= 32 { go = 0 }
2026 else {
2027 if cur >= f.n_values { go = 0 }
2028 else {
2029 if multi[cur] == 1 { go = 0 }
2030 else {
2031 if copy_src[cur] < 0 { go = 0 }
2032 else {
2033 cur = copy_src[cur]
2034 steps = steps + 1
2035 }
2036 }
2037 }
2038 }
2039 }
2040 if cur != read_op(inst, j) {
2041 write_op(inst, j, cur)
2042 changed = 1
2043 }
2044 j = j + 1
2045 }
2046 }
2047 inst = inst.next
2048 }
2049 bi = bi + 1
2050 }
2051 return changed
2052}
2053
2054// Helpers: BR_COND has 1 value-op (condition) + 2 block-id ops;
2055// everything else uses n_operands value-ops.
2056func if_br_cond_then_1_else_n(inst: *Instr) -> i64 {
2057 if inst.op == OP_BR_COND { return 1 }
2058 return inst.n_operands
2059}
2060
2061// Cover op0..op15. Prior versions handled only op0..op3, which
2062// silently truncated rewrites on OP_SYSCALL (7 operands) and on
2063// the OP_CALL family extended to op0..op15 in the off-C arc
2064// (2026-05-20, Instr stride 128 -> 192). Bug-class fix per
2065// [[project-call-arity-silent-dropping-fix-2026-05-20]] sibling --
2066// the OPT-pass side of the same arity-extension surface.
2067func read_op(inst: *Instr, idx: i64) -> i64 {
2068 if idx == 0 { return inst.op0 }
2069 if idx == 1 { return inst.op1 }
2070 if idx == 2 { return inst.op2 }
2071 if idx == 3 { return inst.op3 }
2072 if idx == 4 { return inst.op4 }
2073 if idx == 5 { return inst.op5 }
2074 if idx == 6 { return inst.op6 }
2075 if idx == 7 { return inst.op7 }
2076 if idx == 8 { return inst.op8 }
2077 if idx == 9 { return inst.op9 }
2078 if idx == 10 { return inst.op10 }
2079 if idx == 11 { return inst.op11 }
2080 if idx == 12 { return inst.op12 }
2081 if idx == 13 { return inst.op13 }
2082 if idx == 14 { return inst.op14 }
2083 if idx == 15 { return inst.op15 }
2084 if idx == 16 { return inst.op16 }
2085 if idx == 17 { return inst.op17 }
2086 if idx == 18 { return inst.op18 }
2087 if idx == 19 { return inst.op19 }
2088 if idx == 20 { return inst.op20 }
2089 if idx == 21 { return inst.op21 }
2090 if idx == 22 { return inst.op22 }
2091 return inst.op23
2092}
2093
2094func write_op(inst: *Instr, idx: i64, v: i64) -> i64 {
2095 if idx == 0 { inst.op0 = v; return 0 }
2096 if idx == 1 { inst.op1 = v; return 0 }
2097 if idx == 2 { inst.op2 = v; return 0 }
2098 if idx == 3 { inst.op3 = v; return 0 }
2099 if idx == 4 { inst.op4 = v; return 0 }
2100 if idx == 5 { inst.op5 = v; return 0 }
2101 if idx == 6 { inst.op6 = v; return 0 }
2102 if idx == 7 { inst.op7 = v; return 0 }
2103 if idx == 8 { inst.op8 = v; return 0 }
2104 if idx == 9 { inst.op9 = v; return 0 }
2105 if idx == 10 { inst.op10 = v; return 0 }
2106 if idx == 11 { inst.op11 = v; return 0 }
2107 if idx == 12 { inst.op12 = v; return 0 }
2108 if idx == 13 { inst.op13 = v; return 0 }
2109 if idx == 14 { inst.op14 = v; return 0 }
2110 if idx == 15 { inst.op15 = v; return 0 }
2111 if idx == 16 { inst.op16 = v; return 0 }
2112 if idx == 17 { inst.op17 = v; return 0 }
2113 if idx == 18 { inst.op18 = v; return 0 }
2114 if idx == 19 { inst.op19 = v; return 0 }
2115 if idx == 20 { inst.op20 = v; return 0 }
2116 if idx == 21 { inst.op21 = v; return 0 }
2117 if idx == 22 { inst.op22 = v; return 0 }
2118 if idx == 23 { inst.op23 = v; return 0 }
2119 return 0
2120}
2121
2122// opt_cse: port of opt.c's per-block Common Subexpression
2123// Elimination. Within each block, maintain a table of
2124// (op, operand_a, operand_b) -> result_value_id. When a
2125// subsequent pure binop matches a table entry, rewrite the new
2126// instruction to OP_COPY of the earlier result.
2127//
2128// Scope: per-block only. Cross-block CSE is GVN's job
2129// (opt_gvn_block handles same-block; a future opt_gvn_module
2130// would do cross-block).
2131//
2132// Commutativity: for +/*/& /| /^ /== /!=, checks swapped-operand
2133// form too so `a + b` matches `b + a` in the table.
2134//
2135// Three invariants:
2136// CSE1 Only pure binops (is_const_foldable_binop / _cmp) go
2137// into the table; calls / loads / stores have memory
2138// effects and can't be dedup'd.
2139// CSE2 Table is per-block; cross-block reuse relies on
2140// dominator info we don't have in opt.nx yet.
2141// CSE3 Bounded table (256 entries); overflow stops adding new
2142// entries -- correctness preserved, but redundant
2143// computations leak through downstream.
2144// STUB(opt, T#opt-001): cap of 256 was chosen for the first port
2145// pass. Real-world basic blocks in nxc.nx self-compile likely
2146// exceed this on functions like parse_stmt; we silently degrade.
2147// Plan: bounds-check + diagnostic when full + hit-counter so we
2148// can size correctly, OR replace with a grow path.
2149// Closes when: any per-block CSE candidate count exceeds 256.
2150
2151const CSE_TABLE_CAP: i64 = 256
2152
2153// Is `op` a pure binop CSE can dedup? Mirrors
2154// is_const_foldable_binop + is_const_foldable_cmp in opt.c.
2155func cse_is_candidate(op: i64) -> i64 {
2156 if op == OP_ADD { return 1 }
2157 if op == OP_SUB { return 1 }
2158 if op == OP_MUL { return 1 }
2159 if op == OP_DIV_S { return 1 }
2160 if op == OP_DIV_U { return 1 }
2161 if op == OP_REM_S { return 1 }
2162 if op == OP_REM_U { return 1 }
2163 if op == OP_AND { return 1 }
2164 if op == OP_OR { return 1 }
2165 if op == OP_XOR { return 1 }
2166 if op == OP_SHL { return 1 }
2167 if op == OP_SHR_S { return 1 }
2168 if op == OP_SHR_U { return 1 }
2169 if op == OP_EQ { return 1 }
2170 if op == OP_NE { return 1 }
2171 if op == OP_LT_S { return 1 }
2172 if op == OP_LE_S { return 1 }
2173 if op == OP_GT_S { return 1 }
2174 if op == OP_GE_S { return 1 }
2175 return 0
2176}
2177
2178// Commutative ops where (a, b) in table should also match (b, a)
2179// for a query.
2180func cse_is_commutative(op: i64) -> i64 {
2181 if op == OP_ADD { return 1 }
2182 if op == OP_MUL { return 1 }
2183 if op == OP_AND { return 1 }
2184 if op == OP_OR { return 1 }
2185 if op == OP_XOR { return 1 }
2186 if op == OP_EQ { return 1 }
2187 if op == OP_NE { return 1 }
2188 return 0
2189}
2190
2191func opt_cse(f: *Function) -> i64 {
2192 var changed: i64 = 0
2193 // Per-block table: (op, a, b, result_vid) quadruples.
2194 let tab_raw: *u8 = sys_mmap(CSE_TABLE_CAP * 32 + 64)
2195 let tab: *i64 = tab_raw as *i64 // stride 4 i64 slots per entry
2196
2197 var bi: i64 = 0
2198 while bi < f.n_blocks {
2199 let bb: *BasicBlock = block_at(f, bi)
2200 var n_entries: i64 = 0
2201 var inst: *Instr = bb.head
2202 while inst != (0 as *Instr) {
2203 if inst.op != OP_COPY {
2204 if inst.n_operands == 2 {
2205 if cse_is_candidate(inst.op) == 1 {
2206 let op: i64 = inst.op
2207 let a: i64 = inst.op0
2208 let b: i64 = inst.op1
2209 // Linear-probe the table.
2210 var hit: i64 = -1
2211 var k: i64 = 0
2212 while k < n_entries {
2213 let e_op: i64 = tab[k * 4 + 0]
2214 let e_a: i64 = tab[k * 4 + 1]
2215 let e_b: i64 = tab[k * 4 + 2]
2216 if e_op == op {
2217 if e_a == a {
2218 if e_b == b { hit = k; k = n_entries }
2219 }
2220 }
2221 k = k + 1
2222 }
2223 // Commuted match for commutative ops.
2224 if hit < 0 {
2225 if cse_is_commutative(op) == 1 {
2226 k = 0
2227 while k < n_entries {
2228 let e_op: i64 = tab[k * 4 + 0]
2229 let e_a: i64 = tab[k * 4 + 1]
2230 let e_b: i64 = tab[k * 4 + 2]
2231 if e_op == op {
2232 if e_a == b {
2233 if e_b == a { hit = k; k = n_entries }
2234 }
2235 }
2236 k = k + 1
2237 }
2238 }
2239 }
2240 if hit >= 0 {
2241 // Rewrite current instr to OP_COPY of the
2242 // earlier result.
2243 let src: i64 = tab[hit * 4 + 3]
2244 inst.op = OP_COPY
2245 inst.op0 = src
2246 inst.op1 = 0
2247 inst.n_operands = 1
2248 changed = 1
2249 } else {
2250 // Add to table if room.
2251 if n_entries < CSE_TABLE_CAP {
2252 tab[n_entries * 4 + 0] = op
2253 tab[n_entries * 4 + 1] = a
2254 tab[n_entries * 4 + 2] = b
2255 tab[n_entries * 4 + 3] = inst.result
2256 n_entries = n_entries + 1
2257 }
2258 }
2259 }
2260 }
2261 }
2262 inst = inst.next
2263 }
2264 bi = bi + 1
2265 }
2266 return changed
2267}
2268
2269// opt_thread_jumps: port of opt.c's bridge-collapsing pass.
2270//
2271// After SCCP + DCE + mem2reg we end up with long chains of
2272// single-instruction BR blocks (bridges): entry -> bridge1 ->
2273// bridge2 -> tail. Each hop is a wasted jump at runtime.
2274// Collapse them: for every BR / BR_COND target T, if T is a
2275// bridge, point the edge at T's successor directly.
2276//
2277// A bridge = block with single-instruction OP_BR body (head ==
2278// tail, op == OP_BR, not the entry block).
2279//
2280// Chain length capped at 64 to avoid pathological loops of
2281// bridges that all point at each other.
2282//
2283// After collapsing, the pred/succ lists for all blocks need to
2284// be rebuilt (dominator analysis + sweep rely on accurate CFG).
2285// In opt.nx's inline-slot scheme, "rebuild" means clear pred0..2
2286// + succ0..1 on every block, then re-populate from terminator
2287// analysis.
2288
2289// Chase bridges from `b`; returns the final non-bridge block, or
2290// `b` itself if it isn't a bridge. `hops_left` caps iteration.
2291func skip_bridges(f: *Function, b: *BasicBlock, hops_left: i64) -> *BasicBlock {
2292 var cur: *BasicBlock = b
2293 var hops: i64 = hops_left
2294 while hops > 0 {
2295 if cur == f.entry { return cur }
2296 if cur.head == (0 as *Instr) { return cur }
2297 if cur.head != cur.tail { return cur }
2298 if cur.head.op != OP_BR { return cur }
2299 let next_id: i64 = cur.head.op0
2300 if next_id >= f.n_blocks { return cur }
2301 let nxt: *BasicBlock = block_at(f, next_id)
2302 if nxt == cur { return cur }
2303 cur = nxt
2304 hops = hops - 1
2305 }
2306 return cur
2307}
2308
2309// Append a pred/succ slot (inline 3-pred / 2-succ scheme).
2310// Returns 0 on success, -1 if slots exhausted.
2311func add_succ(bb: *BasicBlock, t: *BasicBlock) -> i64 {
2312 if bb.succ0 == (0 as *BasicBlock) { bb.succ0 = t; bb.n_succs = bb.n_succs + 1; return 0 }
2313 if bb.succ1 == (0 as *BasicBlock) { bb.succ1 = t; bb.n_succs = bb.n_succs + 1; return 0 }
2314 return -1
2315}
2316
2317func add_pred(bb: *BasicBlock, p: *BasicBlock) -> i64 {
2318 if bb.pred0 == (0 as *BasicBlock) { bb.pred0 = p; bb.n_preds = bb.n_preds + 1; return 0 }
2319 if bb.pred1 == (0 as *BasicBlock) { bb.pred1 = p; bb.n_preds = bb.n_preds + 1; return 0 }
2320 if bb.pred2 == (0 as *BasicBlock) { bb.pred2 = p; bb.n_preds = bb.n_preds + 1; return 0 }
2321 return -1
2322}
2323
2324func opt_thread_jumps(f: *Function) -> i64 {
2325 var changed: i64 = 0
2326 var bi: i64 = 0
2327 while bi < f.n_blocks {
2328 let bb: *BasicBlock = block_at(f, bi)
2329 let term: *Instr = bb.tail
2330 if term != (0 as *Instr) {
2331 if term.op == OP_BR {
2332 let t_id: i64 = term.op0
2333 if t_id < f.n_blocks {
2334 let t: *BasicBlock = block_at(f, t_id)
2335 let r: *BasicBlock = skip_bridges(f, t, 64)
2336 if r != t {
2337 term.op0 = r.id
2338 changed = 1
2339 }
2340 }
2341 }
2342 if term.op == OP_BR_COND {
2343 let t1_id: i64 = term.op1
2344 let t2_id: i64 = term.op2
2345 if t1_id < f.n_blocks {
2346 let t1: *BasicBlock = block_at(f, t1_id)
2347 let r1: *BasicBlock = skip_bridges(f, t1, 64)
2348 if r1 != t1 {
2349 term.op1 = r1.id
2350 changed = 1
2351 }
2352 }
2353 if t2_id < f.n_blocks {
2354 let t2: *BasicBlock = block_at(f, t2_id)
2355 let r2: *BasicBlock = skip_bridges(f, t2, 64)
2356 if r2 != t2 {
2357 term.op2 = r2.id
2358 changed = 1
2359 }
2360 }
2361 }
2362 }
2363 bi = bi + 1
2364 }
2365
2366 if changed == 0 { return 0 }
2367
2368 // Rebuild CFG. Clear all pred/succ slots, then re-add from
2369 // every block's terminator.
2370 bi = 0
2371 while bi < f.n_blocks {
2372 let bb: *BasicBlock = block_at(f, bi)
2373 bb.pred0 = 0 as *BasicBlock
2374 bb.pred1 = 0 as *BasicBlock
2375 bb.pred2 = 0 as *BasicBlock
2376 bb.succ0 = 0 as *BasicBlock
2377 bb.succ1 = 0 as *BasicBlock
2378 bb.n_preds = 0
2379 bb.n_succs = 0
2380 bi = bi + 1
2381 }
2382
2383 bi = 0
2384 while bi < f.n_blocks {
2385 let bb: *BasicBlock = block_at(f, bi)
2386 let term: *Instr = bb.tail
2387 if term != (0 as *Instr) {
2388 if term.op == OP_BR {
2389 let t_id: i64 = term.op0
2390 if t_id < f.n_blocks {
2391 let t: *BasicBlock = block_at(f, t_id)
2392 add_succ(bb, t)
2393 add_pred(t, bb)
2394 }
2395 }
2396 if term.op == OP_BR_COND {
2397 let t1_id: i64 = term.op1
2398 let t2_id: i64 = term.op2
2399 if t1_id < f.n_blocks {
2400 let t1: *BasicBlock = block_at(f, t1_id)
2401 add_succ(bb, t1)
2402 add_pred(t1, bb)
2403 }
2404 if t2_id < f.n_blocks {
2405 let t2: *BasicBlock = block_at(f, t2_id)
2406 add_succ(bb, t2)
2407 add_pred(t2, bb)
2408 }
2409 }
2410 }
2411 bi = bi + 1
2412 }
2413 return 1
2414}
2415
2416// Forward declarations for LICM helpers.
2417func licm_is_hoistable(op: i64) -> i64;
2418// ARITY FIX 2026-07-20: this forward declaration was STALE -- it still described the pre-dominance
2419// 3-parameter signature while the real definition (:2900) and the only call site (:2728) had long
2420// since moved to 5 params (`info`, `header` were added by the LICM dominance rewrite). Nothing
2421// caught it because nx_cc had no call-arity checking; the new check flagged it on its first
2422// full-coverage run. Exactly the "a signature changed and something silently didn't follow" class.
2423func operands_are_loop_invariant(f: *Function, inst: *Instr,
2424 in_loop: *u8, info: *DomInfoFn,
2425 header: *BasicBlock) -> i64;
2426func hoist_instr(inst: *Instr, preheader: *BasicBlock) -> i64;
2427func mark_loop_body_nx(f: *Function, header: *BasicBlock,
2428 from: *BasicBlock, in_loop: *u8) -> i64;
2429
2430// opt_licm: port of opt.c's LICM pass, now using dom_fn.nx for
2431// dominance info instead of opt.c's DomInfo. Hoists computations
2432// whose operands don't change across loop iterations out to the
2433// loop's preheader. Classical optimization (Allen-Cocke 1971).
2434//
2435// Algorithm:
2436// 1. Compute dom info via dom_compute_fn.
2437// 2. Find back-edges: any edge b -> s where s dominates b.
2438// s is the loop header; b is the back-edge source.
2439// 3. Find preheader: if s has exactly one forward predecessor p
2440// whose only successor is s, p is the preheader. Otherwise
2441// skip the loop (no edge-splitting in this pass).
2442// 4. Mark loop body: walk back from b through preds, stopping at
2443// s. Mark reached blocks in the in_loop bitmap.
2444// 5. Fixpoint-hoist: for each instr in the loop, if operands are
2445// all loop-invariant AND opcode is pure+non-trapping, move to
2446// preheader. Repeat until no more hoists.
2447//
2448// Invariants (LI1-LI5):
2449// LI1 Only hoist opcodes that are pure AND cannot fault.
2450// Excludes DIV/REM (trap on zero), LOAD/STORE (memory
2451// effects), CALL (unknown effects), PHI (control-dependent),
2452// ALLOCA (stack-position-dependent).
2453// LI2 Operand definitions must dominate the preheader after
2454// hoisting. Trivially true since operand sources are
2455// outside the loop or hoisted earlier in this pass.
2456// LI3 Hoisted instructions preserve SSA: each value has
2457// exactly one def; moving the def earlier keeps that.
2458// LI4 Iteration order follows f.blocks[] array order
2459// (deterministic, F6-compliant).
2460// LI5 No block-storage mutation outside pred/succ slots. No
2461// freeing; pruned instr slots remain allocated.
2462
2463// Safety bound on LICM block count. With the single-pass RPO hoist
2464// (see opt_licm body) LICM is near-linear, so this is a generous
2465// backstop against pathological CFGs, not the old perf crutch (the
2466// fixpoint-rescan era needed 64 to keep the 707KB sites daemon under
2467// 10s; the single-pass fix is the real cure). Tunable -- belongs in
2468// svc-config eventually (Rule #11).
2469const LICM_MAX_BLOCKS: i64 = 4096
2470
2471// ===== CFG edge rebuild from TERMINATORS (2026-07-15) ================
2472// The dominance brute-force oracle (nx_dom_oracle_gate, the banked 06-10
2473// awakening rung) MEASURED: dom_fn is exact on post-parse CFGs (553,761
2474// pairs, 0 mismatches) but WRONG on post-opt CFGs (5,697 mismatches incl
2475// FALSE dominance claims -- the unsound-hoist source behind the 06-10
2476// awakening SIGSEGVs). Root: opt passes leave succ/pred pointers STALE
2477// (T#opt-002, "SWEEP3 leaves succ pointers stale"). The TERMINATORS
2478// (OP_BR op0 / OP_BR_COND op1,op2 -- block IDS) are the ground truth the
2479// EMITTER compiles, so edges rebuilt from them match the generated code
2480// BY CONSTRUCTION. Called at the top of opt_licm so dominance + loop
2481// bodies are computed on the real graph. Returns 0 ok; 1 = REFUSE (a
2482// reachable block has no recognized terminator -- possible fallthrough
2483// edge the rebuild cannot see; LICM declines the function, never guesses).
2484
2485func cfg_block_by_id(f: *Function, id: i64) -> *BasicBlock {
2486 var i: i64 = 0
2487 while i < f.n_blocks {
2488 let b: *BasicBlock = block_at(f, i)
2489 if b.id == id { return b }
2490 i = i + 1
2491 }
2492 return 0 as *BasicBlock
2493}
2494
2495func cfg_add_pred(b: *BasicBlock, p: *BasicBlock) -> i64 {
2496 if b.n_preds == 0 { b.pred0 = p }
2497 if b.n_preds == 1 { b.pred1 = p }
2498 if b.n_preds == 2 { b.pred2 = p }
2499 b.n_preds = b.n_preds + 1
2500 return 0
2501}
2502
2503func cfg_rebuild_edges(f: *Function) -> i64 {
2504 var i: i64 = 0
2505 while i < f.n_blocks {
2506 let b: *BasicBlock = block_at(f, i)
2507 b.n_preds = 0
2508 b.n_succs = 0
2509 b.pred0 = 0 as *BasicBlock
2510 b.pred1 = 0 as *BasicBlock
2511 b.pred2 = 0 as *BasicBlock
2512 b.succ0 = 0 as *BasicBlock
2513 b.succ1 = 0 as *BasicBlock
2514 i = i + 1
2515 }
2516 var refuse: i64 = 0
2517 i = 0
2518 while i < f.n_blocks {
2519 let b2: *BasicBlock = block_at(f, i)
2520 let t: *Instr = b2.tail
2521 if t == (0 as *Instr) { refuse = 1 }
2522 if t != (0 as *Instr) {
2523 var ok: i64 = 0
2524 if t.op == OP_RETURN { ok = 1 }
2525 if t.op == OP_TAIL_CALL { ok = 1 }
2526 if t.op == OP_BR {
2527 let s: *BasicBlock = cfg_block_by_id(f, t.op0)
2528 if s == (0 as *BasicBlock) { refuse = 1 }
2529 if s != (0 as *BasicBlock) {
2530 b2.succ0 = s
2531 b2.n_succs = 1
2532 cfg_add_pred(s, b2)
2533 ok = 1
2534 }
2535 }
2536 if t.op == OP_BR_COND {
2537 let st: *BasicBlock = cfg_block_by_id(f, t.op1)
2538 let sf: *BasicBlock = cfg_block_by_id(f, t.op2)
2539 if st == (0 as *BasicBlock) { refuse = 1 }
2540 if sf == (0 as *BasicBlock) { refuse = 1 }
2541 if st != (0 as *BasicBlock) { if sf != (0 as *BasicBlock) {
2542 b2.succ0 = st
2543 b2.succ1 = sf
2544 b2.n_succs = 2
2545 cfg_add_pred(st, b2)
2546 if sf != st { cfg_add_pred(sf, b2) }
2547 ok = 1
2548 } }
2549 }
2550 if ok == 0 { refuse = 1 }
2551 }
2552 i = i + 1
2553 }
2554 return refuse
2555}
2556
2557// Rebuild every instruction's .parent from GROUND-TRUTH block membership.
2558// ROOT CAUSE (2026-07-15, T#opt-003): opt_block_merge splices block B's instr
2559// list into block A but NEVER updates the moved instrs' .parent -- they keep
2560// pointing at B, which is then emptied + marked unreachable. LICM's
2561// dominance-based invariance then reads such an operand's def-block (= the
2562// stale, unreachable B), finds the header does NOT dominate B, and deems the
2563// operand loop-INVARIANT -- but the instr actually lives in loop-block A and is
2564// VARIANT. Result: a loop-carried value is hoisted -> the loop freezes ->
2565// ed25519 sign hangs. Same class as the stale succ/pred edges (T#opt-002); same
2566// fix: recompute the derived pointer from the block that actually contains the
2567// instruction, so the dominance check is sound. Robust to ANY pass that moves
2568// instrs without maintaining .parent, not just block_merge.
2569func cfg_rebuild_parents(f: *Function) -> i64 {
2570 var i: i64 = 0
2571 while i < f.n_blocks {
2572 let b: *BasicBlock = block_at(f, i)
2573 var inst: *Instr = b.head
2574 while inst != (0 as *Instr) {
2575 inst.parent = b
2576 inst = inst.next
2577 }
2578 i = i + 1
2579 }
2580 return 0
2581}
2582
2583// GATE (2026-07-15): the RPO hoist is AWAKENED behind an oracle-proven dominance
2584// fix (cfg_rebuild_edges), but sha512 exposed a SECOND soundness bug beyond
2585// dominance -- a loop-terminating instr is hoisted -> infinite loop (correct
2586// early vectors, then hang; the 06-10 symptom). Dominance is now PROVEN correct
2587// (nx_dom_oracle_gate GREEN, 683,696 pairs) so the residual is in loop-body
2588// marking (mark_loop_body_nx) or hoist safety, NOT the dominator -- its own
2589// focused arc needing a HOIST-LEVEL differential oracle. Gated OFF (=0) keeps the
2590// compiler byte-safe (LICM stays the effective no-op it was for a year) while the
2591// infra (cfg_rebuild_edges, the oracle, the G9 scalar-alloca-load-hoist) lives in
2592// the tree for that session. Flip to 1 to resume; the matmul i*N hoist rides on it.
2593const LICM_HOIST_LIVE: i64 = 1
2594// BISECT-2026-07-15: isolate the ed25519 test-2 hang. 0 = arithmetic/compare
2595// hoisting only (no G9 scalar-alloca LOAD hoist). If ed25519 stops hanging with
2596// this at 0, the second bug is in G9's load-hoist safety, not the general path.
2597const LICM_G9_LOAD_HOIST: i64 = 1
2598// 2026-07-15: full dominance-based invariance is sound now that inst.parent is
2599// rebuilt (T#opt-003). This flag stays as a fast bisect lever (1 = provably-safe
2600// loop-constant-input hoists only) but defaults OFF -- the real fix is upstream.
2601const LICM_CONSERVATIVE: i64 = 0
2602// NEGATIVE CONTROL (2026-07-15): flip to 1 to skip cfg_rebuild_parents and
2603// reintroduce the stale-.parent miscompile (T#opt-003) -- the gauntlet MUST go
2604// RED (ed25519 HANG caught by the KAT timeout). Proves the gate has teeth.
2605// Ships at 0.
2606const NEGCTL_SKIP_PARENT_REBUILD: i64 = 0
2607
2608func opt_licm(f: *Function) -> i64 {
2609 if LICM_HOIST_LIVE == 0 { return 0 }
2610 if f.n_blocks < 2 { return 0 }
2611 if f.n_blocks > LICM_MAX_BLOCKS { return 0 }
2612
2613 // 2026-07-15: recompute succ/pred from the terminators (see above).
2614 // A function the rebuild cannot fully resolve is DECLINED, not guessed.
2615 if cfg_rebuild_edges(f) != 0 { return 0 }
2616 // 2026-07-15 (T#opt-003): recompute inst.parent from block membership --
2617 // opt_block_merge leaves it stale, which corrupts dominance-invariance.
2618 if NEGCTL_SKIP_PARENT_REBUILD == 0 { cfg_rebuild_parents(f) }
2619
2620 // SOUNDNESS GUARD (2026-06-10, T#ir-pred-list-truncation): blocks
2621 // store only pred0..pred2; a 4th+ predecessor edge increments
2622 // n_preds but is DROPPED (nx_ir.nx add-pred sites). On such a
2623 // CFG both dominator computation and mark_loop_body_nx walk an
2624 // INCOMPLETE edge set, so "operand defined outside the loop" can
2625 // be a lie -> unsound hoist. Found by nx_cc_equiv_gate when the
2626 // dormant RPO hoist walk was awakened: every corpus row passed
2627 // but the self-host stage broke -- merge-heavy compiler functions
2628 // are exactly where preds exceed 3. Until the pred list is
2629 // widened (named follow-up), LICM declines functions whose CFG
2630 // info is truncated; small/medium fns (incl. hot crypto loops)
2631 // keep full hoisting.
2632 var tb: i64 = 0
2633 while tb < f.n_blocks {
2634 let tbb: *BasicBlock = block_at(f, tb)
2635 if tbb.n_preds > 3 { return 0 }
2636 tb = tb + 1
2637 }
2638
2639 // Compute dom info.
2640 let info_raw: *u8 = sys_mmap(128)
2641 let info: *DomInfoFn = info_raw as *DomInfoFn
2642 dom_compute_fn(f, info)
2643
2644 var any_hoisted: i64 = 0
2645 let in_loop_raw: *u8 = sys_mmap(f.n_blocks + 16)
2646 let in_loop: *u8 = in_loop_raw
2647
2648 // C2 PERF FIX (2026-06-09): process each loop HEADER once, not once
2649 // per back-edge. The old code re-marked the loop body and re-ran the
2650 // full-block fixpoint hoist for EVERY back-edge into the same header.
2651 // On many-back-edge state machines (the HTTP parser / HTML routers in
2652 // the 707KB sites daemon) that is O(back_edges * blocks * instrs) per
2653 // header and made the daemon take >2 min in LICM alone (the rest of
2654 // the compile is ~1.8s). Each header now processed once over its FULL
2655 // natural-loop body (union over all its back-edges); the union also
2656 // fixes a latent bug where partial per-back-edge bodies let an operand
2657 // defined in an unmarked loop block look invariant (unsound hoist).
2658 // Pass 1: collect ALL back-edges ONCE (b -> s is a back-edge iff s
2659 // dominates b). O(B * dom_dominates); with df_block_index O(1) that
2660 // is ~O(B^2), paid ONCE -- not re-scanned per header (the per-header
2661 // rescan was the O(H*B^2)/O(H*B^3) blowup that kept the daemon >2min).
2662 let be_cap: i64 = f.n_blocks * 2 + 4
2663 let be_tail_raw: *u8 = sys_mmap(be_cap * 8 + 16)
2664 let be_tail: *i64 = be_tail_raw as *i64
2665 let be_head_raw: *u8 = sys_mmap(be_cap * 8 + 16)
2666 let be_head: *i64 = be_head_raw as *i64
2667 var n_be: i64 = 0
2668 var bi: i64 = 0
2669 while bi < f.n_blocks {
2670 let b: *BasicBlock = block_at(f, bi)
2671 let s0: *BasicBlock = b.succ0
2672 if s0 != (0 as *BasicBlock) {
2673 if dom_dominates(info, s0, b) == 1 {
2674 if n_be < be_cap {
2675 be_tail[n_be] = bi
2676 be_head[n_be] = df_block_index(f, s0)
2677 n_be = n_be + 1
2678 }
2679 }
2680 }
2681 let s1: *BasicBlock = b.succ1
2682 if s1 != (0 as *BasicBlock) {
2683 if dom_dominates(info, s1, b) == 1 {
2684 if n_be < be_cap {
2685 be_tail[n_be] = bi
2686 be_head[n_be] = df_block_index(f, s1)
2687 n_be = n_be + 1
2688 }
2689 }
2690 }
2691 bi = bi + 1
2692 }
2693
2694 // Pass 2: process each loop header ONCE, over the union of its
2695 // back-edges' natural-loop bodies, then hoist once.
2696 let header_done_raw: *u8 = sys_mmap(f.n_blocks + 16)
2697 let header_done: *u8 = header_done_raw
2698 var hd_i: i64 = 0
2699 while hd_i < f.n_blocks { header_done[hd_i] = 0; hd_i = hd_i + 1 }
2700
2701 var k: i64 = 0
2702 while k < n_be {
2703 let hidx: i64 = be_head[k]
2704 var do_header: i64 = 0
2705 if hidx >= 0 {
2706 if hidx < f.n_blocks {
2707 if header_done[hidx] == 0 {
2708 header_done[hidx] = 1
2709 do_header = 1
2710 }
2711 }
2712 }
2713 if do_header == 1 {
2714 let s: *BasicBlock = block_at(f, hidx)
2715 // Union loop body from EVERY back-edge into s (bucketed by
2716 // header in the collected list -- no dom_dominates here).
2717 var j: i64 = 0
2718 while j < f.n_blocks { in_loop[j] = 0; j = j + 1 }
2719 var m: i64 = 0
2720 while m < n_be {
2721 if be_head[m] == hidx {
2722 mark_loop_body_nx(f, s, block_at(f, be_tail[m]), in_loop)
2723 }
2724 m = m + 1
2725 }
2726
2727 // Find preheader: pred of s NOT in loop, with s as only succ.
2728 let preds_raw: *u8 = sys_mmap(32)
2729 let preds: *i64 = preds_raw as *i64
2730 preds[0] = s.pred0 as i64
2731 preds[1] = s.pred1 as i64
2732 preds[2] = s.pred2 as i64
2733 var preheader: *BasicBlock = 0 as *BasicBlock
2734 var pi: i64 = 0
2735 var multiple: i64 = 0
2736 while pi < 3 {
2737 let p: *BasicBlock = preds[pi] as *BasicBlock
2738 if p != (0 as *BasicBlock) {
2739 let p_idx: i64 = df_block_index(f, p)
2740 if p_idx >= 0 {
2741 if in_loop[p_idx] == 0 {
2742 if preheader != (0 as *BasicBlock) {
2743 multiple = 1
2744 }
2745 preheader = p
2746 }
2747 }
2748 }
2749 pi = pi + 1
2750 }
2751 if multiple == 0 {
2752 if preheader != (0 as *BasicBlock) {
2753 // Verify preheader has s as only successor.
2754 if preheader.n_succs == 1 {
2755 if preheader.succ0 == s {
2756 // C2 PERF FIX (2026-06-09): SINGLE-pass hoist
2757 // in RPO order, replacing the `while
2758 // changed_inner` full-rescan fixpoint (the
2759 // measured O(hoists x blocks x instrs) blowup
2760 // -- phase markers showed every big fn stall
2761 // here, not in dom/back-edge scan). Defs
2762 // dominate uses in this SSA-form IR, so
2763 // visiting blocks in RPO + instrs in order
2764 // resolves transitive invariance chains in
2765 // ONE pass: a hoisted def's parent becomes
2766 // the preheader (not in_loop) before its
2767 // users are examined. Any case this order
2768 // misses is only a missed optional hoist,
2769 // never a miscompile. dom_compute_fn already
2770 // built info.rpo -- reuse it.
2771 let rpo_base: i64 = info.rpo as i64
2772 var ri: i64 = 0
2773 while ri < info.rpo_n {
2774 let rslot: *i64 = (rpo_base + ri * 8) as *i64
2775 // AWAKENED 2026-07-15. History: re-pinned
2776 // dormant 2026-06-10 when awakening broke
2777 // selfhost (4 SIGSEGV victim fns; suspicion =
2778 // the dominator fixpoint). The banked rung --
2779 // "dominance brute-force oracle FIRST" -- ran
2780 // as nx_dom_oracle_gate: dom_fn EXACT on
2781 // post-parse CFGs (553,761 pairs, 0 mismatch)
2782 // but 5,697 mismatches on post-opt CFGs incl
2783 // FALSE dominance claims = the unsound-hoist
2784 // source. ROOT was never the dominator: opt
2785 // passes leave succ/pred STALE (T#opt-002).
2786 // cfg_rebuild_edges (top of this pass) now
2787 // recomputes edges from the TERMINATORS, the
2788 // oracle re-runs GREEN, and this load is live.
2789 // Two-step slot read (inline cast-then-index
2790 // miscompiles -- gotchas 2026-07-09).
2791 let bptr: i64 = rslot[0]
2792 let body: *BasicBlock = bptr as *BasicBlock
2793 let bidx: i64 = df_block_index(f, body)
2794 // SOURCE must be DOMINATED BY THE HEADER (2026-07-15):
2795 // hoisting instr I (block B) to the preheader P is only
2796 // sound if P dominates B's uses -- true iff H dominates B
2797 // (then P dom H dom B). mark_loop_body's in_loop can
2798 // OVER-mark a block NOT dominated by this header (the
2799 // sha512 sc_ge_l hang: a load in such a block looked
2800 // invariant vs H and was hoisted). The dominance guard
2801 // makes the source exactly the loop region, so with the
2802 // dominance-based invariance the whole pass is sound.
2803 if bidx >= 0 {
2804 if in_loop[bidx] == 1 { if dom_dominates(info, s, body) == 1 {
2805 var cur: *Instr = body.head
2806 while cur != (0 as *Instr) {
2807 let next: *Instr = cur.next
2808 var hb: i64 = licm_is_hoistable(cur.op)
2809 // G9: a loop-invariant non-escaping
2810 // scalar-alloca LOAD is hoistable too.
2811 // (Bisected clear of the ed25519 hang, which
2812 // was stale .parent (T#opt-003), not G9; the
2813 // flag stays as a lever, default ON.)
2814 if LICM_G9_LOAD_HOIST == 1 { if hb == 0 { hb = licm_load_hoistable(f, cur, info, s) } }
2815 if hb == 1 {
2816 if operands_are_loop_invariant(f, cur, in_loop, info, s) == 1 {
2817 hoist_instr(cur, preheader)
2818 any_hoisted = 1
2819 }
2820 }
2821 cur = next
2822 }
2823 } }
2824 }
2825 ri = ri + 1
2826 }
2827 }
2828 }
2829 }
2830 }
2831 }
2832 k = k + 1
2833 }
2834 return any_hoisted
2835}
2836
2837// Is `op` a pure non-trapping opcode safe to hoist?
2838func licm_is_hoistable(op: i64) -> i64 {
2839 if op == OP_ADD { return 1 }
2840 if op == OP_SUB { return 1 }
2841 if op == OP_MUL { return 1 }
2842 if op == OP_AND { return 1 }
2843 if op == OP_OR { return 1 }
2844 if op == OP_XOR { return 1 }
2845 if op == OP_SHL { return 1 }
2846 if op == OP_SHR_S { return 1 }
2847 if op == OP_SHR_U { return 1 }
2848 if op == OP_NEG { return 1 }
2849 if op == OP_NOT { return 1 }
2850 if op == OP_EQ { return 1 }
2851 if op == OP_NE { return 1 }
2852 if op == OP_LT_S { return 1 }
2853 if op == OP_LE_S { return 1 }
2854 if op == OP_GT_S { return 1 }
2855 if op == OP_GE_S { return 1 }
2856 if op == OP_COPY { return 1 }
2857 if op == OP_GEP { return 1 }
2858 // Explicitly NOT hoistable:
2859 // OP_DIV_S/U, OP_REM_S/U (trap on zero)
2860 // OP_LOAD, OP_STORE, OP_CALL (memory / side effects)
2861 // OP_ALLOCA (stack-position-dependent)
2862 // OP_BR, OP_BR_COND, OP_RETURN
2863 return 0
2864}
2865
2866// ===== G9 (2026-07-15): loop-invariant SCALAR-ALLOCA LOAD hoisting =====
2867// The matmul residual: `i*N` (imulq $192) stays in the k-loop because it is
2868// MUL(LOAD(i_alloca), 192) and LOAD is not hoistable -> the MUL's operand is
2869// "defined in the loop" -> not invariant. Root = the 2026-05-29 plan's DEEPEST
2870// FINDING: loop-carried state lives in ALLOCAS (SSA-no-phi mem2reg can't promote
2871// them). Fix: hoist LOAD(alloca) when the alloca is a scalar that NEVER escapes
2872// (address only ever op0 of LOAD/STORE) AND is NOT stored inside the loop -- then
2873// the value is provably loop-invariant, and the dependent MUL hoists after it in
2874// the same RPO pass. Aliasing-safe by construction: a non-escaping alloca has no
2875// pointer alias, so the only writes are direct STOREs to it, all checked.
2876
2877// k-th operand (op0..op23), or -1 past n_operands.
2878func opt_opk(inst: *Instr, k: i64) -> i64 {
2879 if k == 0 { return inst.op0 }
2880 if k == 1 { return inst.op1 }
2881 if k == 2 { return inst.op2 }
2882 if k == 3 { return inst.op3 }
2883 if k == 4 { return inst.op4 }
2884 if k == 5 { return inst.op5 }
2885 if k == 6 { return inst.op6 }
2886 if k == 7 { return inst.op7 }
2887 if k == 8 { return inst.op8 }
2888 if k == 9 { return inst.op9 }
2889 if k == 10 { return inst.op10 }
2890 if k == 11 { return inst.op11 }
2891 if k == 12 { return inst.op12 }
2892 if k == 13 { return inst.op13 }
2893 if k == 14 { return inst.op14 }
2894 if k == 15 { return inst.op15 }
2895 if k == 16 { return inst.op16 }
2896 if k == 17 { return inst.op17 }
2897 if k == 18 { return inst.op18 }
2898 if k == 19 { return inst.op19 }
2899 if k == 20 { return inst.op20 }
2900 if k == 21 { return inst.op21 }
2901 if k == 22 { return inst.op22 }
2902 if k == 23 { return inst.op23 }
2903 return 0 - 1
2904}
2905
2906// The alloca `aid` NEVER escapes: it appears as an operand ONLY at (OP_LOAD,k=0)
2907// or (OP_STORE,k=0). Any other position (STORE value, GEP base, ADDR_OF, call
2908// arg, ...) means a pointer to it could exist -> a store could alias -> unsafe.
2909func licm_alloca_noescape(f: *Function, aid: i64) -> i64 {
2910 var bi: i64 = 0
2911 while bi < f.n_blocks {
2912 let b: *BasicBlock = block_at(f, bi)
2913 var inst: *Instr = b.head
2914 while inst != (0 as *Instr) {
2915 var no: i64 = inst.n_operands
2916 if no > 24 { no = 24 }
2917 var k: i64 = 0
2918 while k < no {
2919 if opt_opk(inst, k) == aid {
2920 var okpos: i64 = 0
2921 if inst.op == OP_LOAD { if k == 0 { okpos = 1 } }
2922 if inst.op == OP_STORE { if k == 0 { okpos = 1 } }
2923 if okpos == 0 { return 0 }
2924 }
2925 k = k + 1
2926 }
2927 inst = inst.next
2928 }
2929 bi = bi + 1
2930 }
2931 return 1
2932}
2933
2934// Is there a STORE to alloca `aid` in any block DOMINATED BY THE HEADER (i.e.
2935// inside/below the loop)? DOMINANCE-BASED (2026-07-15): sound independent of
2936// mark_loop_body -- if a store to the counter is missed (as mark_loop_body did),
2937// G9 would hoist LOAD(counter) -> the counter freezes -> the loop hangs. The
2938// over-approximation (a store in a block dominated by the header but outside the
2939// loop still blocks the hoist) is CONSERVATIVE -- it can only refuse a valid
2940// hoist, never allow an invalid one.
2941func licm_store_dominated_by_header(f: *Function, aid: i64, info: *DomInfoFn,
2942 header: *BasicBlock) -> i64 {
2943 var bi: i64 = 0
2944 while bi < f.n_blocks {
2945 let b: *BasicBlock = block_at(f, bi)
2946 if dom_dominates(info, header, b) == 1 {
2947 var inst: *Instr = b.head
2948 while inst != (0 as *Instr) {
2949 if inst.op == OP_STORE { if inst.op0 == aid { return 1 } }
2950 inst = inst.next
2951 }
2952 }
2953 bi = bi + 1
2954 }
2955 return 0
2956}
2957
2958// A LOAD is hoistable iff its address op0 is a NON-ESCAPING scalar-alloca result
2959// with NO store inside the loop (so the loaded value is loop-invariant).
2960func licm_load_hoistable(f: *Function, inst: *Instr, info: *DomInfoFn,
2961 header: *BasicBlock) -> i64 {
2962 if inst.op != OP_LOAD { return 0 }
2963 let aid: i64 = inst.op0
2964 if aid < 0 { return 0 }
2965 if aid >= f.n_values { return 0 }
2966 let av: *Value = val_at(f, aid)
2967 if av.kind != VK_INSTR { return 0 }
2968 if av.instr == (0 as *Instr) { return 0 }
2969 if av.instr.op != OP_ALLOCA { return 0 }
2970 if licm_alloca_noescape(f, aid) == 0 { return 0 }
2971 if licm_store_dominated_by_header(f, aid, info, header) == 1 { return 0 }
2972 return 1
2973}
2974
2975// Returns 1 iff every operand of inst is loop-invariant.
2976// DOMINANCE-BASED (2026-07-15): an operand is loop-VARIANT iff its def-block is
2977// dominated by the loop HEADER (i.e. defined inside/below the loop). This uses
2978// the PROVEN-CORRECT dominance (nx_dom_oracle_gate GREEN, 683,696 pairs), so it
2979// is sound INDEPENDENT of mark_loop_body's exactness -- the old in_loop check
2980// was unsound because mark_loop_body under-marks loop-body blocks on some
2981// post-opt CFGs (sha512 sc_ge_l: loop-body loads had in_loop=0 -> looked
2982// invariant -> the whole loop body + counter hoisted -> the loop froze -> hang).
2983// A def ABOVE the loop (header does NOT dominate it, incl. the preheader) is
2984// invariant; a hoisted def now lives in the preheader (header doesn't dominate
2985// it) so the transitive-invariance cascade still resolves in one RPO pass.
2986// (in_loop is kept only as the hoist SOURCE set -- iterating fewer blocks than
2987// the true loop is safe; it can only MISS hoists, never enable a bad one.)
2988func operands_are_loop_invariant(f: *Function, inst: *Instr,
2989 in_loop: *u8, info: *DomInfoFn,
2990 header: *BasicBlock) -> i64 {
2991 var k: i64 = 0
2992 while k < inst.n_operands {
2993 // Cover op0..op15. Prior was op0..op3 only -- LICM thought
2994 // op4..op15 were always loop-invariant (since it always read
2995 // op0), enabling false-positive hoists. Partner to commits
2996 // 8efd1949 / 0ac961db / 2e03449b / ba2999b3.
2997 var op: i64 = inst.op0
2998 if k == 1 { op = inst.op1 }
2999 if k == 2 { op = inst.op2 }
3000 if k == 3 { op = inst.op3 }
3001 if k == 4 { op = inst.op4 }
3002 if k == 5 { op = inst.op5 }
3003 if k == 6 { op = inst.op6 }
3004 if k == 7 { op = inst.op7 }
3005 if k == 8 { op = inst.op8 }
3006 if k == 9 { op = inst.op9 }
3007 if k == 10 { op = inst.op10 }
3008 if k == 11 { op = inst.op11 }
3009 if k == 12 { op = inst.op12 }
3010 if k == 13 { op = inst.op13 }
3011 if k == 14 { op = inst.op14 }
3012 if k == 15 { op = inst.op15 }
3013 if k == 16 { op = inst.op16 }
3014 if k == 17 { op = inst.op17 }
3015 if k == 18 { op = inst.op18 }
3016 if k == 19 { op = inst.op19 }
3017 if k == 20 { op = inst.op20 }
3018 if k == 21 { op = inst.op21 }
3019 if k == 22 { op = inst.op22 }
3020 if k == 23 { op = inst.op23 }
3021 // SOUNDNESS FLIP (2026-06-10): the old shape treated every
3022 // UNRESOLVABLE case (missing v.instr link, missing .parent,
3023 // df_block_index -1 from a stale parent pointer) as
3024 // INVARIANT -- optimist-unsound. nx_cc_equiv_gate caught it
3025 // the moment the dormant RPO hoist awakened: a loop-counter
3026 // increment in lookup_struct was hoisted to the preheader and
3027 // gen2 lost type-alias resolution. Rule now: CONST / PARAM /
3028 // GLOBAL are invariant by kind; a VK_INSTR operand is
3029 // invariant ONLY when its defining block is positively known
3030 // and positively outside the loop. Unknown = assume in-loop.
3031 if op >= f.n_values { return 0 }
3032 let v: *Value = val_at(f, op)
3033 if v.kind == VK_INSTR {
3034 // BISECT-2026-07-15: conservative probe. Treat ANY instruction
3035 // operand as variant -> only expressions of loop-CONSTANT inputs
3036 // (const/param/global) hoist. Such a hoist is provably safe (no
3037 // use-before-def, no loop-derived value). If ed25519 STILL hangs
3038 // under this, the bug is LICM EXPOSING a latent regalloc/schedule
3039 // bug, not a wrong-hoist -- opposite fixes.
3040 if LICM_CONSERVATIVE == 1 { return 0 }
3041 if v.instr == (0 as *Instr) { return 0 }
3042 let def_bb: *BasicBlock = v.instr.parent
3043 if def_bb == (0 as *BasicBlock) { return 0 }
3044 // CONSERVATIVE (2026-07-15): an UNRESOLVABLE def-block (df_block_index
3045 // == -1, i.e. a stale/dangling parent pointer left by an earlier pass)
3046 // must be treated as VARIANT -- NOT invariant. This was THE bug: the
3047 // dominance rewrite dropped the old `di<0 -> return 0` guard, so
3048 // dom_dominates (which returns 0 for an unfindable block) made the
3049 // operand look invariant -> la_emit_i64's `while v>0` condition (op
3050 // with a -1 def-block operand) was hoisted -> the condition froze ->
3051 // infinite loop. Minimal repro: runtime/nx_licm_adversary.nx.
3052 if df_block_index(f, def_bb) < 0 { return 0 }
3053 // VARIANT iff the header dominates the def block (def in/below loop).
3054 if dom_dominates(info, header, def_bb) == 1 { return 0 }
3055 }
3056 k = k + 1
3057 }
3058 return 1
3059}
3060
3061// Unlink inst from its current block and append to preheader just
3062// before the terminator (preheader has exactly one successor so
3063// terminator is a single OP_BR). Updates parent pointer.
3064func hoist_instr(inst: *Instr, preheader: *BasicBlock) -> i64 {
3065 let src: *BasicBlock = inst.parent
3066 // Unlink from src's list.
3067 if inst.prev != (0 as *Instr) {
3068 inst.prev.next = inst.next
3069 } else {
3070 src.head = inst.next
3071 }
3072 if inst.next != (0 as *Instr) {
3073 inst.next.prev = inst.prev
3074 } else {
3075 src.tail = inst.prev
3076 }
3077 inst.prev = 0 as *Instr
3078 inst.next = 0 as *Instr
3079
3080 // Append before preheader's terminator.
3081 let term: *Instr = preheader.tail
3082 if term != (0 as *Instr) {
3083 inst.prev = term.prev
3084 inst.next = term
3085 if term.prev != (0 as *Instr) {
3086 term.prev.next = inst
3087 } else {
3088 preheader.head = inst
3089 }
3090 term.prev = inst
3091 } else {
3092 preheader.head = inst
3093 preheader.tail = inst
3094 }
3095 inst.parent = preheader
3096 return 0
3097}
3098
3099// Walk back through preds from `from`, marking every block reached
3100// (without crossing `header`) in in_loop = the natural loop body.
3101// ITERATIVE worklist (2026-07-15): the old RECURSIVE form recursed once per
3102// loop-body block; a large loop (sha512/ed25519 field arithmetic has 20-40+
3103// block loops) overflowed the NishiLang stack, silently UNDER-marking the body
3104// -> operands defined in the unmarked tail looked loop-invariant -> the loop
3105// counter/condition got hoisted -> the loop froze (sc_ge_l collapsed, ed25519
3106// rejection loop hung). Iteration has no depth limit; the marked set is
3107// identical for small loops (byte-stable) and COMPLETE for large ones.
3108func mark_loop_body_nx(f: *Function, header: *BasicBlock,
3109 from: *BasicBlock, in_loop: *u8) -> i64 {
3110 let n: i64 = f.n_blocks
3111 let wl_raw: *u8 = sys_mmap(n * 8 + 16)
3112 let wl: *i64 = wl_raw as *i64
3113 var wh: i64 = 0
3114 var wt: i64 = 0
3115 let fi: i64 = df_block_index(f, from)
3116 if fi < 0 { return 0 }
3117 if in_loop[fi] == 0 { in_loop[fi] = 1; wl[wt] = fi; wt = wt + 1 }
3118 while wh < wt {
3119 let ci: i64 = wl[wh]
3120 wh = wh + 1
3121 let cb: *BasicBlock = block_at(f, ci)
3122 // The header is in the body but we do NOT walk above it.
3123 if cb != header {
3124 let p0: *BasicBlock = cb.pred0
3125 if p0 != (0 as *BasicBlock) {
3126 let pi: i64 = df_block_index(f, p0)
3127 if pi >= 0 { if in_loop[pi] == 0 { in_loop[pi] = 1; wl[wt] = pi; wt = wt + 1 } }
3128 }
3129 let p1: *BasicBlock = cb.pred1
3130 if p1 != (0 as *BasicBlock) {
3131 let pi1: i64 = df_block_index(f, p1)
3132 if pi1 >= 0 { if in_loop[pi1] == 0 { in_loop[pi1] = 1; wl[wt] = pi1; wt = wt + 1 } }
3133 }
3134 let p2: *BasicBlock = cb.pred2
3135 if p2 != (0 as *BasicBlock) {
3136 let pi2: i64 = df_block_index(f, p2)
3137 if pi2 >= 0 { if in_loop[pi2] == 0 { in_loop[pi2] = 1; wl[wt] = pi2; wt = wt + 1 } }
3138 }
3139 }
3140 }
3141 return 0
3142}
3143
3144// ===== VRA-guided comparison folding =================================
3145//
3146// When VRA gives us known ranges [a_lo, a_hi] for x and [b_lo, b_hi]
3147// for y, many comparisons collapse to constants:
3148//
3149// x < y: true if a_hi < b_lo
3150// false if a_lo >= b_hi
3151// x > y: true if a_lo > b_hi
3152// false if a_hi <= b_lo
3153// x == y: false if ranges don't overlap
3154// x != y: true if ranges don't overlap
3155//
3156// SSA + VRA makes this cheap to check. Catches patterns like:
3157// let i = 0
3158// while i < 10 { ... i = i + 1 } -- i known in [0, 10]
3159// if i == 20 { ... } -- dead branch
3160// which GVN + SCCP miss because they don't track ranges.
3161//
3162// Returns the count of rewrites.
3163
3164// Struct + forward decls so opt_compare_fold can reference VRA.
3165// The body is below this section (order preserved for readability).
3166struct VraResult {
3167 min: *i64,
3168 max: *i64,
3169 known: *i64,
3170 n: i64,
3171}
3172func opt_vra_compute(f: *Function) -> *VraResult;
3173func vra_is_nonneg(vra: *VraResult, v: i64) -> i64;
3174
3175func opt_compare_fold(f: *Function) -> i64 {
3176 var changed: i64 = 0
3177 let vra: *VraResult = opt_vra_compute(f)
3178 var bi: i64 = 0
3179 while bi < f.n_blocks {
3180 let b: *BasicBlock = block_at(f, bi)
3181 var inst: *Instr = b.head
3182 while inst != (0 as *Instr) {
3183 if inst.n_operands == 2 {
3184 let a: i64 = inst.op0
3185 let y: i64 = inst.op1
3186 if a < vra.n {
3187 if y < vra.n {
3188 if vra.known[a] == 1 {
3189 if vra.known[y] == 1 {
3190 let a_lo: i64 = vra.min[a]
3191 let a_hi: i64 = vra.max[a]
3192 let b_lo: i64 = vra.min[y]
3193 let b_hi: i64 = vra.max[y]
3194 // OP_LT_S = 22
3195 if inst.op == 22 {
3196 if a_hi < b_lo {
3197 rewrite_to_copy(inst, const_one(f))
3198 changed = changed + 1
3199 }
3200 if a_lo >= b_hi {
3201 make_result_zero(f, inst)
3202 changed = changed + 1
3203 }
3204 }
3205 // OP_GT_S = 24
3206 if inst.op == 24 {
3207 if a_lo > b_hi {
3208 rewrite_to_copy(inst, const_one(f))
3209 changed = changed + 1
3210 }
3211 if a_hi <= b_lo {
3212 make_result_zero(f, inst)
3213 changed = changed + 1
3214 }
3215 }
3216 // OP_EQ = 20
3217 if inst.op == 20 {
3218 if a_hi < b_lo { make_result_zero(f, inst); changed = changed + 1 }
3219 if a_lo > b_hi { make_result_zero(f, inst); changed = changed + 1 }
3220 }
3221 // OP_NE = 21
3222 if inst.op == 21 {
3223 if a_hi < b_lo { rewrite_to_copy(inst, const_one(f)); changed = changed + 1 }
3224 if a_lo > b_hi { rewrite_to_copy(inst, const_one(f)); changed = changed + 1 }
3225 }
3226 }
3227 }
3228 }
3229 }
3230 }
3231 inst = inst.next
3232 }
3233 bi = bi + 1
3234 }
3235 return changed
3236}
3237
3238// ===== value-range analysis (VRA) ===================================
3239//
3240// For each SSA Value, compute (known_min, known_max, is_known).
3241// Constants carry exact ranges. Arithmetic propagates. Unknown
3242// or unsupported ops leave the value unknown.
3243//
3244// Consumers (to be added in future commits):
3245// - opt_strength_reduce: x / 2^k safe to replace with x >> k
3246// only when x >= 0 -- VRA tells us when that's true
3247// - opt_bounds_check_elim: drop array bounds checks when index
3248// is provably within range
3249// - opt_compare_fold: x < y reduces to const when ranges don't
3250// overlap (x.max < y.min -> always true)
3251// - opt_narrow: i64 ops on values that fit in i32 -> faster
3252// int32 ops when target supports
3253//
3254// Storage: parallel arrays keyed by value_id, sized n_values.
3255// vra_min[v] -- known lower bound (i64)
3256// vra_max[v] -- known upper bound (i64)
3257// vra_known[v] -- 1 if bounded, 0 if unknown / unbounded
3258//
3259// Returns: pointer to a VraResult struct the caller owns.
3260
3261// Compute VRA for every Value in f. Single forward pass in SSA
3262// definition order; for straight-line code this achieves the full
3263// fixed-point in one pass. Loops + PHIs stay unknown (no proper
3264// widening operator yet -- v0.1.0 adds Cousot widening).
3265func opt_vra_compute(f: *Function) -> *VraResult {
3266 let res_raw: *u8 = sys_mmap(64)
3267 let res: *VraResult = res_raw as *VraResult
3268 res.min = sys_mmap(f.n_values * 8 + 64) as *i64
3269 res.max = sys_mmap(f.n_values * 8 + 64) as *i64
3270 res.known = sys_mmap(f.n_values * 8 + 64) as *i64
3271 res.n = f.n_values
3272
3273 // Seed: constants get exact range; everything else unknown.
3274 var v: i64 = 0
3275 let base: i64 = f.values as i64
3276 while v < f.n_values {
3277 let vv: *Value = (base + v * 48) as *Value
3278 if vv.kind == VAL_CONST {
3279 res.min[v] = vv.const_int
3280 res.max[v] = vv.const_int
3281 res.known[v] = 1
3282 }
3283 if vv.kind != VAL_CONST {
3284 res.known[v] = 0
3285 }
3286 v = v + 1
3287 }
3288
3289 // Walk instructions in block order; propagate through SSA.
3290 var bi: i64 = 0
3291 while bi < f.n_blocks {
3292 let b: *BasicBlock = block_at(f, bi)
3293 var inst: *Instr = b.head
3294 while inst != (0 as *Instr) {
3295 if inst.result < f.n_values {
3296 let r: i64 = inst.result
3297 if inst.n_operands == 2 {
3298 let a: i64 = inst.op0
3299 let bv: i64 = inst.op1
3300 if res.known[a] == 1 {
3301 if res.known[bv] == 1 {
3302 let amin: i64 = res.min[a]
3303 let amax: i64 = res.max[a]
3304 let bmin: i64 = res.min[bv]
3305 let bmax: i64 = res.max[bv]
3306 // ADD: [a_min+b_min, a_max+b_max]
3307 if inst.op == 1 {
3308 res.min[r] = amin + bmin
3309 res.max[r] = amax + bmax
3310 res.known[r] = 1
3311 }
3312 // SUB: [a_min-b_max, a_max-b_min]
3313 if inst.op == 2 {
3314 res.min[r] = amin - bmax
3315 res.max[r] = amax - bmin
3316 res.known[r] = 1
3317 }
3318 // AND with non-negative mask: result fits
3319 // in [0, mask]. Limited scope; extended
3320 // in v0.1.0.
3321 if inst.op == 10 {
3322 if bmin >= 0 {
3323 res.min[r] = 0
3324 res.max[r] = bmax
3325 res.known[r] = 1
3326 }
3327 }
3328 // OR with non-negative: [max(a_min, b_min), a_max | b_max]
3329 // Conservative: use sum as upper bound.
3330 if inst.op == 11 {
3331 if amin >= 0 {
3332 if bmin >= 0 {
3333 res.min[r] = amin
3334 if bmin > amin { res.min[r] = bmin }
3335 res.max[r] = amax + bmax
3336 res.known[r] = 1
3337 }
3338 }
3339 }
3340 }
3341 }
3342 }
3343 }
3344 inst = inst.next
3345 }
3346 bi = bi + 1
3347 }
3348 return res
3349}
3350
3351// Query helper: is Value v provably non-negative? Used by future
3352// strength reduction / bounds check elimination passes.
3353func vra_is_nonneg(vra: *VraResult, v: i64) -> i64 {
3354 if v >= vra.n { return 0 }
3355 if vra.known[v] != 1 { return 0 }
3356 if vra.min[v] >= 0 { return 1 }
3357 return 0
3358}
3359
3360// ===== reassociation =================================================
3361//
3362// For commutative/associative ops (ADD, MUL, AND, OR, XOR), rotate
3363// chains so constants cluster to one side where const_fold catches
3364// them in the next round:
3365//
3366// (x + 2) + 3 -> x + (2 + 3) -> const_fold -> x + 5
3367// (c1 * x) * c2 -> x * (c1 * c2) -> const_fold -> x * c
3368// ((x & mask1) & mask2) -> x & (mask1 & mask2) -> const_fold
3369//
3370// Standard LLVM "Reassociate" pass shape. Run BEFORE const_fold
3371// in the pipeline so folded constants feed back into the same
3372// round.
3373//
3374// Detection rule (canonical left-skewed tree):
3375// inst = binop(a, b) where a is binop of same op with const
3376// ^^^ ^^^
3377// inner_l = a's first operand (the variable x)
3378// inner_r = a's second operand (a constant c1)
3379// and
3380// b is a constant (c2)
3381//
3382// Rewrites to:
3383// inst.op0 = inner_l (x)
3384// inst.op1 = (new VAL_CONST with c1 + c2, for ADD/MUL etc.)
3385// Next round's const_fold absorbs the new constant.
3386//
3387// v0.0.1 scope: two-level chains only; deeper chains get rewritten
3388// gradually across multiple rounds of opt_run.
3389
3390func opt_reassoc(f: *Function) -> i64 {
3391 var changed: i64 = 0
3392 var bi: i64 = 0
3393 while bi < f.n_blocks {
3394 let b: *BasicBlock = block_at(f, bi)
3395 var inst: *Instr = b.head
3396 while inst != (0 as *Instr) {
3397 if inst.n_operands == 2 {
3398 let op: i64 = inst.op
3399 // Only commutative+associative binops.
3400 var ok: i64 = 0
3401 if op == 1 { ok = 1 } // ADD
3402 if op == 3 { ok = 1 } // MUL
3403 if op == 10 { ok = 1 } // AND
3404 if op == 11 { ok = 1 } // OR
3405 if op == 12 { ok = 1 } // XOR
3406
3407 if ok == 1 {
3408 let b_val: *Value = val_at(f, inst.op1)
3409 if b_val.kind == VAL_CONST {
3410 let a_val: *Value = val_at(f, inst.op0)
3411 if a_val.kind == VAL_INSTR {
3412 if a_val.instr != (0 as *Instr) {
3413 let inner: *Instr = a_val.instr
3414 if inner.op == op {
3415 if inner.n_operands == 2 {
3416 let ir: *Value = val_at(f, inner.op1)
3417 if ir.kind == VAL_CONST {
3418 // Pattern match: (x op c1) op c2
3419 // Rewrite to x op (c1 op c2).
3420 let folded: i64 = comptime_eval_binop(op, ir.const_int, b_val.const_int)
3421 // Replace b_val's const_int with folded; reuse
3422 // the existing Value to avoid ir_new_const complexity.
3423 b_val.const_int = folded
3424 inst.op0 = inner.op0
3425 changed = changed + 1
3426 }
3427 }
3428 }
3429 }
3430 }
3431 }
3432 }
3433 }
3434 inst = inst.next
3435 }
3436 bi = bi + 1
3437 }
3438 return changed
3439}
3440
3441// ===== block merging =================================================
3442//
3443// When block A's sole successor is B and B's sole predecessor is A,
3444// concatenate B's instructions into A and unlink B. Standard CFG
3445// simplification used by every optimizer (LLVM SimplifyCFG, gcc
3446// cfgcleanup); enables further optimizations because previously-
3447// cross-block redundancies become same-block (visible to GVN, CSE,
3448// DSE, etc).
3449//
3450// Conservative scope:
3451// - A must end with an unconditional branch to B (OP_BR)
3452// - B must have exactly one predecessor (== A)
3453// - B must not be the function entry
3454//
3455// Returns the count of merges performed.
3456
3457func opt_block_merge(f: *Function) -> i64 {
3458 var changed: i64 = 0
3459 var ai: i64 = 0
3460 while ai < f.n_blocks {
3461 let a: *BasicBlock = block_at(f, ai)
3462 if a.tail == (0 as *Instr) { ai = ai + 1; continue }
3463 if a.tail.op != OP_BR { ai = ai + 1; continue }
3464 // OP_BR's op0 is the target block id.
3465 let b_id: i64 = a.tail.op0
3466 if b_id < 0 { ai = ai + 1; continue }
3467 if b_id >= f.n_blocks { ai = ai + 1; continue }
3468 let b: *BasicBlock = block_at(f, b_id)
3469 if b == f.entry { ai = ai + 1; continue }
3470 // B must have exactly one predecessor.
3471 if b.n_preds != 1 { ai = ai + 1; continue }
3472 if b.pred0 != a { ai = ai + 1; continue }
3473
3474 // Drop A's terminating OP_BR.
3475 unlink(a, a.tail)
3476 // Splice B's instruction list onto A's.
3477 if b.head != (0 as *Instr) {
3478 if a.tail == (0 as *Instr) {
3479 a.head = b.head
3480 b.head.prev = 0 as *Instr
3481 } else {
3482 a.tail.next = b.head
3483 b.head.prev = a.tail
3484 }
3485 a.tail = b.tail
3486 }
3487 // Re-point B's successors' predecessors at A.
3488 if b.succ0 != (0 as *BasicBlock) {
3489 if b.succ0.pred0 == b { b.succ0.pred0 = a }
3490 if b.succ0.pred1 == b { b.succ0.pred1 = a }
3491 if b.succ0.pred2 == b { b.succ0.pred2 = a }
3492 }
3493 if b.succ1 != (0 as *BasicBlock) {
3494 if b.succ1.pred0 == b { b.succ1.pred0 = a }
3495 if b.succ1.pred1 == b { b.succ1.pred1 = a }
3496 if b.succ1.pred2 == b { b.succ1.pred2 = a }
3497 }
3498 // Adopt B's successor edges.
3499 a.n_succs = b.n_succs
3500 a.succ0 = b.succ0
3501 a.succ1 = b.succ1
3502 // B is now empty + unreachable; sweep_unreachable_function
3503 // will clean it up next round.
3504 b.head = 0 as *Instr
3505 b.tail = 0 as *Instr
3506 b.n_succs = 0
3507 b.n_preds = 0
3508 changed = changed + 1
3509 ai = ai + 1
3510 }
3511 return changed
3512}
3513
3514// ===== dead store elimination (intra-block) =========================
3515//
3516// Removes stores whose value is overwritten before any read.
3517// Per-block scope only -- inter-block needs MUST-alias analysis
3518// + dataflow which is v0.1.0 work.
3519//
3520// Tracks: address-Value -> last STORE instruction we saw to it.
3521// On a second STORE to the same address with no intervening LOAD,
3522// the previous store is dead. CALL / SYSCALL / TAIL_CALL kill
3523// the entire tracking map (could write anywhere via aliasing).
3524//
3525// Returns the number of dead stores removed.
3526
3527const DSE_MAX_TRACKED: i64 = 256
3528
3529func opt_dse(f: *Function) -> i64 {
3530 var removed: i64 = 0
3531 let track_addrs_raw: *u8 = sys_mmap(DSE_MAX_TRACKED * 8 + 16)
3532 let track_addrs: *i64 = track_addrs_raw as *i64
3533 let track_insts_raw: *u8 = sys_mmap(DSE_MAX_TRACKED * 8 + 16)
3534 let track_insts: *i64 = track_insts_raw as *i64
3535
3536 var bi: i64 = 0
3537 while bi < f.n_blocks {
3538 let bb: *BasicBlock = block_at(f, bi)
3539 var n_track: i64 = 0
3540 var inst: *Instr = bb.head
3541 while inst != (0 as *Instr) {
3542 let next_inst: *Instr = inst.next
3543 if inst.op == OP_STORE {
3544 let addr_id: i64 = inst.op0
3545 // Look up addr in tracked.
3546 var found: i64 = -1
3547 var ti: i64 = 0
3548 while ti < n_track {
3549 if track_addrs[ti] == addr_id { found = ti }
3550 ti = ti + 1
3551 }
3552 if found >= 0 {
3553 // Previous store to same address is dead.
3554 let prev_addr: i64 = track_insts[found]
3555 let prev_inst: *Instr = prev_addr as *Instr
3556 unlink(bb, prev_inst)
3557 track_insts[found] = inst as i64
3558 removed = removed + 1
3559 }
3560 if found < 0 {
3561 if n_track < DSE_MAX_TRACKED {
3562 track_addrs[n_track] = addr_id
3563 track_insts[n_track] = inst as i64
3564 n_track = n_track + 1
3565 }
3566 }
3567 }
3568 if inst.op == OP_LOAD {
3569 let addr_id: i64 = inst.op0
3570 // Read from this address -- the most recent store
3571 // is now necessary, drop it from tracking (the
3572 // value's been observed).
3573 var ti: i64 = 0
3574 while ti < n_track {
3575 if track_addrs[ti] == addr_id {
3576 // Remove by swap with last.
3577 track_addrs[ti] = track_addrs[n_track - 1]
3578 track_insts[ti] = track_insts[n_track - 1]
3579 n_track = n_track - 1
3580 ti = n_track // exit loop
3581 }
3582 ti = ti + 1
3583 }
3584 }
3585 // Calls + syscalls + tail-calls are arbitrary writers;
3586 // discard everything we were tracking.
3587 if inst.op == OP_CALL { n_track = 0 }
3588 if inst.op == OP_CALL_INDIRECT { n_track = 0 }
3589 if inst.op == OP_SYSCALL { n_track = 0 }
3590 if inst.op == OP_TAIL_CALL { n_track = 0 }
3591 // G3 FIX-1: __adc_acc reads+writes acc[0..2] through its pointer; DSE
3592 // is blind to it -> treat as an arbitrary memory writer (barrier).
3593 if inst.op == OP_ADC_ACC { n_track = 0 }
3594 inst = next_inst
3595 }
3596 bi = bi + 1
3597 }
3598 return removed
3599}
3600
3601// ===== strength reduction ==========================================
3602//
3603// Rewrites expensive ops with constant power-of-two operands as
3604// cheaper shift / mask ops:
3605//
3606// x * (1 << k) -> x << k (any k, any sign)
3607// x * 1 -> x (identity; already in simplify)
3608// x * 0 -> 0 (already in simplify)
3609// x / (1 << k) -> x >> k (signed arithmetic shift)
3610// x % (1 << k) -> x & ((1<<k) - 1) (unsigned)
3611//
3612// Standard since Wulf & Lo's BLISS-11 compiler (1972). Modern
3613// compilers also do (a+b) * c -> a*c + b*c (distributive
3614// strength reduction in loops); we don't yet -- v0.1.0 work.
3615//
3616// Returns the count of rewrites.
3617
3618// True iff `n` is exactly 2^k for some k >= 1. Returns the k value
3619// in `*shift_out`; returns 0 if n isn't a power-of-two we can
3620// usefully reduce (excludes 1 and 0; those are simpler-pass cases).
3621func is_pow2(n: i64, shift_out: *i64) -> i64 {
3622 if n <= 1 { return 0 }
3623 var v: i64 = n
3624 var k: i64 = 0
3625 while v > 1 {
3626 if v & 1 { return 0 } // odd bit before reaching 1 -> not pow2
3627 v = v >> 1
3628 k = k + 1
3629 }
3630 *shift_out = k
3631 return 1
3632}
3633
3634func opt_strength_reduce(f: *Function) -> i64 {
3635 var changed: i64 = 0
3636 // Compute VRA once per pass; consulting vra_is_nonneg lets us
3637 // safely turn signed-division-by-pow-2 into arithmetic-shift
3638 // only on values proven >= 0.
3639 let vra: *VraResult = opt_vra_compute(f)
3640 var bi: i64 = 0
3641 while bi < f.n_blocks {
3642 let bb: *BasicBlock = block_at(f, bi)
3643 var inst: *Instr = bb.head
3644 while inst != (0 as *Instr) {
3645 let next_inst: *Instr = inst.next
3646 if inst.n_operands == 2 {
3647 // Try to find a constant operand to reduce against.
3648 let op0v: *Value = val_at(f, inst.op0)
3649 let op1v: *Value = val_at(f, inst.op1)
3650 var const_id: i64 = -1
3651 var var_id: i64 = -1
3652 var const_val: i64 = 0
3653 var const_first: i64 = 0
3654 if op1v.kind == VAL_CONST {
3655 const_id = inst.op1
3656 var_id = inst.op0
3657 const_val = op1v.const_int
3658 }
3659 if op1v.kind != VAL_CONST {
3660 if op0v.kind == VAL_CONST {
3661 const_id = inst.op0
3662 var_id = inst.op1
3663 const_val = op0v.const_int
3664 const_first = 1
3665 }
3666 }
3667 if const_id >= 0 {
3668 let shift_raw: *u8 = sys_mmap(16)
3669 let shift_p: *i64 = shift_raw as *i64
3670 *shift_p = 0
3671 let pow: i64 = is_pow2(const_val, shift_p)
3672 if pow == 1 {
3673 if inst.op == OP_MUL {
3674 // x * 2^k -> x << k (always safe). CORRECTNESS FIX
3675 // (silent miscompile, 2026-06-02): allocate a FRESH
3676 // constant for the shift amount. Mutating the shared
3677 // multiplier constant in place (const_int = shift)
3678 // corrupted EVERY OTHER instruction referencing the
3679 // same deduplicated constant Value -> e.g. the video
3680 // codec's img_w = 16*8 read back as 7. Constants are
3681 // shared; never mutate one in place.
3682 let shamt_id: i64 = ir_const_i64(f, *shift_p)
3683 inst.op = OP_SHL
3684 inst.op0 = var_id
3685 inst.op1 = shamt_id
3686 changed = changed + 1
3687 }
3688 if inst.op == OP_DIV_S {
3689 // x / 2^k -> x >> k ONLY when x is
3690 // provably non-negative. Signed shift
3691 // of a negative value differs from
3692 // signed division by 1 (rounding
3693 // direction). VRA gates the rewrite.
3694 if const_first == 0 {
3695 if vra_is_nonneg(vra, var_id) == 1 {
3696 // FRESH shift constant (same fix as MUL->SHL):
3697 // never mutate the shared divisor constant.
3698 let shamt_id: i64 = ir_const_i64(f, *shift_p)
3699 inst.op = OP_SHR_S
3700 inst.op0 = var_id
3701 inst.op1 = shamt_id
3702 changed = changed + 1
3703 }
3704 }
3705 }
3706 }
3707 }
3708 }
3709 inst = next_inst
3710 }
3711 bi = bi + 1
3712 }
3713 return changed
3714}
3715
3716// ===== tail-call optimisation =======================================
3717//
3718// Transforms `result = CALL X(args); RETURN result` at the end of a
3719// basic block into `TAIL_CALL X(args)`. Callee returns directly to
3720// our caller, eliminating one stack frame -- recursive functions
3721// run in O(1) stack depth.
3722//
3723// This is a classic 1990s-era pass (Steele 1977; Appel SML/NJ
3724// 1990s). Modern compilers (LLVM, gcc) do a more general version
3725// that handles tail-position calls anywhere in the CFG; v0.0.1 only
3726// catches the textbook last-block case.
3727//
3728// NishiLang IR already has OP_TAIL_CALL (types.nx const = 34) and
3729// runtime/riscv.nx has rv_emit_tail_call (since the parser used to
3730// detect this pattern in a few hand-coded sites). This pass turns
3731// every qualifying CALL into a TAIL_CALL automatically.
3732//
3733// Returns the number of rewrites.
3734
3735func opt_tail_call(f: *Function) -> i64 {
3736 var changed: i64 = 0
3737 var bi: i64 = 0
3738 while bi < f.n_blocks {
3739 let bb: *BasicBlock = block_at(f, bi)
3740 let last: *Instr = bb.tail
3741 if last == (0 as *Instr) { bi = bi + 1; continue }
3742 // Last must be a return with one operand (the value being
3743 // returned).
3744 if last.op != OP_RETURN { bi = bi + 1; continue }
3745 if last.n_operands != 1 { bi = bi + 1; continue }
3746 // Penultimate must be a CALL whose result is the same Value
3747 // the return propagates.
3748 let prev: *Instr = last.prev
3749 if prev == (0 as *Instr) { bi = bi + 1; continue }
3750 if prev.op != OP_CALL { bi = bi + 1; continue }
3751 if prev.result != last.op0 { bi = bi + 1; continue }
3752 // SOUNDNESS (sibcall legality, found 2026-06-10): the x86
3753 // tail-call emitter tears down our frame BEFORE the jmp, so
3754 // stack-passed arguments (7th+) cannot be materialized --
3755 // converting a >6-arg call silently DROPPED arg 7 (proven:
3756 // tls13_derive_secret -> tls13_hkdf_expand_label zeroed every
3757 // TLS Derive-Secret; regression gate _arg7_minrepro.nx).
3758 // Same legality rule gcc uses to decline sibcalls.
3759 if prev.n_operands > 6 { bi = bi + 1; continue }
3760 // Indirect calls (no resolved callee) have no jmp target in
3761 // the x86 emitter -- it emits a comment and FALLS THROUGH a
3762 // torn-down frame. Never convert those.
3763 if prev.callee == (0 as *Function) { bi = bi + 1; continue }
3764
3765 // Match: rewrite CALL -> TAIL_CALL, drop the RETURN.
3766 prev.op = OP_TAIL_CALL
3767 prev.next = 0 as *Instr
3768 bb.tail = prev
3769 changed = changed + 1
3770 bi = bi + 1
3771 }
3772 return changed
3773}
3774
3775func opt_run(f: *Function) -> i64 {
3776 var round: i64 = 0
3777 while round < 8 {
3778 var changed: i64 = 0
3779 if opt_mem2reg_simple(f) > 0 { changed = 1 }
3780 if opt_sccp_branches(f) > 0 { changed = 1 }
3781 if opt_const_fold(f) > 0 { changed = 1 }
3782 if opt_copyprop(f) > 0 { changed = 1 }
3783 if opt_copy_forward(f) > 0 { changed = 1 }
3784 if opt_cse(f) > 0 { changed = 1 }
3785 if opt_gvn_block(f) > 0 { changed = 1 }
3786 if opt_licm(f) > 0 { changed = 1 }
3787 // BISECT-DISABLED: if opt_simplify(f) > 0 { changed = 1 }
3788 // STUB(opt, T#selfhost-001): opt_load_forward re-disabled.
3789 // Same out_i64-shape IR (multi-block while-loop, alloca'd
3790 // vars, OP_REM_S/OP_DIV_S) hangs again. The 64-instr
3791 // backward-walk bound is insufficient.
3792 // Plan: rewrite the load-forward backward walk to use a
3793 // dataflow lattice (gen/kill per block) instead of a
3794 // per-load deep traversal; OR accept the missed opt and
3795 // leave it disabled until a sound version lands.
3796 // Closes when: out_i64 self-compiles AND all _offc tests
3797 // in test_offc.sh pass with opt_load_forward enabled.
3798 // RE-ENABLE ATTEMPTED AND REFUSED BY THE GATE, 2026-07-25. Result recorded so the next
3799 // session does not repeat it: with this line live the compiler BUILDS FINE and the bless
3800 // corpus is unchanged at 127 fails -- but nx_selfhost_gauntlet_sov goes
3801 // **RED 11/24**: `deref_read_write` fails to COMPILE and the self-host fixpoint breaks,
3802 // taking every downstream cell with it.
3803 //
3804 // So the disable was correct, but the REASON in the note above is not: there is no hang,
3805 // and there cannot be one -- `walks` increments every iteration and forces stopped=1 at
3806 // the cap. The failure is a MISCOMPILE, not a non-termination. Whoever picks this up
3807 // should debug it as a wrong-value bug on the `deref_read_write` shape
3808 // (`var x; let p = &x; *p = 33; return *p`), where forwarding a store through a pointer
3809 // alias is exactly the unsound case this pass has no alias analysis to rule out.
3810 //
3811 // ATTEMPT 2, 2026-07-26 -- ALSO REFUSED BY THE GATE, but it MOVED THE DIAGNOSIS.
3812 // Added the missing alias rule: forward only from an alloca whose address NEVER
3813 // ESCAPES (reusing licm_alloca_noescape, the predicate G9's load-hoist already uses).
3814 // With no escape, no other value id can name that memory, so id comparison IS a sound
3815 // alias test, and `&x` is excluded by construction.
3816 // RESULT: the WRONG-VALUE bug is GONE -- `deref_read_write` compiles and passes.
3817 // A SEPARATE defect remains: nx_cc itself SEGVs (exit 139, no diagnostic, symbol dump
3818 // stops at `main`) while compiling several bounds-check witnesses, and the self-host
3819 // fixpoint diverges. So this pass has at least TWO defects, and the escape rule fixes
3820 // only the first. Next attempt should debug the CRASH first, in isolation, with the
3821 // escape rule kept -- it is a real improvement even though it does not make the pass
3822 // shippable on its own. Witness: runtime/_bchk/nx_bchk_w2_oob_read.nx.
3823 // if opt_load_forward(f) > 0 { changed = 1 }
3824 if opt_alloca_const(f) > 0 { changed = 1 }
3825 if opt_dce(f) > 0 { changed = 1 }
3826 if opt_thread_jumps(f) > 0 { changed = 1 }
3827 if opt_tail_call(f) > 0 { changed = 1 }
3828 if opt_strength_reduce(f) > 0 { changed = 1 }
3829 if opt_dse(f) > 0 { changed = 1 }
3830 if opt_block_merge(f) > 0 { changed = 1 }
3831 if opt_reassoc(f) > 0 { changed = 1 }
3832 if opt_sweep_unreachable_function(f) > 0 { changed = 1 }
3833 if changed == 0 { return round }
3834 round = round + 1
3835 }
3836 return round
3837}
3838
3839// ===== self-test ====================================================
3840//
3841// Mini-IR construction plus opt pipeline, all at runtime. Verifies:
3842// * (10 + 20) * 3 folds to 90 as CONST_INT
3843// * The three arithmetic instructions get DCE'd
3844// * The return's operand is now a constant Value
3845
3846// Library only; self-test lives in opt_test.nx.