nx_ir.nx source
↩ module page · 1804 lines · 76588 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// DDR-006: statement line info is stamped at alloc_instr, the one chokepoint every IR builder in
59// this file passes through. Imported EXPLICITLY rather than relying on the flattened closure
60// happening to contain it -- a transitive symbol is a coincidence, not a dependency.
61import "nx_linemap.nx"
62
63// ===== primitive type singletons ==================================
64
65// We stash a few Types in a small pool sized at module-init time.
66// Each call to ir_type_* returns the same pointer.
67
68func alloc_type(kind: i64, size: i64, align: i64) -> *Type {
69 // 96 bytes covers the primitive fields (32) plus struct metadata
70 // (name_bytes, name_len, fields, n_fields = 32 more) plus template
71 // + TY_PARAM extras (n_type_params, type_params, param_name,
72 // param_name_len = 32 more). Struct/template/param kinds
73 // populate their respective extras via ir_type_struct_* etc.
74 let raw: *u8 = sys_mmap(104) // 13 fields x 8B (was 96/12; +sext tags signed-subword-load types)
75 let t: *Type = raw as *Type
76 t.kind = kind
77 t.size = size
78 t.align = align
79 t.pointee = 0 as *Type
80 t.name_bytes = 0 as *u8
81 t.name_len = 0
82 t.fields = 0 as *StructField
83 t.n_fields = 0
84 t.n_type_params = 0
85 t.type_params = 0 as *i64
86 t.param_name = 0 as *u8
87 t.param_name_len = 0
88 t.sext = 0
89 return t
90}
91// alloc a SIGNED subword type (sext=1) -> its subword loads sign-extend on every backend. Use for the
92// i8/i16/i32 annotations only; u8/u16/u32 use plain alloc_type (sext=0 -> zero-extend). 8-byte types
93// don't care (qword load, no extension). Primitive leaf Types are shared BY REFERENCE through
94// clone_type_substituting, so the bit travels with the leaf -- no clone propagation needed.
95func alloc_type_s(kind: i64, size: i64, align: i64) -> *Type {
96 let t: *Type = alloc_type(kind, size, align)
97 t.sext = 1
98 return t
99}
100
101// Allocate a fresh TY_STRUCT with the given declared name. Size
102// starts at 0; callers call ir_type_struct_add_field for each member,
103// which updates size + assigns the member's offset.
104// ---- TY_VEC (kind 11): the vector type -------------------------------
105// WIDTH IS DATA, NOT A CEILING. There is deliberately no MAX_VEC_WIDTH
106// constant here: lane_count is a PARAMETER, so no width is ever picked.
107// Rationale, measured 2026-08-23 (knowledge/vectypes.conf): this estate
108// already carries THREE incompatible vector shapes -- 256-bit SIMD ops in
109// this very file (i8x32/i16x16/i32x8/i64x4 == AVX2), WebAssembly's v128
110// ("the _only_ type introduced in this extension ... bits numbered 0-127",
111// WebAssembly/simd SIMD.md, pinned h548fecfa), and GLSL ES 3.00 / WGSL's
112// 2/3/4-component vectors. Any single blessed width would be wrong for two
113// of the three and would become an estate-wide cap of exactly the class the
114// 2026-08-23 census found 16,773 of (only 2.56% of them DERIVED).
115// Width limits therefore live in each BACKEND's lowering, where they are
116// true and attributable to a cited structure, and a backend that cannot
117// lower a given (lane, count) REFUSES BY NAME carrying BOTH values.
118//
119// NEUTRALITY PROVEN, not asserted (2026-08-23): nx_lang_struct rebuilt WITH
120// these three functions present and WITHOUT them is BYTE-IDENTICAL --
121// sha256 9f0ce0f7ed1d0e6f36f4424b1d42c11a41ea58575d16ef5cae7fffd0e718766a,
122// 351430 B, both ways. (A first comparison against a 352913 B stored
123// artifact showed a 1483-byte shrink; the matched-vintage control proved
124// that delta was TOOLCHAIN VINTAGE, not this edit.)
125//
126// Field reuse is deliberate and keeps Type at 104 bytes:
127// pointee = the LANE type (exactly parallel to its PTR/ARRAY meaning)
128// n_fields = the LANE COUNT (already means "element count" for structs)
129// size/align are DERIVED from the lane, never passed in.
130func ir_type_vec(lane: *Type, lane_count: i64) -> *Type {
131 if lane == (0 as *Type) { return 0 as *Type }
132 if lane_count < 1 { return 0 as *Type }
133 let t: *Type = alloc_type(11, lane.size * lane_count, lane.align)
134 t.pointee = lane
135 t.n_fields = lane_count
136 return t
137}
138
139// Lane type of a TY_VEC, or null if t is not a vector. Callers use the
140// null to refuse by name rather than guessing a lane width.
141func ir_type_vec_lane(t: *Type) -> *Type {
142 if t == (0 as *Type) { return 0 as *Type }
143 if t.kind != 11 { return 0 as *Type }
144 return t.pointee
145}
146
147// Lane count of a TY_VEC, or 0 if t is not a vector (0 is not a legal
148// vector width, so it can never be confused with a real answer).
149func ir_type_vec_lanes(t: *Type) -> i64 {
150 if t == (0 as *Type) { return 0 }
151 if t.kind != 11 { return 0 }
152 return t.n_fields
153}
154
155func ir_type_struct_new(name_bytes: *u8, name_len: i64) -> *Type {
156 let t: *Type = alloc_type(TY_STRUCT, 0, 8)
157 t.name_bytes = name_bytes
158 t.name_len = name_len
159 // Backing array big enough for typical compiler records (~16
160 // fields); wider structs will need a realloc path later.
161 let raw: *u8 = sys_mmap(16 * 32 + 16)
162 t.fields = raw as *StructField
163 return t
164}
165
166// Append one field to a struct type. Assigns the offset (running
167// sum of previous sizes) and grows the struct's total size. Field
168// records are 32 bytes: name_bytes(8) + name_len(8) + ty(8) + offset(8).
169func ir_type_struct_add_field(t: *Type, name_bytes: *u8, name_len: i64,
170 fty: *Type) -> i64 {
171 let base: i64 = t.fields as i64
172 let f: *StructField = (base + t.n_fields * 32) as *StructField
173 f.name_bytes = name_bytes
174 f.name_len = name_len
175 f.ty = fty
176 f.offset = t.size
177 t.size = t.size + fty.size
178 if fty.align > t.align { t.align = fty.align }
179 t.n_fields = t.n_fields + 1
180 return 0
181}
182
183// Find a field by name on a struct Type. Returns null if missing.
184func ir_type_struct_find_field(t: *Type, name_bytes: *u8, name_len: i64) -> *StructField {
185 let base: i64 = t.fields as i64
186 var i: i64 = 0
187 while i < t.n_fields {
188 let f: *StructField = (base + i * 32) as *StructField
189 if f.name_len == name_len {
190 var j: i64 = 0
191 var ok: i64 = 1
192 while j < name_len {
193 if f.name_bytes[j] != name_bytes[j] { ok = 0 }
194 j = j + 1
195 }
196 if ok == 1 { return f }
197 }
198 i = i + 1
199 }
200 return 0 as *StructField
201}
202
203func ir_type_void() -> *Type { return alloc_type(0, 0, 1) }
204func ir_type_bool() -> *Type { return alloc_type(1, 1, 1) }
205func ir_type_i32() -> *Type { return alloc_type(4, 4, 4) }
206func ir_type_i64() -> *Type { return alloc_type(5, 8, 8) }
207// Floating-point types. kind values mirror TY_F32/TY_F64 in types.nx
208// (9 and 10). Size/align follow the RV64F + RV64D ABI: f32 is
209// 4 bytes 4-aligned, f64 is 8 bytes 8-aligned.
210func ir_type_f32() -> *Type { return alloc_type(9, 4, 4) }
211func ir_type_f64() -> *Type { return alloc_type(10, 8, 8) }
212
213// ===== function / block / value construction ======================
214
215// Allocate an empty Function with a backing arena big enough for
216// the self-host compiling itself (nxc.nx -> Wheeler DDC byte-equal
217// proof). Pool sizes raised 2026-05-16 from "small benchmarks
218// (~4k values, 256 blocks, 4k instrs)" because compiling nxc.nx
219// itself hit blocks_cap=256 in the assert_lt guard:
220//
221// nx_assert_lt: idx=256 cap=256 tag=ir_block_new: blocks pool
222//
223// nxc.nx is ~6KLOC NishiLang with ~419 functions; some single
224// functions (parser dispatch, opt passes) have well over 256
225// basic blocks. Bumped to 4096 blocks for headroom.
226//
227// Backing arena: 128 (header) + 32768 * 48 (values) +
228// 4096 * 96 (blocks) + 32768 * 192 (instrs)
229// = ~8.4 MB per function, all from sys_mmap which is
230// page-aligned + lazy-faulted so unused pages cost
231// ~zero RSS. Instr stride bumped 128 -> 192 on
232// 2026-05-20 to add op8..op15 for >8-arg calls
233// (11-arg AEAD primitives in the HTTPS chain).
234
235func ir_function_new(m: *Module, name: *u8, name_len: i64,
236 ret_ty: *Type) -> *Function {
237 let raw: *u8 = sys_mmap(128 + 32768*48 + 4096*96 + 32768*256)
238 let f: *Function = raw as *Function
239 let base: i64 = raw as i64
240 // Until the Module gets a proper name table, stash the *u8 name
241 // pointer directly in f.name_start (both are i64-sized). The
242 // driver casts back to *u8 when it needs to emit the label.
243 f.name_start = name as i64
244 f.name_len = name_len
245 f.ret_ty = ret_ty
246 f.n_params = 0
247 f.param_ptr_mask = 0 // 0 = signature not yet known; call sites skip type checking
248 f.values = (base + 128) as *Value
249 f.n_values = 0
250 f.values_cap = 32768
251 f.blocks = (base + 128 + 32768*48) as *BasicBlock
252 f.n_blocks = 0
253 f.blocks_cap = 4096
254 f.instrs = (base + 128 + 32768*48 + 4096*96) as *Instr
255 f.n_instrs = 0
256 f.instrs_cap = 32768
257 f.entry = 0 as *BasicBlock
258
259 // Append to module's functions pool so m.n_functions reflects
260 // reality and the driver can walk m.functions to emit each one.
261 // Stride is 176 bytes per Function pool slot.
262 //
263 // Skip the copy if m.functions is null (test harnesses + the
264 // top-level parse_module bootstrap that haven't allocated a pool
265 // yet); just return the heap-allocated f. Otherwise NULL+offset
266 // = SIGSEGV when ir_test or parse_test creates Functions without
267 // a pre-allocated pool.
268 if m != (0 as *Module) {
269 let fn_base: i64 = m.functions as i64
270 if fn_base == 0 { return f }
271 // THE CHECK THE fn_cap FIELD WAS DECLARED FOR, AND NEVER GIVEN. Without it this append walks
272 // straight off the end of the pool into whatever the kernel mapped next -- which is the globals
273 // pool, because consecutive anonymous mmaps are neighbours. The symptom is not a crash at the
274 // overflow: it is globals reading back as ZEROS later, i.e. a compiler reporting names it really
275 // did define as undefined. That has already happened twice here (T#selfhost-004 and -006, 256
276 // slots vs nxc.nx's 419 functions) and both times the remedy was to raise the number.
277 // Refusing is fail-closed and cheap: a build that stops is recoverable, a silently corrupted one
278 // costs the next reader the entire investigation.
279 if m.n_functions >= m.fn_cap {
280 diag_capacity_named("function pool (functions PER UNIT)" as *u8, "unit" as *u8, 0, m.n_functions + 1, m.fn_cap,
281 "the cap is DERIVED from the unit's func count by parse_module and set by ir_module_new, so reaching it means a function was created that the pre-pass did not count (a generic instantiation or a synthesized helper) -- raise the derivation in parse_module or ir_module_new, never a literal here." as *u8)
282 }
283 nx_assert_lt(m.n_functions, m.fn_cap,
284 "ir_new_function: module function pool exhausted -- fn_cap is set by parse_module (derived from the unit's func count) and by ir_module_new" as *u8)
285 let dst: i64 = fn_base + m.n_functions * NX_MODULE_FN_STRIDE
286 // Copy the Function struct we allocated into the pool slot so
287 // pool iteration walks contiguous records. We ALSO preserve
288 // the fresh heap allocation for values/blocks/instrs pointers
289 // which live inside `raw` (the 128-byte header shares space
290 // with the Function struct itself, so the copy is the first
291 // 128 bytes of raw).
292 var k: i64 = 0
293 let src_u8: *u8 = raw
294 let dst_u8: *u8 = dst as *u8
295 while k < NX_MODULE_FN_STRIDE {
296 dst_u8[k] = src_u8[k]
297 k = k + 1
298 }
299 m.n_functions = m.n_functions + 1
300 return (dst as *Function)
301 }
302 return f
303}
304
305// Resolve a Value by id in f's values pool. Pure pointer arithmetic;
306// used by every pass that inspects existing SSA values.
307func val_at(f: *Function, id: i64) -> *Value {
308 let base: i64 = f.values as i64
309 return (base + id * 48) as *Value
310}
311
312// Resolve a BasicBlock by id in f's blocks pool.
313func block_at(f: *Function, id: i64) -> *BasicBlock {
314 let base: i64 = f.blocks as i64
315 return (base + id * 96) as *BasicBlock
316}
317
318// Allocate one Value in `f`'s pool; return its id.
319func alloc_value(f: *Function, kind: i64, ty: *Type) -> i64 {
320 nx_assert_ptr(f as *u8, "alloc_value: f" as *u8)
321 let id: i64 = f.n_values
322 let cap: i64 = f.values_cap
323 nx_assert(cap > 0, "alloc_value: f.values_cap > 0 (f initialised)" as *u8)
324 if id >= cap {
325 diag_capacity_named("value pool (SSA values PER FUNCTION)" as *u8, f.name_start as *u8, f.name_len, id + 1, cap,
326 "split this function -- every temporary, constant and parameter is one value, so a very long body or a large literal table belongs in a helper function of its own." as *u8)
327 }
328 nx_assert_lt(id, cap, "alloc_value: values pool" as *u8)
329 let base: i64 = f.values as i64
330 nx_assert(base != 0, "alloc_value: f.values != NULL" as *u8)
331 let v: *Value = (base + id * 48) as *Value
332 v.id = id
333 v.kind = kind
334 v.ty = ty
335 v.const_int = 0
336 v.param_index = 0
337 v.instr = 0 as *Instr
338 f.n_values = id + 1
339 return id
340}
341
342// LN25 + LN27 (2026-09-02): THE ONE VOICE FOR EVERY CAPACITY CAP IN THE COMPILER -- the symbol the
343// lang.matrix watch names (diag_capacity_named). A pool sized once (per function or per unit) and
344// overrun would silently corrupt its neighbour and surface far away as a parser desync or a name reading
345// back as undefined (T#selfhost-004/-006; the 253-name if-chain of 2026-09-02). So every cap is a compile
346// ERROR in the teaching voice: where (the function or the unit, by name), what (the pool), how far
347// (measured against the cap), why the build stopped, and the fix -- never a bare assert naming only the
348// pool. nx_ir cannot import nx_parse (nx_parse imports nx_ir), so the voice is spoken here with the
349// runtime-length printers from nx_assert.nx (no hand-counted lengths); the "error at " prefix is the
350// same anchor nx_diag_at prints, so the build door's error window and the LN27 gate recognise it.
351// Exit 2 = a refused build, the code the parser uses for its own errors. name_len 0 names the unit.
352// decimal on stderr with NO trailing newline (nx_puti_err appends one, which would break the sentence).
353func diag_put_dec(v: i64) -> i64 {
354 if v == 0 { sys_write(2, "0" as *u8, 1); return 0 }
355 var m: i64 = v
356 if m < 0 { sys_write(2, "-" as *u8, 1); m = 0 - m }
357 let t: *u8 = sys_mmap(24)
358 var k: i64 = 0
359 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
360 let o: *u8 = sys_mmap(24)
361 var w: i64 = 0
362 var q: i64 = k - 1
363 while q >= 0 { o[w] = t[q]; w = w + 1; q = q - 1 }
364 sys_write(2, o, w)
365 return 0
366}
367func diag_capacity_named(pool: *u8, name: *u8, name_len: i64, measured: i64, cap: i64, remedy: *u8) -> i64 {
368 nx_puts_err("error at function " as *u8)
369 if name_len > 0 { sys_write(2, name, name_len) } else { nx_puts_err("(this unit)" as *u8) }
370 nx_puts_err(": the " as *u8)
371 nx_puts_err(pool)
372 nx_puts_err(" capacity is reached -- measured " as *u8)
373 diag_put_dec(measured)
374 nx_puts_err(" against a cap of " as *u8)
375 diag_put_dec(cap)
376 nx_puts_err(".\n" as *u8)
377 nx_puts_err(" why the build stopped: this pool is sized once and cannot grow. Writing past it would silently corrupt the neighbouring allocation and surface far away as a wrong value, or as a name that reads as undefined -- so the compiler refuses here, at the cause, instead of building on.\n" as *u8)
378 nx_puts_err(" fix: " as *u8)
379 nx_puts_err(remedy)
380 nx_puts_err("\n" as *u8)
381 sys_exit(2)
382 return 0
383}
384
385// Create a BasicBlock and append to f's list.
386func ir_block_new(f: *Function) -> *BasicBlock {
387 let id: i64 = f.n_blocks
388 let base: i64 = f.blocks as i64
389 nx_assert_ptr(f as *u8, "ir_block_new: f" as *u8)
390 nx_assert(f.blocks_cap > 0, "ir_block_new: f.blocks_cap > 0" as *u8)
391 // LN25 (2026-09-02): a full pool is a compile error naming the FUNCTION, never an assert naming only
392 // the pool. Only the failing path pays; the assert below stays as the unreachable backstop.
393 if id >= f.blocks_cap {
394 diag_capacity_named("basic-block pool (blocks PER FUNCTION)" as *u8, f.name_start as *u8, f.name_len, id + 1, f.blocks_cap,
395 "split this function into smaller ones. Every if, else and loop nesting costs blocks, and a long if-chain costs one block per arm (a 253-name entity table did exactly this) -- a few helper functions of 200 arms each compile where one of 253 does not." as *u8)
396 }
397 nx_assert_lt(id, f.blocks_cap, "ir_block_new: blocks pool" as *u8)
398 nx_assert(base != 0, "ir_block_new: f.blocks != NULL" as *u8)
399 let b: *BasicBlock = (base + id * 96) as *BasicBlock
400 b.id = id
401 b.head = 0 as *Instr
402 b.tail = 0 as *Instr
403 b.parent = f
404 b.n_preds = 0
405 b.n_succs = 0
406 f.n_blocks = id + 1
407 if f.entry == (0 as *BasicBlock) {
408 f.entry = b
409 }
410 return b
411}
412
413// Integer literal.
414func ir_const_i64(f: *Function, n: i64) -> i64 {
415 nx_assert_ptr(f as *u8, "ir_const_i64: f" as *u8)
416 nx_assert(f.values_cap > 0, "ir_const_i64: f init" as *u8)
417 let id: i64 = alloc_value(f, 0, ir_type_i64())
418 let base: i64 = f.values as i64
419 let v: *Value = (base + id * 48) as *Value
420 v.const_int = n
421 return id
422}
423
424// Parameter Value.
425func ir_param(f: *Function, idx: i64, ty: *Type) -> i64 {
426 nx_assert_ptr(f as *u8, "ir_param: f" as *u8)
427 nx_assert(f.values_cap > 0, "ir_param: f init" as *u8)
428 let id: i64 = alloc_value(f, 1, ty)
429 let base: i64 = f.values as i64
430 let v: *Value = (base + id * 48) as *Value
431 v.param_index = idx
432 return id
433}
434
435// Append `inst` to `bb`'s instruction list. Updates prev/next/tail/head.
436func append_instr(bb: *BasicBlock, inst: *Instr) -> i64 {
437 inst.parent = bb
438 inst.prev = bb.tail
439 inst.next = 0 as *Instr
440 if bb.tail != (0 as *Instr) {
441 bb.tail.next = inst
442 }
443 if bb.head == (0 as *Instr) {
444 bb.head = inst
445 }
446 bb.tail = inst
447 return 0
448}
449
450// Allocate a fresh Instr from the function's pool.
451func alloc_instr(f: *Function, op: i64, ty: *Type) -> *Instr {
452 nx_assert_ptr(f as *u8, "alloc_instr: f" as *u8)
453 nx_assert(f.instrs_cap > 0, "alloc_instr: f.instrs_cap > 0" as *u8)
454 if f.n_instrs >= f.instrs_cap {
455 diag_capacity_named("instruction pool (IR instructions PER FUNCTION)" as *u8, f.name_start as *u8, f.name_len, f.n_instrs + 1, f.instrs_cap,
456 "split this function -- the cap counts every IR instruction one body emits; move a long computation, a large literal table or a deep if-chain into helper functions." as *u8)
457 }
458 nx_assert_lt(f.n_instrs, f.instrs_cap, "alloc_instr: instrs pool" as *u8)
459 let id: i64 = f.n_instrs
460 let base: i64 = f.instrs as i64
461 nx_assert(base != 0, "alloc_instr: f.instrs != NULL" as *u8)
462 // Stride 256 = sizeof(Instr) after op16..op23 added 2026-06-18 (was 192 after op8..op15).
463 let i: *Instr = (base + id * 256) as *Instr
464 i.op = op
465 i.result = 0
466 i.ty = ty
467 i.n_operands = 0
468 i.op0 = 0; i.op1 = 0; i.op2 = 0; i.op3 = 0
469 i.op4 = 0; i.op5 = 0; i.op6 = 0; i.op7 = 0
470 i.op8 = 0; i.op9 = 0; i.op10 = 0; i.op11 = 0
471 i.op12 = 0; i.op13 = 0; i.op14 = 0; i.op15 = 0
472 i.op16 = 0; i.op17 = 0; i.op18 = 0; i.op19 = 0
473 i.op20 = 0; i.op21 = 0; i.op22 = 0; i.op23 = 0
474 i.callee = 0 as *Function
475 i.parent = 0 as *BasicBlock
476 i.prev = 0 as *Instr
477 i.next = 0 as *Instr
478 f.n_instrs = id + 1
479 // STATEMENT LINE INFO (DDR-006). THE ONE CHOKEPOINT: all 72 IR-construction sites in this file
480 // funnel through here, so one stamp covers every builder without touching struct Instr -- whose
481 // own comments record that widening it moved the stride 128->192->256 and needed lockstep edits
482 // across nx_ir, nx_opt, nx_x86_regalloc and nx_parse. Gated on -g: a normal build pays a branch.
483 if lm_debug_on() == 1 { lm_stmt_stamp(i as i64) }
484 return i
485}
486
487// Emit a binary op. Result Value's id is stored in inst.result
488// and returned so the caller can use it as an operand.
489func ir_emit_binop(bb: *BasicBlock, op: i64,
490 a: i64, b: i64, ret_ty: *Type) -> i64 {
491 nx_assert_ptr(bb as *u8, "ir_emit_binop: bb" as *u8)
492 let f: *Function = bb.parent
493 nx_assert_ptr(f as *u8, "ir_emit_binop: bb.parent" as *u8)
494 nx_assert(f.values_cap > 0, "ir_emit_binop: bb.parent init" as *u8)
495 let i: *Instr = alloc_instr(f, op, ret_ty)
496 let rid: i64 = alloc_value(f, 2, ret_ty)
497 let base: i64 = f.values as i64
498 let v: *Value = (base + rid * 48) as *Value
499 v.instr = i
500 i.result = rid
501 i.n_operands = 2
502 i.op0 = a
503 i.op1 = b
504 append_instr(bb, i)
505 return rid
506}
507
508// Single-operand instruction (bswap / clz / ctz / popcnt / casts).
509// op0 = a; void-free, returns a fresh value id of ret_ty.
510func ir_emit_unop(bb: *BasicBlock, op: i64, a: i64, ret_ty: *Type) -> i64 {
511 nx_assert_ptr(bb as *u8, "ir_emit_unop: bb" as *u8)
512 let f: *Function = bb.parent
513 nx_assert_ptr(f as *u8, "ir_emit_unop: bb.parent" as *u8)
514 nx_assert(f.values_cap > 0, "ir_emit_unop: bb.parent init" as *u8)
515 let i: *Instr = alloc_instr(f, op, ret_ty)
516 let rid: i64 = alloc_value(f, 2, ret_ty)
517 let base: i64 = f.values as i64
518 let v: *Value = (base + rid * 48) as *Value
519 v.instr = i
520 i.result = rid
521 i.n_operands = 1
522 i.op0 = a
523 append_instr(bb, i)
524 return rid
525}
526
527// ---- Atomic intrinsics (C bootstrap parity) ----------------------
528// Memory-order operand is last; backend emits conservative-strong.
529
530func ir_emit_atomic_load_i64(bb: *BasicBlock, addr: i64, mo: i64) -> i64 {
531 let f: *Function = bb.parent
532 let i: *Instr = alloc_instr(f, OP_ATOMIC_LOAD_I64, ir_type_i64())
533 let rid: i64 = alloc_value(f, 2, ir_type_i64())
534 let v: *Value = ((f.values as i64) + rid * 48) as *Value
535 v.instr = i
536 i.result = rid
537 i.n_operands = 2
538 i.op0 = addr
539 i.op1 = mo
540 append_instr(bb, i)
541 return rid
542}
543
544func ir_emit_atomic_store_i64(bb: *BasicBlock, addr: i64, val: i64, mo: i64) -> i64 {
545 let f: *Function = bb.parent
546 let i: *Instr = alloc_instr(f, OP_ATOMIC_STORE_I64, ir_type_void())
547 i.n_operands = 3
548 i.op0 = addr
549 i.op1 = val
550 i.op2 = mo
551 append_instr(bb, i)
552 return 0
553}
554
555// G3: add the 128-bit (hi:lo) into the 3-word accumulator at acc_ptr with carry.
556// Void + 3-operand (acc_ptr escaping pointer + lo + hi), so it is opt-opaque and
557// never homed; the backend lowers it to one contiguous addq;adcq;adcq block.
558func ir_emit_adc_acc(bb: *BasicBlock, acc_ptr: i64, lo: i64, hi: i64) -> i64 {
559 let f: *Function = bb.parent
560 let i: *Instr = alloc_instr(f, OP_ADC_ACC, ir_type_void())
561 i.n_operands = 3
562 i.op0 = acc_ptr
563 i.op1 = lo
564 i.op2 = hi
565 append_instr(bb, i)
566 return 0
567}
568
569func ir_emit_q8rowdot(bb: *BasicBlock, qrow: i64, arow: i64, nblk: i64) -> i64 {
570 let f: *Function = bb.parent
571 let i: *Instr = alloc_instr(f, OP_Q8ROWDOT, ir_type_i64())
572 let rid: i64 = alloc_value(f, 2, ir_type_i64())
573 let v: *Value = ((f.values as i64) + rid * 48) as *Value
574 v.instr = i
575 i.result = rid
576 i.n_operands = 3
577 i.op0 = qrow
578 i.op1 = arow
579 i.op2 = nblk
580 append_instr(bb, i)
581 return rid
582}
583
584func ir_emit_i8fma32(bb: *BasicBlock, a: i64, b: i64, d: i64, acc: i64) -> i64 {
585 let f: *Function = bb.parent
586 let i: *Instr = alloc_instr(f, OP_I8FMA32, ir_type_i64())
587 let rid: i64 = alloc_value(f, 2, ir_type_i64())
588 let v: *Value = ((f.values as i64) + rid * 48) as *Value
589 v.instr = i
590 i.result = rid
591 i.n_operands = 4
592 i.op0 = a
593 i.op1 = b
594 i.op2 = d
595 i.op3 = acc
596 append_instr(bb, i)
597 return rid
598}
599
600func ir_emit_atomic_cas_i64(bb: *BasicBlock, addr: i64, exp: i64, newv: i64, mo: i64) -> i64 {
601 let f: *Function = bb.parent
602 let i: *Instr = alloc_instr(f, OP_ATOMIC_CAS_I64, ir_type_i64())
603 let rid: i64 = alloc_value(f, 2, ir_type_i64())
604 let v: *Value = ((f.values as i64) + rid * 48) as *Value
605 v.instr = i
606 i.result = rid
607 i.n_operands = 4
608 i.op0 = addr
609 i.op1 = exp
610 i.op2 = newv
611 i.op3 = mo
612 append_instr(bb, i)
613 return rid
614}
615
616func ir_emit_atomic_faa_i64(bb: *BasicBlock, addr: i64, delta: i64, mo: i64) -> i64 {
617 let f: *Function = bb.parent
618 let i: *Instr = alloc_instr(f, OP_ATOMIC_FAA_I64, ir_type_i64())
619 let rid: i64 = alloc_value(f, 2, ir_type_i64())
620 let v: *Value = ((f.values as i64) + rid * 48) as *Value
621 v.instr = i
622 i.result = rid
623 i.n_operands = 3
624 i.op0 = addr
625 i.op1 = delta
626 i.op2 = mo
627 append_instr(bb, i)
628 return rid
629}
630
631func ir_emit_atomic_fence(bb: *BasicBlock, mo: i64) -> i64 {
632 let f: *Function = bb.parent
633 let i: *Instr = alloc_instr(f, OP_ATOMIC_FENCE, ir_type_void())
634 i.n_operands = 1
635 i.op0 = mo
636 append_instr(bb, i)
637 return 0
638}
639
640// __thread_clone(stack_top, entry_fn, ctx) -> child_tid. 3 operands.
641func ir_emit_thread_clone(bb: *BasicBlock, stack_top: i64, entry_fn: i64, ctx: i64) -> i64 {
642 let f: *Function = bb.parent
643 let i: *Instr = alloc_instr(f, OP_THREAD_CLONE, ir_type_i64())
644 let rid: i64 = alloc_value(f, 2, ir_type_i64())
645 let v: *Value = ((f.values as i64) + rid * 48) as *Value
646 v.instr = i
647 i.result = rid
648 i.n_operands = 3
649 i.op0 = stack_top
650 i.op1 = entry_fn
651 i.op2 = ctx
652 append_instr(bb, i)
653 return rid
654}
655
656// A block that already ends in an unconditional terminator is SEALED: emitting
657// another terminator into it is a no-op. Without this, `break`/`continue`/`return`
658// inside an if-arm left TWO terminators in the arm block (the statement's br plus
659// parse_stmt_if's merge br): bb.tail then pointed at the DEAD second br, every
660// CFG pass saw the wrong edge, and the G12/G15 emit-time fallthrough made the
661// dead br REACHABLE -- the dt_parse_attrs infinite-spin miscompile (2026-07-27,
662// minimal repro nx_breakprobe P4; oracle nx_cc_known_good disagrees-and-is-right).
663// Defined ABOVE every caller (ir_emit_return + the br emitters below) per the
664// define-before-use rule.
665func ir_bb_sealed(bb: *BasicBlock) -> i64 {
666 let t: *Instr = bb.tail
667 if t == (0 as *Instr) { return 0 }
668 if t.op == OP_BR { return 1 }
669 if t.op == OP_BR_COND { return 1 }
670 if t.op == OP_RETURN { return 1 }
671 if t.op == OP_TAIL_CALL { return 1 }
672 return 0
673}
674
675// Emit return. Void-typed instruction.
676func ir_emit_return(bb: *BasicBlock, v: i64) -> i64 {
677 if ir_bb_sealed(bb) == 1 { return 0 }
678 let f: *Function = bb.parent
679 let i: *Instr = alloc_instr(f, 30, ir_type_void())
680 i.n_operands = 1
681 i.op0 = v
682 append_instr(bb, i)
683 return 0
684}
685
686// Kernel intrinsics -- wait-for-interrupt, CSR read/write, fence,
687// mret. These don't have return values (except CSR_READ); we still
688// allocate a Value for them so the IR stays uniform and opt passes
689// that walk values-by-id don't crash.
690
691// wfi: no operands, void result. Lowered to `wfi` instruction.
692func ir_emit_wfi(bb: *BasicBlock) -> i64 {
693 let f: *Function = bb.parent
694 let i: *Instr = alloc_instr(f, OP_WFI, ir_type_void())
695 i.n_operands = 0
696 append_instr(bb, i)
697 return 0
698}
699
700// csrr(csr_num) -> i64. csr_num must be a constant at codegen time.
701// Lowered to `csrr <dst>, <csr>`.
702func ir_emit_csr_read(bb: *BasicBlock, csr_num: i64) -> i64 {
703 let f: *Function = bb.parent
704 let i: *Instr = alloc_instr(f, OP_CSR_READ, ir_type_i64())
705 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
706 let v: *Value = val_at(f, rid)
707 v.instr = i
708 i.result = rid
709 i.n_operands = 1
710 i.op0 = csr_num
711 append_instr(bb, i)
712 return rid
713}
714
715// csrw(csr_num, val). val is a Value id; csr_num is a constant.
716// Lowered to `csrw <csr>, <src>`.
717func ir_emit_csr_write(bb: *BasicBlock, csr_num: i64, val: i64) -> i64 {
718 let f: *Function = bb.parent
719 let i: *Instr = alloc_instr(f, OP_CSR_WRITE, ir_type_void())
720 i.n_operands = 2
721 i.op0 = csr_num
722 i.op1 = val
723 append_instr(bb, i)
724 return 0
725}
726
727// fence -- full memory barrier. Lowered to `fence rw, rw`.
728func ir_emit_fence(bb: *BasicBlock) -> i64 {
729 let f: *Function = bb.parent
730 let i: *Instr = alloc_instr(f, OP_FENCE, ir_type_void())
731 i.n_operands = 0
732 append_instr(bb, i)
733 return 0
734}
735
736// mret -- machine-mode return. Lowered to `mret`. Used at the end
737// of trap handlers to return from M-mode trap to the originating
738// privilege + pc captured in mepc/mstatus.
739func ir_emit_mret(bb: *BasicBlock) -> i64 {
740 let f: *Function = bb.parent
741 let i: *Instr = alloc_instr(f, OP_MRET, ir_type_void())
742 i.n_operands = 0
743 append_instr(bb, i)
744 return 0
745}
746
747// Emit a raw syscall (ECALL). op0 = syscall number, op1..op6 = args.
748// Linux RV64 ABI: a7 = syscall number, a0..a5 = args, result in a0.
749// Up to 6 args supported (the kernel ABI limit); extras would need
750// a side-operands array, not warranted today.
751func ir_emit_syscall(bb: *BasicBlock, args: *i64, n_args: i64) -> i64 {
752 let f: *Function = bb.parent
753 let i: *Instr = alloc_instr(f, OP_SYSCALL, ir_type_i64())
754 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
755 let v: *Value = val_at(f, rid)
756 v.instr = i
757 i.result = rid
758 i.n_operands = n_args
759 if n_args > 0 { i.op0 = args[0] }
760 if n_args > 1 { i.op1 = args[1] }
761 if n_args > 2 { i.op2 = args[2] }
762 if n_args > 3 { i.op3 = args[3] }
763 if n_args > 4 { i.op4 = args[4] }
764 if n_args > 5 { i.op5 = args[5] }
765 if n_args > 6 { i.op6 = args[6] }
766 append_instr(bb, i)
767 return rid
768}
769
770// Emit a hardware-f32 BINARY op (OP_FADD/FSUB/FMUL/FDIV). a, b, and the result are
771// i64-CARRIED IEEE-754 binary32 bit-patterns -- NishiLang has no f32 type, so a float
772// rides in the low 32 bits of an i64; the x86 backend moves it GPR<->xmm and computes
773// with SSE scalar-single. Result Value is typed i64 (the carrier); lowering owns the
774// float semantics. The OP_F* opcodes were reserved in nx_types.nx awaiting this.
775func ir_emit_f32_binop(bb: *BasicBlock, op: i64, a: i64, b: i64) -> i64 {
776 let f: *Function = bb.parent
777 let i: *Instr = alloc_instr(f, op, ir_type_i64())
778 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
779 let v: *Value = val_at(f, rid)
780 v.instr = i
781 i.result = rid
782 i.n_operands = 2
783 i.op0 = a
784 i.op1 = b
785 append_instr(bb, i)
786 return rid
787}
788// Emit a hardware-f32 UNARY cast: OP_FCAST_I_TO_F (i64 int -> f32 bits, cvtsi2ss) or
789// OP_FCAST_F_TO_I (f32 bits -> i64 int, cvttss2si truncate). One operand, i64 carrier.
790func ir_emit_f32_unop(bb: *BasicBlock, op: i64, a: i64) -> i64 {
791 let f: *Function = bb.parent
792 let i: *Instr = alloc_instr(f, op, ir_type_i64())
793 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
794 let v: *Value = val_at(f, rid)
795 v.instr = i
796 i.result = rid
797 i.n_operands = 1
798 i.op0 = a
799 append_instr(bb, i)
800 return rid
801}
802// f64-RESULT unop (OP_FCAST_I_TO_F, OP_FSQRT): the result carrier is TY_F64 so
803// the backend's x86ctx_emit_float routes it to double-precision codegen and
804// downstream f64 arithmetic sees a float operand. (F_TO_I keeps ir_emit_f32_unop:
805// its result is a true i64, and the backend reads op0's f64 type for precision.)
806func ir_emit_f64_unop(bb: *BasicBlock, op: i64, a: i64) -> i64 {
807 let f: *Function = bb.parent
808 let fty: *Type = alloc_type(TY_F64, 8, 8)
809 let i: *Instr = alloc_instr(f, op, fty)
810 let rid: i64 = alloc_value(f, VK_INSTR, fty)
811 let v: *Value = val_at(f, rid)
812 v.instr = i
813 i.result = rid
814 i.n_operands = 1
815 i.op0 = a
816 append_instr(bb, i)
817 return rid
818}
819// FMA vector-accumulate: *acc += a*b (8-wide fused). 3 operands; result carrier i64 (unused).
820func ir_emit_f32x8_fma(bb: *BasicBlock, acc: i64, a: i64, b: i64) -> i64 {
821 let f: *Function = bb.parent
822 let i: *Instr = alloc_instr(f, OP_F32X8_FMA, ir_type_i64())
823 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
824 let v: *Value = val_at(f, rid)
825 v.instr = i
826 i.result = rid
827 i.n_operands = 3
828 i.op0 = acc
829 i.op1 = a
830 i.op2 = b
831 append_instr(bb, i)
832 return rid
833}
834// NO-FLOAT integer madd-accumulate: *acc(i32x8) += vpmaddwd(a(i16x16), b(i16x16)). 3 operands.
835// R0r-b: __i16_dot(a, b, n) -> i64, the whole-chunk twin of the madd (see OP_I16DOT in nx_types.nx).
836func ir_emit_i16dot(bb: *BasicBlock, a: i64, b: i64, n: i64) -> i64 {
837 let f: *Function = bb.parent
838 let i: *Instr = alloc_instr(f, OP_I16DOT, ir_type_i64())
839 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
840 let v: *Value = val_at(f, rid)
841 v.instr = i
842 i.result = rid
843 i.n_operands = 3
844 i.op0 = a
845 i.op1 = b
846 i.op2 = n
847 append_instr(bb, i)
848 return rid
849}
850// R0s-b: __q8blk_i16dot(codes, x) -> i64, one Q8_0 block's int8 x i16 dot (see OP_Q8BLKDOT in nx_types.nx). 2 operands.
851func ir_emit_q8blkdot(bb: *BasicBlock, a: i64, b: i64) -> i64 {
852 let f: *Function = bb.parent
853 let i: *Instr = alloc_instr(f, OP_Q8BLKDOT, ir_type_i64())
854 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
855 let v: *Value = val_at(f, rid)
856 v.instr = i
857 i.result = rid
858 i.n_operands = 2
859 i.op0 = a
860 i.op1 = b
861 append_instr(bb, i)
862 return rid
863}
864
865func ir_emit_i16x16_madd(bb: *BasicBlock, acc: i64, a: i64, b: i64) -> i64 {
866 let f: *Function = bb.parent
867 let i: *Instr = alloc_instr(f, OP_I16X16_MADD, ir_type_i64())
868 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
869 let v: *Value = val_at(f, rid)
870 v.instr = i
871 i.result = rid
872 i.n_operands = 3
873 i.op0 = acc
874 i.op1 = a
875 i.op2 = b
876 append_instr(bb, i)
877 return rid
878}
879// Hardware SHA-NI: one full SHA-256 block compression IN PLACE. op0=state ptr (*u32[8]),
880// op1=block ptr (*u8[64] big-endian msg), op2=K ptr (*u32[64]). 3 operands; i64 result (0).
881func ir_emit_q5unpack32(bb: *BasicBlock, qhqs: i64, out: i64, consts: i64) -> i64 {
882 let f: *Function = bb.parent
883 let i: *Instr = alloc_instr(f, OP_Q5UNPACK32, ir_type_i64())
884 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
885 let v: *Value = val_at(f, rid)
886 v.instr = i
887 i.result = rid
888 i.n_operands = 3
889 i.op0 = qhqs
890 i.op1 = out
891 i.op2 = consts
892 append_instr(bb, i)
893 return rid
894}
895
896// Q4_K AVX2 unpack-and-scale: op0=qs ptr (*u8[32]), op1=out ptr (*i16[64]), op2=scpack i64 (sc_lo | sc_hi<<16).
897// 3 operands; i64 result (0). See OP_Q4KUNPACK32S in nx_types.nx.
898func ir_emit_q4kunpack32s(bb: *BasicBlock, qs: i64, out: i64, scpack: i64) -> i64 {
899 let f: *Function = bb.parent
900 let i: *Instr = alloc_instr(f, OP_Q4KUNPACK32S, ir_type_i64())
901 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
902 let v: *Value = val_at(f, rid)
903 v.instr = i
904 i.result = rid
905 i.n_operands = 3
906 i.op0 = qs
907 i.op1 = out
908 i.op2 = scpack
909 append_instr(bb, i)
910 return rid
911}
912
913// Q4_K whole-super-block dot: op0=sb ptr (*u8[144]), op1=col ptr (*i16[256]), op2=scpre ptr (*i64[8]), op3=out ptr
914// (*i64[5]). 4 operands; i64 result (0). See OP_Q4KSBDOT in nx_types.nx.
915func ir_emit_q4ksbdot(bb: *BasicBlock, sb: i64, col: i64, scpre: i64, out: i64) -> i64 {
916 let f: *Function = bb.parent
917 let i: *Instr = alloc_instr(f, OP_Q4KSBDOT, ir_type_i64())
918 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
919 let v: *Value = val_at(f, rid)
920 v.instr = i
921 i.result = rid
922 i.n_operands = 4
923 i.op0 = sb
924 i.op1 = col
925 i.op2 = scpre
926 i.op3 = out
927 append_instr(bb, i)
928 return rid
929}
930
931func ir_emit_sha256_ni_block(bb: *BasicBlock, state: i64, block: i64, k: i64) -> i64 {
932 let f: *Function = bb.parent
933 let i: *Instr = alloc_instr(f, OP_SHA256_NI_BLOCK, ir_type_i64())
934 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
935 let v: *Value = val_at(f, rid)
936 v.instr = i
937 i.result = rid
938 i.n_operands = 3
939 i.op0 = state
940 i.op1 = block
941 i.op2 = k
942 append_instr(bb, i)
943 return rid
944}
945
946// Fused 4x64-limb wide multiply: *dst(u64[8]) = *a(u64[4]) * *b(u64[4]) (512-bit product).
947// op0=dst ptr, op1=a ptr, op2=b ptr. 3 operands; i64 result (0). Lowers to the ADX/BMI2
948// mulx+adcx+adox dual-carry kernel; the pure 8x32 u256_mul_wide stays the byte-exact oracle.
949func ir_emit_mul256_wide(bb: *BasicBlock, dst: i64, a: i64, b: i64) -> i64 {
950 let f: *Function = bb.parent
951 let i: *Instr = alloc_instr(f, OP_MUL256_WIDE, 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
955 i.result = rid
956 i.n_operands = 3
957 i.op0 = dst
958 i.op1 = a
959 i.op2 = b
960 append_instr(bb, i)
961 return rid
962}
963
964// Emit a widening SIMD dot product i16x16 -> i64. Consumes two
965// i16-lane-loaded vectors (whose pointers callers materialise into
966// stack slots ahead of time -- the v0.0.1 SIMD shape is
967// stack-slot-based, no v-reg allocator yet). Returns the i64
968// scalar sum, just like the C-side OP_SIMD_VDOT_I16_X16.
969func ir_emit_simd_vdot_i16_x16(bb: *BasicBlock, a: i64, b: i64) -> i64 {
970 let f: *Function = bb.parent
971 let i: *Instr = alloc_instr(f, OP_SIMD_VDOT_I16_X16, ir_type_i64())
972 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
973 let v: *Value = val_at(f, rid)
974 v.instr = i
975 i.result = rid
976 i.n_operands = 2
977 i.op0 = a
978 i.op1 = b
979 append_instr(bb, i)
980 return rid
981}
982// i16x16 horizontal min/max: read 16 i16 lanes from *i64 src, return
983// sign-extended i64 scalar min/max-of-lanes.
984func ir_emit_simd_vreduce_min_i16_x16(bb: *BasicBlock, p: i64) -> i64 {
985 let f: *Function = bb.parent
986 let i: *Instr = alloc_instr(f, OP_SIMD_VREDUCE_MIN_I16_X16, ir_type_i64())
987 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
988 let v: *Value = val_at(f, rid)
989 v.instr = i
990 i.result = rid
991 i.n_operands = 1
992 i.op0 = p
993 append_instr(bb, i)
994 return rid
995}
996func ir_emit_simd_vreduce_max_i16_x16(bb: *BasicBlock, p: i64) -> i64 {
997 let f: *Function = bb.parent
998 let i: *Instr = alloc_instr(f, OP_SIMD_VREDUCE_MAX_I16_X16, ir_type_i64())
999 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1000 let v: *Value = val_at(f, rid)
1001 v.instr = i
1002 i.result = rid
1003 i.n_operands = 1
1004 i.op0 = p
1005 append_instr(bb, i)
1006 return rid
1007}
1008// i16x16 signed saturating add: a + b per-lane, clipped to INT16
1009// bounds, stored to *out as packed i16x16. Returns 0.
1010func ir_emit_simd_vsadd_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1011 let f: *Function = bb.parent
1012 let i: *Instr = alloc_instr(f, OP_SIMD_VSADD_I16_X16, ir_type_i64())
1013 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1014 let v: *Value = val_at(f, rid)
1015 v.instr = i
1016 i.result = rid
1017 i.n_operands = 3
1018 i.op0 = a
1019 i.op1 = b
1020 i.op2 = out
1021 append_instr(bb, i)
1022 return rid
1023}
1024// Per-lane signed saturating sub. Same shape as vsadd.
1025func ir_emit_simd_vssub_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1026 let f: *Function = bb.parent
1027 let i: *Instr = alloc_instr(f, OP_SIMD_VSSUB_I16_X16, ir_type_i64())
1028 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1029 let v: *Value = val_at(f, rid)
1030 v.instr = i
1031 i.result = rid
1032 i.n_operands = 3
1033 i.op0 = a; i.op1 = b; i.op2 = out
1034 append_instr(bb, i)
1035 return rid
1036}
1037func ir_emit_simd_vsaddu_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1038 let f: *Function = bb.parent
1039 let i: *Instr = alloc_instr(f, OP_SIMD_VSADDU_I16_X16, ir_type_i64())
1040 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1041 let v: *Value = val_at(f, rid)
1042 v.instr = i
1043 i.result = rid
1044 i.n_operands = 3
1045 i.op0 = a; i.op1 = b; i.op2 = out
1046 append_instr(bb, i)
1047 return rid
1048}
1049func ir_emit_simd_vssubu_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1050 let f: *Function = bb.parent
1051 let i: *Instr = alloc_instr(f, OP_SIMD_VSSUBU_I16_X16, ir_type_i64())
1052 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1053 let v: *Value = val_at(f, rid)
1054 v.instr = i
1055 i.result = rid
1056 i.n_operands = 3
1057 i.op0 = a; i.op1 = b; i.op2 = out
1058 append_instr(bb, i)
1059 return rid
1060}
1061// Per-lane min/max/add/sub/mul -- same 3-arg shape.
1062func ir_emit_simd_vmin_lane_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1063 let f: *Function = bb.parent
1064 let i: *Instr = alloc_instr(f, OP_SIMD_VMIN_LANE_I16_X16, ir_type_i64())
1065 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1066 let v: *Value = val_at(f, rid)
1067 v.instr = i; i.result = rid; i.n_operands = 3
1068 i.op0 = a; i.op1 = b; i.op2 = out
1069 append_instr(bb, i)
1070 return rid
1071}
1072func ir_emit_simd_vmax_lane_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1073 let f: *Function = bb.parent
1074 let i: *Instr = alloc_instr(f, OP_SIMD_VMAX_LANE_I16_X16, ir_type_i64())
1075 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1076 let v: *Value = val_at(f, rid)
1077 v.instr = i; i.result = rid; i.n_operands = 3
1078 i.op0 = a; i.op1 = b; i.op2 = out
1079 append_instr(bb, i)
1080 return rid
1081}
1082func ir_emit_simd_vadd_lane_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1083 let f: *Function = bb.parent
1084 let i: *Instr = alloc_instr(f, OP_SIMD_VADD_LANE_I16_X16, ir_type_i64())
1085 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1086 let v: *Value = val_at(f, rid)
1087 v.instr = i; i.result = rid; i.n_operands = 3
1088 i.op0 = a; i.op1 = b; i.op2 = out
1089 append_instr(bb, i)
1090 return rid
1091}
1092func ir_emit_simd_vsub_lane_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1093 let f: *Function = bb.parent
1094 let i: *Instr = alloc_instr(f, OP_SIMD_VSUB_LANE_I16_X16, ir_type_i64())
1095 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1096 let v: *Value = val_at(f, rid)
1097 v.instr = i; i.result = rid; i.n_operands = 3
1098 i.op0 = a; i.op1 = b; i.op2 = out
1099 append_instr(bb, i)
1100 return rid
1101}
1102func ir_emit_simd_vmul_lane_i16_x16(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1103 let f: *Function = bb.parent
1104 let i: *Instr = alloc_instr(f, OP_SIMD_VMUL_LANE_I16_X16, ir_type_i64())
1105 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1106 let v: *Value = val_at(f, rid)
1107 v.instr = i; i.result = rid; i.n_operands = 3
1108 i.op0 = a; i.op1 = b; i.op2 = out
1109 append_instr(bb, i)
1110 return rid
1111}
1112// Per-lane immediate-count shifts. op0 = *i64 source, op1 = count
1113// (scalar i64, lowered into t4 at codegen), op2 = *i64 out.
1114func ir_emit_simd_vsll_i16_x16(bb: *BasicBlock, a: i64, count: i64, out: i64) -> i64 {
1115 let f: *Function = bb.parent
1116 let i: *Instr = alloc_instr(f, OP_SIMD_VSLL_I16_X16, 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 = count; i.op2 = out
1121 append_instr(bb, i)
1122 return rid
1123}
1124func ir_emit_simd_vsrl_i16_x16(bb: *BasicBlock, a: i64, count: i64, out: i64) -> i64 {
1125 let f: *Function = bb.parent
1126 let i: *Instr = alloc_instr(f, OP_SIMD_VSRL_I16_X16, ir_type_i64())
1127 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1128 let v: *Value = val_at(f, rid)
1129 v.instr = i; i.result = rid; i.n_operands = 3
1130 i.op0 = a; i.op1 = count; i.op2 = out
1131 append_instr(bb, i)
1132 return rid
1133}
1134func ir_emit_simd_vsra_i16_x16(bb: *BasicBlock, a: i64, count: i64, out: i64) -> i64 {
1135 let f: *Function = bb.parent
1136 let i: *Instr = alloc_instr(f, OP_SIMD_VSRA_I16_X16, ir_type_i64())
1137 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1138 let v: *Value = val_at(f, rid)
1139 v.instr = i; i.result = rid; i.n_operands = 3
1140 i.op0 = a; i.op1 = count; i.op2 = out
1141 append_instr(bb, i)
1142 return rid
1143}
1144// Horizontal sum of 16 i16 lanes -> i64 (widening, sign-extended).
1145func ir_emit_simd_vreduce_sum_i16_x16(bb: *BasicBlock, p: i64) -> i64 {
1146 let f: *Function = bb.parent
1147 let i: *Instr = alloc_instr(f, OP_SIMD_VREDUCE_SUM_I16_X16, ir_type_i64())
1148 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1149 let v: *Value = val_at(f, rid)
1150 v.instr = i; i.result = rid; i.n_operands = 1
1151 i.op0 = p
1152 append_instr(bb, i)
1153 return rid
1154}
1155// Broadcast scalar into 16 i16 lanes -> *i64 out.
1156func ir_emit_simd_vbroadcast_i16_x16(bb: *BasicBlock, scalar: i64, out: i64) -> i64 {
1157 let f: *Function = bb.parent
1158 let i: *Instr = alloc_instr(f, OP_SIMD_VBROADCAST_I16_X16, ir_type_i64())
1159 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1160 let v: *Value = val_at(f, rid)
1161 v.instr = i; i.result = rid; i.n_operands = 2
1162 i.op0 = scalar; i.op1 = out
1163 append_instr(bb, i)
1164 return rid
1165}
1166// i8x32 ops -- same shape as i16x16 lane binops (3-arg) + reduce
1167// (1-arg) + broadcast (2-arg). Builder body identical modulo
1168// the OP_SIMD_V*_I8_X32 opcode tag.
1169func ir_emit_simd_vadd_i8_x32(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1170 let f: *Function = bb.parent
1171 let i: *Instr = alloc_instr(f, OP_SIMD_VADD_I8_X32, ir_type_i64())
1172 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1173 let v: *Value = val_at(f, rid)
1174 v.instr = i; i.result = rid; i.n_operands = 3
1175 i.op0 = a; i.op1 = b; i.op2 = out
1176 append_instr(bb, i)
1177 return rid
1178}
1179func ir_emit_simd_vsub_i8_x32(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1180 let f: *Function = bb.parent
1181 let i: *Instr = alloc_instr(f, OP_SIMD_VSUB_I8_X32, ir_type_i64())
1182 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1183 let v: *Value = val_at(f, rid)
1184 v.instr = i; i.result = rid; i.n_operands = 3
1185 i.op0 = a; i.op1 = b; i.op2 = out
1186 append_instr(bb, i)
1187 return rid
1188}
1189func ir_emit_simd_vsadd_i8_x32(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1190 let f: *Function = bb.parent
1191 let i: *Instr = alloc_instr(f, OP_SIMD_VSADD_I8_X32, ir_type_i64())
1192 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1193 let v: *Value = val_at(f, rid)
1194 v.instr = i; i.result = rid; i.n_operands = 3
1195 i.op0 = a; i.op1 = b; i.op2 = out
1196 append_instr(bb, i)
1197 return rid
1198}
1199func ir_emit_simd_vssub_i8_x32(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1200 let f: *Function = bb.parent
1201 let i: *Instr = alloc_instr(f, OP_SIMD_VSSUB_I8_X32, ir_type_i64())
1202 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1203 let v: *Value = val_at(f, rid)
1204 v.instr = i; i.result = rid; i.n_operands = 3
1205 i.op0 = a; i.op1 = b; i.op2 = out
1206 append_instr(bb, i)
1207 return rid
1208}
1209func ir_emit_simd_vreduce_sum_i8_x32(bb: *BasicBlock, p: i64) -> i64 {
1210 let f: *Function = bb.parent
1211 let i: *Instr = alloc_instr(f, OP_SIMD_VREDUCE_SUM_I8_X32, ir_type_i64())
1212 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1213 let v: *Value = val_at(f, rid)
1214 v.instr = i; i.result = rid; i.n_operands = 1
1215 i.op0 = p
1216 append_instr(bb, i)
1217 return rid
1218}
1219func ir_emit_simd_vbroadcast_i8_x32(bb: *BasicBlock, scalar: i64, out: i64) -> i64 {
1220 let f: *Function = bb.parent
1221 let i: *Instr = alloc_instr(f, OP_SIMD_VBROADCAST_I8_X32, ir_type_i64())
1222 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1223 let v: *Value = val_at(f, rid)
1224 v.instr = i; i.result = rid; i.n_operands = 2
1225 i.op0 = scalar; i.op1 = out
1226 append_instr(bb, i)
1227 return rid
1228}
1229// i32x8 set -- same 3-arg / 1-arg / 2-arg shapes as i8 and i16 families.
1230func ir_emit_simd_vadd_i32_x8(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1231 let f: *Function = bb.parent
1232 let i: *Instr = alloc_instr(f, OP_SIMD_VADD_I32_X8, ir_type_i64())
1233 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1234 let v: *Value = val_at(f, rid)
1235 v.instr = i; i.result = rid; i.n_operands = 3
1236 i.op0 = a; i.op1 = b; i.op2 = out
1237 append_instr(bb, i)
1238 return rid
1239}
1240func ir_emit_simd_vsub_i32_x8(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1241 let f: *Function = bb.parent
1242 let i: *Instr = alloc_instr(f, OP_SIMD_VSUB_I32_X8, ir_type_i64())
1243 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1244 let v: *Value = val_at(f, rid)
1245 v.instr = i; i.result = rid; i.n_operands = 3
1246 i.op0 = a; i.op1 = b; i.op2 = out
1247 append_instr(bb, i)
1248 return rid
1249}
1250func ir_emit_simd_vmul_i32_x8(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1251 let f: *Function = bb.parent
1252 let i: *Instr = alloc_instr(f, OP_SIMD_VMUL_I32_X8, ir_type_i64())
1253 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1254 let v: *Value = val_at(f, rid)
1255 v.instr = i; i.result = rid; i.n_operands = 3
1256 i.op0 = a; i.op1 = b; i.op2 = out
1257 append_instr(bb, i)
1258 return rid
1259}
1260func ir_emit_simd_vsadd_i32_x8(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1261 let f: *Function = bb.parent
1262 let i: *Instr = alloc_instr(f, OP_SIMD_VSADD_I32_X8, ir_type_i64())
1263 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1264 let v: *Value = val_at(f, rid)
1265 v.instr = i; i.result = rid; i.n_operands = 3
1266 i.op0 = a; i.op1 = b; i.op2 = out
1267 append_instr(bb, i)
1268 return rid
1269}
1270func ir_emit_simd_vssub_i32_x8(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1271 let f: *Function = bb.parent
1272 let i: *Instr = alloc_instr(f, OP_SIMD_VSSUB_I32_X8, ir_type_i64())
1273 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1274 let v: *Value = val_at(f, rid)
1275 v.instr = i; i.result = rid; i.n_operands = 3
1276 i.op0 = a; i.op1 = b; i.op2 = out
1277 append_instr(bb, i)
1278 return rid
1279}
1280func ir_emit_simd_vreduce_sum_i32_x8(bb: *BasicBlock, p: i64) -> i64 {
1281 let f: *Function = bb.parent
1282 let i: *Instr = alloc_instr(f, OP_SIMD_VREDUCE_SUM_I32_X8, ir_type_i64())
1283 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1284 let v: *Value = val_at(f, rid)
1285 v.instr = i; i.result = rid; i.n_operands = 1
1286 i.op0 = p
1287 append_instr(bb, i)
1288 return rid
1289}
1290func ir_emit_simd_vbroadcast_i32_x8(bb: *BasicBlock, scalar: i64, out: i64) -> i64 {
1291 let f: *Function = bb.parent
1292 let i: *Instr = alloc_instr(f, OP_SIMD_VBROADCAST_I32_X8, ir_type_i64())
1293 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1294 let v: *Value = val_at(f, rid)
1295 v.instr = i; i.result = rid; i.n_operands = 2
1296 i.op0 = scalar; i.op1 = out
1297 append_instr(bb, i)
1298 return rid
1299}
1300// i64x4 set.
1301func ir_emit_simd_vadd_i64_x4(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1302 let f: *Function = bb.parent
1303 let i: *Instr = alloc_instr(f, OP_SIMD_VADD_I64_X4, ir_type_i64())
1304 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1305 let v: *Value = val_at(f, rid)
1306 v.instr = i; i.result = rid; i.n_operands = 3
1307 i.op0 = a; i.op1 = b; i.op2 = out
1308 append_instr(bb, i); return rid
1309}
1310func ir_emit_simd_vsub_i64_x4(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1311 let f: *Function = bb.parent
1312 let i: *Instr = alloc_instr(f, OP_SIMD_VSUB_I64_X4, ir_type_i64())
1313 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1314 let v: *Value = val_at(f, rid)
1315 v.instr = i; i.result = rid; i.n_operands = 3
1316 i.op0 = a; i.op1 = b; i.op2 = out
1317 append_instr(bb, i); return rid
1318}
1319func ir_emit_simd_vmul_i64_x4(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1320 let f: *Function = bb.parent
1321 let i: *Instr = alloc_instr(f, OP_SIMD_VMUL_I64_X4, ir_type_i64())
1322 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1323 let v: *Value = val_at(f, rid)
1324 v.instr = i; i.result = rid; i.n_operands = 3
1325 i.op0 = a; i.op1 = b; i.op2 = out
1326 append_instr(bb, i); return rid
1327}
1328func ir_emit_simd_vsadd_i64_x4(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1329 let f: *Function = bb.parent
1330 let i: *Instr = alloc_instr(f, OP_SIMD_VSADD_I64_X4, ir_type_i64())
1331 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1332 let v: *Value = val_at(f, rid)
1333 v.instr = i; i.result = rid; i.n_operands = 3
1334 i.op0 = a; i.op1 = b; i.op2 = out
1335 append_instr(bb, i); return rid
1336}
1337func ir_emit_simd_vssub_i64_x4(bb: *BasicBlock, a: i64, b: i64, out: i64) -> i64 {
1338 let f: *Function = bb.parent
1339 let i: *Instr = alloc_instr(f, OP_SIMD_VSSUB_I64_X4, ir_type_i64())
1340 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1341 let v: *Value = val_at(f, rid)
1342 v.instr = i; i.result = rid; i.n_operands = 3
1343 i.op0 = a; i.op1 = b; i.op2 = out
1344 append_instr(bb, i); return rid
1345}
1346func ir_emit_simd_vreduce_sum_i64_x4(bb: *BasicBlock, p: i64) -> i64 {
1347 let f: *Function = bb.parent
1348 let i: *Instr = alloc_instr(f, OP_SIMD_VREDUCE_SUM_I64_X4, ir_type_i64())
1349 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1350 let v: *Value = val_at(f, rid)
1351 v.instr = i; i.result = rid; i.n_operands = 1
1352 i.op0 = p
1353 append_instr(bb, i); return rid
1354}
1355func ir_emit_simd_vbroadcast_i64_x4(bb: *BasicBlock, scalar: i64, out: i64) -> i64 {
1356 let f: *Function = bb.parent
1357 let i: *Instr = alloc_instr(f, OP_SIMD_VBROADCAST_I64_X4, ir_type_i64())
1358 let rid: i64 = alloc_value(f, VK_INSTR, ir_type_i64())
1359 let v: *Value = val_at(f, rid)
1360 v.instr = i; i.result = rid; i.n_operands = 2
1361 i.op0 = scalar; i.op1 = out
1362 append_instr(bb, i); return rid
1363}
1364
1365// Splice instruction `movee` (currently in its block's list) to
1366// appear immediately before `anchor` in the same block. No-op if
1367// `movee == anchor` or `movee` is already just before `anchor`.
1368// Used by the inliner to relocate cloned instrs from the block's
1369// tail (where ir_emit_* append them) to just before the call site.
1370func ir_move_instr_before(anchor: *Instr, movee: *Instr) -> i64 {
1371 if movee == anchor { return 0 }
1372 let bb: *BasicBlock = anchor.parent
1373 // Unlink movee from current position.
1374 let mp: *Instr = movee.prev
1375 let mn: *Instr = movee.next
1376 if mp != (0 as *Instr) { mp.next = mn }
1377 if mn != (0 as *Instr) { mn.prev = mp }
1378 if bb.head == movee { bb.head = mn }
1379 if bb.tail == movee { bb.tail = mp }
1380 // Insert before anchor.
1381 let ap: *Instr = anchor.prev
1382 movee.prev = ap
1383 movee.next = anchor
1384 anchor.prev = movee
1385 if ap != (0 as *Instr) {
1386 ap.next = movee
1387 } else {
1388 bb.head = movee
1389 }
1390 return 0
1391}
1392
1393// Emit a getelementptr: result = base + offset. Used for struct
1394// field address computation (after parse_primary sees `.field`).
1395// Result type is the field's pointee-ish type the caller provides
1396// -- the IR's "this Value holds an address" convention.
1397func ir_emit_gep(bb: *BasicBlock, base: i64, offset: i64,
1398 field_ty: *Type) -> i64 {
1399 nx_assert_ptr(bb as *u8, "ir_emit_gep: bb" as *u8)
1400 let f: *Function = bb.parent
1401 nx_assert(f.values_cap > 0, "ir_emit_gep: bb.parent init" as *u8)
1402 let i: *Instr = alloc_instr(f, OP_GEP, field_ty)
1403 let rid: i64 = alloc_value(f, VK_INSTR, field_ty)
1404 let v: *Value = val_at(f, rid)
1405 v.instr = i
1406 i.result = rid
1407 i.n_operands = 2
1408 i.op0 = base
1409 i.op1 = offset
1410 append_instr(bb, i)
1411 return rid
1412}
1413
1414// Emit a call to `callee` with up to 4 positional args (inline op
1415// slots). Return the result Value's id. Callers pass args as an
1416// array of ids plus the count; we copy into op0..op3. More than
1417// 4 args is a follow-up (needs an operands-array side store).
1418func ir_emit_call(bb: *BasicBlock, callee: *Function,
1419 args: *i64, n_args: i64) -> i64 {
1420 nx_assert_ptr(bb as *u8, "ir_emit_call: bb" as *u8)
1421 let f: *Function = bb.parent
1422 nx_assert(f.values_cap > 0, "ir_emit_call: bb.parent init" as *u8)
1423 let ret_ty: *Type = callee.ret_ty
1424 let i: *Instr = alloc_instr(f, OP_CALL, ret_ty)
1425 let rid: i64 = alloc_value(f, VK_INSTR, ret_ty)
1426 let v: *Value = val_at(f, rid)
1427 v.instr = i
1428 i.result = rid
1429 i.callee = callee
1430 i.n_operands = n_args
1431 if n_args > 0 { i.op0 = args[0] }
1432 if n_args > 1 { i.op1 = args[1] }
1433 if n_args > 2 { i.op2 = args[2] }
1434 if n_args > 3 { i.op3 = args[3] }
1435 if n_args > 4 { i.op4 = args[4] }
1436 if n_args > 5 { i.op5 = args[5] }
1437 if n_args > 6 { i.op6 = args[6] }
1438 if n_args > 7 { i.op7 = args[7] }
1439 if n_args > 8 { i.op8 = args[8] }
1440 if n_args > 9 { i.op9 = args[9] }
1441 if n_args > 10 { i.op10 = args[10] }
1442 if n_args > 11 { i.op11 = args[11] }
1443 if n_args > 12 { i.op12 = args[12] }
1444 if n_args > 13 { i.op13 = args[13] }
1445 if n_args > 14 { i.op14 = args[14] }
1446 if n_args > 15 { i.op15 = args[15] }
1447 if n_args > 16 { i.op16 = args[16] }
1448 if n_args > 17 { i.op17 = args[17] }
1449 if n_args > 18 { i.op18 = args[18] }
1450 if n_args > 19 { i.op19 = args[19] }
1451 if n_args > 20 { i.op20 = args[20] }
1452 if n_args > 21 { i.op21 = args[21] }
1453 if n_args > 22 { i.op22 = args[22] }
1454 if n_args > 23 { i.op23 = args[23] }
1455 append_instr(bb, i)
1456 return rid
1457}
1458
1459// fp(args) IR: indirect call through a func-pointer VALUE `callee_val` (stored in op0). Args -> op1.. (MVP caps
1460// at 6, matching the x86 register-arg codegen). n_operands = n_args+1. callee = null (no static callee, so
1461// inlining/tail-call/call-graph passes skip it). Result typed by the fn-ptr's declared return type.
1462func ir_emit_call_indirect(bb: *BasicBlock, callee_val: i64, ret_ty: *Type, args: *i64, n_args: i64) -> i64 {
1463 let f: *Function = bb.parent
1464 let i: *Instr = alloc_instr(f, OP_CALL_INDIRECT, ret_ty)
1465 let rid: i64 = alloc_value(f, VK_INSTR, ret_ty)
1466 let v: *Value = val_at(f, rid)
1467 v.instr = i
1468 i.result = rid
1469 i.callee = 0 as *Function
1470 i.n_operands = n_args + 1
1471 i.op0 = callee_val
1472 if n_args > 0 { i.op1 = args[0] }
1473 if n_args > 1 { i.op2 = args[1] }
1474 if n_args > 2 { i.op3 = args[2] }
1475 if n_args > 3 { i.op4 = args[3] }
1476 if n_args > 4 { i.op5 = args[4] }
1477 if n_args > 5 { i.op6 = args[5] }
1478 // Args 7..23 (op7..op23). Until 2026-07-25 this emitter stopped at op6 while
1479 // STILL setting n_operands = n_args+1, so a backend walking n_operands read
1480 // operand slots that were never written -- garbage arguments, silently. Only
1481 // the parser's 6-arg cap kept it off the road (seq715 family). The IR now
1482 // carries every argument the SysV ABI can actually pass.
1483 if n_args > 6 { i.op7 = args[6] }
1484 if n_args > 7 { i.op8 = args[7] }
1485 if n_args > 8 { i.op9 = args[8] }
1486 if n_args > 9 { i.op10 = args[9] }
1487 if n_args > 10 { i.op11 = args[10] }
1488 if n_args > 11 { i.op12 = args[11] }
1489 if n_args > 12 { i.op13 = args[12] }
1490 if n_args > 13 { i.op14 = args[13] }
1491 if n_args > 14 { i.op15 = args[14] }
1492 if n_args > 15 { i.op16 = args[15] }
1493 if n_args > 16 { i.op17 = args[16] }
1494 if n_args > 17 { i.op18 = args[17] }
1495 if n_args > 18 { i.op19 = args[18] }
1496 if n_args > 19 { i.op20 = args[19] }
1497 if n_args > 20 { i.op21 = args[20] }
1498 if n_args > 21 { i.op22 = args[21] }
1499 if n_args > 22 { i.op23 = args[22] }
1500 append_instr(bb, i)
1501 return rid
1502}
1503
1504// Allocate a fresh Module with pre-sized pools. Callers can keep
1505// using this shape directly; the fields stay public so passes can
1506// walk functions/globals with pointer arithmetic.
1507// Slots in a standalone Module's pools. Unlike parse_module -- which DERIVES its function pool from the
1508// token stream it is about to parse -- ir_module_new is handed no source, so this is a declared bound.
1509// It is therefore named once and read by the allocation AND by the recorded caps, so the pool and the
1510// number that describes it cannot disagree. The bound is not load-bearing on its own: ir_new_function
1511// asserts against fn_cap, so overshooting REFUSES LOUDLY instead of writing past the pool into the
1512// adjacent globals region -- the exact corruption this number was already raised twice to dodge
1513// (256 -> 4096, T#selfhost-004 and -006, when nxc.nx's 419 functions overran a 256-slot pool).
1514const NX_IR_MODULE_POOL_SLOTS: i64 = 4096
1515
1516func ir_module_new(name_bytes: *u8) -> *Module {
1517 // Globals pool grown from 128 -> 4096 slots (task #21 stage-2):
1518 // self-compile of nxc.nx generates >128 string-literal globals.
1519 // Functions pool grown from 256 -> 4096 (closes T#selfhost-004,
1520 // 2026-04-25): nxc.nx has 419 functions; the 256-cap pool
1521 // overflowed at function #257 and corrupted the adjacent
1522 // globals region, causing nxc.elf to SIGSEGV during opt or
1523 // codegen (varied by where the corruption landed).
1524 // nx_pass_bisect.sh confirmed the bug was OUTSIDE opt_run.
1525 // Each part bound to a named value and combined on ONE line: a continuation line that LEADS with an
1526 // operator is silently truncated by this parser (it keeps only the first line's part), which is the
1527 // defect its own diagnostic warns about -- and I hit it writing this very line.
1528 let fn_bytes: i64 = NX_IR_MODULE_POOL_SLOTS * NX_MODULE_FN_STRIDE
1529 let glob_bytes: i64 = NX_IR_MODULE_POOL_SLOTS * NX_MODULE_GLOBAL_STRIDE
1530 let raw: *u8 = sys_mmap(128 + fn_bytes + glob_bytes)
1531 let m: *Module = raw as *Module
1532 m.name = name_bytes
1533 let base: i64 = raw as i64
1534 m.functions = (base + 128) as *Function
1535 m.n_functions = 0
1536 m.fn_cap = NX_IR_MODULE_POOL_SLOTS
1537 m.globals = (base + 128 + NX_IR_MODULE_POOL_SLOTS * NX_MODULE_FN_STRIDE) as *Global
1538 m.n_globals = 0
1539 m.globals_cap = NX_IR_MODULE_POOL_SLOTS
1540 return m
1541}
1542
1543// Append a zero-initialised BSS global. Returns its id; also writes
1544// the Global record into the module's globals pool. Each Global is
1545// 72 bytes (id+name_bytes+name_len+bytes+len+is_string+zero_init+
1546// writable = 9 * 8).
1547func ir_add_global_bss(m: *Module, name_bytes: *u8, name_len: i64,
1548 size: i64) -> i64 {
1549 let id: i64 = m.n_globals
1550 if id >= m.globals_cap {
1551 diag_capacity_named("global pool (statics and literals PER UNIT)" as *u8, "unit" as *u8, 0, id + 1, m.globals_cap,
1552 "this unit declares more statics, string literals and data than the pool ir_module_new sizes for one unit -- split the unit, share repeated literals through one const, or raise the derivation in parse_module (count_global_tokens / NX_PARSE_GLOBALS_SLACK -- the pool of every COMPILED unit is derived there; ir_module_new sizes only hand-built test modules)." as *u8)
1553 }
1554 nx_assert_lt(id, m.globals_cap,
1555 "ir_add_global_bss: globals pool exhausted" as *u8)
1556 let base: i64 = m.globals as i64
1557 // Stride 80 -- matches ir_module_new's allocator (4096 * 80).
1558 // Was 72 by accident; the 8-byte slack-per-slot gap meant
1559 // readers using stride 80 (the dump in nxc.nx) drifted past
1560 // the live entries by global #9. Closes T#selfhost-006.
1561 let g: *Global = (base + id * 80) as *Global
1562 g.id = id
1563 g.name_bytes = name_bytes
1564 g.name_len = name_len
1565 g.bytes = 0 as *u8
1566 g.len = size
1567 g.is_string = 0
1568 g.zero_init = 1
1569 g.writable = 1
1570 m.n_globals = id + 1
1571 return id
1572}
1573
1574// Append an INITIALISED (non-zero) data global holding an integer value.
1575// `init_val` is written little-endian into a fresh `size`-byte buffer that
1576// backs g.bytes, so the emitter's `zero_init==0` path lists it as `.byte`
1577// storage (module statics live in the loaded image; writable in the bare-
1578// metal / RISC-V RAM target). Unlike ir_add_global_string this is is_string=0
1579// + writable=1 -- a mutable module datum, the storage a `static NAME = INIT`
1580// (or a zero-initialised `static NAME`, init_val=0) resolves to. Returns id.
1581func ir_add_global_data(m: *Module, name_bytes: *u8, name_len: i64,
1582 init_val: i64, size: i64) -> i64 {
1583 let id: i64 = m.n_globals
1584 if id >= m.globals_cap {
1585 diag_capacity_named("global pool (statics and literals PER UNIT)" as *u8, "unit" as *u8, 0, id + 1, m.globals_cap,
1586 "this unit declares more statics, string literals and data than the pool ir_module_new sizes for one unit -- split the unit, share repeated literals through one const, or raise the derivation in parse_module (count_global_tokens / NX_PARSE_GLOBALS_SLACK -- the pool of every COMPILED unit is derived there; ir_module_new sizes only hand-built test modules)." as *u8)
1587 }
1588 nx_assert_lt(id, m.globals_cap,
1589 "ir_add_global_data: globals pool exhausted" as *u8)
1590 var nbytes: i64 = size
1591 if nbytes <= 0 { nbytes = 8 }
1592 let buf: *u8 = sys_mmap(nbytes)
1593 var b: i64 = 0
1594 var v: i64 = init_val
1595 while b < nbytes {
1596 buf[b] = (v & 0xFF) as u8
1597 v = v >> 8
1598 b = b + 1
1599 }
1600 let base: i64 = m.globals as i64
1601 let g: *Global = (base + id * 80) as *Global
1602 g.id = id
1603 g.name_bytes = name_bytes
1604 g.name_len = name_len
1605 g.bytes = buf
1606 g.len = nbytes
1607 g.is_string = 0
1608 g.zero_init = 0
1609 g.writable = 1
1610 m.n_globals = id + 1
1611 return id
1612}
1613
1614// Append a string literal global. Returns its id. `bytes` is
1615// null-terminated; len does NOT include the terminator. Emits as
1616// `.asciz` in .rodata.
1617func ir_add_global_string(m: *Module, bytes: *u8, len: i64) -> i64 {
1618 let id: i64 = m.n_globals
1619 if id >= m.globals_cap {
1620 diag_capacity_named("global pool (statics and literals PER UNIT)" as *u8, "unit" as *u8, 0, id + 1, m.globals_cap,
1621 "this unit holds more string literals, statics and data than the pool ir_module_new sizes for one unit -- split the unit, share repeated literals through one const, or raise the derivation in parse_module (count_global_tokens / NX_PARSE_GLOBALS_SLACK -- the pool of every COMPILED unit is derived there; ir_module_new sizes only hand-built test modules)." as *u8)
1622 }
1623 nx_assert_lt(id, m.globals_cap,
1624 "ir_add_global_string: globals pool exhausted" as *u8)
1625 let base: i64 = m.globals as i64
1626 let g: *Global = (base + id * 80) as *Global
1627 g.id = id
1628 g.name_bytes = 0 as *u8
1629 g.name_len = 0
1630 g.bytes = bytes
1631 g.len = len
1632 g.is_string = 1
1633 g.zero_init = 0
1634 g.writable = 0
1635 m.n_globals = id + 1
1636 return id
1637}
1638
1639// Build a VK_GLOBAL Value referring to global id `gid`. Backend
1640// lowers this to `la <reg>, .Lg<gid>` (auipc + addi) so the address
1641// at runtime is the loaded virtual address of the global, not the
1642// literal id.
1643//
1644// Previously this tagged as VK_CONST_INT and stuffed gid into
1645// const_int -- backend then emitted `li <reg>, <gid>` and runtime
1646// dereferenced the integer id as if it were an address. Worked at
1647// the C-anchor level (because main.c happens to do whole-program
1648// rewriting) but blew up immediately on self-host: write(2, 0x1d3,
1649// 18) = EFAULT on every stderr message. Closes T#selfhost-003.
1650func ir_global_value(f: *Function, gid: i64, ty: *Type) -> i64 {
1651 let rid: i64 = alloc_value(f, VK_GLOBAL, ty)
1652 let v: *Value = val_at(f, rid)
1653 v.const_int = gid
1654 return rid
1655}
1656
1657// Address of a named function `fn` as a value. Rematerialised at each use site as
1658// `leaq <fn.name>(%rip), %reg` (VK_FUNC_ADDR). const_int holds the *Function pointer.
1659// Mirrors ir_global_value; used by `&fn`, bare-fn-name-as-value, and __thread_clone's entry.
1660func ir_func_addr_value(f: *Function, fn: *Function, ty: *Type) -> i64 {
1661 let rid: i64 = alloc_value(f, VK_FUNC_ADDR, ty)
1662 let v: *Value = val_at(f, rid)
1663 v.const_int = fn as i64
1664 return rid
1665}
1666
1667// Find a function by name in a module's function table. Linear
1668// scan; fine until modules get large. Returns null if not found.
1669func find_function(m: *Module, name: *u8, name_len: i64) -> *Function {
1670 var i: i64 = 0
1671 while i < m.n_functions {
1672 let base: i64 = m.functions as i64
1673 let f: *Function = (base + i * 176) as *Function
1674 if f.name_len == name_len {
1675 // f.name_start holds the *u8 name pointer directly
1676 // (see ir_function_new comment). Compare bytes.
1677 let fname: *u8 = f.name_start as *u8
1678 var j: i64 = 0
1679 var eq: i64 = 1
1680 while j < name_len {
1681 if fname[j] != name[j] { eq = 0; j = name_len }
1682 if eq == 1 { j = j + 1 }
1683 }
1684 if eq == 1 { return f }
1685 }
1686 i = i + 1
1687 }
1688 return 0 as *Function
1689}
1690
1691// Emit unconditional branch.
1692func ir_emit_br(bb: *BasicBlock, target: *BasicBlock) -> i64 {
1693 if ir_bb_sealed(bb) == 1 { return 0 }
1694 let f: *Function = bb.parent
1695 let i: *Instr = alloc_instr(f, OP_BR, ir_type_void())
1696 i.n_operands = 1
1697 i.op0 = target.id
1698 append_instr(bb, i)
1699 // Wire CFG edges. Inline up to 2 succs per block.
1700 if bb.n_succs == 0 {
1701 bb.succ0 = target
1702 }
1703 if bb.n_succs == 1 {
1704 bb.succ1 = target
1705 }
1706 bb.n_succs = bb.n_succs + 1
1707 if target.n_preds == 0 { target.pred0 = bb }
1708 if target.n_preds == 1 { target.pred1 = bb }
1709 if target.n_preds == 2 { target.pred2 = bb }
1710 target.n_preds = target.n_preds + 1
1711 return 0
1712}
1713
1714// Emit conditional branch: br_cond cond, on_true, on_false.
1715// Operands: [cond_value_id, on_true_bb_id, on_false_bb_id].
1716// Wires both targets as successors and both back-edges as preds.
1717func ir_emit_br_cond(bb: *BasicBlock, cond: i64,
1718 on_true: *BasicBlock, on_false: *BasicBlock) -> i64 {
1719 if ir_bb_sealed(bb) == 1 { return 0 }
1720 let f: *Function = bb.parent
1721 let i: *Instr = alloc_instr(f, OP_BR_COND, ir_type_void())
1722 i.n_operands = 3
1723 i.op0 = cond
1724 i.op1 = on_true.id
1725 i.op2 = on_false.id
1726 append_instr(bb, i)
1727
1728 // CFG wiring -- br_cond is always a 2-succ instruction.
1729 // Wire both succs in a single shot. The previous "step n_succs
1730 // twice with overlapping conditions" pattern overwrote succ0 with
1731 // on_false and never set succ1, leaving on_true unreachable from
1732 // opt_sweep's BFS so most blocks looked dead.
1733 bb.succ0 = on_true
1734 bb.succ1 = on_false
1735 bb.n_succs = 2
1736
1737 if on_true.n_preds == 0 { on_true.pred0 = bb }
1738 if on_true.n_preds == 1 { on_true.pred1 = bb }
1739 if on_true.n_preds == 2 { on_true.pred2 = bb }
1740 on_true.n_preds = on_true.n_preds + 1
1741
1742 if on_false.n_preds == 0 { on_false.pred0 = bb }
1743 if on_false.n_preds == 1 { on_false.pred1 = bb }
1744 if on_false.n_preds == 2 { on_false.pred2 = bb }
1745 on_false.n_preds = on_false.n_preds + 1
1746 return 0
1747}
1748
1749// Emit alloca for a single value of `elem_type`. The result Value's
1750// id is returned; it's typed as a pointer to elem_type in the IR's
1751// "the Value holds an address" convention.
1752func ir_emit_alloca(bb: *BasicBlock, elem_ty: *Type) -> i64 {
1753 nx_assert_ptr(bb as *u8, "ir_emit_alloca: bb" as *u8)
1754 let f: *Function = bb.parent
1755 nx_assert(f.values_cap > 0, "ir_emit_alloca: bb.parent init" as *u8)
1756 let i: *Instr = alloc_instr(f, OP_ALLOCA, elem_ty)
1757 let rid: i64 = alloc_value(f, VK_INSTR, elem_ty)
1758 let base: i64 = f.values as i64
1759 let v: *Value = (base + rid * 48) as *Value
1760 v.instr = i
1761 i.result = rid
1762 i.n_operands = 0
1763 append_instr(bb, i)
1764 return rid
1765}
1766
1767// Emit load from `addr`, producing a Value of `load_ty`. Operands:
1768// [addr_value_id].
1769func ir_emit_load(bb: *BasicBlock, addr: i64, load_ty: *Type) -> i64 {
1770 nx_assert_ptr(bb as *u8, "ir_emit_load: bb" as *u8)
1771 let f: *Function = bb.parent
1772 nx_assert_ptr(f as *u8, "ir_emit_load: bb.parent" as *u8)
1773 let i: *Instr = alloc_instr(f, OP_LOAD, load_ty)
1774 let rid: i64 = alloc_value(f, VK_INSTR, load_ty)
1775 let base: i64 = f.values as i64
1776 let v: *Value = (base + rid * 48) as *Value
1777 v.instr = i
1778 i.result = rid
1779 i.n_operands = 1
1780 i.op0 = addr
1781 append_instr(bb, i)
1782 return rid
1783}
1784
1785// Emit store of `val` into `addr`. No result Value (control-flow
1786// instruction shape), but i.ty carries the ELEMENT type so codegen
1787// can pick the right store width (sb/sh/sw/sd). Previously the
1788// type was discarded and i.ty was TY_VOID, so OP_STORE always
1789// emitted `sd` regardless of the actual element width -- a `*u8`
1790// write picked up the next 7 bytes (close T#types-001-codegen).
1791func ir_emit_store(bb: *BasicBlock, addr: i64, val: i64, ty: *Type) -> i64 {
1792 let f: *Function = bb.parent
1793 var st_ty: *Type = ty
1794 if st_ty == (0 as *Type) { st_ty = ir_type_i64() }
1795 let i: *Instr = alloc_instr(f, OP_STORE, st_ty)
1796 i.n_operands = 2
1797 i.op0 = addr
1798 i.op1 = val
1799 append_instr(bb, i)
1800 return 0
1801}
1802
1803// Library only; self-test lives in ir_test.nx. Compile this file as
1804// part of a multi-file build via the `import "nx_ir.nx"` directive.