nx_ir.nx source
↩ module page · 1582 lines · 62557 B
1// ir.nx -- nxc2's SSA IR, ported to NishiLang.
2//
3// Mirrors ir.c: Type, Value, Instr, BasicBlock, Function, Module.
4// Uses fixed-capacity arena pools instead of per-object malloc --
5// simple, fast, and no free path needed for a run-once compiler.
6//
7// Layout choices:
8// * Each entity is a struct allocated in a dedicated pool (big
9// up-front arena, indexed by a small int id).
10// * Instructions are linked into their block's doubly-linked list.
11// * Values carry their type as a pointer to a shared Type table.
12// * No TypeTable interning yet -- each call to ir_type_i64 returns
13// the same shared Type pointer.
14//
15// Scope this turn: enough to emit a small function body (consts,
16// binops, returns, branches). Covers the opcodes the fib and
17// const benches need. Extending to the full opset is mechanical.
18//
19// Opcode/TypeKind/ValueKind integer assignments live in types.nx
20// comments and are reproduced in callers that actually emit them.
21
22// Shared IR layouts (Type, Value, Instr, BasicBlock, Function, Module).
23import "nx_types.nx"
24
25// sys_mmap + friends live here; ir.nx's own copy was removed during
26// the module-import refactor to avoid duplicate-symbol clashes at
27// link time.
28//
29// nx_safety_envelope: (schema: nishi-library/seeds/safety-critical-standards.toml)
30// intended_use: "SSA IR builder + Module/Function/BasicBlock/
31// Instr/Value/Type pools. Foundation for the
32// self-host compile pipeline (parse -> ir ->
33// opt -> regalloc -> riscv)."
34// sil_target: SIL2 (IR shape correctness affects every
35// downstream pass)
36// asil_target: QM
37// dal_target: DAL B
38// iec_62304_class: NONE
39// evidence: [no_floating_point_in_pool_management,
40// fixed_capacity_arena_no_realloc,
41// typed_Value_kind_VAL_CONST_PARAM_INSTR_GLOBAL,
42// doubly_linked_Instr_chain_per_block,
43// sealed_opcode_constants_complete]
44// hazard_register: [bug-tape-F16-self-compile-via-this-file,
45// bug-tape-pool-overflow-cap-256-BB-class,
46// bug-tape-arena-exhaustion-silent-failure]
47// residual_risk: "Pool capacities are static (BB pool cap=256
48// documented as scale-dependent failure mode).
49// Future grower-or-die discipline needed for
50// large modules. No realloc currently."
51// verdict: NOT_YET_EVALUATED
52
53import "nx_syscalls.nx"
54
55// nx_assert_*, nx_puts_err, etc. The ASAN-style runtime self-check
56// helpers used in alloc_value / alloc_instr / ir_emit_* bounds guards.
57import "nx_assert.nx"
58
59// ===== primitive type singletons ==================================
60
61// We stash a few Types in a small pool sized at module-init time.
62// Each call to ir_type_* returns the same pointer.
63
64func alloc_type(kind: i64, size: i64, align: i64) -> *Type {
65 // 96 bytes covers the primitive fields (32) plus struct metadata
66 // (name_bytes, name_len, fields, n_fields = 32 more) plus template
67 // + TY_PARAM extras (n_type_params, type_params, param_name,
68 // param_name_len = 32 more). Struct/template/param kinds
69 // populate their respective extras via ir_type_struct_* etc.
70 let raw: *u8 = sys_mmap(104) // 13 fields x 8B (was 96/12; +sext tags signed-subword-load types)
71 let t: *Type = raw as *Type
72 t.kind = kind
73 t.size = size
74 t.align = align
75 t.pointee = 0 as *Type
76 t.name_bytes = 0 as *u8
77 t.name_len = 0
78 t.fields = 0 as *StructField
79 t.n_fields = 0
80 t.n_type_params = 0
81 t.type_params = 0 as *i64
82 t.param_name = 0 as *u8
83 t.param_name_len = 0
84 t.sext = 0
85 return t
86}
87// alloc a SIGNED subword type (sext=1) -> its subword loads sign-extend on every backend. Use for the
88// i8/i16/i32 annotations only; u8/u16/u32 use plain alloc_type (sext=0 -> zero-extend). 8-byte types
89// don't care (qword load, no extension). Primitive leaf Types are shared BY REFERENCE through
90// clone_type_substituting, so the bit travels with the leaf -- no clone propagation needed.
91func alloc_type_s(kind: i64, size: i64, align: i64) -> *Type {
92 let t: *Type = alloc_type(kind, size, align)
93 t.sext = 1
94 return t
95}
96
97// Allocate a fresh TY_STRUCT with the given declared name. Size
98// starts at 0; callers call ir_type_struct_add_field for each member,
99// which updates size + assigns the member's offset.
100func ir_type_struct_new(name_bytes: *u8, name_len: i64) -> *Type {
101 let t: *Type = alloc_type(TY_STRUCT, 0, 8)
102 t.name_bytes = name_bytes
103 t.name_len = name_len
104 // Backing array big enough for typical compiler records (~16
105 // fields); wider structs will need a realloc path later.
106 let raw: *u8 = sys_mmap(16 * 32 + 16)
107 t.fields = raw as *StructField
108 return t
109}
110
111// Append one field to a struct type. Assigns the offset (running
112// sum of previous sizes) and grows the struct's total size. Field
113// records are 32 bytes: name_bytes(8) + name_len(8) + ty(8) + offset(8).
114func ir_type_struct_add_field(t: *Type, name_bytes: *u8, name_len: i64,
115 fty: *Type) -> i64 {
116 let base: i64 = t.fields as i64
117 let f: *StructField = (base + t.n_fields * 32) as *StructField
118 f.name_bytes = name_bytes
119 f.name_len = name_len
120 f.ty = fty
121 f.offset = t.size
122 t.size = t.size + fty.size
123 if fty.align > t.align { t.align = fty.align }
124 t.n_fields = t.n_fields + 1
125 return 0
126}
127
128// Find a field by name on a struct Type. Returns null if missing.
129func ir_type_struct_find_field(t: *Type, name_bytes: *u8, name_len: i64) -> *StructField {
130 let base: i64 = t.fields as i64
131 var i: i64 = 0
132 while i < t.n_fields {
133 let f: *StructField = (base + i * 32) as *StructField
134 if f.name_len == name_len {
135 var j: i64 = 0
136 var ok: i64 = 1
137 while j < name_len {
138 if f.name_bytes[j] != name_bytes[j] { ok = 0 }
139 j = j + 1
140 }
141 if ok == 1 { return f }
142 }
143 i = i + 1
144 }
145 return 0 as *StructField
146}
147
148func ir_type_void() -> *Type { return alloc_type(0, 0, 1) }
149func ir_type_bool() -> *Type { return alloc_type(1, 1, 1) }
150func ir_type_i32() -> *Type { return alloc_type(4, 4, 4) }
151func ir_type_i64() -> *Type { return alloc_type(5, 8, 8) }
152// Floating-point types. kind values mirror TY_F32/TY_F64 in types.nx
153// (9 and 10). Size/align follow the RV64F + RV64D ABI: f32 is
154// 4 bytes 4-aligned, f64 is 8 bytes 8-aligned.
155func ir_type_f32() -> *Type { return alloc_type(9, 4, 4) }
156func ir_type_f64() -> *Type { return alloc_type(10, 8, 8) }
157
158// ===== function / block / value construction ======================
159
160// Allocate an empty Function with a backing arena big enough for
161// the self-host compiling itself (nxc.nx -> Wheeler DDC byte-equal
162// proof). Pool sizes raised 2026-05-16 from "small benchmarks
163// (~4k values, 256 blocks, 4k instrs)" because compiling nxc.nx
164// itself hit blocks_cap=256 in the assert_lt guard:
165//
166// nx_assert_lt: idx=256 cap=256 tag=ir_block_new: blocks pool
167//
168// nxc.nx is ~6KLOC NishiLang with ~419 functions; some single
169// functions (parser dispatch, opt passes) have well over 256
170// basic blocks. Bumped to 4096 blocks for headroom.
171//
172// Backing arena: 128 (header) + 32768 * 48 (values) +
173// 4096 * 96 (blocks) + 32768 * 192 (instrs)
174// = ~8.4 MB per function, all from sys_mmap which is
175// page-aligned + lazy-faulted so unused pages cost
176// ~zero RSS. Instr stride bumped 128 -> 192 on
177// 2026-05-20 to add op8..op15 for >8-arg calls
178// (11-arg AEAD primitives in the HTTPS chain).
179
180func ir_function_new(m: *Module, name: *u8, name_len: i64,
181 ret_ty: *Type) -> *Function {
182 let raw: *u8 = sys_mmap(128 + 32768*48 + 4096*96 + 32768*256)
183 let f: *Function = raw as *Function
184 let base: i64 = raw as i64
185 // Until the Module gets a proper name table, stash the *u8 name
186 // pointer directly in f.name_start (both are i64-sized). The
187 // driver casts back to *u8 when it needs to emit the label.
188 f.name_start = name as i64
189 f.name_len = name_len
190 f.ret_ty = ret_ty
191 f.n_params = 0
192 f.param_ptr_mask = 0 // 0 = signature not yet known; call sites skip type checking
193 f.values = (base + 128) as *Value
194 f.n_values = 0
195 f.values_cap = 32768
196 f.blocks = (base + 128 + 32768*48) as *BasicBlock
197 f.n_blocks = 0
198 f.blocks_cap = 4096
199 f.instrs = (base + 128 + 32768*48 + 4096*96) as *Instr
200 f.n_instrs = 0
201 f.instrs_cap = 32768
202 f.entry = 0 as *BasicBlock
203
204 // Append to module's functions pool so m.n_functions reflects
205 // reality and the driver can walk m.functions to emit each one.
206 // Stride is 176 bytes per Function pool slot.
207 //
208 // Skip the copy if m.functions is null (test harnesses + the
209 // top-level parse_module bootstrap that haven't allocated a pool
210 // yet); just return the heap-allocated f. Otherwise NULL+offset
211 // = SIGSEGV when ir_test or parse_test creates Functions without
212 // a pre-allocated pool.
213 if m != (0 as *Module) {
214 let fn_base: i64 = m.functions as i64
215 if fn_base == 0 { return f }
216 let dst: i64 = fn_base + m.n_functions * 176
217 // Copy the Function struct we allocated into the pool slot so
218 // pool iteration walks contiguous records. We ALSO preserve
219 // the fresh heap allocation for values/blocks/instrs pointers
220 // which live inside `raw` (the 128-byte header shares space
221 // with the Function struct itself, so the copy is the first
222 // 128 bytes of raw).
223 var k: i64 = 0
224 let src_u8: *u8 = raw
225 let dst_u8: *u8 = dst as *u8
226 while k < 176 {
227 dst_u8[k] = src_u8[k]
228 k = k + 1
229 }
230 m.n_functions = m.n_functions + 1
231 return (dst as *Function)
232 }
233 return f
234}
235
236// Resolve a Value by id in f's values pool. Pure pointer arithmetic;
237// used by every pass that inspects existing SSA values.
238func val_at(f: *Function, id: i64) -> *Value {
239 let base: i64 = f.values as i64
240 return (base + id * 48) as *Value
241}
242
243// Resolve a BasicBlock by id in f's blocks pool.
244func block_at(f: *Function, id: i64) -> *BasicBlock {
245 let base: i64 = f.blocks as i64
246 return (base + id * 96) as *BasicBlock
247}
248
249// Allocate one Value in `f`'s pool; return its id.
250func alloc_value(f: *Function, kind: i64, ty: *Type) -> i64 {
251 nx_assert_ptr(f as *u8, "alloc_value: f" as *u8)
252 let id: i64 = f.n_values
253 let cap: i64 = f.values_cap
254 nx_assert(cap > 0, "alloc_value: f.values_cap > 0 (f initialised)" as *u8)
255 nx_assert_lt(id, cap, "alloc_value: values pool" as *u8)
256 let base: i64 = f.values as i64
257 nx_assert(base != 0, "alloc_value: f.values != NULL" as *u8)
258 let v: *Value = (base + id * 48) as *Value
259 v.id = id
260 v.kind = kind
261 v.ty = ty
262 v.const_int = 0
263 v.param_index = 0
264 v.instr = 0 as *Instr
265 f.n_values = id + 1
266 return id
267}
268
269// Create a BasicBlock and append to f's list.
270func ir_block_new(f: *Function) -> *BasicBlock {
271 let id: i64 = f.n_blocks
272 let base: i64 = f.blocks as i64
273 nx_assert_ptr(f as *u8, "ir_block_new: f" as *u8)
274 nx_assert(f.blocks_cap > 0, "ir_block_new: f.blocks_cap > 0" as *u8)
275 nx_assert_lt(id, f.blocks_cap, "ir_block_new: blocks pool" as *u8)
276 nx_assert(base != 0, "ir_block_new: f.blocks != NULL" as *u8)
277 let b: *BasicBlock = (base + id * 96) as *BasicBlock
278 b.id = id
279 b.head = 0 as *Instr
280 b.tail = 0 as *Instr
281 b.parent = f
282 b.n_preds = 0
283 b.n_succs = 0
284 f.n_blocks = id + 1
285 if f.entry == (0 as *BasicBlock) {
286 f.entry = b
287 }
288 return b
289}
290
291// Integer literal.
292func ir_const_i64(f: *Function, n: i64) -> i64 {
293 nx_assert_ptr(f as *u8, "ir_const_i64: f" as *u8)
294 nx_assert(f.values_cap > 0, "ir_const_i64: f init" as *u8)
295 let id: i64 = alloc_value(f, 0, ir_type_i64())
296 let base: i64 = f.values as i64
297 let v: *Value = (base + id * 48) as *Value
298 v.const_int = n
299 return id
300}
301
302// Parameter Value.
303func ir_param(f: *Function, idx: i64, ty: *Type) -> i64 {
304 nx_assert_ptr(f as *u8, "ir_param: f" as *u8)
305 nx_assert(f.values_cap > 0, "ir_param: f init" as *u8)
306 let id: i64 = alloc_value(f, 1, ty)
307 let base: i64 = f.values as i64
308 let v: *Value = (base + id * 48) as *Value
309 v.param_index = idx
310 return id
311}
312
313// Append `inst` to `bb`'s instruction list. Updates prev/next/tail/head.
314func append_instr(bb: *BasicBlock, inst: *Instr) -> i64 {
315 inst.parent = bb
316 inst.prev = bb.tail
317 inst.next = 0 as *Instr
318 if bb.tail != (0 as *Instr) {
319 bb.tail.next = inst
320 }
321 if bb.head == (0 as *Instr) {
322 bb.head = inst
323 }
324 bb.tail = inst
325 return 0
326}
327
328// Allocate a fresh Instr from the function's pool.
329func alloc_instr(f: *Function, op: i64, ty: *Type) -> *Instr {
330 nx_assert_ptr(f as *u8, "alloc_instr: f" as *u8)
331 nx_assert(f.instrs_cap > 0, "alloc_instr: f.instrs_cap > 0" as *u8)
332 nx_assert_lt(f.n_instrs, f.instrs_cap, "alloc_instr: instrs pool" as *u8)
333 let id: i64 = f.n_instrs
334 let base: i64 = f.instrs as i64
335 nx_assert(base != 0, "alloc_instr: f.instrs != NULL" as *u8)
336 // Stride 256 = sizeof(Instr) after op16..op23 added 2026-06-18 (was 192 after op8..op15).
337 let i: *Instr = (base + id * 256) as *Instr
338 i.op = op
339 i.result = 0
340 i.ty = ty
341 i.n_operands = 0
342 i.op0 = 0; i.op1 = 0; i.op2 = 0; i.op3 = 0
343 i.op4 = 0; i.op5 = 0; i.op6 = 0; i.op7 = 0
344 i.op8 = 0; i.op9 = 0; i.op10 = 0; i.op11 = 0
345 i.op12 = 0; i.op13 = 0; i.op14 = 0; i.op15 = 0
346 i.op16 = 0; i.op17 = 0; i.op18 = 0; i.op19 = 0
347 i.op20 = 0; i.op21 = 0; i.op22 = 0; i.op23 = 0
348 i.callee = 0 as *Function
349 i.parent = 0 as *BasicBlock
350 i.prev = 0 as *Instr
351 i.next = 0 as *Instr
352 f.n_instrs = id + 1
353 return i
354}
355
356// Emit a binary op. Result Value's id is stored in inst.result
357// and returned so the caller can use it as an operand.
358func ir_emit_binop(bb: *BasicBlock, op: i64,
359 a: i64, b: i64, ret_ty: *Type) -> i64 {
360 nx_assert_ptr(bb as *u8, "ir_emit_binop: bb" as *u8)
361 let f: *Function = bb.parent
362 nx_assert_ptr(f as *u8, "ir_emit_binop: bb.parent" as *u8)
363 nx_assert(f.values_cap > 0, "ir_emit_binop: bb.parent init" as *u8)
364 let i: *Instr = alloc_instr(f, op, ret_ty)
365 let rid: i64 = alloc_value(f, 2, ret_ty)
366 let base: i64 = f.values as i64
367 let v: *Value = (base + rid * 48) as *Value
368 v.instr = i
369 i.result = rid
370 i.n_operands = 2
371 i.op0 = a
372 i.op1 = b
373 append_instr(bb, i)
374 return rid
375}
376
377// Single-operand instruction (bswap / clz / ctz / popcnt / casts).
378// op0 = a; void-free, returns a fresh value id of ret_ty.
379func ir_emit_unop(bb: *BasicBlock, op: i64, a: i64, ret_ty: *Type) -> i64 {
380 nx_assert_ptr(bb as *u8, "ir_emit_unop: bb" as *u8)
381 let f: *Function = bb.parent
382 nx_assert_ptr(f as *u8, "ir_emit_unop: bb.parent" as *u8)
383 nx_assert(f.values_cap > 0, "ir_emit_unop: bb.parent init" as *u8)
384 let i: *Instr = alloc_instr(f, op, ret_ty)
385 let rid: i64 = alloc_value(f, 2, ret_ty)
386 let base: i64 = f.values as i64
387 let v: *Value = (base + rid * 48) as *Value
388 v.instr = i
389 i.result = rid
390 i.n_operands = 1
391 i.op0 = a
392 append_instr(bb, i)
393 return rid
394}
395
396// ---- Atomic intrinsics (C bootstrap parity) ----------------------
397// Memory-order operand is last; backend emits conservative-strong.
398
399func ir_emit_atomic_load_i64(bb: *BasicBlock, addr: i64, mo: i64) -> i64 {
400 let f: *Function = bb.parent
401 let i: *Instr = alloc_instr(f, OP_ATOMIC_LOAD_I64, ir_type_i64())
402 let rid: i64 = alloc_value(f, 2, ir_type_i64())
403 let v: *Value = ((f.values as i64) + rid * 48) as *Value
404 v.instr = i
405 i.result = rid
406 i.n_operands = 2
407 i.op0 = addr
408 i.op1 = mo
409 append_instr(bb, i)
410 return rid
411}
412
413func ir_emit_atomic_store_i64(bb: *BasicBlock, addr: i64, val: i64, mo: i64) -> i64 {
414 let f: *Function = bb.parent
415 let i: *Instr = alloc_instr(f, OP_ATOMIC_STORE_I64, ir_type_void())
416 i.n_operands = 3
417 i.op0 = addr
418 i.op1 = val
419 i.op2 = mo
420 append_instr(bb, i)
421 return 0
422}
423
424// G3: add the 128-bit (hi:lo) into the 3-word accumulator at acc_ptr with carry.
425// Void + 3-operand (acc_ptr escaping pointer + lo + hi), so it is opt-opaque and
426// never homed; the backend lowers it to one contiguous addq;adcq;adcq block.
427func ir_emit_adc_acc(bb: *BasicBlock, acc_ptr: i64, lo: i64, hi: i64) -> i64 {
428 let f: *Function = bb.parent
429 let i: *Instr = alloc_instr(f, OP_ADC_ACC, ir_type_void())
430 i.n_operands = 3
431 i.op0 = acc_ptr
432 i.op1 = lo
433 i.op2 = hi
434 append_instr(bb, i)
435 return 0
436}
437
438func ir_emit_q8rowdot(bb: *BasicBlock, qrow: i64, arow: i64, nblk: i64) -> i64 {
439 let f: *Function = bb.parent
440 let i: *Instr = alloc_instr(f, OP_Q8ROWDOT, ir_type_i64())
441 let rid: i64 = alloc_value(f, 2, ir_type_i64())
442 let v: *Value = ((f.values as i64) + rid * 48) as *Value
443 v.instr = i
444 i.result = rid
445 i.n_operands = 3
446 i.op0 = qrow
447 i.op1 = arow
448 i.op2 = nblk
449 append_instr(bb, i)
450 return rid
451}
452
453func ir_emit_i8fma32(bb: *BasicBlock, a: i64, b: i64, d: i64, acc: i64) -> i64 {
454 let f: *Function = bb.parent
455 let i: *Instr = alloc_instr(f, OP_I8FMA32, ir_type_i64())
456 let rid: i64 = alloc_value(f, 2, ir_type_i64())
457 let v: *Value = ((f.values as i64) + rid * 48) as *Value
458 v.instr = i
459 i.result = rid
460 i.n_operands = 4
461 i.op0 = a
462 i.op1 = b
463 i.op2 = d
464 i.op3 = acc
465 append_instr(bb, i)
466 return rid
467}
468
469func ir_emit_atomic_cas_i64(bb: *BasicBlock, addr: i64, exp: i64, newv: i64, mo: i64) -> i64 {
470 let f: *Function = bb.parent
471 let i: *Instr = alloc_instr(f, OP_ATOMIC_CAS_I64, ir_type_i64())
472 let rid: i64 = alloc_value(f, 2, ir_type_i64())
473 let v: *Value = ((f.values as i64) + rid * 48) as *Value
474 v.instr = i
475 i.result = rid
476 i.n_operands = 4
477 i.op0 = addr
478 i.op1 = exp
479 i.op2 = newv
480 i.op3 = mo
481 append_instr(bb, i)
482 return rid
483}
484
485func ir_emit_atomic_faa_i64(bb: *BasicBlock, addr: i64, delta: i64, mo: i64) -> i64 {
486 let f: *Function = bb.parent
487 let i: *Instr = alloc_instr(f, OP_ATOMIC_FAA_I64, ir_type_i64())
488 let rid: i64 = alloc_value(f, 2, ir_type_i64())
489 let v: *Value = ((f.values as i64) + rid * 48) as *Value
490 v.instr = i
491 i.result = rid
492 i.n_operands = 3
493 i.op0 = addr
494 i.op1 = delta
495 i.op2 = mo
496 append_instr(bb, i)
497 return rid
498}
499
500func ir_emit_atomic_fence(bb: *BasicBlock, mo: i64) -> i64 {
501 let f: *Function = bb.parent
502 let i: *Instr = alloc_instr(f, OP_ATOMIC_FENCE, ir_type_void())
503 i.n_operands = 1
504 i.op0 = mo
505 append_instr(bb, i)
506 return 0
507}
508
509// __thread_clone(stack_top, entry_fn, ctx) -> child_tid. 3 operands.
510func ir_emit_thread_clone(bb: *BasicBlock, stack_top: i64, entry_fn: i64, ctx: i64) -> i64 {
511 let f: *Function = bb.parent
512 let i: *Instr = alloc_instr(f, OP_THREAD_CLONE, ir_type_i64())
513 let rid: i64 = alloc_value(f, 2, ir_type_i64())
514 let v: *Value = ((f.values as i64) + rid * 48) as *Value
515 v.instr = i
516 i.result = rid
517 i.n_operands = 3
518 i.op0 = stack_top
519 i.op1 = entry_fn
520 i.op2 = ctx
521 append_instr(bb, i)
522 return rid
523}
524
525// A block that already ends in an unconditional terminator is SEALED: emitting
526// another terminator into it is a no-op. Without this, `break`/`continue`/`return`
527// inside an if-arm left TWO terminators in the arm block (the statement's br plus
528// parse_stmt_if's merge br): bb.tail then pointed at the DEAD second br, every
529// CFG pass saw the wrong edge, and the G12/G15 emit-time fallthrough made the
530// dead br REACHABLE -- the dt_parse_attrs infinite-spin miscompile (2026-07-27,
531// minimal repro nx_breakprobe P4; oracle nx_cc_known_good disagrees-and-is-right).
532// Defined ABOVE every caller (ir_emit_return + the br emitters below) per the
533// define-before-use rule.
534func ir_bb_sealed(bb: *BasicBlock) -> i64 {
535 let t: *Instr = bb.tail
536 if t == (0 as *Instr) { return 0 }
537 if t.op == OP_BR { return 1 }
538 if t.op == OP_BR_COND { return 1 }
539 if t.op == OP_RETURN { return 1 }
540 if t.op == OP_TAIL_CALL { return 1 }
541 return 0
542}
543
544// Emit return. Void-typed instruction.
545func ir_emit_return(bb: *BasicBlock, v: i64) -> i64 {
546 if ir_bb_sealed(bb) == 1 { return 0 }
547 let f: *Function = bb.parent
548 let i: *Instr = alloc_instr(f, 30, ir_type_void())
549 i.n_operands = 1
550 i.op0 = v
551 append_instr(bb, i)
552 return 0
553}
554
555// Kernel intrinsics -- wait-for-interrupt, CSR read/write, fence,
556// mret. These don't have return values (except CSR_READ); we still
557// allocate a Value for them so the IR stays uniform and opt passes
558// that walk values-by-id don't crash.
559
560// wfi: no operands, void result. Lowered to `wfi` instruction.
561func ir_emit_wfi(bb: *BasicBlock) -> i64 {
562 let f: *Function = bb.parent
563 let i: *Instr = alloc_instr(f, OP_WFI, ir_type_void())
564 i.n_operands = 0
565 append_instr(bb, i)
566 return 0
567}
568
569// csrr(csr_num) -> i64. csr_num must be a constant at codegen time.
570// Lowered to `csrr <dst>, <csr>`.
571func ir_emit_csr_read(bb: *BasicBlock, csr_num: i64) -> i64 {
572 let f: *Function = bb.parent
573 let i: *Instr = alloc_instr(f, OP_CSR_READ, ir_type_i64())
574 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
575 let v: *Value = val_at(f, rid)
576 v.instr = i
577 i.result = rid
578 i.n_operands = 1
579 i.op0 = csr_num
580 append_instr(bb, i)
581 return rid
582}
583
584// csrw(csr_num, val). val is a Value id; csr_num is a constant.
585// Lowered to `csrw <csr>, <src>`.
586func ir_emit_csr_write(bb: *BasicBlock, csr_num: i64, val: i64) -> i64 {
587 let f: *Function = bb.parent
588 let i: *Instr = alloc_instr(f, OP_CSR_WRITE, ir_type_void())
589 i.n_operands = 2
590 i.op0 = csr_num
591 i.op1 = val
592 append_instr(bb, i)
593 return 0
594}
595
596// fence -- full memory barrier. Lowered to `fence rw, rw`.
597func ir_emit_fence(bb: *BasicBlock) -> i64 {
598 let f: *Function = bb.parent
599 let i: *Instr = alloc_instr(f, OP_FENCE, ir_type_void())
600 i.n_operands = 0
601 append_instr(bb, i)
602 return 0
603}
604
605// mret -- machine-mode return. Lowered to `mret`. Used at the end
606// of trap handlers to return from M-mode trap to the originating
607// privilege + pc captured in mepc/mstatus.
608func ir_emit_mret(bb: *BasicBlock) -> i64 {
609 let f: *Function = bb.parent
610 let i: *Instr = alloc_instr(f, OP_MRET, ir_type_void())
611 i.n_operands = 0
612 append_instr(bb, i)
613 return 0
614}
615
616// Emit a raw syscall (ECALL). op0 = syscall number, op1..op6 = args.
617// Linux RV64 ABI: a7 = syscall number, a0..a5 = args, result in a0.
618// Up to 6 args supported (the kernel ABI limit); extras would need
619// a side-operands array, not warranted today.
620func ir_emit_syscall(bb: *BasicBlock, args: *i64, n_args: i64) -> i64 {
621 let f: *Function = bb.parent
622 let i: *Instr = alloc_instr(f, OP_SYSCALL, ir_type_i64())
623 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
624 let v: *Value = val_at(f, rid)
625 v.instr = i
626 i.result = rid
627 i.n_operands = n_args
628 if n_args > 0 { i.op0 = args[0] }
629 if n_args > 1 { i.op1 = args[1] }
630 if n_args > 2 { i.op2 = args[2] }
631 if n_args > 3 { i.op3 = args[3] }
632 if n_args > 4 { i.op4 = args[4] }
633 if n_args > 5 { i.op5 = args[5] }
634 if n_args > 6 { i.op6 = args[6] }
635 append_instr(bb, i)
636 return rid
637}
638
639// Emit a hardware-f32 BINARY op (OP_FADD/FSUB/FMUL/FDIV). a, b, and the result are
640// i64-CARRIED IEEE-754 binary32 bit-patterns -- NishiLang has no f32 type, so a float
641// rides in the low 32 bits of an i64; the x86 backend moves it GPR<->xmm and computes
642// with SSE scalar-single. Result Value is typed i64 (the carrier); lowering owns the
643// float semantics. The OP_F* opcodes were reserved in nx_types.nx awaiting this.
644func ir_emit_f32_binop(bb: *BasicBlock, op: i64, a: i64, b: i64) -> i64 {
645 let f: *Function = bb.parent
646 let i: *Instr = alloc_instr(f, op, ir_type_i64())
647 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
648 let v: *Value = val_at(f, rid)
649 v.instr = i
650 i.result = rid
651 i.n_operands = 2
652 i.op0 = a
653 i.op1 = b
654 append_instr(bb, i)
655 return rid
656}
657// Emit a hardware-f32 UNARY cast: OP_FCAST_I_TO_F (i64 int -> f32 bits, cvtsi2ss) or
658// OP_FCAST_F_TO_I (f32 bits -> i64 int, cvttss2si truncate). One operand, i64 carrier.
659func ir_emit_f32_unop(bb: *BasicBlock, op: i64, a: i64) -> i64 {
660 let f: *Function = bb.parent
661 let i: *Instr = alloc_instr(f, op, ir_type_i64())
662 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
663 let v: *Value = val_at(f, rid)
664 v.instr = i
665 i.result = rid
666 i.n_operands = 1
667 i.op0 = a
668 append_instr(bb, i)
669 return rid
670}
671// f64-RESULT unop (OP_FCAST_I_TO_F, OP_FSQRT): the result carrier is TY_F64 so
672// the backend's x86ctx_emit_float routes it to double-precision codegen and
673// downstream f64 arithmetic sees a float operand. (F_TO_I keeps ir_emit_f32_unop:
674// its result is a true i64, and the backend reads op0's f64 type for precision.)
675func ir_emit_f64_unop(bb: *BasicBlock, op: i64, a: i64) -> i64 {
676 let f: *Function = bb.parent
677 let fty: *Type = alloc_type(TY_F64, 8, 8)
678 let i: *Instr = alloc_instr(f, op, fty)
679 let rid: i64 = alloc_value(f, VK_INSTR, fty)
680 let v: *Value = val_at(f, rid)
681 v.instr = i
682 i.result = rid
683 i.n_operands = 1
684 i.op0 = a
685 append_instr(bb, i)
686 return rid
687}
688// FMA vector-accumulate: *acc += a*b (8-wide fused). 3 operands; result carrier i64 (unused).
689func ir_emit_f32x8_fma(bb: *BasicBlock, acc: i64, a: i64, b: i64) -> i64 {
690 let f: *Function = bb.parent
691 let i: *Instr = alloc_instr(f, OP_F32X8_FMA, ir_type_i64())
692 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
693 let v: *Value = val_at(f, rid)
694 v.instr = i
695 i.result = rid
696 i.n_operands = 3
697 i.op0 = acc
698 i.op1 = a
699 i.op2 = b
700 append_instr(bb, i)
701 return rid
702}
703// NO-FLOAT integer madd-accumulate: *acc(i32x8) += vpmaddwd(a(i16x16), b(i16x16)). 3 operands.
704func ir_emit_i16x16_madd(bb: *BasicBlock, acc: i64, a: i64, b: i64) -> i64 {
705 let f: *Function = bb.parent
706 let i: *Instr = alloc_instr(f, OP_I16X16_MADD, ir_type_i64())
707 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
708 let v: *Value = val_at(f, rid)
709 v.instr = i
710 i.result = rid
711 i.n_operands = 3
712 i.op0 = acc
713 i.op1 = a
714 i.op2 = b
715 append_instr(bb, i)
716 return rid
717}
718// Hardware SHA-NI: one full SHA-256 block compression IN PLACE. op0=state ptr (*u32[8]),
719// op1=block ptr (*u8[64] big-endian msg), op2=K ptr (*u32[64]). 3 operands; i64 result (0).
720func ir_emit_q5unpack32(bb: *BasicBlock, qhqs: i64, out: i64, consts: i64) -> i64 {
721 let f: *Function = bb.parent
722 let i: *Instr = alloc_instr(f, OP_Q5UNPACK32, ir_type_i64())
723 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
724 let v: *Value = val_at(f, rid)
725 v.instr = i
726 i.result = rid
727 i.n_operands = 3
728 i.op0 = qhqs
729 i.op1 = out
730 i.op2 = consts
731 append_instr(bb, i)
732 return rid
733}
734
735func ir_emit_sha256_ni_block(bb: *BasicBlock, state: i64, block: i64, k: i64) -> i64 {
736 let f: *Function = bb.parent
737 let i: *Instr = alloc_instr(f, OP_SHA256_NI_BLOCK, ir_type_i64())
738 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
739 let v: *Value = val_at(f, rid)
740 v.instr = i
741 i.result = rid
742 i.n_operands = 3
743 i.op0 = state
744 i.op1 = block
745 i.op2 = k
746 append_instr(bb, i)
747 return rid
748}
749
750// Fused 4x64-limb wide multiply: *dst(u64[8]) = *a(u64[4]) * *b(u64[4]) (512-bit product).
751// op0=dst ptr, op1=a ptr, op2=b ptr. 3 operands; i64 result (0). Lowers to the ADX/BMI2
752// mulx+adcx+adox dual-carry kernel; the pure 8x32 u256_mul_wide stays the byte-exact oracle.
753func ir_emit_mul256_wide(bb: *BasicBlock, dst: i64, a: i64, b: i64) -> i64 {
754 let f: *Function = bb.parent
755 let i: *Instr = alloc_instr(f, OP_MUL256_WIDE, ir_type_i64())
756 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
757 let v: *Value = val_at(f, rid)
758 v.instr = i
759 i.result = rid
760 i.n_operands = 3
761 i.op0 = dst
762 i.op1 = a
763 i.op2 = b
764 append_instr(bb, i)
765 return rid
766}
767
768// Emit a widening SIMD dot product i16x16 -> i64. Consumes two
769// i16-lane-loaded vectors (whose pointers callers materialise into
770// stack slots ahead of time -- the v0.0.1 SIMD shape is
771// stack-slot-based, no v-reg allocator yet). Returns the i64
772// scalar sum, just like the C-side OP_SIMD_VDOT_I16_X16.
773func ir_emit_simd_vdot_i16_x16(bb: *BasicBlock, a: i64, b: i64) -> i64 {
774 let f: *Function = bb.parent
775 let i: *Instr = alloc_instr(f, OP_SIMD_VDOT_I16_X16, ir_type_i64())
776 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
777 let v: *Value = val_at(f, rid)
778 v.instr = i
779 i.result = rid
780 i.n_operands = 2
781 i.op0 = a
782 i.op1 = b
783 append_instr(bb, i)
784 return rid
785}
786// i16x16 horizontal min/max: read 16 i16 lanes from *i64 src, return
787// sign-extended i64 scalar min/max-of-lanes.
788func ir_emit_simd_vreduce_min_i16_x16(bb: *BasicBlock, p: i64) -> i64 {
789 let f: *Function = bb.parent
790 let i: *Instr = alloc_instr(f, OP_SIMD_VREDUCE_MIN_I16_X16, ir_type_i64())
791 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
792 let v: *Value = val_at(f, rid)
793 v.instr = i
794 i.result = rid
795 i.n_operands = 1
796 i.op0 = p
797 append_instr(bb, i)
798 return rid
799}
800func ir_emit_simd_vreduce_max_i16_x16(bb: *BasicBlock, p: i64) -> i64 {
801 let f: *Function = bb.parent
802 let i: *Instr = alloc_instr(f, OP_SIMD_VREDUCE_MAX_I16_X16, ir_type_i64())
803 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
804 let v: *Value = val_at(f, rid)
805 v.instr = i
806 i.result = rid
807 i.n_operands = 1
808 i.op0 = p
809 append_instr(bb, i)
810 return rid
811}
812// i16x16 signed saturating add: a + b per-lane, clipped to INT16
813// bounds, stored to *out as packed i16x16. Returns 0.
814func ir_emit_simd_vsadd_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
815 let f: *Function = bb.parent
816 let i: *Instr = alloc_instr(f, OP_SIMD_VSADD_I16_X16, ir_type_i64())
817 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
818 let v: *Value = val_at(f, rid)
819 v.instr = i
820 i.result = rid
821 i.n_operands = 3
822 i.op0 = a
823 i.op1 = b
824 i.op2 = out
825 append_instr(bb, i)
826 return rid
827}
828// Per-lane signed saturating sub. Same shape as vsadd.
829func ir_emit_simd_vssub_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
830 let f: *Function = bb.parent
831 let i: *Instr = alloc_instr(f, OP_SIMD_VSSUB_I16_X16, ir_type_i64())
832 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
833 let v: *Value = val_at(f, rid)
834 v.instr = i
835 i.result = rid
836 i.n_operands = 3
837 i.op0 = a; i.op1 = b; i.op2 = out
838 append_instr(bb, i)
839 return rid
840}
841func ir_emit_simd_vsaddu_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
842 let f: *Function = bb.parent
843 let i: *Instr = alloc_instr(f, OP_SIMD_VSADDU_I16_X16, ir_type_i64())
844 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
845 let v: *Value = val_at(f, rid)
846 v.instr = i
847 i.result = rid
848 i.n_operands = 3
849 i.op0 = a; i.op1 = b; i.op2 = out
850 append_instr(bb, i)
851 return rid
852}
853func ir_emit_simd_vssubu_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
854 let f: *Function = bb.parent
855 let i: *Instr = alloc_instr(f, OP_SIMD_VSSUBU_I16_X16, ir_type_i64())
856 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
857 let v: *Value = val_at(f, rid)
858 v.instr = i
859 i.result = rid
860 i.n_operands = 3
861 i.op0 = a; i.op1 = b; i.op2 = out
862 append_instr(bb, i)
863 return rid
864}
865// Per-lane min/max/add/sub/mul -- same 3-arg shape.
866func ir_emit_simd_vmin_lane_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
867 let f: *Function = bb.parent
868 let i: *Instr = alloc_instr(f, OP_SIMD_VMIN_LANE_I16_X16, ir_type_i64())
869 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
870 let v: *Value = val_at(f, rid)
871 v.instr = i; i.result = rid; i.n_operands = 3
872 i.op0 = a; i.op1 = b; i.op2 = out
873 append_instr(bb, i)
874 return rid
875}
876func ir_emit_simd_vmax_lane_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
877 let f: *Function = bb.parent
878 let i: *Instr = alloc_instr(f, OP_SIMD_VMAX_LANE_I16_X16, ir_type_i64())
879 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
880 let v: *Value = val_at(f, rid)
881 v.instr = i; i.result = rid; i.n_operands = 3
882 i.op0 = a; i.op1 = b; i.op2 = out
883 append_instr(bb, i)
884 return rid
885}
886func ir_emit_simd_vadd_lane_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
887 let f: *Function = bb.parent
888 let i: *Instr = alloc_instr(f, OP_SIMD_VADD_LANE_I16_X16, ir_type_i64())
889 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
890 let v: *Value = val_at(f, rid)
891 v.instr = i; i.result = rid; i.n_operands = 3
892 i.op0 = a; i.op1 = b; i.op2 = out
893 append_instr(bb, i)
894 return rid
895}
896func ir_emit_simd_vsub_lane_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
897 let f: *Function = bb.parent
898 let i: *Instr = alloc_instr(f, OP_SIMD_VSUB_LANE_I16_X16, ir_type_i64())
899 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
900 let v: *Value = val_at(f, rid)
901 v.instr = i; i.result = rid; i.n_operands = 3
902 i.op0 = a; i.op1 = b; i.op2 = out
903 append_instr(bb, i)
904 return rid
905}
906func ir_emit_simd_vmul_lane_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
907 let f: *Function = bb.parent
908 let i: *Instr = alloc_instr(f, OP_SIMD_VMUL_LANE_I16_X16, ir_type_i64())
909 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
910 let v: *Value = val_at(f, rid)
911 v.instr = i; i.result = rid; i.n_operands = 3
912 i.op0 = a; i.op1 = b; i.op2 = out
913 append_instr(bb, i)
914 return rid
915}
916// Per-lane immediate-count shifts. op0 = *i64 source, op1 = count
917// (scalar i64, lowered into t4 at codegen), op2 = *i64 out.
918func ir_emit_simd_vsll_i16_x16(bb: *BasicBlock, a: i64, count: i64, out: i64) -> i64 {
919 let f: *Function = bb.parent
920 let i: *Instr = alloc_instr(f, OP_SIMD_VSLL_I16_X16, ir_type_i64())
921 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
922 let v: *Value = val_at(f, rid)
923 v.instr = i; i.result = rid; i.n_operands = 3
924 i.op0 = a; i.op1 = count; i.op2 = out
925 append_instr(bb, i)
926 return rid
927}
928func ir_emit_simd_vsrl_i16_x16(bb: *BasicBlock, a: i64, count: i64, out: i64) -> i64 {
929 let f: *Function = bb.parent
930 let i: *Instr = alloc_instr(f, OP_SIMD_VSRL_I16_X16, ir_type_i64())
931 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
932 let v: *Value = val_at(f, rid)
933 v.instr = i; i.result = rid; i.n_operands = 3
934 i.op0 = a; i.op1 = count; i.op2 = out
935 append_instr(bb, i)
936 return rid
937}
938func ir_emit_simd_vsra_i16_x16(bb: *BasicBlock, a: i64, count: i64, out: i64) -> i64 {
939 let f: *Function = bb.parent
940 let i: *Instr = alloc_instr(f, OP_SIMD_VSRA_I16_X16, ir_type_i64())
941 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
942 let v: *Value = val_at(f, rid)
943 v.instr = i; i.result = rid; i.n_operands = 3
944 i.op0 = a; i.op1 = count; i.op2 = out
945 append_instr(bb, i)
946 return rid
947}
948// Horizontal sum of 16 i16 lanes -> i64 (widening, sign-extended).
949func ir_emit_simd_vreduce_sum_i16_x16(bb: *BasicBlock, p: i64) -> i64 {
950 let f: *Function = bb.parent
951 let i: *Instr = alloc_instr(f, OP_SIMD_VREDUCE_SUM_I16_X16, ir_type_i64())
952 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
953 let v: *Value = val_at(f, rid)
954 v.instr = i; i.result = rid; i.n_operands = 1
955 i.op0 = p
956 append_instr(bb, i)
957 return rid
958}
959// Broadcast scalar into 16 i16 lanes -> *i64 out.
960func ir_emit_simd_vbroadcast_i16_x16(bb: *BasicBlock, scalar: i64, out: i64) -> i64 {
961 let f: *Function = bb.parent
962 let i: *Instr = alloc_instr(f, OP_SIMD_VBROADCAST_I16_X16, ir_type_i64())
963 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
964 let v: *Value = val_at(f, rid)
965 v.instr = i; i.result = rid; i.n_operands = 2
966 i.op0 = scalar; i.op1 = out
967 append_instr(bb, i)
968 return rid
969}
970// i8x32 ops -- same shape as i16x16 lane binops (3-arg) + reduce
971// (1-arg) + broadcast (2-arg). Builder body identical modulo
972// the OP_SIMD_V*_I8_X32 opcode tag.
973func ir_emit_simd_vadd_i8_x32(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
974 let f: *Function = bb.parent
975 let i: *Instr = alloc_instr(f, OP_SIMD_VADD_I8_X32, ir_type_i64())
976 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
977 let v: *Value = val_at(f, rid)
978 v.instr = i; i.result = rid; i.n_operands = 3
979 i.op0 = a; i.op1 = b; i.op2 = out
980 append_instr(bb, i)
981 return rid
982}
983func ir_emit_simd_vsub_i8_x32(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
984 let f: *Function = bb.parent
985 let i: *Instr = alloc_instr(f, OP_SIMD_VSUB_I8_X32, ir_type_i64())
986 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
987 let v: *Value = val_at(f, rid)
988 v.instr = i; i.result = rid; i.n_operands = 3
989 i.op0 = a; i.op1 = b; i.op2 = out
990 append_instr(bb, i)
991 return rid
992}
993func ir_emit_simd_vsadd_i8_x32(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
994 let f: *Function = bb.parent
995 let i: *Instr = alloc_instr(f, OP_SIMD_VSADD_I8_X32, ir_type_i64())
996 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
997 let v: *Value = val_at(f, rid)
998 v.instr = i; i.result = rid; i.n_operands = 3
999 i.op0 = a; i.op1 = b; i.op2 = out
1000 append_instr(bb, i)
1001 return rid
1002}
1003func ir_emit_simd_vssub_i8_x32(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1004 let f: *Function = bb.parent
1005 let i: *Instr = alloc_instr(f, OP_SIMD_VSSUB_I8_X32, ir_type_i64())
1006 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1007 let v: *Value = val_at(f, rid)
1008 v.instr = i; i.result = rid; i.n_operands = 3
1009 i.op0 = a; i.op1 = b; i.op2 = out
1010 append_instr(bb, i)
1011 return rid
1012}
1013func ir_emit_simd_vreduce_sum_i8_x32(bb: *BasicBlock, p: i64) -> i64 {
1014 let f: *Function = bb.parent
1015 let i: *Instr = alloc_instr(f, OP_SIMD_VREDUCE_SUM_I8_X32, ir_type_i64())
1016 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1017 let v: *Value = val_at(f, rid)
1018 v.instr = i; i.result = rid; i.n_operands = 1
1019 i.op0 = p
1020 append_instr(bb, i)
1021 return rid
1022}
1023func ir_emit_simd_vbroadcast_i8_x32(bb: *BasicBlock, scalar: i64, out: i64) -> i64 {
1024 let f: *Function = bb.parent
1025 let i: *Instr = alloc_instr(f, OP_SIMD_VBROADCAST_I8_X32, ir_type_i64())
1026 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1027 let v: *Value = val_at(f, rid)
1028 v.instr = i; i.result = rid; i.n_operands = 2
1029 i.op0 = scalar; i.op1 = out
1030 append_instr(bb, i)
1031 return rid
1032}
1033// i32x8 set -- same 3-arg / 1-arg / 2-arg shapes as i8 and i16 families.
1034func ir_emit_simd_vadd_i32_x8(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1035 let f: *Function = bb.parent
1036 let i: *Instr = alloc_instr(f, OP_SIMD_VADD_I32_X8, ir_type_i64())
1037 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1038 let v: *Value = val_at(f, rid)
1039 v.instr = i; i.result = rid; i.n_operands = 3
1040 i.op0 = a; i.op1 = b; i.op2 = out
1041 append_instr(bb, i)
1042 return rid
1043}
1044func ir_emit_simd_vsub_i32_x8(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1045 let f: *Function = bb.parent
1046 let i: *Instr = alloc_instr(f, OP_SIMD_VSUB_I32_X8, ir_type_i64())
1047 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1048 let v: *Value = val_at(f, rid)
1049 v.instr = i; i.result = rid; i.n_operands = 3
1050 i.op0 = a; i.op1 = b; i.op2 = out
1051 append_instr(bb, i)
1052 return rid
1053}
1054func ir_emit_simd_vmul_i32_x8(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1055 let f: *Function = bb.parent
1056 let i: *Instr = alloc_instr(f, OP_SIMD_VMUL_I32_X8, ir_type_i64())
1057 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1058 let v: *Value = val_at(f, rid)
1059 v.instr = i; i.result = rid; i.n_operands = 3
1060 i.op0 = a; i.op1 = b; i.op2 = out
1061 append_instr(bb, i)
1062 return rid
1063}
1064func ir_emit_simd_vsadd_i32_x8(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1065 let f: *Function = bb.parent
1066 let i: *Instr = alloc_instr(f, OP_SIMD_VSADD_I32_X8, ir_type_i64())
1067 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1068 let v: *Value = val_at(f, rid)
1069 v.instr = i; i.result = rid; i.n_operands = 3
1070 i.op0 = a; i.op1 = b; i.op2 = out
1071 append_instr(bb, i)
1072 return rid
1073}
1074func ir_emit_simd_vssub_i32_x8(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1075 let f: *Function = bb.parent
1076 let i: *Instr = alloc_instr(f, OP_SIMD_VSSUB_I32_X8, ir_type_i64())
1077 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1078 let v: *Value = val_at(f, rid)
1079 v.instr = i; i.result = rid; i.n_operands = 3
1080 i.op0 = a; i.op1 = b; i.op2 = out
1081 append_instr(bb, i)
1082 return rid
1083}
1084func ir_emit_simd_vreduce_sum_i32_x8(bb: *BasicBlock, p: i64) -> i64 {
1085 let f: *Function = bb.parent
1086 let i: *Instr = alloc_instr(f, OP_SIMD_VREDUCE_SUM_I32_X8, ir_type_i64())
1087 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1088 let v: *Value = val_at(f, rid)
1089 v.instr = i; i.result = rid; i.n_operands = 1
1090 i.op0 = p
1091 append_instr(bb, i)
1092 return rid
1093}
1094func ir_emit_simd_vbroadcast_i32_x8(bb: *BasicBlock, scalar: i64, out: i64) -> i64 {
1095 let f: *Function = bb.parent
1096 let i: *Instr = alloc_instr(f, OP_SIMD_VBROADCAST_I32_X8, ir_type_i64())
1097 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1098 let v: *Value = val_at(f, rid)
1099 v.instr = i; i.result = rid; i.n_operands = 2
1100 i.op0 = scalar; i.op1 = out
1101 append_instr(bb, i)
1102 return rid
1103}
1104// i64x4 set.
1105func ir_emit_simd_vadd_i64_x4(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1106 let f: *Function = bb.parent
1107 let i: *Instr = alloc_instr(f, OP_SIMD_VADD_I64_X4, ir_type_i64())
1108 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1109 let v: *Value = val_at(f, rid)
1110 v.instr = i; i.result = rid; i.n_operands = 3
1111 i.op0 = a; i.op1 = b; i.op2 = out
1112 append_instr(bb, i); return rid
1113}
1114func ir_emit_simd_vsub_i64_x4(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1115 let f: *Function = bb.parent
1116 let i: *Instr = alloc_instr(f, OP_SIMD_VSUB_I64_X4, ir_type_i64())
1117 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1118 let v: *Value = val_at(f, rid)
1119 v.instr = i; i.result = rid; i.n_operands = 3
1120 i.op0 = a; i.op1 = b; i.op2 = out
1121 append_instr(bb, i); return rid
1122}
1123func ir_emit_simd_vmul_i64_x4(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1124 let f: *Function = bb.parent
1125 let i: *Instr = alloc_instr(f, OP_SIMD_VMUL_I64_X4, ir_type_i64())
1126 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1127 let v: *Value = val_at(f, rid)
1128 v.instr = i; i.result = rid; i.n_operands = 3
1129 i.op0 = a; i.op1 = b; i.op2 = out
1130 append_instr(bb, i); return rid
1131}
1132func ir_emit_simd_vsadd_i64_x4(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1133 let f: *Function = bb.parent
1134 let i: *Instr = alloc_instr(f, OP_SIMD_VSADD_I64_X4, ir_type_i64())
1135 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1136 let v: *Value = val_at(f, rid)
1137 v.instr = i; i.result = rid; i.n_operands = 3
1138 i.op0 = a; i.op1 = b; i.op2 = out
1139 append_instr(bb, i); return rid
1140}
1141func ir_emit_simd_vssub_i64_x4(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1142 let f: *Function = bb.parent
1143 let i: *Instr = alloc_instr(f, OP_SIMD_VSSUB_I64_X4, ir_type_i64())
1144 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1145 let v: *Value = val_at(f, rid)
1146 v.instr = i; i.result = rid; i.n_operands = 3
1147 i.op0 = a; i.op1 = b; i.op2 = out
1148 append_instr(bb, i); return rid
1149}
1150func ir_emit_simd_vreduce_sum_i64_x4(bb: *BasicBlock, p: i64) -> i64 {
1151 let f: *Function = bb.parent
1152 let i: *Instr = alloc_instr(f, OP_SIMD_VREDUCE_SUM_I64_X4, ir_type_i64())
1153 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1154 let v: *Value = val_at(f, rid)
1155 v.instr = i; i.result = rid; i.n_operands = 1
1156 i.op0 = p
1157 append_instr(bb, i); return rid
1158}
1159func ir_emit_simd_vbroadcast_i64_x4(bb: *BasicBlock, scalar: i64, out: i64) -> i64 {
1160 let f: *Function = bb.parent
1161 let i: *Instr = alloc_instr(f, OP_SIMD_VBROADCAST_I64_X4, ir_type_i64())
1162 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1163 let v: *Value = val_at(f, rid)
1164 v.instr = i; i.result = rid; i.n_operands = 2
1165 i.op0 = scalar; i.op1 = out
1166 append_instr(bb, i); return rid
1167}
1168
1169// Splice instruction `movee` (currently in its block's list) to
1170// appear immediately before `anchor` in the same block. No-op if
1171// `movee == anchor` or `movee` is already just before `anchor`.
1172// Used by the inliner to relocate cloned instrs from the block's
1173// tail (where ir_emit_* append them) to just before the call site.
1174func ir_move_instr_before(anchor: *Instr, movee: *Instr) -> i64 {
1175 if movee == anchor { return 0 }
1176 let bb: *BasicBlock = anchor.parent
1177 // Unlink movee from current position.
1178 let mp: *Instr = movee.prev
1179 let mn: *Instr = movee.next
1180 if mp != (0 as *Instr) { mp.next = mn }
1181 if mn != (0 as *Instr) { mn.prev = mp }
1182 if bb.head == movee { bb.head = mn }
1183 if bb.tail == movee { bb.tail = mp }
1184 // Insert before anchor.
1185 let ap: *Instr = anchor.prev
1186 movee.prev = ap
1187 movee.next = anchor
1188 anchor.prev = movee
1189 if ap != (0 as *Instr) {
1190 ap.next = movee
1191 } else {
1192 bb.head = movee
1193 }
1194 return 0
1195}
1196
1197// Emit a getelementptr: result = base + offset. Used for struct
1198// field address computation (after parse_primary sees `.field`).
1199// Result type is the field's pointee-ish type the caller provides
1200// -- the IR's "this Value holds an address" convention.
1201func ir_emit_gep(bb: *BasicBlock, base: i64, offset: i64,
1202 field_ty: *Type) -> i64 {
1203 nx_assert_ptr(bb as *u8, "ir_emit_gep: bb" as *u8)
1204 let f: *Function = bb.parent
1205 nx_assert(f.values_cap > 0, "ir_emit_gep: bb.parent init" as *u8)
1206 let i: *Instr = alloc_instr(f, OP_GEP, field_ty)
1207 let rid: i64 = alloc_value(f, VK_INSTR, field_ty)
1208 let v: *Value = val_at(f, rid)
1209 v.instr = i
1210 i.result = rid
1211 i.n_operands = 2
1212 i.op0 = base
1213 i.op1 = offset
1214 append_instr(bb, i)
1215 return rid
1216}
1217
1218// Emit a call to `callee` with up to 4 positional args (inline op
1219// slots). Return the result Value's id. Callers pass args as an
1220// array of ids plus the count; we copy into op0..op3. More than
1221// 4 args is a follow-up (needs an operands-array side store).
1222func ir_emit_call(bb: *BasicBlock, callee: *Function,
1223 args: *i64, n_args: i64) -> i64 {
1224 nx_assert_ptr(bb as *u8, "ir_emit_call: bb" as *u8)
1225 let f: *Function = bb.parent
1226 nx_assert(f.values_cap > 0, "ir_emit_call: bb.parent init" as *u8)
1227 let ret_ty: *Type = callee.ret_ty
1228 let i: *Instr = alloc_instr(f, OP_CALL, ret_ty)
1229 let rid: i64 = alloc_value(f, VK_INSTR, ret_ty)
1230 let v: *Value = val_at(f, rid)
1231 v.instr = i
1232 i.result = rid
1233 i.callee = callee
1234 i.n_operands = n_args
1235 if n_args > 0 { i.op0 = args[0] }
1236 if n_args > 1 { i.op1 = args[1] }
1237 if n_args > 2 { i.op2 = args[2] }
1238 if n_args > 3 { i.op3 = args[3] }
1239 if n_args > 4 { i.op4 = args[4] }
1240 if n_args > 5 { i.op5 = args[5] }
1241 if n_args > 6 { i.op6 = args[6] }
1242 if n_args > 7 { i.op7 = args[7] }
1243 if n_args > 8 { i.op8 = args[8] }
1244 if n_args > 9 { i.op9 = args[9] }
1245 if n_args > 10 { i.op10 = args[10] }
1246 if n_args > 11 { i.op11 = args[11] }
1247 if n_args > 12 { i.op12 = args[12] }
1248 if n_args > 13 { i.op13 = args[13] }
1249 if n_args > 14 { i.op14 = args[14] }
1250 if n_args > 15 { i.op15 = args[15] }
1251 if n_args > 16 { i.op16 = args[16] }
1252 if n_args > 17 { i.op17 = args[17] }
1253 if n_args > 18 { i.op18 = args[18] }
1254 if n_args > 19 { i.op19 = args[19] }
1255 if n_args > 20 { i.op20 = args[20] }
1256 if n_args > 21 { i.op21 = args[21] }
1257 if n_args > 22 { i.op22 = args[22] }
1258 if n_args > 23 { i.op23 = args[23] }
1259 append_instr(bb, i)
1260 return rid
1261}
1262
1263// fp(args) IR: indirect call through a func-pointer VALUE `callee_val` (stored in op0). Args -> op1.. (MVP caps
1264// at 6, matching the x86 register-arg codegen). n_operands = n_args+1. callee = null (no static callee, so
1265// inlining/tail-call/call-graph passes skip it). Result typed by the fn-ptr's declared return type.
1266func ir_emit_call_indirect(bb: *BasicBlock, callee_val: i64, ret_ty: *Type, args: *i64, n_args: i64) -> i64 {
1267 let f: *Function = bb.parent
1268 let i: *Instr = alloc_instr(f, OP_CALL_INDIRECT, ret_ty)
1269 let rid: i64 = alloc_value(f, VK_INSTR, ret_ty)
1270 let v: *Value = val_at(f, rid)
1271 v.instr = i
1272 i.result = rid
1273 i.callee = 0 as *Function
1274 i.n_operands = n_args + 1
1275 i.op0 = callee_val
1276 if n_args > 0 { i.op1 = args[0] }
1277 if n_args > 1 { i.op2 = args[1] }
1278 if n_args > 2 { i.op3 = args[2] }
1279 if n_args > 3 { i.op4 = args[3] }
1280 if n_args > 4 { i.op5 = args[4] }
1281 if n_args > 5 { i.op6 = args[5] }
1282 // Args 7..23 (op7..op23). Until 2026-07-25 this emitter stopped at op6 while
1283 // STILL setting n_operands = n_args+1, so a backend walking n_operands read
1284 // operand slots that were never written -- garbage arguments, silently. Only
1285 // the parser's 6-arg cap kept it off the road (seq715 family). The IR now
1286 // carries every argument the SysV ABI can actually pass.
1287 if n_args > 6 { i.op7 = args[6] }
1288 if n_args > 7 { i.op8 = args[7] }
1289 if n_args > 8 { i.op9 = args[8] }
1290 if n_args > 9 { i.op10 = args[9] }
1291 if n_args > 10 { i.op11 = args[10] }
1292 if n_args > 11 { i.op12 = args[11] }
1293 if n_args > 12 { i.op13 = args[12] }
1294 if n_args > 13 { i.op14 = args[13] }
1295 if n_args > 14 { i.op15 = args[14] }
1296 if n_args > 15 { i.op16 = args[15] }
1297 if n_args > 16 { i.op17 = args[16] }
1298 if n_args > 17 { i.op18 = args[17] }
1299 if n_args > 18 { i.op19 = args[18] }
1300 if n_args > 19 { i.op20 = args[19] }
1301 if n_args > 20 { i.op21 = args[20] }
1302 if n_args > 21 { i.op22 = args[21] }
1303 if n_args > 22 { i.op23 = args[22] }
1304 append_instr(bb, i)
1305 return rid
1306}
1307
1308// Allocate a fresh Module with pre-sized pools. Callers can keep
1309// using this shape directly; the fields stay public so passes can
1310// walk functions/globals with pointer arithmetic.
1311func ir_module_new(name_bytes: *u8) -> *Module {
1312 // Globals pool grown from 128 -> 4096 slots (task #21 stage-2):
1313 // self-compile of nxc.nx generates >128 string-literal globals.
1314 // Functions pool grown from 256 -> 4096 (closes T#selfhost-004,
1315 // 2026-04-25): nxc.nx has 419 functions; the 256-cap pool
1316 // overflowed at function #257 and corrupted the adjacent
1317 // globals region, causing nxc.elf to SIGSEGV during opt or
1318 // codegen (varied by where the corruption landed).
1319 // nx_pass_bisect.sh confirmed the bug was OUTSIDE opt_run.
1320 let raw: *u8 = sys_mmap(128 + 4096 * 176 + 4096 * 80)
1321 let m: *Module = raw as *Module
1322 m.name = name_bytes
1323 let base: i64 = raw as i64
1324 m.functions = (base + 128) as *Function
1325 m.n_functions = 0
1326 m.fn_cap = 4096
1327 m.globals = (base + 128 + 4096 * 176) as *Global
1328 m.n_globals = 0
1329 m.globals_cap = 4096
1330 return m
1331}
1332
1333// Append a zero-initialised BSS global. Returns its id; also writes
1334// the Global record into the module's globals pool. Each Global is
1335// 72 bytes (id+name_bytes+name_len+bytes+len+is_string+zero_init+
1336// writable = 9 * 8).
1337func ir_add_global_bss(m: *Module, name_bytes: *u8, name_len: i64,
1338 size: i64) -> i64 {
1339 let id: i64 = m.n_globals
1340 nx_assert_lt(id, m.globals_cap,
1341 "ir_add_global_bss: globals pool exhausted" as *u8)
1342 let base: i64 = m.globals as i64
1343 // Stride 80 -- matches ir_module_new's allocator (4096 * 80).
1344 // Was 72 by accident; the 8-byte slack-per-slot gap meant
1345 // readers using stride 80 (the dump in nxc.nx) drifted past
1346 // the live entries by global #9. Closes T#selfhost-006.
1347 let g: *Global = (base + id * 80) as *Global
1348 g.id = id
1349 g.name_bytes = name_bytes
1350 g.name_len = name_len
1351 g.bytes = 0 as *u8
1352 g.len = size
1353 g.is_string = 0
1354 g.zero_init = 1
1355 g.writable = 1
1356 m.n_globals = id + 1
1357 return id
1358}
1359
1360// Append an INITIALISED (non-zero) data global holding an integer value.
1361// `init_val` is written little-endian into a fresh `size`-byte buffer that
1362// backs g.bytes, so the emitter's `zero_init==0` path lists it as `.byte`
1363// storage (module statics live in the loaded image; writable in the bare-
1364// metal / RISC-V RAM target). Unlike ir_add_global_string this is is_string=0
1365// + writable=1 -- a mutable module datum, the storage a `static NAME = INIT`
1366// (or a zero-initialised `static NAME`, init_val=0) resolves to. Returns id.
1367func ir_add_global_data(m: *Module, name_bytes: *u8, name_len: i64,
1368 init_val: i64, size: i64) -> i64 {
1369 let id: i64 = m.n_globals
1370 nx_assert_lt(id, m.globals_cap,
1371 "ir_add_global_data: globals pool exhausted" as *u8)
1372 var nbytes: i64 = size
1373 if nbytes <= 0 { nbytes = 8 }
1374 let buf: *u8 = sys_mmap(nbytes)
1375 var b: i64 = 0
1376 var v: i64 = init_val
1377 while b < nbytes {
1378 buf[b] = (v & 0xFF) as u8
1379 v = v >> 8
1380 b = b + 1
1381 }
1382 let base: i64 = m.globals as i64
1383 let g: *Global = (base + id * 80) as *Global
1384 g.id = id
1385 g.name_bytes = name_bytes
1386 g.name_len = name_len
1387 g.bytes = buf
1388 g.len = nbytes
1389 g.is_string = 0
1390 g.zero_init = 0
1391 g.writable = 1
1392 m.n_globals = id + 1
1393 return id
1394}
1395
1396// Append a string literal global. Returns its id. `bytes` is
1397// null-terminated; len does NOT include the terminator. Emits as
1398// `.asciz` in .rodata.
1399func ir_add_global_string(m: *Module, bytes: *u8, len: i64) -> i64 {
1400 let id: i64 = m.n_globals
1401 nx_assert_lt(id, m.globals_cap,
1402 "ir_add_global_string: globals pool exhausted" as *u8)
1403 let base: i64 = m.globals as i64
1404 let g: *Global = (base + id * 80) as *Global
1405 g.id = id
1406 g.name_bytes = 0 as *u8
1407 g.name_len = 0
1408 g.bytes = bytes
1409 g.len = len
1410 g.is_string = 1
1411 g.zero_init = 0
1412 g.writable = 0
1413 m.n_globals = id + 1
1414 return id
1415}
1416
1417// Build a VK_GLOBAL Value referring to global id `gid`. Backend
1418// lowers this to `la <reg>, .Lg<gid>` (auipc + addi) so the address
1419// at runtime is the loaded virtual address of the global, not the
1420// literal id.
1421//
1422// Previously this tagged as VK_CONST_INT and stuffed gid into
1423// const_int -- backend then emitted `li <reg>, <gid>` and runtime
1424// dereferenced the integer id as if it were an address. Worked at
1425// the C-anchor level (because main.c happens to do whole-program
1426// rewriting) but blew up immediately on self-host: write(2, 0x1d3,
1427// 18) = EFAULT on every stderr message. Closes T#selfhost-003.
1428func ir_global_value(f: *Function, gid: i64, ty: *Type) -> i64 {
1429 let rid: i64 = alloc_value(f, VK_GLOBAL, ty)
1430 let v: *Value = val_at(f, rid)
1431 v.const_int = gid
1432 return rid
1433}
1434
1435// Address of a named function `fn` as a value. Rematerialised at each use site as
1436// `leaq <fn.name>(%rip), %reg` (VK_FUNC_ADDR). const_int holds the *Function pointer.
1437// Mirrors ir_global_value; used by `&fn`, bare-fn-name-as-value, and __thread_clone's entry.
1438func ir_func_addr_value(f: *Function, fn: *Function, ty: *Type) -> i64 {
1439 let rid: i64 = alloc_value(f, VK_FUNC_ADDR, ty)
1440 let v: *Value = val_at(f, rid)
1441 v.const_int = fn as i64
1442 return rid
1443}
1444
1445// Find a function by name in a module's function table. Linear
1446// scan; fine until modules get large. Returns null if not found.
1447func find_function(m: *Module, name: *u8, name_len: i64) -> *Function {
1448 var i: i64 = 0
1449 while i < m.n_functions {
1450 let base: i64 = m.functions as i64
1451 let f: *Function = (base + i * 176) as *Function
1452 if f.name_len == name_len {
1453 // f.name_start holds the *u8 name pointer directly
1454 // (see ir_function_new comment). Compare bytes.
1455 let fname: *u8 = f.name_start as *u8
1456 var j: i64 = 0
1457 var eq: i64 = 1
1458 while j < name_len {
1459 if fname[j] != name[j] { eq = 0; j = name_len }
1460 if eq == 1 { j = j + 1 }
1461 }
1462 if eq == 1 { return f }
1463 }
1464 i = i + 1
1465 }
1466 return 0 as *Function
1467}
1468
1469// Emit unconditional branch.
1470func ir_emit_br(bb: *BasicBlock, target: *BasicBlock) -> i64 {
1471 if ir_bb_sealed(bb) == 1 { return 0 }
1472 let f: *Function = bb.parent
1473 let i: *Instr = alloc_instr(f, OP_BR, ir_type_void())
1474 i.n_operands = 1
1475 i.op0 = target.id
1476 append_instr(bb, i)
1477 // Wire CFG edges. Inline up to 2 succs per block.
1478 if bb.n_succs == 0 {
1479 bb.succ0 = target
1480 }
1481 if bb.n_succs == 1 {
1482 bb.succ1 = target
1483 }
1484 bb.n_succs = bb.n_succs + 1
1485 if target.n_preds == 0 { target.pred0 = bb }
1486 if target.n_preds == 1 { target.pred1 = bb }
1487 if target.n_preds == 2 { target.pred2 = bb }
1488 target.n_preds = target.n_preds + 1
1489 return 0
1490}
1491
1492// Emit conditional branch: br_cond cond, on_true, on_false.
1493// Operands: [cond_value_id, on_true_bb_id, on_false_bb_id].
1494// Wires both targets as successors and both back-edges as preds.
1495func ir_emit_br_cond(bb: *BasicBlock, cond: i64,
1496 on_true: *BasicBlock, on_false: *BasicBlock) -> i64 {
1497 if ir_bb_sealed(bb) == 1 { return 0 }
1498 let f: *Function = bb.parent
1499 let i: *Instr = alloc_instr(f, OP_BR_COND, ir_type_void())
1500 i.n_operands = 3
1501 i.op0 = cond
1502 i.op1 = on_true.id
1503 i.op2 = on_false.id
1504 append_instr(bb, i)
1505
1506 // CFG wiring -- br_cond is always a 2-succ instruction.
1507 // Wire both succs in a single shot. The previous "step n_succs
1508 // twice with overlapping conditions" pattern overwrote succ0 with
1509 // on_false and never set succ1, leaving on_true unreachable from
1510 // opt_sweep's BFS so most blocks looked dead.
1511 bb.succ0 = on_true
1512 bb.succ1 = on_false
1513 bb.n_succs = 2
1514
1515 if on_true.n_preds == 0 { on_true.pred0 = bb }
1516 if on_true.n_preds == 1 { on_true.pred1 = bb }
1517 if on_true.n_preds == 2 { on_true.pred2 = bb }
1518 on_true.n_preds = on_true.n_preds + 1
1519
1520 if on_false.n_preds == 0 { on_false.pred0 = bb }
1521 if on_false.n_preds == 1 { on_false.pred1 = bb }
1522 if on_false.n_preds == 2 { on_false.pred2 = bb }
1523 on_false.n_preds = on_false.n_preds + 1
1524 return 0
1525}
1526
1527// Emit alloca for a single value of `elem_type`. The result Value's
1528// id is returned; it's typed as a pointer to elem_type in the IR's
1529// "the Value holds an address" convention.
1530func ir_emit_alloca(bb: *BasicBlock, elem_ty: *Type) -> i64 {
1531 nx_assert_ptr(bb as *u8, "ir_emit_alloca: bb" as *u8)
1532 let f: *Function = bb.parent
1533 nx_assert(f.values_cap > 0, "ir_emit_alloca: bb.parent init" as *u8)
1534 let i: *Instr = alloc_instr(f, OP_ALLOCA, elem_ty)
1535 let rid: i64 = alloc_value(f, VK_INSTR, elem_ty)
1536 let base: i64 = f.values as i64
1537 let v: *Value = (base + rid * 48) as *Value
1538 v.instr = i
1539 i.result = rid
1540 i.n_operands = 0
1541 append_instr(bb, i)
1542 return rid
1543}
1544
1545// Emit load from `addr`, producing a Value of `load_ty`. Operands:
1546// [addr_value_id].
1547func ir_emit_load(bb: *BasicBlock, addr: i64, load_ty: *Type) -> i64 {
1548 nx_assert_ptr(bb as *u8, "ir_emit_load: bb" as *u8)
1549 let f: *Function = bb.parent
1550 nx_assert_ptr(f as *u8, "ir_emit_load: bb.parent" as *u8)
1551 let i: *Instr = alloc_instr(f, OP_LOAD, load_ty)
1552 let rid: i64 = alloc_value(f, VK_INSTR, load_ty)
1553 let base: i64 = f.values as i64
1554 let v: *Value = (base + rid * 48) as *Value
1555 v.instr = i
1556 i.result = rid
1557 i.n_operands = 1
1558 i.op0 = addr
1559 append_instr(bb, i)
1560 return rid
1561}
1562
1563// Emit store of `val` into `addr`. No result Value (control-flow
1564// instruction shape), but i.ty carries the ELEMENT type so codegen
1565// can pick the right store width (sb/sh/sw/sd). Previously the
1566// type was discarded and i.ty was TY_VOID, so OP_STORE always
1567// emitted `sd` regardless of the actual element width -- a `*u8`
1568// write picked up the next 7 bytes (close T#types-001-codegen).
1569func ir_emit_store(bb: *BasicBlock, addr: i64, val: i64, ty: *Type) -> i64 {
1570 let f: *Function = bb.parent
1571 var st_ty: *Type = ty
1572 if st_ty == (0 as *Type) { st_ty = ir_type_i64() }
1573 let i: *Instr = alloc_instr(f, OP_STORE, st_ty)
1574 i.n_operands = 2
1575 i.op0 = addr
1576 i.op1 = val
1577 append_instr(bb, i)
1578 return 0
1579}
1580
1581// Library only; self-test lives in ir_test.nx. Compile this file as
1582// part of a multi-file build via the `import "nx_ir.nx"` directive.