code wiki / (root) / nx_ir.nx

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.