code wiki / (root) / nx_x86_64_ctx.nx

nx_x86_64_ctx.nx source

↩ module page · 4094 lines · 220671 B

1// nx_x86_64_ctx.nx -- IR-driven codegen for x86_64 (session 6). 2// 3// Consumes a parsed/opt'd Function and emits AT&T x86_64 asm using 4// the emit primitives in nx_x86_64.nx (sessions 1-5). Stack-machine 5// layout per nxc2/x86_64.c: every SSA Value that needs storage gets 6// a fixed [rbp - 8*slot] address. Constants are rematerialised at 7// each use via movabsq; globals via leaq label(%rip). 8// 9// Per cardinal feedback-no-nxc2-c-extension-only-nishilang-forward: 10// the C file stays the read-only comparator; new functionality lives 11// here. 12// 13// nx_safety_envelope: 14// intended_use: "IR-driven x86_64 SysV codegen. Walks a 15// Function's blocks/instrs and emits asm via 16// the sessions-1-5 emit primitives." 17// sil_target: SIL3 (codegen correctness) 18// asil_target: QM 19// dal_target: DAL B 20// iec_62304_class: NONE 21// evidence: [no_floating_point_in_layout_logic, 22// stack_machine_no_register_allocation, 23// per_opcode_dispatch_explicit, 24// 16_byte_frame_alignment_enforced, 25// materialise_handles_const_global_spill] 26// hazard_register: [bug-tape-slot-offset-vs-disp-sign, 27// bug-tape-syscall-rax-overwritten-by-clobber, 28// bug-tape-store-result-skipped-for-unused-value] 29// residual_risk: "Subset coverage: arith/bitwise/cmp/branch/ 30// call/load/store/GEP/syscall/alloca/copy. 31// Float / SIMD / atomic / thread ops deferred 32// to sessions 8+ (queued per migration index)." 33// verdict: NOT_YET_EVALUATED 34 35import "nx_syscalls.nx" 36import "nx_types.nx" 37// Line map: this module reads it for .loc emission. Imported HERE rather than relied on via 38// nx_parse's import list, so this file is self-sufficient regardless of splice order. 39import "nx_linemap.nx" 40import "nx_outbuf.nx" 41import "nx_x86_64.nx" 42import "nx_x86_regalloc.nx" 43 44// ===== per-function compilation context ========================== 45// 46// slot_off[v] = sp-relative byte offset for value v's stack home, 47// or -1 if v is rematerialised (const / global). 48// alloca_off[v] = byte offset of an OP_ALLOCA result's STORAGE, 49// or -1 if v is not an alloca. 50// n_slots = count of values with slot homes. 51// alloca_bytes = total bytes used by allocas. 52// frame_size = total frame including 16-byte alignment. 53 54struct X86Ctx { 55 f: *Function, 56 o: *OutBuf, 57 slot_off: *i64, 58 alloca_off: *i64, 59 n_slots: i64, 60 alloca_bytes: i64, 61 frame_size: i64, 62 locs: *ValueLoc, // G1: per-value register/spill home (x86_regalloc_function) 63 used_cs_mask: i64, // G1: callee-saved home bitmask (bit k => home idx k) 64 n_saved: i64, // G1: popcount(used_cs_mask) 65 alloca_home: *i64, // G2: alloca STORAGE home reg idx (0..4) or -1 (registerized var) 66 elide: *i64, // G4: 1 = single-use next-instr temp, slot store skipped 67 uses: *i64, // G8: operand-appearance count per value (SIB single-use test) 68 sib_dead: *i64, // G8: 1 = GEP/SHL result folded into a SIB load/store (emit nothing) 69 fwd_home: *i64, // G10: forwarded load result -> source alloca's home idx, or -1 70 chain_home: *i64, // G11: chain binop result -> IN-PLACE home idx, or -1 71 chain_swap: *i64, // G11: 1 = chain value at op1 (commuted); src operand = op0 72 next_bb: i64, // G12: id of the NEXT emitted block (-1 last); fall-through elision 73} 74 75// FIX-15: keep in EXACT lockstep with the field count -- the mmap in 76// x86ctx_init uses this; a stale value writes new fields past the allocation. 77const NX_X86CTX_BYTES: i64 = 144 78 79// NEGATIVE CONTROL (G14): 1 = also flag-reuse signed LT vs 0 -- the ALU's OF 80// differs from cmp-vs-0's, so (INT_MIN - 1) < 0 evaluates WRONG (adversary T7 81// goes RED). Proves the EQ/NE-only restriction is load-bearing. Ships at 0. 82const X86_NEGCTL_G14_SIGNED: i64 = 0 83 84// NEGATIVE CONTROL (G19 lea-strength): 1 = emit scale=c instead of c-1, so 85// leaq (%n,%n,c) computes (c+1)*n instead of c*n -- every workload that 86// multiplies by 2/3/5/9 checksum-splits vs gcc/clang (matrix RED). Proves the 87// c-1 scale is load-bearing arithmetic, not a free parameter. Ships at 0. 88const X86_NEGCTL_LEA_WRONG_SCALE: i64 = 0 89 90// NEGATIVE CONTROL (G21 commuted rax-consume): 1 = commute WITHOUT the 91// G1_RAX_SLOT match (rax may hold anything at that point) -> a wrong operand 92// rides into commutative binops -> matrix checksum-splits (RED). Proves the 93// G1-slot gate is load-bearing. Ships at 0. 94const X86_NEGCTL_G21_COMMUTE_ANY: i64 = 0 95 96// NEGATIVE CONTROLS (G22 bias-via-cmov division): 97// CMOV_INVERT: 1 = emit cmovns instead of cmovs -- POSITIVE dividends get the 98// bias, negative ones lose it -> divpow2 checksum-splits (RED). 99// RIDE_ALWAYS: 1 = always skip the testq -- cmovs reads whatever stale SF the 100// previous shift/lea left -> wrong side selected -> divpow2 RED. Proves the 101// G14-class producer gate on the flag ride is load-bearing. Both ship at 0. 102const X86_NEGCTL_G22_CMOV_INVERT: i64 = 0 103const X86_NEGCTL_G22_RIDE_ALWAYS: i64 = 0 104 105// NEGATIVE CONTROL (G23 src-direct cmov division): 1 = cmovs instead of 106// cmovns in the HOME-read form -- the select inverts (positives biased, 107// negatives unbiased) -> divpow2 checksum-splits (RED). The elide gate is a 108// perf heuristic (the store_result valve makes a wrong call safe), so only 109// the cmov direction carries a correctness negctl. Ships at 0. 110const X86_NEGCTL_G23_CMOV_INVERT: i64 = 0 111 112// NEGATIVE CONTROL (LN16 f64 ordering): 1 = compare two f64 bit patterns with a 113// RAW signed integer cmpq, which is what this backend did until 2026-08-25. It is 114// correct for two POSITIVE doubles and INVERTED for two negative ones, because the 115// signed-integer order of the bit patterns runs backwards through the sign-magnitude 116// encoding -- so `0.0 - 1.0 < 0.0 - 2.0` answered TRUE. nx_f64_gate MUST go RED when 117// this is 1 and GREEN when it is 0; that pair is what makes the fix non-vacuous. 118// Ships at 0. 119const X86_NEGCTL_F64_CMP_RAW: i64 = 0 120 121// ===== G1 register-residency (intra-block rax reuse) ============== 122// CS:APP SS5.6 "eliminating unneeded memory references": the slot whose 123// value rax currently holds, valid ONLY within straight-line code. 124// SET by store_result(rax); CONSUMED ONCE by an immediately-following 125// load_value_v(spilled->rax) of the same slot (skips the redundant 126// reload); CLEARED at every rax clobber (any load into rax, CALL/ 127// SYSCALL/TAIL_CALL) and every basic-block boundary (control-flow join, 128// where rax is not known). -1 = unknown. Soundness rests on clearing at 129// EVERY rax-writer + every block join; see NX_G1_REGALLOC_PHASE0. 130static G1_RAX_SLOT: i64 131 132// G6 cmp+jcc fusion: a compare whose single consumer is the immediately- 133// following BR_COND skips setcc/movzbq/store entirely; the branch consumes 134// the live FLAGS. PENDING_CC = the NX_X64_CC_* code (-1 = none), PENDING_VAL 135// = the cmp result id it covers (belt: br_cond fuses only on an exact match). 136// Reset at function start + every block boundary (module statics are BSS-zero 137// and 0 is a valid CC code -- never rely on the zero init). 138static G1_PENDING_CC: i64 139static G1_PENDING_VAL: i64 140// LN18 (2026-09-01): instructions the overflow fuse has already rendered (the four LN1 intermediates 141// after a fused add/sub); the block loop skips exactly this many. Reset at every block start. 142static G24_SKIP: i64 143static G24_FUSED: i64 144 145func _g1_is_rax(reg: *u8) -> i64 { 146 if reg[0] != 114 { return 0 } // 'r' 147 if reg[1] != 97 { return 0 } // 'a' 148 if reg[2] != 120 { return 0 } // 'x' 149 if reg[3] != 0 { return 0 } 150 return 1 151} 152 153// ===== helpers ==================================================== 154 155func x86ctx_value_at(f: *Function, id: i64) -> *Value { 156 let base: i64 = f.values as i64 157 return (base + id * 48) as *Value // Value struct = 48 bytes 158} 159 160func x86ctx_block_at(f: *Function, id: i64) -> *BasicBlock { 161 let base: i64 = f.blocks as i64 162 return (base + id * 96) as *BasicBlock 163} 164 165func x86ctx_round_up_16(n: i64) -> i64 { 166 return (n + 15) & (0 - 16) 167} 168 169// ===== G1 register allocation: frame + home-register helpers ======= 170 171// FIX-19: callee-saved homes are saved at the TOP of the frame (rbp-8, rbp-16, 172// ... via x86_home_save_off), and frame_size was already grown by 173// round16(n_saved*8) in x86ctx_init to reserve that region. So value/alloca 174// slots live at the BOTTOM of the (grown) frame -- base = 0-frame_size -- which 175// pushes them down by exactly the reserved amount, guaranteeing no overlap with 176// the save area for any n_saved in 0..5. (n_saved==0 -> identical to before.) 177func x86ctx_value_base(c: *X86Ctx) -> i64 { 178 return 0 - c.frame_size 179} 180 181// FIX-1: the register a producing op should compute its result INTO -- the home 182// register if the result is homed (ALLOCATE-not-COPY, zero copy), else rax. 183func x86ctx_result_reg(c: *X86Ctx, v_id: i64) -> *u8 { 184 if v_id >= 0 { if v_id < c.f.n_values { 185 let lbase: i64 = c.locs as i64 186 let l: *ValueLoc = (lbase + v_id * 16) as *ValueLoc 187 if l.kind == VL_REGISTER { return x86_home_reg_name(l.idx) } 188 } } 189 return "rax" as *u8 190} 191 192// Save the callee-saved home registers this function uses into their reserved 193// frame slots (movq, NOT pushq -> rsp unmoved -> 16-byte alignment preserved). 194// Compacted slot ordering (FIX-6). Emits nothing when no home is used. 195func x86ctx_emit_cs_save(c: *X86Ctx) -> i64 { 196 if c.used_cs_mask == 0 { return 0 } 197 var slot_index: i64 = 0 198 var k: i64 = 0 199 while k < X86_HOME_CAP { 200 if ((c.used_cs_mask >> k) & 1) == 1 { 201 x86_emit_store_qword(c.o, x86_home_reg_name(k), "rbp" as *u8, x86_home_save_off(slot_index)) 202 slot_index = slot_index + 1 203 } 204 k = k + 1 205 } 206 return 0 207} 208 209// Restore on every teardown path (epilogue + inline tail-call) -- SAME compacted 210// slot ordering as the save (FIX-6). 211func x86ctx_emit_cs_restore(c: *X86Ctx) -> i64 { 212 if c.used_cs_mask == 0 { return 0 } 213 var slot_index: i64 = 0 214 var k: i64 = 0 215 while k < X86_HOME_CAP { 216 if ((c.used_cs_mask >> k) & 1) == 1 { 217 x86_emit_load_qword(c.o, "rbp" as *u8, x86_home_save_off(slot_index), x86_home_reg_name(k)) 218 slot_index = slot_index + 1 219 } 220 k = k + 1 221 } 222 return 0 223} 224 225// ===== G8: scaled-addressing (SIB) fold =========================== 226// nx_cc emitted `leaq` ONCE program-wide -- every `a[i*N+k]` was shlq+addq+ 227// deref (+spills). x86 SIB `(%base,%index,scale)` does base+index*scale in ONE 228// memory operand. This fold recognizes LOAD/STORE(GEP(base, SHL/MUL(idx,2^k))) 229// with SINGLE-USE links, elides the dead GEP+SHL, and emits the SIB form. 230// (nxasm gained SIB support 2026-07-15, gas-matched + additive-proven.) 231 232// Lazy scratch for the 5-i64 probe result (static pointer, mmap-once -- the safe 233// scalar-static pattern; single-threaded compile reuses it immediately per call). 234static G8_SCRATCH: i64 235func x86ctx_sib_scratch() -> *i64 { 236 if G8_SCRATCH == 0 { G8_SCRATCH = sys_mmap(64) as i64 } 237 return G8_SCRATCH as *i64 238} 239 240// Count operand appearances of every value + clear sib_dead. 241func x86ctx_count_uses(c: *X86Ctx) -> i64 { 242 let n: i64 = c.f.n_values 243 var v: i64 = 0 244 while v < n { c.uses[v] = 0; c.sib_dead[v] = 0; v = v + 1 } 245 var bi: i64 = 0 246 while bi < c.f.n_blocks { 247 let b: *BasicBlock = x86ctx_block_at(c.f, bi) 248 var inst: *Instr = b.head 249 while inst != (0 as *Instr) { 250 let nv: i64 = x86_n_value_operands(inst) 251 var k: i64 = 0 252 while k < nv { 253 let u: i64 = x86_operand_k(inst, k) 254 if u >= 0 { if u < n { c.uses[u] = c.uses[u] + 1 } } 255 k = k + 1 256 } 257 inst = inst.next 258 } 259 bi = bi + 1 260 } 261 return 0 262} 263 264// Probe: is addr_id = GEP(base, [SHL/MUL idx by 2^k]) with single-use links? 265// Writes out[0]=base_v out[1]=idx_v out[2]=scale(1/2/4/8) out[3]=gep_id 266// out[4]=shl_id(-1 if scale==1, i.e. no shift to elide). Returns 1 on match. 267func x86ctx_sib_probe(c: *X86Ctx, addr_id: i64, out: *i64) -> i64 { 268 let n: i64 = c.f.n_values 269 if addr_id < 0 { return 0 } 270 if addr_id >= n { return 0 } 271 if c.uses[addr_id] != 1 { return 0 } // address value single-use only 272 let av: *Value = x86ctx_value_at(c.f, addr_id) 273 if av.kind != VK_INSTR { return 0 } 274 let gep: *Instr = av.instr 275 if gep == (0 as *Instr) { return 0 } 276 // The address-forming op: OP_GEP (fixed [N]T arrays -> base is a frame addr) 277 // or OP_ADD (pointer indexing `ptr[i]` = ADD(ptr_value, i*esize)). An ADD 278 // feeding a load/store's ADDRESS operand IS an address by construction, so 279 // folding it is always semantically valid. base=op0, offset=op1 (the parser's 280 // convention: ir_emit_gep/binop(OP_ADD, base_first, scaled_offset)). 281 if gep.op != OP_GEP { if gep.op != OP_ADD { return 0 } } 282 let base_v: i64 = gep.op0 283 let off_v: i64 = gep.op1 284 out[0] = base_v 285 out[1] = off_v 286 out[2] = 1 287 out[3] = addr_id 288 out[4] = 0 - 1 289 // out[5]: base kind. GEP base = an ADDRESS (fixed-array alloca -> leaq), 290 // load via load_value. OP_ADD base = a pointer VALUE (load via load_value_v). 291 // Mirroring the wrong one tripwires on a G2-homed pointer var (as-address of 292 // a homed alloca). This is the ONLY difference between the two address ops. 293 out[5] = 0 294 if gep.op == OP_GEP { out[5] = 1 } 295 // Upgrade to a scaled index when the offset is a single-use SHL/MUL by 2^k. 296 if off_v >= 0 { if off_v < n { if c.uses[off_v] == 1 { 297 let ov: *Value = x86ctx_value_at(c.f, off_v) 298 if ov.kind == VK_INSTR { 299 let sh: *Instr = ov.instr 300 if sh != (0 as *Instr) { 301 if sh.op == OP_SHL { 302 let kv: *Value = x86ctx_value_at(c.f, sh.op1) 303 if kv.kind == VK_CONST_INT { 304 if kv.const_int >= 1 { if kv.const_int <= 3 { 305 out[1] = sh.op0 306 out[2] = 1 << kv.const_int 307 out[4] = off_v 308 } } 309 } 310 } 311 if sh.op == OP_MUL { 312 let mv: *Value = x86ctx_value_at(c.f, sh.op1) 313 if mv.kind == VK_CONST_INT { 314 if mv.const_int == 2 { out[1] = sh.op0; out[2] = 2; out[4] = off_v } 315 if mv.const_int == 4 { out[1] = sh.op0; out[2] = 4; out[4] = off_v } 316 if mv.const_int == 8 { out[1] = sh.op0; out[2] = 8; out[4] = off_v } 317 } 318 } 319 } 320 } 321 } } } 322 return 1 323} 324 325// Pre-pass: mark the GEP + SHL of every SIB-foldable 8-byte load/store dead. 326func x86ctx_sib_prepass(c: *X86Ctx) -> i64 { 327 let out: *i64 = x86ctx_sib_scratch() 328 var bi: i64 = 0 329 while bi < c.f.n_blocks { 330 let b: *BasicBlock = x86ctx_block_at(c.f, bi) 331 var inst: *Instr = b.head 332 while inst != (0 as *Instr) { 333 var addr: i64 = 0 - 1 334 if inst.op == OP_LOAD { addr = inst.op0 } 335 if inst.op == OP_STORE { addr = inst.op0 } 336 if addr >= 0 { 337 if x86ctx_type_size(inst.ty) == 8 { 338 if x86ctx_sib_probe(c, addr, out) == 1 { 339 c.sib_dead[out[3]] = 1 340 if out[4] >= 0 { c.sib_dead[out[4]] = 1 } 341 // The SIB fold materializes base/index/value via 342 // load_value(_v) into rcx/rdx/rax -- NOT via the G1 343 // rax-forward path -- so a G4-elided operand (whose slot 344 // store was skipped, value only in rax) would be read 345 // from an empty slot -> the G4 tripwire. Force those 346 // operands to store normally (clearing elide is always 347 // safe: it is the un-optimized default). 348 let n2: i64 = c.f.n_values 349 if out[0] >= 0 { if out[0] < n2 { c.elide[out[0]] = 0 } } 350 if out[1] >= 0 { if out[1] < n2 { c.elide[out[1]] = 0 } } 351 if inst.op == OP_STORE { 352 if inst.op1 >= 0 { if inst.op1 < n2 { c.elide[inst.op1] = 0 } } 353 } 354 } 355 } 356 } 357 inst = inst.next 358 } 359 bi = bi + 1 360 } 361 return 0 362} 363 364// Emit `movq (%base,%index,scale),%dst` (load) or `movq %src,(%base,%index,scale)`. 365func x86ctx_emit_sib_mem(c: *X86Ctx, base: *u8, index: *u8, scale: i64) -> i64 { 366 out_str(c.o, "(%") 367 out_str(c.o, base) 368 out_str(c.o, ",%") 369 out_str(c.o, index) 370 out_str(c.o, ",") 371 out_i64(c.o, scale) 372 out_str(c.o, ")") 373 return 0 374} 375 376// ===== ctx_init =================================================== 377// 378// One pass over Function.values to allocate slots, then one pass 379// over Function.blocks->instrs to find allocas and reserve their 380// storage above the slot area. 381 382func x86ctx_init(f: *Function, o: *OutBuf) -> *X86Ctx { 383 let raw: *u8 = sys_mmap(NX_X86CTX_BYTES) 384 let c: *X86Ctx = raw as *X86Ctx 385 c.f = f 386 c.o = o 387 388 let n: i64 = f.n_values 389 let slot_raw: *u8 = sys_mmap(n * 8 + 16) 390 let alloca_raw: *u8 = sys_mmap(n * 8 + 16) 391 c.slot_off = slot_raw as *i64 392 c.alloca_off = alloca_raw as *i64 393 394 var v: i64 = 0 395 while v < n { 396 c.slot_off[v] = 0 - 1 397 c.alloca_off[v] = 0 - 1 398 v = v + 1 399 } 400 401 // Assign a slot to every Value that needs storage. 402 var slot: i64 = 0 403 var v2: i64 = 0 404 while v2 < n { 405 let val: *Value = x86ctx_value_at(f, v2) 406 var needs: i64 = 1 407 if val.kind == VK_CONST_INT { needs = 0 } 408 if val.kind == VK_GLOBAL { needs = 0 } 409 if val.kind == VK_FUNC_ADDR { needs = 0 } 410 if needs == 1 { 411 c.slot_off[v2] = slot * 8 412 slot = slot + 1 413 } 414 v2 = v2 + 1 415 } 416 c.n_slots = slot 417 418 // Find allocas and assign their storage above the slot area. 419 var ab: i64 = 0 420 var b: i64 = 0 421 while b < f.n_blocks { 422 let bb: *BasicBlock = x86ctx_block_at(f, b) 423 var i: *Instr = bb.head 424 let II_BUDGET: i64 = 65536 425 var iit: i64 = 0 426 while i != (0 as *Instr) { 427 if iit >= II_BUDGET { i = 0 as *Instr } 428 if i != (0 as *Instr) { 429 if i.op == OP_ALLOCA { 430 var sz: i64 = 8 431 if i.ty != (0 as *Type) { 432 if i.ty.kind != TY_VOID { 433 sz = i.ty.size 434 if sz <= 0 { sz = 8 } 435 } 436 } 437 sz = (sz + 7) & (0 - 8) 438 c.alloca_off[i.result] = c.n_slots * 8 + ab 439 ab = ab + sz 440 } 441 i = i.next 442 } 443 iit = iit + 1 444 } 445 b = b + 1 446 } 447 c.alloca_bytes = ab 448 449 let raw_size: i64 = c.n_slots * 8 + ab 450 c.frame_size = x86ctx_round_up_16(raw_size) 451 if c.frame_size == 0 { c.frame_size = 16 } 452 // G1 register allocation. Runs AFTER the alloca pass + frame_size so 453 // alloca_off is populated (FIX-10). STEP 1a inert: x86_regalloc_function 454 // lowers every value to {VL_SPILLED,-1} and returns mask 0, so n_saved=0, 455 // the frame is unchanged, and the emit path (untouched) is byte-identical. 456 let locs_raw: *u8 = sys_mmap(n * 16 + 16) 457 c.locs = locs_raw as *ValueLoc 458 let ah_raw: *u8 = sys_mmap(n * 8 + 16) 459 c.alloca_home = ah_raw as *i64 460 let ge_raw: *u8 = sys_mmap(n * 8 + 16) 461 c.elide = ge_raw as *i64 462 let us_raw: *u8 = sys_mmap(n * 8 + 16) 463 c.uses = us_raw as *i64 464 let sd_raw: *u8 = sys_mmap(n * 8 + 16) 465 c.sib_dead = sd_raw as *i64 466 let fw_raw: *u8 = sys_mmap(n * 8 + 16) 467 c.fwd_home = fw_raw as *i64 468 let ch_raw: *u8 = sys_mmap(n * 8 + 16) 469 c.chain_home = ch_raw as *i64 470 let cs_raw: *u8 = sys_mmap(n * 8 + 16) 471 c.chain_swap = cs_raw as *i64 472 c.next_bb = 0 - 1 473 let mask_raw: *u8 = sys_mmap(16) 474 let mask_p: *i64 = mask_raw as *i64 475 *mask_p = 0 476 x86_regalloc_function(c.f, c.alloca_off, c.locs, mask_p, c.alloca_home, c.elide, c.fwd_home, c.chain_home, c.chain_swap) 477 // G8: scaled-addressing (SIB) fold. Count operand uses, then mark GEP+SHL 478 // chains consumed by an 8-byte load/store as dead (emit nothing) -- the 479 // load/store re-derives (base,index,scale) via the SAME probe, so elision 480 // and emission cannot diverge. 481 x86ctx_count_uses(c) 482 x86ctx_sib_prepass(c) 483 c.used_cs_mask = *mask_p 484 c.n_saved = x86_popcount(c.used_cs_mask) 485 c.frame_size = c.frame_size + x86ctx_round_up_16(c.n_saved * 8) 486 // FIX-2: a homed value has no stack-slot identity -> the G1_RAX_SLOT peephole 487 // (which keys on slot_off) can never alias it, and store_result's slot path 488 // is bypassed. The VL_REGISTER branches handle every homed access. 489 var hv: i64 = 0 490 while hv < n { 491 let hl: *ValueLoc = ((c.locs as i64) + hv * 16) as *ValueLoc 492 if hl.kind == VL_REGISTER { c.slot_off[hv] = 0 - 1 } 493 hv = hv + 1 494 } 495 return c 496} 497 498// ===== load_value (materialise into a named reg) ================= 499// 500// Loads value v into register `reg`. Handles: 501// - VK_CONST_INT: movabsq $val, %reg 502// - VK_GLOBAL: leaq .Lg<id>(%rip), %reg 503// - OP_ALLOCA result: leaq <rbp-offset>(%rbp), %reg 504// - everything else: movq <rbp-offset>(%rbp), %reg 505 506func x86ctx_load_value(c: *X86Ctx, v_id: i64, reg: *u8) -> i64 { 507 let val: *Value = x86ctx_value_at(c.f, v_id) 508 if _g1_is_rax(reg) == 1 { 509 if G1_RAX_SLOT >= 0 { 510 if val.kind != VK_CONST_INT { if val.kind != VK_GLOBAL { if val.kind != VK_FUNC_ADDR { if c.alloca_off[v_id] < 0 { 511 if c.slot_off[v_id] == G1_RAX_SLOT { 512 G1_RAX_SLOT = 0 - 1 513 return 0 514 } 515 } } } } 516 } 517 G1_RAX_SLOT = 0 - 1 518 } 519 // G10: forwarded load result -- for a non-alloca SSA value the as-address 520 // and as-value paths are identical (the slot holds the value), so reading 521 // the source home is correct here too. Defensive: the audited-consumer 522 // whitelist should keep forwarded values out of this path entirely. 523 if c.fwd_home[v_id] >= 0 { 524 let fwr2: *u8 = x86_home_reg_name(c.fwd_home[v_id]) 525 if x86_reg_eq(reg, fwr2) == 0 { 526 x86_emit_movq_reg_reg(c.o, fwr2, reg) 527 } 528 return 0 529 } 530 // G1 (FIX-1): a homed value lives in its home register -> register move. 531 let g1l2: *ValueLoc = ((c.locs as i64) + v_id * 16) as *ValueLoc 532 if g1l2.kind == VL_REGISTER { 533 let g1h2: *u8 = x86_home_reg_name(g1l2.idx) 534 if x86_reg_eq(reg, g1h2) == 0 { 535 x86_emit_movq_reg_reg(c.o, g1h2, reg) 536 } 537 return 0 538 } 539 if val.kind == VK_CONST_INT { 540 x86_emit_movabsq(c.o, reg, val.const_int) 541 return 0 542 } 543 if val.kind == VK_GLOBAL { 544 out_str(c.o, " leaq .Lg") 545 out_i64(c.o, val.const_int) 546 out_str(c.o, "(%rip), %") 547 out_str(c.o, reg) 548 out_char(c.o, 0x0A) 549 return 0 550 } 551 if val.kind == VK_FUNC_ADDR { 552 let fnp: *Function = val.const_int as *Function 553 out_str(c.o, " leaq ") 554 out_str(c.o, fnp.name_start as *u8) 555 out_str(c.o, "(%rip), %") 556 out_str(c.o, reg) 557 out_char(c.o, 0x0A) 558 return 0 559 } 560 if c.alloca_off[v_id] >= 0 { 561 // G2 tripwire: a homed alloca HAS NO ADDRESS -- reaching the 562 // as-address path for one means the eligibility scan missed a use 563 // class. Emit an undefined-label jump so the ASSEMBLE fails loud 564 // (never a silent miscompile: the 2026-05-30 SEV1 lesson). 565 if c.alloca_home[v_id] >= 0 { 566 out_str(c.o, " jmp .G2_addr_of_homed_alloca_bug\n") 567 return 0 568 } 569 let abp_off: i64 = x86ctx_value_base(c) + c.alloca_off[v_id] 570 x86_emit_lea_disp(c.o, "rbp" as *u8, abp_off, reg) 571 return 0 572 } 573 // Spilled SSA value. 574 // G4 tripwire: an elided temp's slot was never written -- a load from it 575 // means the elision criterion missed a consumer; fail the assemble loud. 576 if c.elide[v_id] == 1 { 577 out_str(c.o, " jmp .G4_elided_slot_load_bug\n") 578 return 0 579 } 580 let rbp_off: i64 = x86ctx_value_base(c) + c.slot_off[v_id] 581 x86_emit_load_qword(c.o, "rbp" as *u8, rbp_off, reg) 582 return 0 583} 584 585// ===== store_result (spill reg into value's stack slot) ========== 586 587func x86ctx_store_result(c: *X86Ctx, v_id: i64, reg: *u8) -> i64 { 588 if v_id < 0 { return 0 } 589 if v_id >= c.f.n_values { return 0 } 590 // G11: a chain-fused result already lives IN the home register it was 591 // computed into (in place); its single consumer is the next chain step 592 // (or the suppressed store-back). Emit nothing, touch no G1 state (the 593 // fused op never wrote rax). 594 if c.chain_home[v_id] >= 0 { return 0 } 595 // G1 (FIX-1/FIX-16): a homed result already lives in its home register (the 596 // producing op computed straight into it via x86ctx_result_reg). Emit AT 597 // MOST one move -- zero when reg already IS the home -- REPLACING the slot 598 // store, never adding to it. Placed before the _g1_is_rax handling (FIX-16). 599 let lbase: i64 = c.locs as i64 600 let l: *ValueLoc = (lbase + v_id * 16) as *ValueLoc 601 if l.kind == VL_REGISTER { 602 let h: *u8 = x86_home_reg_name(l.idx) 603 if x86_reg_eq(reg, h) == 0 { 604 x86_emit_movq_reg_reg(c.o, reg, h) 605 } 606 G1_RAX_SLOT = 0 - 1 607 return 0 608 } 609 if c.slot_off[v_id] < 0 { return 0 } // unused result 610 // G4: a single-use next-instruction temp's slot store is DEAD -- the 611 // consumer reads rax through the G1 forwarding path. Skip the store but 612 // keep the G1 contract ("rax holds this slot's value"). Only valid when 613 // the result really is in rax; any other producer reg un-flags and falls 614 // through to a normal store, keeping the slot-load tripwire exact. 615 if c.elide[v_id] == 1 { 616 if _g1_is_rax(reg) == 1 { 617 G1_RAX_SLOT = c.slot_off[v_id] 618 return 0 619 } 620 c.elide[v_id] = 0 621 } 622 let rbp_off: i64 = x86ctx_value_base(c) + c.slot_off[v_id] 623 x86_emit_store_qword(c.o, reg, "rbp" as *u8, rbp_off) 624 // G1: rax now provably holds this value's slot (the store copied it). 625 if _g1_is_rax(reg) == 1 { G1_RAX_SLOT = c.slot_off[v_id] } 626 return 0 627} 628 629// ===== load_value_v (full materialisation) ======================= 630// 631// Defined ahead of its 9 emit_* callers per F7 post-order DFS 632// discipline -- see docs/NISHI_F7_FORWARD_REF_S_CLASS_PLAN.md. 633// Unlike x86ctx_load_value (above) which expects the value already 634// in a register, _v walks alloca/spill/const/global maps and emits 635// the materialising mov. 636 637func x86ctx_load_value_v(c: *X86Ctx, v_id: i64, reg: *u8) -> i64 { 638 let val: *Value = x86ctx_value_at(c.f, v_id) 639 // G1 (CS:APP SS5.6): rax already holds this spilled SSA value -> skip the 640 // reload; else if rax is about to be clobbered by the load -> invalidate. 641 if _g1_is_rax(reg) == 1 { 642 if G1_RAX_SLOT >= 0 { 643 if val.kind != VK_CONST_INT { if val.kind != VK_GLOBAL { if val.kind != VK_FUNC_ADDR { if c.alloca_off[v_id] < 0 { 644 if c.slot_off[v_id] == G1_RAX_SLOT { 645 G1_RAX_SLOT = 0 - 1 646 return 0 647 } 648 } } } } 649 } 650 G1_RAX_SLOT = 0 - 1 651 } 652 // G10 (2026-07-15): a home-FORWARDED load result still lives in its source 653 // alloca's home register (the fwd scan proved no intervening store/call). 654 // Read the home directly -- the load itself emitted NOTHING, its slot was 655 // never written. Must precede the spill/elide paths. 656 if c.fwd_home[v_id] >= 0 { 657 let fwr: *u8 = x86_home_reg_name(c.fwd_home[v_id]) 658 if x86_reg_eq(reg, fwr) == 0 { 659 x86_emit_movq_reg_reg(c.o, fwr, reg) 660 } 661 return 0 662 } 663 // G1 (FIX-1): a homed value LIVES in its home register -> a register move, 664 // NEVER a reload. Zero-cost when reg already IS the home register. 665 let g1l: *ValueLoc = ((c.locs as i64) + v_id * 16) as *ValueLoc 666 if g1l.kind == VL_REGISTER { 667 let g1h: *u8 = x86_home_reg_name(g1l.idx) 668 if x86_reg_eq(reg, g1h) == 0 { 669 x86_emit_movq_reg_reg(c.o, g1h, reg) 670 } 671 return 0 672 } 673 if val.kind == VK_CONST_INT { 674 x86_emit_movabsq(c.o, reg, val.const_int) 675 return 0 676 } 677 if val.kind == VK_GLOBAL { 678 out_str(c.o, " leaq .Lg") 679 out_i64(c.o, val.const_int) 680 out_str(c.o, "(%rip), %") 681 out_str(c.o, reg) 682 out_char(c.o, 0x0A) 683 return 0 684 } 685 if val.kind == VK_FUNC_ADDR { 686 let fnp: *Function = val.const_int as *Function 687 out_str(c.o, " leaq ") 688 out_str(c.o, fnp.name_start as *u8) 689 out_str(c.o, "(%rip), %") 690 out_str(c.o, reg) 691 out_char(c.o, 0x0A) 692 return 0 693 } 694 if c.alloca_off[v_id] >= 0 { 695 // G2: a homed alloca's VALUE lives in its home register -> register 696 // move (zero-cost when reg already IS the home). This is the deref 697 // the comment block below describes, minus the memory. 698 if c.alloca_home[v_id] >= 0 { 699 let ahreg: *u8 = x86_home_reg_name(c.alloca_home[v_id]) 700 if x86_reg_eq(reg, ahreg) == 0 { 701 x86_emit_movq_reg_reg(c.o, ahreg, reg) 702 } 703 return 0 704 } 705 // Peephole 2026-05-20: fold `leaq -off(%rbp), %reg ; movq (%reg), %reg` 706 // into single `movq -off(%rbp), %reg`. Stabilizer-validated 2-4x 707 // gap to gcc -O0 is dominated by this load pattern; eliminating 708 // the indirection drops ~30% of inner-loop instructions. Safe: 709 // both sequences load the same qword into the same register 710 // with no observable intermediate state. 711 let abp_off: i64 = x86ctx_value_base(c) + c.alloca_off[v_id] 712 x86_emit_load_qword(c.o, "rbp" as *u8, abp_off, reg) 713 return 0 714 } 715 // Spilled SSA value. 716 // G4 tripwire (see x86ctx_load_value): elided slots are never loadable. 717 if c.elide[v_id] == 1 { 718 out_str(c.o, " jmp .G4_elided_slot_load_bug\n") 719 return 0 720 } 721 let rbp_off: i64 = x86ctx_value_base(c) + c.slot_off[v_id] 722 x86_emit_load_qword(c.o, "rbp" as *u8, rbp_off, reg) 723 return 0 724} 725 726// G1 consume-side: the home register name if v_id is homed, else null. 727func x86ctx_home_name_or_null(c: *X86Ctx, v_id: i64) -> *u8 { 728 if v_id >= 0 { if v_id < c.f.n_values { 729 let l: *ValueLoc = ((c.locs as i64) + v_id * 16) as *ValueLoc 730 if l.kind == VL_REGISTER { return x86_home_reg_name(l.idx) } 731 } } 732 return 0 as *u8 733} 734 735// G10 consume-side: the SOURCE home register name for a forwarded load 736// result, else null. Reading it directly at an audited consumer position is 737// ZERO instructions (vs movq %home,%rcx) -- safe because a compare/binop 738// source read never mutates the register. 739func x86ctx_fwd_name_or_null(c: *X86Ctx, v_id: i64) -> *u8 { 740 if v_id >= 0 { if v_id < c.f.n_values { 741 if c.fwd_home[v_id] >= 0 { return x86_home_reg_name(c.fwd_home[v_id]) } 742 } } 743 return 0 as *u8 744} 745 746// LN7 (2026-08-23, lang rung "backend consumes its register allocation"): THE 747// consume-side predicate. The register a consumer may read value v_id from IN 748// PLACE -- its own home if the allocator homed it, else the home of the alloca it 749// was forwarded from (x86_fwd_scan), else null (materialize via load_value_v). 750// A value is never both homed and forwarded (the scan releases the home when it 751// forwards), so the two lookups are one question, and every audited consumer 752// position (cmp op0/op1, binop src, SIB index, store value, cmov dividend) asks 753// it here instead of pairing the two lookups by hand. Reading the register as a 754// source operand never mutates it, which is what makes the in-place read sound. 755func x86_use_regalloc(c: *X86Ctx, v_id: i64) -> *u8 { 756 let h: *u8 = x86ctx_home_name_or_null(c, v_id) 757 if h != (0 as *u8) { return h } 758 return x86ctx_fwd_name_or_null(c, v_id) 759} 760 761// Ops whose second operand can be consumed DIRECTLY as the in-place `<op> src, 762// dst` source register (so a homed op1 needs no `movq %home,%rcx`). div/rem 763// (op1 in rcx for idivq) and shifts/rotates (count in cl) are excluded. 764func x86ctx_op1_direct_ok(op: i64) -> i64 { 765 if op == OP_ADD { return 1 } 766 if op == OP_SUB { return 1 } 767 if op == OP_MUL { return 1 } 768 if op == OP_AND { return 1 } 769 if op == OP_OR { return 1 } 770 if op == OP_XOR { return 1 } 771 return 0 772} 773 774// ===== binop dispatch ============================================ 775 776// bit position of a power-of-two value (caller guarantees v == 2^k, k in 0..63). 777func x86ctx_log2_i64(v: i64) -> i64 { 778 var k: i64 = 0 779 var m: i64 = v 780 while m > 1 { m = m >> 1; k = k + 1 } 781 return k 782} 783 784// ===== LN18: CHECKED ARITHMETIC AT add+jo COST (2026-09-01) ============================ 785// nx_parse.nx (LN1, --chkarith) emits every checked i64 add/sub as backend-agnostic IR: 786// r = a + b ; x1 = a ^ r ; x2 = b ^ r ; a1 = x1 & x2 ; c = a1 < 0 ; br_cond c -> fail, ok 787// r = a - b ; x1 = a ^ b ; x2 = a ^ r ; a1 = x1 & x2 ; c = a1 < 0 ; br_cond c -> fail, ok 788// which this emitter rendered as six instructions with spills per add -- measured 2026-09-01 on the 789// /compare/lang receipt as 17,011 us for the daily mode against 7,145 us for the default build. 790// The predicate ((a^r)&(b^r))<0 IS the signed-overflow flag of the add just emitted (and 791// ((a^b)&(a^r))<0 of the sub), so when the four intermediates have no other use the whole chain is 792// `addq` (already emitted) + `jo fail` on the live flags: the pending-compare belt G6 already carries a 793// condition code to the following br_cond, and G24_SKIP makes the block loop step over the four 794// intermediates. Single-use is the emitter's OWN G8 operand count (x86ctx_count_uses), so a program 795// that reads x1 or a1 elsewhere keeps the long form -- the fuse can only remove work nothing observes. 796// The store of r happens BEFORE this runs and is a plain movq, which does not touch the flags. 797func x86ctx_ovf_pair(n: *Instr, x: i64, y: i64) -> i64 { 798 if n.op0 == x { if n.op1 == y { return 1 } } 799 if n.op0 == y { if n.op1 == x { return 1 } } 800 return 0 801} 802func x86ctx_ovf_single(c: *X86Ctx, v: i64) -> i64 { 803 if v < 0 { return 0 } 804 if v >= c.f.n_values { return 0 } 805 if c.uses[v] != 1 { return 0 } 806 return 1 807} 808func x86ctx_ovf_fuse(c: *X86Ctx, i: *Instr) -> i64 { 809 if i.op != OP_ADD { if i.op != OP_SUB { return 0 } } 810 let r: i64 = i.result 811 if r < 0 { return 0 } 812 let a: i64 = i.op0 813 let b: i64 = i.op1 814 let n1: *Instr = i.next 815 if n1 == (0 as *Instr) { return 0 } 816 let n2: *Instr = n1.next 817 if n2 == (0 as *Instr) { return 0 } 818 let n3: *Instr = n2.next 819 if n3 == (0 as *Instr) { return 0 } 820 let n4: *Instr = n3.next 821 if n4 == (0 as *Instr) { return 0 } 822 let n5: *Instr = n4.next 823 if n5 == (0 as *Instr) { return 0 } 824 if n1.op != OP_XOR { return 0 } 825 if n2.op != OP_XOR { return 0 } 826 if n3.op != OP_AND { return 0 } 827 if n4.op != OP_LT_S { return 0 } 828 if n5.op != OP_BR_COND { return 0 } 829 var shape: i64 = 0 830 if i.op == OP_ADD { 831 if x86ctx_ovf_pair(n1, a, r) == 1 { if x86ctx_ovf_pair(n2, b, r) == 1 { shape = 1 } } 832 if x86ctx_ovf_pair(n1, b, r) == 1 { if x86ctx_ovf_pair(n2, a, r) == 1 { shape = 1 } } 833 } 834 if i.op == OP_SUB { 835 if x86ctx_ovf_pair(n1, a, b) == 1 { if x86ctx_ovf_pair(n2, a, r) == 1 { shape = 1 } } 836 if x86ctx_ovf_pair(n1, a, r) == 1 { if x86ctx_ovf_pair(n2, a, b) == 1 { shape = 1 } } 837 } 838 if shape == 0 { return 0 } 839 if x86ctx_ovf_pair(n3, n1.result, n2.result) == 0 { return 0 } 840 if n4.op0 != n3.result { return 0 } 841 if n4.op1 < 0 { return 0 } 842 if n4.op1 >= c.f.n_values { return 0 } 843 let zv: *Value = x86ctx_value_at(c.f, n4.op1) 844 if zv.kind != VK_CONST_INT { return 0 } 845 if zv.const_int != 0 { return 0 } 846 if n5.op0 != n4.result { return 0 } 847 if x86ctx_ovf_single(c, n1.result) == 0 { return 0 } 848 if x86ctx_ovf_single(c, n2.result) == 0 { return 0 } 849 if x86ctx_ovf_single(c, n3.result) == 0 { return 0 } 850 if x86ctx_ovf_single(c, n4.result) == 0 { return 0 } 851 G1_PENDING_CC = NX_X64_CC_O 852 G1_PENDING_VAL = n4.result 853 G24_SKIP = 4 854 G24_FUSED = G24_FUSED + 1 855 return 1 856} 857 858func x86ctx_emit_binop(c: *X86Ctx, i: *Instr) -> i64 { 859 // G8: a SHL/MUL folded into a SIB load/store is dead -- emit nothing. 860 if i.result >= 0 { if i.result < c.f.n_values { if c.sib_dead[i.result] == 1 { return 0 } } } 861 // FIX-1 (ALLOCATE-not-COPY): a homed result is computed straight INTO its 862 // home register. dst = the home for a homed pure-rax binop, else "rax". 863 // Non-homed results (incl. div/rem/shift, never homed in cut-1) get dst=rax, 864 // so op0 lands in rax exactly as before and those cases are unchanged. 865 // G11: a chain-fused binop operates IN PLACE on the source alloca's home -- 866 // dst = that home and op0 is NOT materialized (its value IS the home's 867 // current content; the head load emitted nothing). Only ADD/SUB/MUL/AND/ 868 // OR/XOR are ever chain-marked, so the G7/G5a-shift/UMULHI paths below 869 // never see a chain dst. 870 var g11: i64 = 0 - 1 871 if i.result >= 0 { if i.result < c.f.n_values { g11 = c.chain_home[i.result] } } 872 var dst: *u8 = x86ctx_result_reg(c, i.result) 873 if g11 >= 0 { dst = x86_home_reg_name(g11) } 874 // G21 COMMUTED RAX-CONSUME (2026-07-16): op1 of a commutative, non-chain, 875 // unhomed-result binop is RIDING IN RAX (G1 contract: rax holds exactly the 876 // value of slot G1_RAX_SLOT). Swap operands at emit -- rax IS the dst seed, 877 // so the op0-into-dst load (which would clobber the ride) is skipped and 878 // op0 becomes the src, resolved by the normal src1 machinery (home-direct / 879 // imm-fold / rcx). Kills the op1 rcx-reload; with the mirrored k==1 clause 880 // in x86_g4_pos_ok the producer's slot store dies too (spill+reload pair -> 881 // nothing). dst is already "rax" here (unhomed result). The exact kind/ 882 // alloca guards mirror load_value_v's G1 skip. Negctl COMMUTE_ANY drops the 883 // G1 match -> garbage rides in -> matrix RED. 884 var g21: i64 = 0 885 if g11 < 0 { 886 if x86_chain_op_commutative(i.op) == 1 { 887 if x86ctx_home_name_or_null(c, i.result) == (0 as *u8) { 888 if i.op1 >= 0 { if i.op1 < c.f.n_values { 889 let g21v: *Value = x86ctx_value_at(c.f, i.op1) 890 if g21v.kind != VK_CONST_INT { if g21v.kind != VK_GLOBAL { if g21v.kind != VK_FUNC_ADDR { 891 if c.alloca_off[i.op1] < 0 { 892 if X86_NEGCTL_G21_COMMUTE_ANY == 1 { g21 = 1 } 893 if G1_RAX_SLOT >= 0 { if c.slot_off[i.op1] == G1_RAX_SLOT { g21 = 1 } } 894 } 895 } } } 896 } } 897 } 898 } 899 } 900 // G23 SRC-DIRECT CMOV DIVISION (2026-07-16): a HOMED/FORWARDED sign-unknown 901 // dividend is read straight from its register by the cmov form (leaq 902 // bias(%h),%rcx; testq %h,%h; cmovns %h,%rcx; sarq $k,%rcx) -- the 903 // movq %home,%rax feed dies, and rax (with its G1 ride state) SURVIVES the 904 // whole division untouched. Gated on elide[result]==0: an elided result 905 // must ride out of rax, which this form never writes. Disjoint from G17 906 // (requires val_nonneg==0) and from the k==0 identity (requires 2^k>1). 907 var g23: i64 = 0 908 var g23h: *u8 = 0 as *u8 909 var g23k: i64 = 0 910 var g23d: i64 = 0 911 if g11 < 0 { if g21 == 0 { 912 if i.op == OP_DIV_S { 913 let g23v: *Value = x86ctx_value_at(c.f, i.op1) 914 if g23v.kind == VK_CONST_INT { 915 let g23dd: i64 = g23v.const_int 916 if g23dd > 1 { if (g23dd & (g23dd - 1)) == 0 { 917 g23k = x86ctx_log2_i64(g23dd) 918 if g23k <= 31 { 919 if x86_val_nonneg(c.f, i.op0, 8) == 0 { 920 g23h = x86_use_regalloc(c, i.op0) // LN7: in-place read 921 if g23h != (0 as *u8) { 922 var g23e: i64 = 0 923 if i.result >= 0 { if i.result < c.f.n_values { g23e = c.elide[i.result] } } 924 if g23e == 0 { g23 = 1; g23d = g23dd } 925 } 926 } 927 } 928 } } 929 } 930 } 931 } } 932 if g11 < 0 { if g21 == 0 { if g23 == 0 { x86ctx_load_value_v(c, i.op0, dst) } } } 933 if g21 == 1 { G1_RAX_SLOT = 0 - 1 } 934 // G11 commuted chains (MUL(3,n) canonical const-first forms): the chain 935 // value sits at op1, so the SOURCE operand of the in-place `op src,%hA` is 936 // op0. Everywhere else srcop == op1 -> byte-identical. Only COMMUTATIVE 937 // ops are ever swap-marked (dst OP src == src OP dst). 938 var srcop: i64 = i.op1 939 if g11 >= 0 { if c.chain_swap[i.result] == 1 { srcop = i.op0 } } 940 if g21 == 1 { srcop = i.op0 } 941 // G7: divide by a CONSTANT POWER OF TWO -> shift. idivq is ~20-40 cycles; 942 // a shift is 1. Signed division rounds toward ZERO, so the arithmetic-shift 943 // (floor) result needs a bias when the dividend is negative: add (2^k - 1) 944 // before the sar. Sequence matches gcc/clang exactly (cross-checked bit-for- 945 // bit over negative/positive/INT_MIN by the xlang matrix's signed-div KAT). 946 // DIV_S/DIV_U are never homed (dst=="rax"); rcx is free scratch (not a home). 947 if i.op == OP_DIV_S { 948 let g7v: *Value = x86ctx_value_at(c.f, i.op1) 949 if g7v.kind == VK_CONST_INT { 950 let g7d: i64 = g7v.const_int 951 if g7d > 0 { if (g7d & (g7d - 1)) == 0 { 952 let g7k: i64 = x86ctx_log2_i64(g7d) 953 if g7k == 0 { x86ctx_store_result(c, i.result, dst); return 0 } 954 // G23: home-read cmov form (op0 load was SKIPPED -- dst holds 955 // nothing for this op; the branch reads g23h and writes rcx 956 // only). Checked FIRST: under g23 the dst-based paths below 957 // must not run. Disjointness with G17 is also structural 958 // (g23 requires val_nonneg==0). 959 if g23 == 1 { 960 out_str(c.o, " leaq ") 961 out_i64(c.o, g23d - 1) 962 out_str(c.o, "(%") 963 out_str(c.o, g23h) 964 out_str(c.o, "), %rcx\n") 965 out_str(c.o, " testq %") 966 out_str(c.o, g23h) 967 out_str(c.o, ", %") 968 out_str(c.o, g23h) 969 out_char(c.o, 0x0A) 970 var g23cc: *u8 = "cmovns" as *u8 971 if X86_NEGCTL_G23_CMOV_INVERT == 1 { g23cc = "cmovs" as *u8 } 972 out_str(c.o, " ") 973 out_str(c.o, g23cc) 974 out_str(c.o, " %") 975 out_str(c.o, g23h) 976 out_str(c.o, ", %rcx\n") 977 out_str(c.o, " sarq $") 978 out_i64(c.o, g23k) 979 out_str(c.o, ", %rcx\n") 980 x86ctx_store_result(c, i.result, "rcx" as *u8) 981 return 0 982 } 983 // G17 (2026-07-16): a PROVABLY NON-NEGATIVE dividend needs no 984 // sign-bias dance -- one bare shift (the scientist's 2.063x 985 // proven spot; gcc -O2 emits the dance wherever it cannot see 986 // the range). The lattice excludes every overflow-capable op, 987 // and the negctl (nonneg-always) goes RED on the battery. 988 if x86_val_nonneg(c.f, i.op0, 8) == 1 { 989 out_str(c.o, " shrq $") 990 out_i64(c.o, g7k) 991 out_str(c.o, ", %") 992 out_str(c.o, dst) 993 out_char(c.o, 0x0A) 994 x86ctx_store_result(c, i.result, dst) 995 return 0 996 } 997 // G22 (2026-07-16): bias-via-CMOV -- gcc/clang's own form. 998 // leaq (2^k-1)(%dst), %rcx ; bias candidate (flag-neutral) 999 // testq %dst, %dst ; SF := sign(v) [dies on ride] 1000 // cmovs %rcx, %dst ; v<0 ? v+bias : v 1001 // sarq $k, %dst 1002 // 4 instrs vs the 5-instr shr-dance, dependency depth 3 vs 4. 1003 // The testq DIES when the textually-previous instruction is an 1004 // ALU op that produced THIS dividend: SF already equals its 1005 // sign, and only flag-neutral movs/leas are emitted in between 1006 // (the exact G14 contract). cmovS reads ONLY SF, so the G14 1007 // signed-LT OF-hazard does not apply; same producer set 1008 // {ADD,SUB,AND,OR,XOR} + sib_dead exclusion. Result stays in 1009 // dst(=rax) so every G1/G4 ride downstream is preserved. 1010 // k>31 keeps the dance (the bias immediate exceeds leaq's 1011 // disp32). Negctls: CMOV_INVERT + RIDE_ALWAYS, both matrix-RED. 1012 if g7k <= 31 { 1013 var g22r: i64 = 0 1014 let g22p: *Instr = i.prev 1015 if g22p != (0 as *Instr) { 1016 if g22p.result == i.op0 { 1017 var g22ok: i64 = 0 1018 if g22p.op == OP_ADD { g22ok = 1 } 1019 if g22p.op == OP_SUB { g22ok = 1 } 1020 if g22p.op == OP_AND { g22ok = 1 } 1021 if g22p.op == OP_OR { g22ok = 1 } 1022 if g22p.op == OP_XOR { g22ok = 1 } 1023 if g22ok == 1 { 1024 if g22p.result >= 0 { if g22p.result < c.f.n_values { 1025 if c.sib_dead[g22p.result] == 1 { g22ok = 0 } 1026 } } 1027 } 1028 if g22ok == 1 { g22r = 1 } 1029 } 1030 } 1031 if X86_NEGCTL_G22_RIDE_ALWAYS == 1 { g22r = 1 } 1032 out_str(c.o, " leaq ") 1033 out_i64(c.o, g7d - 1) 1034 out_str(c.o, "(%") 1035 out_str(c.o, dst) 1036 out_str(c.o, "), %rcx\n") 1037 if g22r == 0 { 1038 out_str(c.o, " testq %") 1039 out_str(c.o, dst) 1040 out_str(c.o, ", %") 1041 out_str(c.o, dst) 1042 out_char(c.o, 0x0A) 1043 } 1044 var g22cc: *u8 = "cmovs" as *u8 1045 if X86_NEGCTL_G22_CMOV_INVERT == 1 { g22cc = "cmovns" as *u8 } 1046 out_str(c.o, " ") 1047 out_str(c.o, g22cc) 1048 out_str(c.o, " %rcx, %") 1049 out_str(c.o, dst) 1050 out_char(c.o, 0x0A) 1051 out_str(c.o, " sarq $") 1052 out_i64(c.o, g7k) 1053 out_str(c.o, ", %") 1054 out_str(c.o, dst) 1055 out_char(c.o, 0x0A) 1056 x86ctx_store_result(c, i.result, dst) 1057 return 0 1058 } 1059 x86_emit_movq_reg_reg(c.o, dst, "rcx" as *u8) // rcx = x 1060 out_str(c.o, " sarq $63, %rcx\n") // rcx = 0 or -1 1061 out_str(c.o, " shrq $") 1062 out_i64(c.o, 64 - g7k) 1063 out_str(c.o, ", %rcx\n") // rcx = 0 or (2^k-1) 1064 x86_emit_addq_rr(c.o, "rcx" as *u8, dst) // dst = x + bias 1065 out_str(c.o, " sarq $") 1066 out_i64(c.o, g7k) 1067 out_str(c.o, ", %") 1068 out_str(c.o, dst) 1069 out_char(c.o, 0x0A) 1070 x86ctx_store_result(c, i.result, dst) 1071 return 0 1072 } } 1073 } 1074 } 1075 if i.op == OP_DIV_U { 1076 let g7uv: *Value = x86ctx_value_at(c.f, i.op1) 1077 if g7uv.kind == VK_CONST_INT { 1078 let g7ud: i64 = g7uv.const_int 1079 if g7ud > 0 { if (g7ud & (g7ud - 1)) == 0 { 1080 let g7uk: i64 = x86ctx_log2_i64(g7ud) 1081 if g7uk == 0 { x86ctx_store_result(c, i.result, dst); return 0 } 1082 out_str(c.o, " shrq $") 1083 out_i64(c.o, g7uk) 1084 out_str(c.o, ", %") 1085 out_str(c.o, dst) 1086 out_char(c.o, 0x0A) 1087 x86ctx_store_result(c, i.result, dst) 1088 return 0 1089 } } 1090 } 1091 } 1092 // G5a: imm32 op1 folding -- `addq $imm,%dst` (and shift-by-constant) 1093 // replaces the movabsq-into-rcx + reg-op form: 2 insns -> 1, and the 1094 // per-iteration constant re-materialisation disappears. XOR is excluded 1095 // (nxasm's xorq dispatch is alu_rr-only -- a $imm operand would silently 1096 // mis-encode); MUL/div/rem/UMULHI/CRC32/PDEP/PEXT keep the register path. 1097 let g5v1: *Value = x86ctx_value_at(c.f, srcop) 1098 if g5v1.kind == VK_CONST_INT { 1099 let g5c: i64 = g5v1.const_int 1100 var g5mn: *u8 = 0 as *u8 1101 if i.op == OP_ADD { g5mn = "addq" as *u8 } 1102 if i.op == OP_SUB { g5mn = "subq" as *u8 } 1103 if i.op == OP_AND { g5mn = "andq" as *u8 } 1104 if i.op == OP_OR { g5mn = "orq" as *u8 } 1105 if g5mn != (0 as *u8) { 1106 if g5c <= 2147483647 { if g5c >= (0 - 2147483648) { 1107 out_str(c.o, " ") 1108 out_str(c.o, g5mn) 1109 out_str(c.o, " $") 1110 out_i64(c.o, g5c) 1111 out_str(c.o, ", %") 1112 out_str(c.o, dst) 1113 out_char(c.o, 0x0A) 1114 x86ctx_store_result(c, i.result, dst) 1115 x86ctx_ovf_fuse(c, i) 1116 return 0 1117 } } 1118 } 1119 var g5sh: *u8 = 0 as *u8 1120 if i.op == OP_SHL { g5sh = "shlq" as *u8 } 1121 if i.op == OP_SHR_S { g5sh = "sarq" as *u8 } 1122 if i.op == OP_SHR_U { g5sh = "shrq" as *u8 } 1123 if g5sh != (0 as *u8) { 1124 if g5c >= 0 { if g5c <= 63 { 1125 out_str(c.o, " ") 1126 out_str(c.o, g5sh) 1127 out_str(c.o, " $") 1128 out_i64(c.o, g5c) 1129 out_str(c.o, ", %rax\n") 1130 x86ctx_store_result(c, i.result, "rax" as *u8) 1131 return 0 1132 } } 1133 } 1134 // G19 lea-strength: c*n -> leaq (%n,%n,scale),%n for c in {2,3,5,9}, 1135 // scale=c-1 in {1,2,4,8}. dst already holds op0 (n): the non-chain path 1136 // loads op0 into dst (~line 741); a chain dst holds the running value; 1137 // and srcop/g5c is the CONSTANT factor in every path that reaches here, 1138 // so dst is always the non-const factor n. lea is flag-neutral -- imul 1139 // leaves ZF undefined anyway and MUL is excluded from the G14 flag-reuse 1140 // producer set, so no downstream cmp-elision can depend on these flags. 1141 // c=4/8 are already covered by the shl peephole. The subsequent `+d` 1142 // (e.g. collatz 3n+1) stays a separate imm-folded addq -- sound with no 1143 // lookahead; displacement fusion is a later sub-rung. Negctl WRONG_SCALE 1144 // emits c (=> (c+1)*n, matrix RED). 1145 if i.op == OP_MUL { 1146 var g19s: i64 = 0 1147 if g5c == 2 { g19s = 1 } 1148 if g5c == 3 { g19s = 2 } 1149 if g5c == 5 { g19s = 4 } 1150 if g5c == 9 { g19s = 8 } 1151 if g19s != 0 { 1152 if X86_NEGCTL_LEA_WRONG_SCALE == 1 { g19s = g5c } 1153 out_str(c.o, " leaq (%") 1154 out_str(c.o, dst) 1155 out_str(c.o, ",%") 1156 out_str(c.o, dst) 1157 out_str(c.o, ",") 1158 out_i64(c.o, g19s) 1159 out_str(c.o, "), %") 1160 out_str(c.o, dst) 1161 out_char(c.o, 0x0A) 1162 x86ctx_store_result(c, i.result, dst) 1163 return 0 1164 } 1165 } 1166 } 1167 // G1 consume-side: a homed op1 of a pure-rax op is used DIRECTLY as the src 1168 // register (no `movq %home,%rcx`). Everything else loads op1 into rcx 1169 // (div/rem need it there for idivq; shifts/rotates need cl). When nothing is 1170 // homed, src1 == "rcx" and this is byte-identical to before. 1171 var src1: *u8 = "rcx" as *u8 1172 // G10/LN7: a homed OR forwarded src reads its register directly (zero-cost), 1173 // null when neither. srcop==op1 except for G11 commuted chains (source = op0). 1174 var op1_home: *u8 = x86_use_regalloc(c, srcop) 1175 if op1_home != (0 as *u8) { 1176 if x86ctx_op1_direct_ok(i.op) == 1 { 1177 src1 = op1_home 1178 } 1179 if x86ctx_op1_direct_ok(i.op) == 0 { 1180 x86ctx_load_value_v(c, srcop, "rcx" as *u8) 1181 } 1182 } 1183 if op1_home == (0 as *u8) { 1184 // G16: a CHAIN-FUSED op's dst is a home (never rax), so rax is free for 1185 // the src -- materialize via rax so the G1 store->load forward (and the 1186 // G4 chain-src elide) collapse `spill+reload` to NOTHING for a producer 1187 // in the previous instruction. Non-chain ops keep rcx (byte-identical). 1188 if g11 >= 0 { 1189 x86ctx_load_value_v(c, srcop, "rax" as *u8) 1190 src1 = "rax" as *u8 1191 } 1192 if g11 < 0 { 1193 x86ctx_load_value_v(c, srcop, "rcx" as *u8) 1194 } 1195 } 1196 if i.op == OP_ADD { 1197 x86_emit_addq_rr(c.o, src1, dst) 1198 x86ctx_store_result(c, i.result, dst) 1199 x86ctx_ovf_fuse(c, i) 1200 return 0 1201 } 1202 if i.op == OP_SUB { 1203 x86_emit_subq_rr(c.o, src1, dst) 1204 x86ctx_store_result(c, i.result, dst) 1205 x86ctx_ovf_fuse(c, i) 1206 return 0 1207 } 1208 if i.op == OP_MUL { 1209 x86_emit_imulq_rr(c.o, src1, dst) 1210 x86ctx_store_result(c, i.result, dst) 1211 return 0 1212 } 1213 if i.op == OP_UMULHI { 1214 // G2 unsigned 64x64 -> HIGH 64 bits. mulq sets rdx:rax = rax * src1; we 1215 // keep the rdx half. op0 is in rax (dst -- OP_UMULHI is never homed so 1216 // dst==rax, the implicit multiplicand); op1 is src1 (rcx, or a home reg). 1217 x86_emit_mulq_r(c.o, src1) 1218 x86ctx_store_result(c, i.result, "rdx" as *u8) 1219 return 0 1220 } 1221 if i.op == OP_CRC32 { 1222 // SSE4.2 CRC-32C accumulate: crc32q %src1,%dst -> dst = CRC32C(dst, src1). 1223 // op0 (the running crc) is in dst; op1 (the data word) is src1. CRC32 is 1224 // NOT op1-direct-ok, so src1 is always rcx (op1 loaded there above), and 1225 // dst can be any home reg -- crc32q reg,reg accepts arbitrary GPRs. 1226 x86_emit_crc32q_rr(c.o, src1, dst) 1227 x86ctx_store_result(c, i.result, dst) 1228 return 0 1229 } 1230 if i.op == OP_PDEP { 1231 // BMI2 pdep %src2,%src1,%dst -> deposit src1's low bits into src2's mask. 1232 // __pdep64(value, mask): op0 (value) is in dst (VEX.vvvv src1 = dst reg), 1233 // op1 (mask) is in src1==rcx (ModRM.rm src2). PDEP is NOT op1-direct-ok, 1234 // so the mask is always rcx; the home pool excludes rcx/rdx (FIX-12), so 1235 // dst is never rcx and dst==src1(vvvv)==result is a legal, collision-free 1236 // 3-operand form (pdep permits dst == VEX.vvvv). 1237 x86_emit_pdep_rrr(c.o, src1, dst, dst) 1238 x86ctx_store_result(c, i.result, dst) 1239 return 0 1240 } 1241 if i.op == OP_PEXT { 1242 // BMI2 pext %src2,%src1,%dst -> gather src1 bits at src2's mask positions. 1243 // __pext64(value, mask): op0 (value) in dst (VEX.vvvv src1), op1 (mask) in 1244 // src1==rcx (ModRM.rm src2). Same collision-free layout as PDEP. 1245 x86_emit_pext_rrr(c.o, src1, dst, dst) 1246 x86ctx_store_result(c, i.result, dst) 1247 return 0 1248 } 1249 if i.op == OP_DIV_S { 1250 x86_emit_cqo(c.o) 1251 x86_emit_idivq_r(c.o, "rcx" as *u8) 1252 x86ctx_store_result(c, i.result, "rax" as *u8) 1253 return 0 1254 } 1255 if i.op == OP_REM_S { 1256 x86_emit_cqo(c.o) 1257 x86_emit_idivq_r(c.o, "rcx" as *u8) 1258 x86ctx_store_result(c, i.result, "rdx" as *u8) 1259 return 0 1260 } 1261 if i.op == OP_DIV_U { 1262 x86_emit_xorq_rr(c.o, "rdx" as *u8, "rdx" as *u8) 1263 x86_emit_divq_r(c.o, "rcx" as *u8) 1264 x86ctx_store_result(c, i.result, "rax" as *u8) 1265 return 0 1266 } 1267 if i.op == OP_REM_U { 1268 x86_emit_xorq_rr(c.o, "rdx" as *u8, "rdx" as *u8) 1269 x86_emit_divq_r(c.o, "rcx" as *u8) 1270 x86ctx_store_result(c, i.result, "rdx" as *u8) 1271 return 0 1272 } 1273 if i.op == OP_AND { 1274 x86_emit_andq_rr(c.o, src1, dst) 1275 x86ctx_store_result(c, i.result, dst) 1276 return 0 1277 } 1278 if i.op == OP_OR { 1279 x86_emit_orq_rr(c.o, src1, dst) 1280 x86ctx_store_result(c, i.result, dst) 1281 return 0 1282 } 1283 if i.op == OP_XOR { 1284 x86_emit_xorq_rr(c.o, src1, dst) 1285 x86ctx_store_result(c, i.result, dst) 1286 return 0 1287 } 1288 // shifts: count must be in cl 1289 if i.op == OP_SHL { 1290 x86_emit_shlq_cl(c.o, "rax" as *u8) 1291 x86ctx_store_result(c, i.result, "rax" as *u8) 1292 return 0 1293 } 1294 if i.op == OP_SHR_S { 1295 x86_emit_sarq_cl(c.o, "rax" as *u8) 1296 x86ctx_store_result(c, i.result, "rax" as *u8) 1297 return 0 1298 } 1299 if i.op == OP_SHR_U { 1300 x86_emit_shrq_cl(c.o, "rax" as *u8) 1301 x86ctx_store_result(c, i.result, "rax" as *u8) 1302 return 0 1303 } 1304 // rotates: value in rax, count in cl (low byte of rcx) 1305 if i.op == OP_ROTL64 { 1306 x86_emit_rolq_cl(c.o, "rax" as *u8) 1307 x86ctx_store_result(c, i.result, "rax" as *u8) 1308 return 0 1309 } 1310 if i.op == OP_ROTR64 { 1311 x86_emit_rorq_cl(c.o, "rax" as *u8) 1312 x86ctx_store_result(c, i.result, "rax" as *u8) 1313 return 0 1314 } 1315 out_str(c.o, " # x86_64: binop opcode ") 1316 out_i64(c.o, i.op) 1317 out_str(c.o, " not yet wired\n") 1318 return 0 1319} 1320 1321// ===== unop dispatch (NEG / NOT / TRUNC / SEXT / ZEXT / BITCAST) == 1322 1323const OP_TRUNC: i64 = 17 1324const OP_SEXT: i64 = 18 1325const OP_ZEXT: i64 = 19 1326const OP_BITCAST: i64 = 26 1327 1328func x86ctx_emit_unop(c: *X86Ctx, i: *Instr) -> i64 { 1329 x86ctx_load_value_v(c, i.op0, "rax" as *u8) 1330 if i.op == OP_NEG { 1331 x86_emit_negq_r(c.o, "rax" as *u8) 1332 x86ctx_store_result(c, i.result, "rax" as *u8) 1333 return 0 1334 } 1335 if i.op == OP_NOT { 1336 x86_emit_notq_r(c.o, "rax" as *u8) 1337 x86ctx_store_result(c, i.result, "rax" as *u8) 1338 return 0 1339 } 1340 // Scalar bit unops: value already in rax. 1341 if i.op == OP_BSWAP64 { 1342 x86_emit_bswapq_rax(c.o) 1343 x86ctx_store_result(c, i.result, "rax" as *u8) 1344 return 0 1345 } 1346 if i.op == OP_POPCNT64 { 1347 x86_emit_popcntq_rax(c.o) 1348 x86ctx_store_result(c, i.result, "rax" as *u8) 1349 return 0 1350 } 1351 if i.op == OP_CLZ32 { 1352 x86_emit_lzcntl_eax(c.o) 1353 x86ctx_store_result(c, i.result, "rax" as *u8) 1354 return 0 1355 } 1356 if i.op == OP_CTZ32 { 1357 x86_emit_tzcntl_eax(c.o) 1358 x86ctx_store_result(c, i.result, "rax" as *u8) 1359 return 0 1360 } 1361 // __rdtsc(): read the cycle counter. rdtsc -> EDX:EAX (high:low); 1362 // combine into a full 64-bit value in rax. (op0 dummy already loaded 1363 // into rax above and harmlessly overwritten; rdx clobber is safe in the 1364 // stack-machine model -- operands are reloaded fresh per instruction.) 1365 if i.op == OP_RDTSC { 1366 out_str(c.o, " rdtsc\n") 1367 out_str(c.o, " shlq $32, %rdx\n") 1368 out_str(c.o, " orq %rdx, %rax\n") 1369 x86ctx_store_result(c, i.result, "rax" as *u8) 1370 return 0 1371 } 1372 // TRUNC / SEXT / ZEXT / BITCAST: i64-only stack-machine layer 1373 // treats all widths as i64, so these are no-ops on rax (the 1374 // sign-extend at load time already handled width). 1375 x86ctx_store_result(c, i.result, "rax" as *u8) 1376 return 0 1377} 1378 1379// ===== atomic dispatch =========================================== 1380// Address -> %r11; conservative-strong ordering (mo operand ignored, 1381// always correct on x86 TSO). C bootstrap parity (x86_64.c). 1382 1383func x86ctx_emit_atomic(c: *X86Ctx, i: *Instr) -> i64 { 1384 if i.op == OP_ATOMIC_LOAD_I64 { 1385 x86ctx_load_value_v(c, i.op0, "r11" as *u8) // addr 1386 x86_emit_load_qword(c.o, "r11" as *u8, 0, "rax" as *u8) 1387 x86ctx_store_result(c, i.result, "rax" as *u8) 1388 return 0 1389 } 1390 if i.op == OP_ATOMIC_STORE_I64 { 1391 x86ctx_load_value_v(c, i.op0, "r11" as *u8) // addr 1392 x86ctx_load_value_v(c, i.op1, "rax" as *u8) // val 1393 x86_emit_xchgq_rax_mem_r11(c.o) 1394 return 0 1395 } 1396 if i.op == OP_ATOMIC_CAS_I64 { 1397 x86ctx_load_value_v(c, i.op0, "r11" as *u8) // addr 1398 x86ctx_load_value_v(c, i.op1, "rax" as *u8) // expected 1399 x86ctx_load_value_v(c, i.op2, "rcx" as *u8) // new 1400 x86_emit_lock_cmpxchgq_rcx_mem_r11(c.o) 1401 x86_emit_sete_al(c.o) 1402 x86_emit_movzbq_rr(c.o, "al" as *u8, "rax" as *u8) 1403 x86ctx_store_result(c, i.result, "rax" as *u8) 1404 return 0 1405 } 1406 if i.op == OP_ATOMIC_FAA_I64 { 1407 x86ctx_load_value_v(c, i.op0, "r11" as *u8) // addr 1408 x86ctx_load_value_v(c, i.op1, "rax" as *u8) // delta (returns prior) 1409 x86_emit_lock_xaddq_rax_mem_r11(c.o) 1410 x86ctx_store_result(c, i.result, "rax" as *u8) 1411 return 0 1412 } 1413 if i.op == OP_ATOMIC_FENCE { 1414 x86_emit_mfence(c.o) 1415 return 0 1416 } 1417 out_str(c.o, " # x86_64: atomic opcode not wired\n") 1418 return 0 1419} 1420 1421// G3 __adc_acc(acc_ptr, lo, hi): add the 128-bit (hi:lo) into the 3-word 1422// accumulator at acc_ptr with carry, as ONE contiguous addq;adcq;adcq block so 1423// CF stays live across the chain (no IR boundary can inject a flag-clobber). 1424// Operand loads (flag-safe movq/movabsq/leaq) FIRST; the block uses only 1425// movq/addq/adcq with constant disp(%r11) (movq preserves CF). Scratch 1426// r11/rax/rcx/rdx are all OUTSIDE the home pool {r12-r15,rbx}, so no homed value 1427// can be corrupted (same discipline as UMULHI/atomic-CAS/thread_clone). 1428func x86ctx_emit_adc_acc(c: *X86Ctx, i: *Instr) -> i64 { 1429 x86ctx_load_value_v(c, i.op0, "r11" as *u8) // acc_ptr 1430 x86ctx_load_value_v(c, i.op1, "rax" as *u8) // lo 1431 x86ctx_load_value_v(c, i.op2, "rcx" as *u8) // hi 1432 out_str(c.o, " movq 0(%r11), %rdx\n") // rdx = acc0 1433 out_str(c.o, " addq %rax, %rdx\n") // acc0 += lo -> CF 1434 out_str(c.o, " movq %rdx, 0(%r11)\n") // store acc0 (CF preserved) 1435 out_str(c.o, " movq 8(%r11), %rdx\n") // rdx = acc1 (CF preserved) 1436 out_str(c.o, " adcq %rcx, %rdx\n") // acc1 += hi + CF -> CF 1437 out_str(c.o, " movq %rdx, 8(%r11)\n") // store acc1 1438 out_str(c.o, " movq 16(%r11), %rdx\n") // rdx = acc2 (CF preserved) 1439 out_str(c.o, " adcq $0, %rdx\n") // acc2 += CF 1440 out_str(c.o, " movq %rdx, 16(%r11)\n") // store acc2 1441 G1_RAX_SLOT = 0 - 1 // FIX-3: block clobbered rax (no store_result to clear it) 1442 return 0 1443} 1444 1445// G3 gate: __cpuid_ebx(leaf, subleaf) -> the x86 EBX feature register. cpuid 1446// clobbers eax/ebx/ecx/edx, and rbx is a G1 callee-saved HOME -> save/restore it 1447// around the instruction (pushq/popq, net rsp unchanged; no call between, so 1448// 16-byte alignment is irrelevant for cpuid). leaf in eax, subleaf in ecx; the 1449// 32-bit ebx output is zero-extended into rbx, captured into rax. 1450func x86ctx_emit_cpuid_ebx(c: *X86Ctx, i: *Instr) -> i64 { 1451 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // leaf -> eax 1452 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // subleaf -> ecx 1453 out_str(c.o, " pushq %rbx\n") // save the home (cpuid clobbers rbx) 1454 out_str(c.o, " .byte 15, 162\n") // cpuid (0F A2) -- emitted as bytes; the 1455 // sovereign nxasm has no `cpuid` mnemonic 1456 out_str(c.o, " movq %rbx, %rax\n") // capture EBX (zero-extended) 1457 out_str(c.o, " popq %rbx\n") // restore the home 1458 x86ctx_store_result(c, i.result, "rax" as *u8) 1459 G1_RAX_SLOT = 0 - 1 1460 return 0 1461} 1462 1463// __thread_clone(stack_top, entry_fn, ctx) -> child_tid. C bootstrap 1464// parity (x86_64.c emit_thread_clone). SYS_clone(56) + child 1465// trampoline. Unique label per emission = function name + result id. 1466func x86ctx_emit_clone_label(c: *X86Ctx, rid: i64) -> i64 { 1467 out_str(c.o, ".Lclone_parent_") 1468 let name: *u8 = c.f.name_start as *u8 1469 if name != (0 as *u8) { out_str(c.o, name) } 1470 out_char(c.o, 0x5F) // '_' 1471 out_i64(c.o, rid) 1472 return 0 1473} 1474 1475func x86ctx_emit_thread_clone(c: *X86Ctx, i: *Instr) -> i64 { 1476 x86ctx_load_value_v(c, i.op0, "r11" as *u8) // stack_top 1477 x86ctx_load_value_v(c, i.op1, "rax" as *u8) // entry_fn 1478 x86ctx_load_value_v(c, i.op2, "rcx" as *u8) // ctx 1479 out_str(c.o, " movq %rax, -16(%r11)\n") // [stk-16] = entry 1480 out_str(c.o, " movq %rcx, -8(%r11)\n") // [stk-8] = ctx 1481 out_str(c.o, " leaq -16(%r11), %rsi\n") // rsi = child stack 1482 x86_emit_movabsq(c.o, "rdi" as *u8, 0x50f00) // CLONE_VM|FS|FILES|SIGHAND|THREAD|SYSVSEM 1483 out_str(c.o, " xorq %rdx, %rdx\n") 1484 out_str(c.o, " xorq %r10, %r10\n") 1485 out_str(c.o, " xorq %r8, %r8\n") 1486 x86_emit_movabsq(c.o, "rax" as *u8, 56) // SYS_clone 1487 out_str(c.o, " syscall\n") 1488 out_str(c.o, " testq %rax, %rax\n") 1489 // `jne` (== `jnz`, both 0F 85) -- the rest of the compiler emits `jne`, and nxasm's jcc table only 1490 // knows `jne`; the trampoline previously emitted the `jnz` synonym, which nxasm rejected ("cannot 1491 // encode: jnz"), blocking sovereign-lane threading. Consistent mnemonic = assembles on both lanes. 1492 out_str(c.o, " jne ") 1493 x86ctx_emit_clone_label(c, i.result) 1494 out_char(c.o, 0x0A) 1495 // child: rsp = stack_top-16; pop entry, ctx; call entry(ctx) 1496 out_str(c.o, " movq 0(%rsp), %rax\n") // entry 1497 out_str(c.o, " movq 8(%rsp), %rdi\n") // ctx -> arg0 1498 // `call *%rax` (indirect) -- nxasm's `call` dispatch handles the K_IND operand; the `callq` suffix 1499 // form was rejected ("cannot encode: callq *%rax"). GNU as accepts both, so this assembles on both lanes. 1500 out_str(c.o, " call *%rax\n") 1501 out_str(c.o, " movq %rax, %rdi\n") // entry returned -> exit thread 1502 x86_emit_movabsq(c.o, "rax" as *u8, 60) // SYS_exit 1503 out_str(c.o, " syscall\n") 1504 x86ctx_emit_clone_label(c, i.result) 1505 out_str(c.o, ":\n") 1506 x86ctx_store_result(c, i.result, "rax" as *u8) 1507 return 0 1508} 1509 1510// ===== cmp dispatch ============================================== 1511 1512// LN16 (2026-08-25) -- IEEE-754 binary64 ORDERING KEY, computed in place in `reg`. 1513// 1514// THE DEFECT IT REMOVES. A double rides in an i64 register and x86ctx_emit_cmp 1515// lowered EVERY compare to a signed integer cmpq on the raw bits. binary64 is 1516// SIGN-MAGNITUDE, not two's complement, so among two NEGATIVE doubles the 1517// integer order is the exact REVERSE of the numeric order: bits(-1.0) = 1518// 0xBFF0000000000000 is arithmetically LESS than bits(-2.0) = 0xC000000000000000, 1519// so `0.0 - 1.0 < 0.0 - 2.0` evaluated TRUE. Silent, and invisible to every 1520// positive-only test -- which is why nx_f64_adversary, whose six checks are all 1521// non-negative, ran green over it since 2026-07-16. 1522// 1523// THE TRANSFORM. m = x >> 63 arithmetic (all ones iff the sign bit is set), then 1524// key = (x XOR (m >>> 1)) - m 1525// For a non-negative double m is 0 and the key IS the bit pattern, which is 1526// already monotone. For a negative double m is -1, so the 63 magnitude bits are 1527// inverted (larger magnitude -> smaller key) while the sign bit stays set (every 1528// negative key stays below every non-negative one), and the final `- m` adds one, 1529// which is what maps -0.0 and +0.0 onto the SAME key. Signed zero comparing equal 1530// is the IEEE answer and is a second defect this fixes: the raw compare called 1531// them different. After the transform the EXISTING signed cmpq and the existing 1532// signed condition codes are correct, so nothing downstream changes. 1533// 1534// WHY A KEY AND NOT ucomisd. The SSE compare needs unsigned and parity condition 1535// codes that neither nx_x86_64's setcc set nor the sovereign assembler carries; 1536// adding them means editing nxasm, a binary every concurrent build lane forks. 1537// Every instruction below is one this backend already emits (the sarq/shrq pair 1538// is the same idiom as the signed-divide bias dance at OP_DIV_S), so the fix 1539// needs no assembler change and no new encoding. 1540// 1541// DECLARED IMPRECISION. A key is a TOTAL order and therefore cannot express IEEE 1542// unorderedness: a comparison against NaN answers as if NaN were an ordered 1543// value, where IEEE says every predicate except `not equal` is false. That is a 1544// strictly smaller residual than what it replaces (which was wrong for every 1545// negative operand) and it is NAMED here, in nx_f64_gate's verdict note and on 1546// the /compare/lang row rather than left for the next reader to find. Ordered-NaN 1547// costs nothing under the estate's integer-deterministic doctrine, where NaN 1548// cannot arise from the sanctioned paths at all. 1549// 1550// r10/r11 are the declared scratch registers (never in the home pool, never live 1551// across this sequence); `reg` is always rax or rcx here, both caller-saved, and 1552// the caller invalidates G1_RAX_SLOT because rax no longer holds its slot value. 1553// Cost: six integer instructions per f64 operand, on f64 compares only. The 1554// integer compare path is not touched, which nx_f64_gate proves by byte-comparing 1555// the assembly of an integer program against a reference compiler. 1556func x86ctx_emit_f64_ordkey(c: *X86Ctx, reg: *u8) -> i64 { 1557 out_str(c.o, " movq %") 1558 out_str(c.o, reg) 1559 out_str(c.o, ", %r10\n") 1560 out_str(c.o, " sarq $63, %r10\n") // r10 = m: all ones iff negative 1561 out_str(c.o, " movq %r10, %r11\n") 1562 out_str(c.o, " shrq $1, %r11\n") // r11 = 0x7fffffffffffffff iff negative 1563 out_str(c.o, " xorq %r11, %") 1564 out_str(c.o, reg) 1565 out_char(c.o, 0x0A) // invert the magnitude bits 1566 out_str(c.o, " subq %r10, %") 1567 out_str(c.o, reg) 1568 out_char(c.o, 0x0A) // + 1 iff negative: ties -0.0 to +0.0 1569 return 0 1570} 1571 1572// LN38 (2026-09-03): IEEE-754 UNORDERED compares. The ordering-key transform below is a TOTAL order on the 1573// bit pattern, so under it a NaN equalled itself and a sign-set NaN (the x86 default from 0.0/0.0) sorted 1574// BELOW every number: `n != n` read FALSE and `n < 1.0` read TRUE (found by the CM4 quadrature gate NaN 1575// neg-control). IEEE says every ordered predicate on a NaN operand is FALSE and only != is TRUE. The 1576// assembler has no ucomisd/jp, so the guard is integer-only: NaN iff exponent==2047 (bits<<1>>53) AND 1577// mantissa!=0 (bits<<12). When either operand is NaN the ordkey compare is skipped and FLAGS are forced to 1578// the ordered state that gives the IEEE answer for THIS predicate: a GREATER (1 vs 0) makes < <= == false 1579// and != true; a LESS (0 vs 1) makes > >= false. The setcc/branch-fusion tail and G12 inversion read the 1580// same flags unchanged. Labels are unique per emission: function name + result id. 1581func x86ctx_cmp_f64_operands(c: *X86Ctx, i: *Instr) -> i64 { 1582 x86ctx_load_value_v(c, i.op0, "rax" as *u8) 1583 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) 1584 return 0 1585} 1586func x86ctx_emit_f64_nan_label(c: *X86Ctx, i: *Instr, tag: *u8) -> i64 { 1587 out_str(c.o, ".LN38") 1588 out_str(c.o, tag) 1589 out_char(c.o, 0x5F) 1590 let name: *u8 = c.f.name_start as *u8 1591 if name != (0 as *u8) { out_str(c.o, name) } 1592 out_char(c.o, 0x5F) 1593 out_i64(c.o, i.result) 1594 return 0 1595} 1596// NaN test on one operand register: jumps to the shared nan label iff REG holds a NaN, else falls through. 1597func x86ctx_emit_f64_nan_guard(c: *X86Ctx, i: *Instr, reg: *u8, oktag: *u8) -> i64 { 1598 out_str(c.o, " movq %") 1599 out_str(c.o, reg) 1600 out_str(c.o, ", %r10\n") 1601 out_str(c.o, " shlq $1, %r10\n") 1602 out_str(c.o, " shrq $53, %r10\n") 1603 out_str(c.o, " cmpq $2047, %r10\n") 1604 out_str(c.o, " jne ") 1605 x86ctx_emit_f64_nan_label(c, i, oktag) 1606 out_char(c.o, 0x0A) 1607 out_str(c.o, " movq %") 1608 out_str(c.o, reg) 1609 out_str(c.o, ", %r11\n") 1610 out_str(c.o, " shlq $12, %r11\n") 1611 out_str(c.o, " testq %r11, %r11\n") 1612 out_str(c.o, " jne ") 1613 x86ctx_emit_f64_nan_label(c, i, "nan" as *u8) 1614 out_char(c.o, 0x0A) 1615 x86ctx_emit_f64_nan_label(c, i, oktag) 1616 out_str(c.o, ":\n") 1617 return 0 1618} 1619func x86ctx_emit_f64_cmp(c: *X86Ctx, i: *Instr, cc: i64) -> i64 { 1620 x86ctx_cmp_f64_operands(c, i) 1621 x86ctx_emit_f64_nan_guard(c, i, "rax" as *u8, "oka" as *u8) 1622 x86ctx_emit_f64_nan_guard(c, i, "rcx" as *u8, "okb" as *u8) 1623 x86ctx_emit_f64_ordkey(c, "rax" as *u8) 1624 x86ctx_emit_f64_ordkey(c, "rcx" as *u8) 1625 x86_emit_cmpq_rr(c.o, "rcx" as *u8, "rax" as *u8) 1626 out_str(c.o, " jmp ") 1627 x86ctx_emit_f64_nan_label(c, i, "done" as *u8) 1628 out_char(c.o, 0x0A) 1629 x86ctx_emit_f64_nan_label(c, i, "nan" as *u8) 1630 out_str(c.o, ":\n") 1631 var force_less: i64 = 0 1632 if cc == NX_X64_CC_GT_S { force_less = 1 } 1633 if cc == NX_X64_CC_GE_S { force_less = 1 } 1634 if force_less == 1 { 1635 out_str(c.o, " movq $0, %r10\n") 1636 out_str(c.o, " cmpq $1, %r10\n") 1637 } 1638 if force_less == 0 { 1639 out_str(c.o, " movq $1, %r10\n") 1640 out_str(c.o, " cmpq $0, %r10\n") 1641 } 1642 x86ctx_emit_f64_nan_label(c, i, "done" as *u8) 1643 out_str(c.o, ":\n") 1644 return 0 1645} 1646 1647// 1 iff either operand of this compare is a binary64 value. The RESULT type of a 1648// compare is the boolean, so the precision has to come from the OPERANDS -- the 1649// same reason x86ctx_emit_float reads op0's type for OP_FCAST_F_TO_I. TY_F32 is 1650// deliberately NOT included: a f32 rides in the LOW 32 bits of the carrier, so its 1651// sign bit is 31 and this 64-bit key would be wrong for it. NishiLang has no f32 1652// type a program can declare (nx_parse: "a float rides as the low 32 bits of an 1653// i64 carrier"), so the reachable surface is the __f32_* intrinsic family only; 1654// naming that boundary here beats implying a coverage this has not earned. 1655func x86ctx_cmp_is_f64(c: *X86Ctx, i: *Instr) -> i64 { 1656 if X86_NEGCTL_F64_CMP_RAW == 1 { return 0 } 1657 let a: *Value = x86ctx_value_at(c.f, i.op0) 1658 if a.ty != (0 as *Type) { if a.ty.kind == TY_F64 { return 1 } } 1659 let b: *Value = x86ctx_value_at(c.f, i.op1) 1660 if b.ty != (0 as *Type) { if b.ty.kind == TY_F64 { return 1 } } 1661 return 0 1662} 1663 1664func x86ctx_cmp_to_cc(op: i64) -> i64 { 1665 if op == OP_EQ { return NX_X64_CC_EQ } 1666 if op == OP_NE { return NX_X64_CC_NE } 1667 if op == OP_LT_S { return NX_X64_CC_LT_S } 1668 if op == OP_LE_S { return NX_X64_CC_LE_S } 1669 if op == OP_GT_S { return NX_X64_CC_GT_S } 1670 if op == OP_GE_S { return NX_X64_CC_GE_S } 1671 return 0 - 1 1672} 1673 1674func x86ctx_emit_cmp(c: *X86Ctx, i: *Instr) -> i64 { 1675 let cc: i64 = x86ctx_cmp_to_cc(i.op) 1676 if cc < 0 { 1677 out_str(c.o, " # x86_64: cmp opcode unknown\n") 1678 x86_emit_xorq_rr(c.o, "rax" as *u8, "rax" as *u8) 1679 x86ctx_store_result(c, i.result, "rax" as *u8) 1680 return 0 1681 } 1682 // G14 (2026-07-16): FLAG REUSE -- `cmp X, 0` for EQ/NE where X's producer 1683 // is the textually PREVIOUS instruction and an ALU op that sets ZF per its 1684 // result (ADD/SUB/AND/OR/XOR; MUL/shift flags are unreliable) is REDUNDANT 1685 // -- ZF already reflects X, and only movs (flag-neutral) are emitted after 1686 // the ALU. Skip BOTH the operand materialization and the cmpq; the fused 1687 // jcc / setcc tail below reads the live FLAGS. STRICTLY EQ/NE: signed 1688 // LT/GE read SF^OF, and the ALU's OF differs from cmp-vs-0's (witness: 1689 // (INT_MIN - 1) < 0 -- adversary T7 + the negctl prove the restriction). 1690 // A sib-dead producer emitted NOTHING (folded) -> its flags never set -> 1691 // excluded. 1692 var g14: i64 = 0 1693 if i.op == OP_EQ { g14 = 1 } 1694 if i.op == OP_NE { g14 = 1 } 1695 if X86_NEGCTL_G14_SIGNED == 1 { if i.op == OP_LT_S { g14 = 1 } } 1696 if g14 == 1 { 1697 g14 = 0 1698 let g14z: *Value = x86ctx_value_at(c.f, i.op1) 1699 if g14z.kind == VK_CONST_INT { if g14z.const_int == 0 { 1700 let g14p: *Instr = i.prev 1701 if g14p != (0 as *Instr) { 1702 if g14p.result == i.op0 { 1703 var zok: i64 = 0 1704 if g14p.op == OP_ADD { zok = 1 } 1705 if g14p.op == OP_SUB { zok = 1 } 1706 if g14p.op == OP_AND { zok = 1 } 1707 if g14p.op == OP_OR { zok = 1 } 1708 if g14p.op == OP_XOR { zok = 1 } 1709 if zok == 1 { 1710 if g14p.result >= 0 { if g14p.result < c.f.n_values { 1711 if c.sib_dead[g14p.result] == 1 { zok = 0 } 1712 } } 1713 } 1714 if zok == 1 { g14 = 1 } 1715 } 1716 } 1717 } } 1718 } 1719 // ---- LN16 (2026-08-25): f64 ORDERING ----------------------------------- 1720 // A binary64 compare cannot use the raw signed integer cmpq below -- see 1721 // x86ctx_emit_f64_ordkey for why, and for the two defects it removes. Both 1722 // operands are materialized into CALLER-SAVED scratch first: a homed or 1723 // forwarded operand may only be READ in place, and the key transform REWRITES 1724 // its register, so transforming r12 directly would corrupt that value for 1725 // every later consumer. Then map each to its ordering key, compare, and tell 1726 // the generic path that the flags are already live. The setcc / branch-fusion 1727 // tail below is untouched and needs no change: after the key the SIGNED 1728 // condition codes are exactly the right ones. 1729 if x86ctx_cmp_is_f64(c, i) == 1 { 1730 x86ctx_emit_f64_cmp(c, i, cc) 1731 G1_RAX_SLOT = 0 - 1 1732 g14 = 1 1733 } 1734 if g14 == 0 { 1735 // G10: a forwarded or HOMED op0 is compared IN its register directly (cmp 1736 // writes only FLAGS, never its operands) -- kills the movq %home,%rax. 1737 // Unforwarded/unhomed op0 keeps the rax materialization (byte-identical). 1738 var g6left: *u8 = x86_use_regalloc(c, i.op0) // LN7: in-place read 1739 if g6left == (0 as *u8) { 1740 x86ctx_load_value_v(c, i.op0, "rax" as *u8) 1741 g6left = "rax" as *u8 1742 } 1743 // G5a: imm32 op1 -> cmpq $imm,%left; homed/forwarded op1 -> compare the 1744 // home reg directly; everything else keeps the rcx path. 1745 var g6imm: i64 = 0 1746 let g6v1: *Value = x86ctx_value_at(c.f, i.op1) 1747 if g6v1.kind == VK_CONST_INT { 1748 if g6v1.const_int <= 2147483647 { 1749 if g6v1.const_int >= (0 - 2147483648) { g6imm = 1 } 1750 } 1751 } 1752 if g6imm == 1 { 1753 out_str(c.o, " cmpq $") 1754 out_i64(c.o, g6v1.const_int) 1755 out_str(c.o, ", %") 1756 out_str(c.o, g6left) 1757 out_char(c.o, 0x0A) 1758 } 1759 if g6imm == 0 { 1760 var g6src: *u8 = x86_use_regalloc(c, i.op1) // LN7: in-place read 1761 if g6src == (0 as *u8) { 1762 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) 1763 g6src = "rcx" as *u8 1764 } 1765 x86_emit_cmpq_rr(c.o, g6src, g6left) 1766 } 1767 } 1768 // G6: fuse into an immediately-following BR_COND on this result -- the 1769 // branch consumes the LIVE FLAGS; setcc/movzbq/store are skipped. The 1770 // elide flag already encodes single-use-in-next-instruction, so the 1771 // boolean cannot be observed anywhere else. 1772 if c.elide[i.result] == 1 { 1773 if i.next != (0 as *Instr) { 1774 if i.next.op == OP_BR_COND { 1775 if i.next.op0 == i.result { 1776 G1_PENDING_CC = cc 1777 G1_PENDING_VAL = i.result 1778 return 0 1779 } 1780 } 1781 } 1782 } 1783 x86_emit_setcc(c.o, cc, "al" as *u8) 1784 x86_emit_movzbq_rr(c.o, "al" as *u8, "rax" as *u8) 1785 x86ctx_store_result(c, i.result, "rax" as *u8) 1786 return 0 1787} 1788 1789// ===== branch emission ============================================ 1790 1791func x86ctx_emit_bb_label(c: *X86Ctx, bb_id: i64) -> i64 { 1792 out_str(c.o, ".L") 1793 let name: *u8 = c.f.name_start as *u8 1794 if name != (0 as *u8) { out_str(c.o, name) } 1795 out_str(c.o, "_bb") 1796 out_i64(c.o, bb_id) 1797 return 0 1798} 1799 1800func x86ctx_emit_jmp_to_bb(c: *X86Ctx, bb_id: i64) -> i64 { 1801 out_str(c.o, " jmp ") 1802 x86ctx_emit_bb_label(c, bb_id) 1803 out_char(c.o, 0x0A) 1804 return 0 1805} 1806 1807func x86ctx_emit_jcc_to_bb(c: *X86Ctx, cc: i64, bb_id: i64) -> i64 { 1808 out_str(c.o, " j") 1809 out_str(c.o, x86_cc_suffix(cc)) 1810 out_char(c.o, 0x20) 1811 x86ctx_emit_bb_label(c, bb_id) 1812 out_char(c.o, 0x0A) 1813 return 0 1814} 1815 1816// G12: condition-code inversion (jcc T; jmp F -> j!cc F when T falls through). 1817// Pairs: EQ<->NE, LT_S<->GE_S, LE_S<->GT_S, LT_U<->GE_U, LE_U<->GT_U. 1818func x86ctx_cc_invert(cc: i64) -> i64 { 1819 if cc == NX_X64_CC_EQ { return NX_X64_CC_NE } 1820 if cc == NX_X64_CC_NE { return NX_X64_CC_EQ } 1821 if cc == NX_X64_CC_LT_S { return NX_X64_CC_GE_S } 1822 if cc == NX_X64_CC_LE_S { return NX_X64_CC_GT_S } 1823 if cc == NX_X64_CC_GT_S { return NX_X64_CC_LE_S } 1824 if cc == NX_X64_CC_GE_S { return NX_X64_CC_LT_S } 1825 if cc == NX_X64_CC_LT_U { return NX_X64_CC_GE_U } 1826 if cc == NX_X64_CC_LE_U { return NX_X64_CC_GT_U } 1827 if cc == NX_X64_CC_GT_U { return NX_X64_CC_LE_U } 1828 if cc == NX_X64_CC_GE_U { return NX_X64_CC_LT_U } 1829 if cc == NX_X64_CC_O { return NX_X64_CC_NO } 1830 if cc == NX_X64_CC_NO { return NX_X64_CC_O } 1831 return 0 - 1 1832} 1833 1834// G12: emit jcc T / jmp F with FALL-THROUGH ELISION against c.next_bb -- 1835// the taken `jmp` per iteration in every branchy loop was 2-3 of the loop's 1836// instructions (gap tool). F==next -> jcc T only; T==next -> j!cc F only. 1837func x86ctx_emit_condjump(c: *X86Ctx, cc: i64, t: i64, fjb: i64) -> i64 { 1838 if fjb == c.next_bb { 1839 x86ctx_emit_jcc_to_bb(c, cc, t) 1840 return 0 1841 } 1842 if t == c.next_bb { 1843 let icc: i64 = x86ctx_cc_invert(cc) 1844 if icc >= 0 { 1845 x86ctx_emit_jcc_to_bb(c, icc, fjb) 1846 return 0 1847 } 1848 } 1849 x86ctx_emit_jcc_to_bb(c, cc, t) 1850 x86ctx_emit_jmp_to_bb(c, fjb) 1851 return 0 1852} 1853 1854// G15: block lookup by id (ids may differ from storage order post-opt). 1855func x86ctx_br_target_block(c: *X86Ctx, id: i64) -> *BasicBlock { 1856 var bi: i64 = 0 1857 while bi < c.f.n_blocks { 1858 let b: *BasicBlock = x86ctx_block_at(c.f, bi) 1859 if b.id == id { return b } 1860 bi = bi + 1 1861 } 1862 return 0 as *BasicBlock 1863} 1864 1865func x86ctx_emit_br(c: *X86Ctx, i: *Instr) -> i64 { 1866 // G12: unconditional jump to the very next emitted block = fall through. 1867 if i.op0 == c.next_bb { return 0 } 1868 // G15 peephole disabled 2026-08-01 see debt 1785569152 1869 x86ctx_emit_jmp_to_bb(c, i.op0) 1870 return 0 1871} 1872 1873func x86ctx_emit_br_cond(c: *X86Ctx, i: *Instr) -> i64 { 1874 // G6: consume a pending fused compare -- branch on the live flags. The 1875 // value-id match is the belt: any mismatch clears the pending state and 1876 // falls through to the load+test path (never a stale-flags branch). 1877 if G1_PENDING_CC >= 0 { 1878 if G1_PENDING_VAL == i.op0 { 1879 let g6cc: i64 = G1_PENDING_CC 1880 G1_PENDING_CC = 0 - 1 1881 G1_PENDING_VAL = 0 - 1 1882 x86ctx_emit_condjump(c, g6cc, i.op1, i.op2) 1883 return 0 1884 } 1885 G1_PENDING_CC = 0 - 1 1886 G1_PENDING_VAL = 0 - 1 1887 } 1888 x86ctx_load_value_v(c, i.op0, "rax" as *u8) 1889 x86_emit_testq_rr(c.o, "rax" as *u8, "rax" as *u8) 1890 x86ctx_emit_condjump(c, NX_X64_CC_NE, i.op1, i.op2) 1891 return 0 1892} 1893 1894// ===== load / store / GEP ======================================== 1895 1896func x86ctx_type_size(t: *Type) -> i64 { 1897 if t == (0 as *Type) { return 8 } 1898 if t.kind == TY_VOID { return 8 } 1899 if t.size <= 0 { return 8 } 1900 return t.size 1901} 1902 1903func x86ctx_type_signed(t: *Type) -> i64 { 1904 // Default to UNSIGNED (0) when type is missing: a missing type 1905 // on a sub-word load is most often a `*u8`/`*u16` dereference 1906 // emitted without explicit type tagging. Sign-extending those 1907 // corrupts values >= 0x80 to negative i64. Witness: x509_parse 1908 // sign-extending the [0] EXPLICIT version tag 0xA0 -> -96 on 1909 // live Mozilla bundle (diagnosed 2026-05-20). Unsigned default 1910 // is strictly safer because 0..127 round-trip either way and 1911 // 128..255 only preserve via zero-extend. 1912 if t == (0 as *Type) { return 0 } 1913 if t.kind == TY_I8 { return 1 } 1914 if t.kind == TY_I16 { return 1 } 1915 if t.kind == TY_I32 { return 1 } 1916 if t.kind == TY_I64 { return 1 } 1917 return 0 1918} 1919 1920// null-safe sext flag: 1 only for types minted from a SIGNED subword annotation (i8/i16/i32 via 1921// alloc_type_s). u8/u16/u32 and untyped loads stay 0 -> zero-extend (protects the x509 0xA0 witness). 1922func x86ctx_type_sext(t: *Type) -> i64 { 1923 if t == (0 as *Type) { return 0 } 1924 return t.sext 1925} 1926 1927func x86ctx_emit_load(c: *X86Ctx, i: *Instr) -> i64 { 1928 // G10: a home-forwarded load emits NOTHING -- its single consumer reads the 1929 // source alloca's home register directly (fwd scan proved no intervening 1930 // store/call; rax and FLAGS untouched, so G1/G6 state stays valid). 1931 if i.result >= 0 { if i.result < c.f.n_values { 1932 if c.fwd_home[i.result] >= 0 { return 0 } 1933 } } 1934 let sz: i64 = x86ctx_type_size(i.ty) 1935 // 2026-07-10 debt fix: subword loads SIGN-extend when the pointee was declared signed (sext=1, 1936 // set by the parser via alloc_type_s for i8/i16/i32) and ZERO-extend otherwise. Previously ALL 1937 // subword loads zero-extended because u8/i8 were indistinguishable at the IR level (no unsigned 1938 // kinds); the sext bit now carries the distinction, fixing *i8/*i16/*i32 (witness: SIMD hsum 1939 // ~4e9 garbage on negative int32 lanes) while leaving *u8 byte code (x509 0xA0 -> 160) untouched. 1940 // This also ALIGNS x86 with the RV64 backend, which already sign-extended signed loads. 1941 let sx: i64 = x86ctx_type_sext(i.ty) 1942 // STAGE 5 (2026-07-15): target the RESULT's home register (rax if unhomed) so 1943 // a homed 8-byte load lands directly in its reg -- no spill+reload. dst==rax 1944 // for every unhomed value and every subword load (only 8-byte loads are 1945 // homeable), so this is byte-identical wherever homing is off. 1946 let dst: *u8 = x86ctx_result_reg(c, i.result) 1947 1948 // G8: scaled-addressing fold. LOAD(GEP(base, SHL/MUL(idx,2^k))) single-use 1949 // -> movq (%base,%idx,scale),%dst (the GEP+SHL are elided). 8-byte only. 1950 if sz == 8 { 1951 let lout: *i64 = x86ctx_sib_scratch() 1952 if x86ctx_sib_probe(c, i.op0, lout) == 1 { 1953 let lb: i64 = lout[0] 1954 let li: i64 = lout[1] 1955 let lsc: i64 = lout[2] 1956 if lout[5] == 1 { x86ctx_load_value(c, lb, "rcx" as *u8) } // GEP base = address 1957 if lout[5] == 0 { x86ctx_load_value_v(c, lb, "rcx" as *u8) } // ADD base = pointer value 1958 // G16: a homed/forwarded INDEX is used in the SIB directly (homes 1959 // are never rcx/rax -> no clash with base or dst-as-rax). 1960 var lidx: *u8 = x86_use_regalloc(c, li) // LN7: in-place SIB index 1961 if lidx == (0 as *u8) { 1962 x86ctx_load_value_v(c, li, "rax" as *u8) 1963 lidx = "rax" as *u8 1964 } 1965 out_str(c.o, " movq ") 1966 x86ctx_emit_sib_mem(c, "rcx" as *u8, lidx, lsc) 1967 out_str(c.o, ", %") 1968 out_str(c.o, dst) 1969 out_char(c.o, 0x0A) 1970 x86ctx_store_result(c, i.result, dst) 1971 return 0 1972 } 1973 } 1974 1975 // Peephole 2026-05-20: when the pointer operand is an alloca-result, 1976 // fold `leaq disp(%rbp), %rcx ; mov(z)? (%rcx), %rax` into a direct 1977 // `mov(z)? disp(%rbp), %rax`. No semantic change; eliminates one 1978 // instruction per stack-variable load -- the dominant pattern in 1979 // every inner loop measured by paired Stabilizer. 1980 if i.op0 >= 0 && i.op0 < c.f.n_values && c.alloca_off[i.op0] >= 0 { 1981 // G2: homed alloca read = one register move (the home IS the 1982 // storage). Eligibility guarantees sz==8 at every access of a 1983 // homed alloca, so the subword paths below cannot be reached. 1984 if c.alloca_home[i.op0] >= 0 { 1985 let g2h: *u8 = x86_home_reg_name(c.alloca_home[i.op0]) 1986 x86_emit_movq_reg_reg(c.o, g2h, dst) 1987 x86ctx_store_result(c, i.result, dst) 1988 return 0 1989 } 1990 let abp_off: i64 = x86ctx_value_base(c) + c.alloca_off[i.op0] 1991 // STAGE 5 ROOT CAUSE (2026-08-14): these MUST write `dst`, not rax. 1992 // x86ctx_result_reg returns the HOME register for a homed value, and 1993 // store_result(result, dst) emits nothing when reg already IS the home -- 1994 // so a subword load that wrote rax while dst was a home register left that 1995 // home NEVER WRITTEN and the result garbage. That is the "miscompiles the 1996 // compiler on some pattern the KATs don't hit" that reverted STAGE 5: the 1997 // compiler byte-walks source through *u8/*i32 loads constantly, while the 1998 // matmul KATs are all 8-byte and cannot reach these branches. 1999 // Inert until loads are homeable (dst == rax for every unhomed value). 2000 if sz == 1 { 2001 if sx == 1 { x86_emit_load_byte_signed(c.o, "rbp" as *u8, abp_off, dst) } 2002 else { x86_emit_load_byte_unsigned(c.o, "rbp" as *u8, abp_off, dst) } 2003 } 2004 if sz == 2 { 2005 if sx == 1 { x86_emit_load_word_signed(c.o, "rbp" as *u8, abp_off, dst) } 2006 else { x86_emit_load_word_unsigned(c.o, "rbp" as *u8, abp_off, dst) } 2007 } 2008 if sz == 4 { 2009 if sx == 1 { x86_emit_load_dword_signed(c.o, "rbp" as *u8, abp_off, dst) } 2010 else { x86_emit_load_dword_unsigned(c.o, "rbp" as *u8, abp_off, dst) } 2011 } 2012 if sz == 8 { 2013 x86_emit_load_qword(c.o, "rbp" as *u8, abp_off, dst) 2014 } 2015 x86ctx_store_result(c, i.result, dst) 2016 return 0 2017 } 2018 2019 x86ctx_load_value(c, i.op0, "rcx" as *u8) 2020 // Same STAGE 5 root cause as the alloca block above: write `dst`, never rax. 2021 if sz == 1 { 2022 // sext=1 (declared *i8) sign-extends via movsbq; else (*u8/untyped) zero-extends via movzbq -- 2023 // the x509 0xA0-must-stay-160 witness lives on this default. 2024 if sx == 1 { x86_emit_load_byte_signed(c.o, "rcx" as *u8, 0, dst) } 2025 else { x86_emit_load_byte_unsigned(c.o, "rcx" as *u8, 0, dst) } 2026 } 2027 if sz == 2 { 2028 if sx == 1 { x86_emit_load_word_signed(c.o, "rcx" as *u8, 0, dst) } 2029 else { x86_emit_load_word_unsigned(c.o, "rcx" as *u8, 0, dst) } 2030 } 2031 if sz == 4 { 2032 if sx == 1 { x86_emit_load_dword_signed(c.o, "rcx" as *u8, 0, dst) } 2033 else { x86_emit_load_dword_unsigned(c.o, "rcx" as *u8, 0, dst) } 2034 } 2035 if sz == 8 { 2036 x86_emit_load_qword(c.o, "rcx" as *u8, 0, dst) 2037 } 2038 x86ctx_store_result(c, i.result, dst) 2039 return 0 2040} 2041 2042func x86ctx_emit_store(c: *X86Ctx, i: *Instr) -> i64 { 2043 let sz: i64 = x86ctx_type_size(i.ty) 2044 2045 // G8: scaled-addressing fold, symmetric to emit_load. STORE to 2046 // GEP(base, SHL/MUL(idx,2^k)) single-use -> movq %rax,(%base,%idx,scale). 2047 if sz == 8 { 2048 let sout: *i64 = x86ctx_sib_scratch() 2049 if x86ctx_sib_probe(c, i.op0, sout) == 1 { 2050 let sb: i64 = sout[0] 2051 let si: i64 = sout[1] 2052 let ssc: i64 = sout[2] 2053 if sout[5] == 1 { x86ctx_load_value(c, sb, "rcx" as *u8) } // GEP base = address 2054 if sout[5] == 0 { x86ctx_load_value_v(c, sb, "rcx" as *u8) } // ADD base = pointer value 2055 // G16: homed/forwarded INDEX and VALUE go into the SIB store 2056 // directly (homes never collide with rcx/rdx/rax). 2057 var sidx: *u8 = x86_use_regalloc(c, si) // LN7: in-place SIB index 2058 if sidx == (0 as *u8) { 2059 x86ctx_load_value_v(c, si, "rdx" as *u8) 2060 sidx = "rdx" as *u8 2061 } 2062 var sval: *u8 = x86_use_regalloc(c, i.op1) // LN7: in-place store value 2063 if sval == (0 as *u8) { 2064 if i.op1 >= 0 { if i.op1 < c.f.n_values { 2065 if c.alloca_home[i.op1] >= 0 { sval = x86_home_reg_name(c.alloca_home[i.op1]) } 2066 } } 2067 } 2068 if sval == (0 as *u8) { 2069 x86ctx_load_value_v(c, i.op1, "rax" as *u8) 2070 sval = "rax" as *u8 2071 } 2072 out_str(c.o, " movq %") 2073 out_str(c.o, sval) 2074 out_str(c.o, ", ") 2075 x86ctx_emit_sib_mem(c, "rcx" as *u8, sidx, ssc) 2076 out_char(c.o, 0x0A) 2077 return 0 2078 } 2079 } 2080 2081 // Peephole 2026-05-20 (symmetric to emit_load): when the destination 2082 // pointer is an alloca-result, fold `leaq disp(%rbp), %rcx ; movq 2083 // %rax, (%rcx)` into direct `movq %rax, disp(%rbp)`. Eliminates 2084 // one instruction per stack-variable store. 2085 if i.op0 >= 0 && i.op0 < c.f.n_values && c.alloca_off[i.op0] >= 0 { 2086 // G2: homed alloca write = load the value straight into the home. 2087 // Direct forms (const/global/func-addr/homed-value/homed-alloca) 2088 // materialise INTO the home in one instruction with rax untouched 2089 // (G1_RAX_SLOT stays valid). Everything else rides the usual rax 2090 // path (G1 store->load forwarding applies) + one reg move. 2091 if c.alloca_home[i.op0] >= 0 { 2092 // G11: the chain's terminal store-back is a no-op -- the final 2093 // chain value was computed IN PLACE in this very home register. 2094 if i.op1 >= 0 { if i.op1 < c.f.n_values { 2095 if c.chain_home[i.op1] >= 0 { 2096 if c.chain_home[i.op1] == c.alloca_home[i.op0] { return 0 } 2097 } 2098 } } 2099 let g2h: *u8 = x86_home_reg_name(c.alloca_home[i.op0]) 2100 let g2v: *Value = x86ctx_value_at(c.f, i.op1) 2101 var g2direct: i64 = 0 2102 if g2v.kind == VK_CONST_INT { g2direct = 1 } 2103 if g2v.kind == VK_GLOBAL { g2direct = 1 } 2104 if g2v.kind == VK_FUNC_ADDR { g2direct = 1 } 2105 // G10/LN7: a homed or forwarded op1 materializes straight into the 2106 // destination home (movq %hsrc,%hdst -- one instruction, rax untouched). 2107 if x86_use_regalloc(c, i.op1) != (0 as *u8) { g2direct = 1 } 2108 if i.op1 >= 0 { if i.op1 < c.f.n_values { 2109 if c.alloca_home[i.op1] >= 0 { g2direct = 1 } 2110 } } 2111 if g2direct == 1 { 2112 x86ctx_load_value_v(c, i.op1, g2h) 2113 return 0 2114 } 2115 x86ctx_load_value_v(c, i.op1, "rax" as *u8) 2116 x86_emit_movq_reg_reg(c.o, "rax" as *u8, g2h) 2117 return 0 2118 } 2119 let abp_off: i64 = x86ctx_value_base(c) + c.alloca_off[i.op0] 2120 x86ctx_load_value_v(c, i.op1, "rax" as *u8) 2121 if sz == 1 { x86_emit_store_byte(c.o, "rax" as *u8, "rbp" as *u8, abp_off) } 2122 if sz == 2 { x86_emit_store_word(c.o, "rax" as *u8, "rbp" as *u8, abp_off) } 2123 if sz == 4 { x86_emit_store_dword(c.o, "rax" as *u8, "rbp" as *u8, abp_off) } 2124 if sz == 8 { x86_emit_store_qword(c.o, "rax" as *u8, "rbp" as *u8, abp_off) } 2125 return 0 2126 } 2127 2128 // op0 is the ADDRESS to store to -- keep load_value (gives address 2129 // when alloca-result; gives stored ptr when SSA value). 2130 x86ctx_load_value(c, i.op0, "rcx" as *u8) 2131 // op1 is the VALUE to store -- use _v so alloca-result is derefed. 2132 x86ctx_load_value_v(c, i.op1, "rax" as *u8) 2133 if sz == 1 { x86_emit_store_byte(c.o, "rax" as *u8, "rcx" as *u8, 0) } 2134 if sz == 2 { x86_emit_store_word(c.o, "rax" as *u8, "rcx" as *u8, 0) } 2135 if sz == 4 { x86_emit_store_dword(c.o, "rax" as *u8, "rcx" as *u8, 0) } 2136 if sz == 8 { x86_emit_store_qword(c.o, "rax" as *u8, "rcx" as *u8, 0) } 2137 return 0 2138} 2139 2140func x86ctx_emit_gep(c: *X86Ctx, i: *Instr) -> i64 { 2141 // op0 is the BASE ADDRESS. Load it AS-ADDRESS (x86ctx_load_value): for an 2142 // alloca result that emits `leaq storage(%rbp)` (the address). The as-VALUE 2143 // variant (load_value_v) DEREFERENCES an alloca (movq storage(%rbp)) -- which 2144 // read the struct's own bytes as the base pointer => wild store => SIGSEGV 2145 // (the tagged-enum constructor `Opt::Some(v)` bug, nx_probe_ctor). For every 2146 // non-alloca base the two variants are byte-identical, so this is a no-op 2147 // there and correct for alloca bases. op1 (the offset) stays as-value. 2148 // G8: a GEP folded into a SIB load/store is dead -- emit nothing. 2149 if i.result >= 0 { if i.result < c.f.n_values { if c.sib_dead[i.result] == 1 { return 0 } } } 2150 x86ctx_load_value(c, i.op0, "rax" as *u8) 2151 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) 2152 x86_emit_gep_add(c.o, "rax" as *u8, "rcx" as *u8) 2153 x86ctx_store_result(c, i.result, "rax" as *u8) 2154 return 0 2155} 2156 2157// ===== load_value_v -- derefs alloca-results ====================== 2158// 2159// The C-bootstrap parser inserts an implicit "load pointer from 2160// alloca" between an alloca-result and any pointer-USE. The self- 2161// host parser (nx_parse.nx) does NOT. So when our codegen sees an 2162// alloca-result Value being USED AS A VALUE (binop / cmp / call / 2163// return / etc.), we need to materialise the missing load here. 2164// 2165// load_value_v is the "as-value" variant; load_value is the "as- 2166// address" variant used by STORE op0 (where we want to store TO the 2167// alloca) and LOAD op0 (where the subsequent movq deref reads 2168// the stored value). 2169 2170// ===== syscall ==================================================== 2171// 2172// nxc2 IR: op0 = syscall number, op1..op6 = up to 6 args (Linux ABI). 2173// 2174// The source typically imports nx_syscalls.nx which hard-codes RV64 2175// syscall numbers (read=63, write=64, mmap=222, exit=93, ...). On 2176// x86_64 those numbers mean different things (or nothing). When 2177// op0 is a VK_CONST_INT we translate at codegen time via the 2178// rv64->x86_64 map below. When op0 is a runtime-computed value 2179// the translator is a no-op (caller already loaded x86_64 num). 2180 2181func x86ctx_rv64_to_x86_64_syscall(num: i64) -> i64 { 2182 if num == 63 { return 0 } // read 2183 if num == 64 { return 1 } // write 2184 if num == 56 { return 257 } // openat 2185 if num == 57 { return 3 } // close 2186 if num == 80 { return 5 } // fstat 2187 if num == 93 { return 60 } // exit 2188 if num == 222 { return 9 } // mmap 2189 if num == 220 { return 56 } // clone 2190 if num == 221 { return 59 } // execve 2191 if num == 260 { return 61 } // wait4 2192 if num == 95 { return 247 } // waitid: WNOWAIT preserves worker identity until watchdog is reaped 2193 if num == 167 { return 157 } // prctl: parent-death binding and subreaper ownership 2194 if num == 198 { return 41 } // socket 2195 if num == 200 { return 49 } // bind 2196 if num == 201 { return 50 } // listen 2197 if num == 202 { return 43 } // accept 2198 if num == 203 { return 42 } // connect 2199 if num == 206 { return 44 } // sendto 2200 if num == 207 { return 45 } // recvfrom 2201 if num == 208 { return 54 } // setsockopt 2202 if num == 154 { return 109 } // setpgid -- MEASURED 2026-08-10: bounding a subject that FORKS 2203 // needs a PROCESS-GROUP kill, and rv64/asm-generic 154 fell 2204 // through `return num` to x86_64 154, which is not setpgid. The 2205 // call returned -38 ENOSYS, so the group-kill silently reached 2206 // NOTHING and a 2000ms bound still took 60,060ms. The same shape 2207 // as the getpid/fcntl rows below: an unmapped number is not an 2208 // error, it is a DIFFERENT syscall. 2209 // ACTIVATES ON THE NEXT nx_cc SELF-HOST REBUILD. Until then 2210 // gk_run_cap3's poll deadline bounds the wait without it, so 2211 // nothing depends on this row landing first. 2212 // ---- ROWS ADDED 2026-07-31 after auditing every rv64 const the runtime actually uses against 2213 // this table (nx_connect_sweep/xlate_audit). The translator's default is `return num`, so an 2214 // unmapped number is NOT an error -- it silently becomes a DIFFERENT x86_64 syscall. Each row 2215 // below was conflict-checked first: nothing calls __syscall with these as x86 numbers. 2216 // DELIBERATELY NOT ADDED, because live code passes them as X86 numbers already and a row would 2217 // BREAK it: 51 (nx_upnp_igd uses it as x86 getsockname) and 124 (nx_daemon_gate / nx_thread use 2218 // it as x86 getsid). Those two need their call sites disambiguated first -- filed, not guessed. 2219 // ---- 2026-08-01 round 2: the last two audited gaps, unblocked by disambiguating their call 2220 // sites first. 51 and 124 could not be mapped while live code still passed them as X86 numbers 2221 // (nx_upnp_igd as getsockname, nx_daemon_gate/nx_thread as getsid). Those now use the RV64 2222 // numbers 204/156, so each number means ONE thing and the real rows can land. 2223 if num == 204 { return 51 } // getsockname (rv64 204 -> x86_64 51) 2224 if num == 156 { return 124 } // getsid (rv64 156 -> x86_64 124) 2225 if num == 51 { return 161 } // chroot (rv64 51 -> x86_64 161). nx_syscalls.nx claimed this 2226 // row shipped long ago; it did not. SYS_CHROOT was falling through 2227 // to x86_64 51 = getsockname on every container path. 2228 if num == 124 { return 24 } // sched_yield (rv64 124 -> x86_64 24) 2229 if num == 210 { return 48 } // shutdown: rv64 210 has NO x86_64 counterpart, so every 2230 // sys_shutdown() fell through to a nonexistent syscall. Called by 2231 // nx_h2_serve, nx_h2_serve_multi, nx_acme_http, nx_aw_sni_router -- 2232 // a half-close that never happened. 2233 if num == 23 { return 32 } // dup (rv64 23 -> x86_64 32; was falling through to x86 select) 2234 if num == 40 { return 165 } // mount (rv64 40 -> x86_64 165). nx_syscalls.nx's comment CLAIMS 2235 // this row "was added and shipped FIRST" -- it was not present. 2236 if num == 241 { return 298 } // perf_event_open (rv64 241 -> x86_64 298) 2237 if num == 278 { return 318 } // getrandom (rv64 278 -> x86_64 318; x86 278 is vmsplice) 2238 if num == 25 { return 72 } // fcntl (rv64 25 -> x86_64 72): THE MISSING ROW. Fell through 2239 // `return num` to x86_64 25 = mremap, so EVERY nx_fcntl caller 2240 // silently got -EINVAL: F_GETFL/F_SETFL, and therefore 2241 // nx_fcntl_set_cloexec and nx_fcntl_set_nonblock, were no-ops that 2242 // REPORTED FAILURE nobody checked. Probed live 2026-07-31: 2243 // __syscall(25,fd,3,0)=-22 vs __syscall(72,fd,3,0)=2. Same class as 2244 // the flock row below (silent wrong-syscall via pass-through). 2245 if num == 73 { return 7 } // poll (rv64 SYS_POLL const=73 -> x86_64 poll=7; sys_poll's 3-arg fds/nfds/timeout_ms maps directly; the unused 4th arg is harmless) 2246 if num == 24 { return 292 } // dup3 2247 if num == 29 { return 16 } // ioctl 2248 if num == 32 { return 73 } // flock (rv64 32 -> x86_64 73): THE registry-write-race ROOT FIX -- 2249 // was falling through `return num` to x86_64 32 = dup2 (a silent no-op 2250 // "lock"), so concurrent writers could not serialize. BOOTSTRAP_MAP s4#2 / 2251 // X-SYSXLATE-FLOCK. Activates on the next nx_cc self-host rebuild. 2252 if num == 59 { return 22 } // pipe2 (rv64 59 = pipe2, x86_64 22 = pipe) 2253 if num == 134 { return 13 } // rt_sigaction: asm-generic 134 -> x86_64 13 2254 if num == 113 { return 228 } // clock_gettime 2255 if num == 115 { return 230 } // clock_nanosleep 2256 if num == 78 { return 217 } // getdents64 (legacy RV64 syscall number) 2257 if num == 61 { return 217 } // getdents64 (current RV64 generic ABI) 2258 if num == 62 { return 8 } // lseek 2259 if num == 33 { return 165 } // mount (rv64) -> mount (x86_64) 2260 if num == 161 { return 161 } // chroot 2261 if num == 41 { return 272 } // unshare (rv64) -- mapping rough 2262 if num == 97 { return 272 } // unshare (TRUE rv64 asm-generic 97 -> x86 272; R2-A containers. 2263 // nx_ns_probe proved 97 fell through to x86 getrlimit -> EINVAL. 2264 // NOTE 2026-06-09: rows 41/268/33 above are MISLABELED vs the real 2265 // asm-generic table (41=pivot_root, 268=setns, 40=mount) -- kept 2266 // untouched (callers may bind to them); audit + fix as R2-A rung-1. 2267 if num == 268 { return 155 } // pivot_root 2268 if num == 39 { return 16 } // umount2 (rv64) -- mapping rough 2269 if num == 116 { return 145 } // syslog (rv64) -- mapping rough 2270 if num == 129 { return 62 } // kill (rv64 129 -> x86_64 62) -- host control plane 2271 if num == 34 { return 258 } // mkdirat (rv64 34 -> x86_64 258) -- doc-root creation 2272 if num == 52 { return 91 } // fchmod: descriptor-bound permission updates (asm-generic -> x86-64). 2273 if num == 205 { return 52 } // getpeername: migrated raw-52 caller retains socket semantics. 2274 if num == 53 { return 268 } // fchmodat (rv64 53 -> x86_64 268) -- +x deployed binaries 2275 if num == 276 { return 316 } // renameat2 (rv64 276 -> x86_64 316) -- atomic content publish 2276 if num == 98 { return 202 } // futex (rv64 98 -> x86_64 202) -- spin-then-BLOCK thread pool 2277 // (2026-07-10): idle pool workers yield-spun forever (nx_chan_recv), 2278 // burning cores + ~1.5ms/dispatch scheduler tax. NOTE x86 202 was only 2279 // reachable as rv64 accept's TARGET before; raw __syscall(202) would 2280 // translate as rv64 accept -> 43. This row gives futex its lawful name. 2281 if num == 122 { return 203 } // sched_setaffinity (rv64 122 -> x86_64 203) -- pin pteam workers 2282 // to distinct cores (gcc/OpenMP default; the barrier-quality lever). 2283 // NOTE raw __syscall(203) alone = rv64 CONNECT -> x86 42; the table row 2284 // is the only lawful route, like the futex 98->202 row above. 2285 if num == 123 { return 204 } // sched_getaffinity (rv64 123 -> x86_64 204) -- hw cpu-count probe. 2286 // Root of the 2026-07-07 silent-1 bug: nx_hw used 122, which is rv64 2287 // sched_SETaffinity AND passes through to x86_64 setfsgid ("succeeds", 2288 // zero mask, popcount 0 -> 1 CPU reported -> every auto-sized pool 2289 // silently serial). nx_hw runs raw 204 until this row is blessed live, 2290 // then flips to the portable 123. 2291 // Process-identity family (asm-generic/rv64 -> x86_64). These were MISSING: rv64 getpid(172) 2292 // fell through `return num` to x86_64 172=iopl -> -ENOSYS. That silently filled a loop-singleton 2293 // pidfile with -38 and the gate FAILED OPEN -- two daemons raced (X-GETPID-XLATE, see 2294 // nx_gate_loop_register + nx_proc_kat T4). nx_signal/nx_swarm_queue/nx_model_lane(ml_pid_alive) 2295 // all wanted getpid via 172 and were silently broken; callers worked around it with a runtime 2296 // register (untranslated) or /proc/self/stat. These rows give the identity syscalls their lawful 2297 // name so a const 172/173 translates correctly ecosystem-wide. 2298 if num == 94 { return 231 } // exit_group (rv64 94 -> x86_64 231). Was MISSING: 94 fell through 2299 // `return num` to x86_64 94 = getgroups, so a const-numbered 2300 // exit_group silently did NOT exit. sys_exit_group works only 2301 // because it hardcodes the raw x86 231. The compiler-injected 2302 // bounds trap (nx_parse emit_bounds_trap) needs the portable 2303 // RV64 name so the same IR is correct on both backends. 2304 if num == 172 { return 39 } // getpid (rv64 172 -> x86_64 39) 2305 if num == 173 { return 110 } // getppid (rv64 173 -> x86_64 110) 2306 if num == 174 { return 102 } // getuid (rv64 174 -> x86_64 102) 2307 if num == 175 { return 107 } // geteuid (rv64 175 -> x86_64 107) 2308 if num == 176 { return 104 } // getgid (rv64 176 -> x86_64 104) 2309 if num == 177 { return 108 } // getegid (rv64 177 -> x86_64 108) 2310 if num == 233 { return 28 } // madvise (rv64/asm-generic 233 -> x86_64 28). Was MISSING: a 2311 // portable const falls through `return num` to x86_64 233 = 2312 // epoll_ctl (the wrong-syscall-not-an-error class, same as the 2313 // setpgid/flock rows above). sys_madvise (nx_syscalls.nx) runs 2314 // the RAW x86 28 until this row is blessed by the next nx_cc 2315 // self-host rebuild -- flip it to the portable const then. 2316 // Conflict-checked 2026-08-12: nothing calls __syscall(233,...) 2317 // and nothing passes 28 as an x86 number. 2318 return num // unknown -> pass through (will likely fail at runtime) 2319} 2320 2321func x86ctx_emit_syscall(c: *X86Ctx, i: *Instr) -> i64 { 2322 let n_args: i64 = i.n_operands - 1 2323 if n_args >= 1 { x86ctx_load_value_v(c, i.op1, "rdi" as *u8) } 2324 if n_args >= 2 { x86ctx_load_value_v(c, i.op2, "rsi" as *u8) } 2325 if n_args >= 3 { x86ctx_load_value_v(c, i.op3, "rdx" as *u8) } 2326 if n_args >= 4 { x86ctx_load_value_v(c, i.op4, "r10" as *u8) } 2327 if n_args >= 5 { x86ctx_load_value_v(c, i.op5, "r8" as *u8) } 2328 if n_args >= 6 { x86ctx_load_value_v(c, i.op6, "r9" as *u8) } 2329 2330 // Translate the syscall number if it's a compile-time constant. 2331 let num_val: *Value = x86ctx_value_at(c.f, i.op0) 2332 if num_val.kind == VK_CONST_INT { 2333 let x86_num: i64 = x86ctx_rv64_to_x86_64_syscall(num_val.const_int) 2334 x86_emit_movabsq(c.o, "rax" as *u8, x86_num) 2335 } 2336 if num_val.kind != VK_CONST_INT { 2337 // Runtime-computed syscall number -- load as-is. Caller is 2338 // responsible for using x86_64 numbers when this path runs. 2339 x86ctx_load_value(c, i.op0, "rax" as *u8) 2340 } 2341 x86_emit_syscall(c.o) 2342 x86ctx_store_result(c, i.result, "rax" as *u8) 2343 return 0 2344} 2345 2346// ===== call ======================================================= 2347// 2348// V2 (2026-05-20, Task #93): arity 1..8 supported. Args 1..6 in 2349// registers (SysV ABI rdi/rsi/rdx/rcx/r8/r9); args 7..8 pushed on 2350// stack in REVERSE order (rightmost first), as required by SysV. 2351// Indirect call still deferred. 2352// 2353// Witness of fix: 2354// - tls13_server_hello_parse (7 args) -- recv_sh SIGSEGV 2355// - nx_http_resp_parse_header_line (8 args) -- response parse SEGV 2356// Both were dropping args 7+ silently. Callee read garbage from 2357// stack slots above saved RA, dereferenced as pointers, NULL-faulted. 2358// 2359// Stack alignment: SysV requires 16-aligned %rsp at the call 2360// instruction. When n_stack_args is odd we push an 8-byte pad 2361// to maintain alignment. 2362 2363func x86ctx_emit_call(c: *X86Ctx, i: *Instr) -> i64 { 2364 let n_args: i64 = i.n_operands 2365 2366 // Load register args first (1..6). 2367 if n_args >= 1 { x86ctx_load_value_v(c, i.op0, "rdi" as *u8) } 2368 if n_args >= 2 { x86ctx_load_value_v(c, i.op1, "rsi" as *u8) } 2369 if n_args >= 3 { x86ctx_load_value_v(c, i.op2, "rdx" as *u8) } 2370 if n_args >= 4 { x86ctx_load_value_v(c, i.op3, "rcx" as *u8) } 2371 if n_args >= 5 { x86ctx_load_value_v(c, i.op4, "r8" as *u8) } 2372 if n_args >= 6 { x86ctx_load_value_v(c, i.op5, "r9" as *u8) } 2373 2374 // Stack args 7..24. n_stack_args bytes pushed; if odd, prepend 2375 // an 8-byte alignment pad so rsp stays 16-aligned at the call. 2376 var n_stack_args: i64 = 0 2377 if n_args >= 7 { n_stack_args = n_stack_args + 1 } 2378 if n_args >= 8 { n_stack_args = n_stack_args + 1 } 2379 if n_args >= 9 { n_stack_args = n_stack_args + 1 } 2380 if n_args >= 10 { n_stack_args = n_stack_args + 1 } 2381 if n_args >= 11 { n_stack_args = n_stack_args + 1 } 2382 if n_args >= 12 { n_stack_args = n_stack_args + 1 } 2383 if n_args >= 13 { n_stack_args = n_stack_args + 1 } 2384 if n_args >= 14 { n_stack_args = n_stack_args + 1 } 2385 if n_args >= 15 { n_stack_args = n_stack_args + 1 } 2386 if n_args >= 16 { n_stack_args = n_stack_args + 1 } 2387 if n_args >= 17 { n_stack_args = n_stack_args + 1 } 2388 if n_args >= 18 { n_stack_args = n_stack_args + 1 } 2389 if n_args >= 19 { n_stack_args = n_stack_args + 1 } 2390 if n_args >= 20 { n_stack_args = n_stack_args + 1 } 2391 if n_args >= 21 { n_stack_args = n_stack_args + 1 } 2392 if n_args >= 22 { n_stack_args = n_stack_args + 1 } 2393 if n_args >= 23 { n_stack_args = n_stack_args + 1 } 2394 if n_args >= 24 { n_stack_args = n_stack_args + 1 } 2395 2396 var pad: i64 = 0 2397 if n_stack_args > 0 { 2398 if (n_stack_args & 1) == 1 { 2399 pad = 8 2400 out_str(c.o, " subq $8, %rsp\n") 2401 } 2402 } 2403 2404 // Push in REVERSE order: arg N first (highest), down to arg 7. 2405 if n_args >= 24 { x86ctx_load_value_v(c, i.op23, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2406 if n_args >= 23 { x86ctx_load_value_v(c, i.op22, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2407 if n_args >= 22 { x86ctx_load_value_v(c, i.op21, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2408 if n_args >= 21 { x86ctx_load_value_v(c, i.op20, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2409 if n_args >= 20 { x86ctx_load_value_v(c, i.op19, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2410 if n_args >= 19 { x86ctx_load_value_v(c, i.op18, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2411 if n_args >= 18 { x86ctx_load_value_v(c, i.op17, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2412 if n_args >= 17 { x86ctx_load_value_v(c, i.op16, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2413 if n_args >= 16 { x86ctx_load_value_v(c, i.op15, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2414 if n_args >= 15 { x86ctx_load_value_v(c, i.op14, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2415 if n_args >= 14 { x86ctx_load_value_v(c, i.op13, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2416 if n_args >= 13 { x86ctx_load_value_v(c, i.op12, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2417 if n_args >= 12 { x86ctx_load_value_v(c, i.op11, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2418 if n_args >= 11 { x86ctx_load_value_v(c, i.op10, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2419 if n_args >= 10 { x86ctx_load_value_v(c, i.op9, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2420 if n_args >= 9 { x86ctx_load_value_v(c, i.op8, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2421 if n_args >= 8 { x86ctx_load_value_v(c, i.op7, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2422 if n_args >= 7 { x86ctx_load_value_v(c, i.op6, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2423 2424 if i.callee != (0 as *Function) { 2425 let cn: *u8 = i.callee.name_start as *u8 2426 x86_emit_call_label(c.o, cn) 2427 x86ctx_store_result(c, i.result, "rax" as *u8) 2428 } 2429 if i.callee == (0 as *Function) { 2430 out_str(c.o, " # x86_64: indirect call deferred\n") 2431 } 2432 2433 // Restore stack: pop stack args + alignment pad. 2434 let pop_bytes: i64 = n_stack_args * 8 + pad 2435 if pop_bytes > 0 { 2436 out_str(c.o, " addq $") 2437 out_i64(c.o, pop_bytes) 2438 out_str(c.o, ", %rsp\n") 2439 } 2440 return 0 2441} 2442 2443// fp(args) -- INDIRECT call through a func-pointer VALUE (op0). Args are op1.. (SysV rdi..r9; this MVP caps at 2444// 6 register args -- thread_pool/callbacks use <=6, no stack args). The fn-ptr is spilled to the stack across 2445// the arg-register loads so none can clobber it, then popped into r11 (caller-saved, NOT an arg reg). The 2446// push+pop are balanced, so rsp stays 16-aligned at the call. 2447func x86ctx_emit_call_indirect(c: *X86Ctx, i: *Instr) -> i64 { 2448 let n_args: i64 = i.n_operands - 1 2449 // LOUD-FAIL GUARD: the IR carries op0 (fn-ptr) + op1..op23, so 23 is the hard 2450 // ceiling. Fail the BUILD, never drop an argument -- dropping is precisely the 2451 // defect this function shipped with (see the stack-arg note below). 2452 if n_args > 23 { 2453 out_str(c.o, " .error \"nx x86: indirect call with >23 args (IR operand cap)\"\n") 2454 return 0 2455 } 2456 // The fn-ptr is spilled across the arg-register loads so none can clobber it, 2457 // then popped into r11 -- reserved scratch, never in the allocation pool 2458 // (FIX-12 invariant). The pop happens BEFORE the stack args are pushed so those 2459 // pushes land directly above the return address, where SysV expects them. 2460 x86ctx_load_value_v(c, i.op0, "rax" as *u8) 2461 out_str(c.o, " pushq %rax\n") 2462 if n_args >= 1 { x86ctx_load_value_v(c, i.op1, "rdi" as *u8) } 2463 if n_args >= 2 { x86ctx_load_value_v(c, i.op2, "rsi" as *u8) } 2464 if n_args >= 3 { x86ctx_load_value_v(c, i.op3, "rdx" as *u8) } 2465 if n_args >= 4 { x86ctx_load_value_v(c, i.op4, "rcx" as *u8) } 2466 if n_args >= 5 { x86ctx_load_value_v(c, i.op5, "r8" as *u8) } 2467 if n_args >= 6 { x86ctx_load_value_v(c, i.op6, "r9" as *u8) } 2468 out_str(c.o, " popq %r11\n") 2469 2470 // STACK ARGS 7..23 (2026-07-25). This emitter used to stop at r9 and SILENTLY 2471 // DROP every argument past the 6th -- the identical failure that cost 2472 // tls13_server_hello_parse and nx_http_resp_parse_header_line a SEGV each on the 2473 // DIRECT path (fixed there 2026-06-10; the indirect path kept the bug, and only 2474 // the parser's 6-arg cap kept it off the road). Mirrors the direct path exactly: 2475 // rightmost-first pushes, plus an 8-byte pad when the count is odd so rsp is 2476 // 16-aligned at the call. The fn-ptr push/pop above cancel out, so rsp is back 2477 // to its entry alignment here and the direct path's pad rule applies unchanged. 2478 var n_stack_args: i64 = 0 2479 if n_args > 6 { n_stack_args = n_args - 6 } 2480 var pad: i64 = 0 2481 if n_stack_args > 0 { 2482 if (n_stack_args & 1) == 1 { 2483 pad = 8 2484 out_str(c.o, " subq $8, %rsp\n") 2485 } 2486 } 2487 if n_args >= 23 { x86ctx_load_value_v(c, i.op23, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2488 if n_args >= 22 { x86ctx_load_value_v(c, i.op22, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2489 if n_args >= 21 { x86ctx_load_value_v(c, i.op21, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2490 if n_args >= 20 { x86ctx_load_value_v(c, i.op20, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2491 if n_args >= 19 { x86ctx_load_value_v(c, i.op19, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2492 if n_args >= 18 { x86ctx_load_value_v(c, i.op18, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2493 if n_args >= 17 { x86ctx_load_value_v(c, i.op17, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2494 if n_args >= 16 { x86ctx_load_value_v(c, i.op16, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2495 if n_args >= 15 { x86ctx_load_value_v(c, i.op15, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2496 if n_args >= 14 { x86ctx_load_value_v(c, i.op14, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2497 if n_args >= 13 { x86ctx_load_value_v(c, i.op13, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2498 if n_args >= 12 { x86ctx_load_value_v(c, i.op12, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2499 if n_args >= 11 { x86ctx_load_value_v(c, i.op11, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2500 if n_args >= 10 { x86ctx_load_value_v(c, i.op10, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2501 if n_args >= 9 { x86ctx_load_value_v(c, i.op9, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2502 if n_args >= 8 { x86ctx_load_value_v(c, i.op8, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2503 if n_args >= 7 { x86ctx_load_value_v(c, i.op7, "rax" as *u8); out_str(c.o, " pushq %rax\n") } 2504 2505 x86_emit_call_indirect(c.o, "r11" as *u8) 2506 x86ctx_store_result(c, i.result, "rax" as *u8) 2507 2508 // Restore stack: pop stack args + alignment pad. 2509 let pop_bytes: i64 = n_stack_args * 8 + pad 2510 if pop_bytes > 0 { 2511 out_str(c.o, " addq $") 2512 out_i64(c.o, pop_bytes) 2513 out_str(c.o, ", %rsp\n") 2514 } 2515 return 0 2516} 2517 2518// ===== tail_call (session 6b) ===================================== 2519// 2520// Same arg-loading as call, but emit `jmp <label>` after tearing 2521// down the current frame. The callee will execute its own 2522// prologue + epilogue + ret; control returns directly to OUR caller. 2523 2524func x86ctx_emit_tail_call(c: *X86Ctx, i: *Instr) -> i64 { 2525 let n_args: i64 = i.n_operands 2526 // LOUD-FAIL GUARD (2026-06-10): stack args (7th+) are impossible 2527 // after frame teardown; silently dropping them zeroed every TLS 2528 // Derive-Secret (see _arg7_minrepro.nx). opt_tail_call no longer 2529 // converts these, but if one reaches us, fail the BUILD, not the 2530 // runtime. 2531 if n_args > 6 { 2532 out_str(c.o, " .error \"nx x86: tail_call with >6 args (stack args would be dropped)\"\n") 2533 return 0 2534 } 2535 if n_args >= 1 { x86ctx_load_value_v(c, i.op0, "rdi" as *u8) } 2536 if n_args >= 2 { x86ctx_load_value_v(c, i.op1, "rsi" as *u8) } 2537 if n_args >= 3 { x86ctx_load_value_v(c, i.op2, "rdx" as *u8) } 2538 if n_args >= 4 { x86ctx_load_value_v(c, i.op3, "rcx" as *u8) } 2539 if n_args >= 5 { x86ctx_load_value_v(c, i.op4, "r8" as *u8) } 2540 if n_args >= 6 { x86ctx_load_value_v(c, i.op5, "r9" as *u8) } 2541 // G1 FIX-B: this path bypasses x86_emit_epilogue -> restore callee-saved 2542 // homes here, AFTER the arg reads, BEFORE teardown, or the caller's homes 2543 // are corrupted (self-host-fatal). 2544 x86ctx_emit_cs_restore(c) 2545 // Tear down our frame: restore rsp + rbp, then jmp (not call). 2546 out_str(c.o, " movq %rbp, %rsp\n") 2547 out_str(c.o, " popq %rbp\n") 2548 if i.callee != (0 as *Function) { 2549 let cn: *u8 = i.callee.name_start as *u8 2550 x86_emit_tail_call_label(c.o, cn) 2551 } 2552 if i.callee == (0 as *Function) { 2553 out_str(c.o, " # x86_64: indirect tail_call deferred\n") 2554 } 2555 return 0 2556} 2557 2558// ===== copy ======================================================= 2559 2560func x86ctx_emit_copy(c: *X86Ctx, i: *Instr) -> i64 { 2561 x86ctx_load_value_v(c, i.op0, "rax" as *u8) 2562 x86ctx_store_result(c, i.result, "rax" as *u8) 2563 return 0 2564} 2565 2566// ===== return ===================================================== 2567 2568func x86ctx_emit_return(c: *X86Ctx, i: *Instr) -> i64 { 2569 if i.n_operands >= 1 { 2570 x86ctx_load_value_v(c, i.op0, "rax" as *u8) 2571 } 2572 if i.n_operands == 0 { 2573 x86_emit_movabsq(c.o, "rax" as *u8, 0) 2574 } 2575 x86ctx_emit_cs_restore(c) 2576 x86_emit_epilogue(c.o) 2577 return 0 2578} 2579 2580// ===== hardware f32 (SSE scalar-single) =========================== 2581// f32 values are i64-CARRIED bit-patterns (the float lives in the low 32 bits; 2582// NishiLang has no f32 type). Shuttle GPR<->xmm through the SysV red zone (-8(%rsp); 2583// the sequence is call-free, so the 128-byte red zone is safe transient scratch). 2584// rax/rcx are caller-saved scratch (the same regs the integer binop uses) and 2585// xmm0/xmm1 are outside the GPR allocator, so nothing live is clobbered. The dispatch 2586// sets G1_RAX_SLOT=-1 before calling so the rax-tracking peephole is invalidated. 2587func x86ctx_emit_f32(c: *X86Ctx, i: *Instr) -> i64 { 2588 let op: i64 = i.op 2589 if op == OP_FCAST_I_TO_F { // i64 int -> f32 bits (cvtsi2ss) 2590 x86ctx_load_value_v(c, i.op0, "rax" as *u8) 2591 out_str(c.o, " cvtsi2ss %rax, %xmm0\n") 2592 out_str(c.o, " movss %xmm0, -8(%rsp)\n") 2593 out_str(c.o, " movl -8(%rsp), %eax\n") 2594 x86ctx_store_result(c, i.result, "rax" as *u8) 2595 return 0 2596 } 2597 if op == OP_FCAST_F_TO_I { // f32 bits -> i64 int, truncate (cvttss2si) 2598 x86ctx_load_value_v(c, i.op0, "rax" as *u8) 2599 out_str(c.o, " movq %rax, -8(%rsp)\n") 2600 out_str(c.o, " movss -8(%rsp), %xmm0\n") 2601 out_str(c.o, " cvttss2si %xmm0, %rax\n") 2602 x86ctx_store_result(c, i.result, "rax" as *u8) 2603 return 0 2604 } 2605 // binary: op0,op1 are f32 bits -> xmm0,xmm1 -> SSE compute -> bits back to rax. 2606 x86ctx_load_value_v(c, i.op0, "rax" as *u8) 2607 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) 2608 out_str(c.o, " movq %rax, -8(%rsp)\n") 2609 out_str(c.o, " movss -8(%rsp), %xmm0\n") 2610 out_str(c.o, " movq %rcx, -8(%rsp)\n") 2611 out_str(c.o, " movss -8(%rsp), %xmm1\n") 2612 if op == OP_FADD { out_str(c.o, " addss %xmm1, %xmm0\n") } 2613 if op == OP_FSUB { out_str(c.o, " subss %xmm1, %xmm0\n") } 2614 if op == OP_FMUL { out_str(c.o, " mulss %xmm1, %xmm0\n") } 2615 if op == OP_FDIV { out_str(c.o, " divss %xmm1, %xmm0\n") } 2616 out_str(c.o, " movss %xmm0, -8(%rsp)\n") 2617 out_str(c.o, " movl -8(%rsp), %eax\n") 2618 x86ctx_store_result(c, i.result, "rax" as *u8) 2619 return 0 2620} 2621 2622// ===== hardware f64 (SSE scalar-double) =========================== 2623// f64 values are i64-carried bit-patterns (the double IS the full 64 bits). 2624// Same red-zone shuttle discipline as x86ctx_emit_f32 but with double-precision 2625// instructions (movsd/addsd/... + sqrtsd + cvtsi2sd/cvttsd2si). Dispatched from 2626// the binop router when i.ty.kind == TY_F64 so f64 arithmetic keeps full 2627// precision (the pre-2026-07-16 path forced everything through movss = silent 2628// truncation to f32). rax/rcx caller-saved scratch, xmm0/xmm1 outside the GPR 2629// allocator; G1_RAX_SLOT invalidated by the caller. 2630func x86ctx_emit_f64(c: *X86Ctx, i: *Instr) -> i64 { 2631 // f64-in-registers (2026-07-16): DIRECT GPR<->xmm movq (SSE2 66 REX.W 0F 2632 // 6E/7E) replaces the red-zone memory shuttle -- ~7 instrs/op -> ~4, and the 2633 // memory round-trip (a false dependency chain through -8(%rsp)) is gone. The 2634 // named spectral-norm perf rung; bit-exact (nx_f64_adversary + the matrix's 2635 // 1274219991 checksum unchanged). GNU as also accepts `movq %rax,%xmm0`. 2636 let op: i64 = i.op 2637 if op == OP_FCAST_I_TO_F { // i64 int -> f64 bits (cvtsi2sd) 2638 x86ctx_load_value_v(c, i.op0, "rax" as *u8) 2639 out_str(c.o, " cvtsi2sd %rax, %xmm0\n") 2640 out_str(c.o, " movq %xmm0, %rax\n") 2641 x86ctx_store_result(c, i.result, "rax" as *u8) 2642 return 0 2643 } 2644 if op == OP_FCAST_F_TO_I { // f64 bits -> i64 int, truncate (cvttsd2si) 2645 x86ctx_load_value_v(c, i.op0, "rax" as *u8) 2646 out_str(c.o, " movq %rax, %xmm0\n") 2647 out_str(c.o, " cvttsd2si %xmm0, %rax\n") 2648 x86ctx_store_result(c, i.result, "rax" as *u8) 2649 return 0 2650 } 2651 if op == OP_FSQRT { // f64 sqrt (sqrtsd), unary 2652 x86ctx_load_value_v(c, i.op0, "rax" as *u8) 2653 out_str(c.o, " movq %rax, %xmm0\n") 2654 out_str(c.o, " sqrtsd %xmm0, %xmm0\n") 2655 out_str(c.o, " movq %xmm0, %rax\n") 2656 x86ctx_store_result(c, i.result, "rax" as *u8) 2657 return 0 2658 } 2659 // binary: op0,op1 are f64 bits -> xmm0,xmm1 -> SSE double compute -> bits back. 2660 x86ctx_load_value_v(c, i.op0, "rax" as *u8) 2661 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) 2662 out_str(c.o, " movq %rax, %xmm0\n") 2663 out_str(c.o, " movq %rcx, %xmm1\n") 2664 if op == OP_FADD { out_str(c.o, " addsd %xmm1, %xmm0\n") } 2665 if op == OP_FSUB { out_str(c.o, " subsd %xmm1, %xmm0\n") } 2666 if op == OP_FMUL { out_str(c.o, " mulsd %xmm1, %xmm0\n") } 2667 if op == OP_FDIV { out_str(c.o, " divsd %xmm1, %xmm0\n") } 2668 out_str(c.o, " movq %xmm0, %rax\n") 2669 x86ctx_store_result(c, i.result, "rax" as *u8) 2670 return 0 2671} 2672 2673// Route an fp op to f64 or f32 codegen by its float precision. For most ops the 2674// RESULT type (i.ty) is the float type; for FCAST_F_TO_I the result is INT, so 2675// the precision comes from the SOURCE operand (op0) instead. 2676func x86ctx_emit_float(c: *X86Ctx, i: *Instr) -> i64 { 2677 var is64: i64 = 0 2678 if i.op == OP_FCAST_F_TO_I { 2679 let sv: *Value = x86ctx_value_at(c.f, i.op0) 2680 if sv.ty != (0 as *Type) { if sv.ty.kind == TY_F64 { is64 = 1 } } 2681 } 2682 if i.op != OP_FCAST_F_TO_I { 2683 if i.ty != (0 as *Type) { if i.ty.kind == TY_F64 { is64 = 1 } } 2684 } 2685 if is64 == 1 { return x86ctx_emit_f64(c, i) } 2686 return x86ctx_emit_f32(c, i) 2687} 2688 2689// PACKED f32x4 dot: op0,op1 are pointers to 4 CONTIGUOUS 4-byte f32. movups loads 4 lanes each; 2690// mulps multiplies all 4 pairs in ONE instruction (vs 4 scalar mulss); scalar horizontal-sum via 2691// movss+addss (sidesteps the shufps operand-parse bug, uses only verified ops) -> f32 bits in eax. 2692// This is the compute-physics lever: 4 f32 MACs per mulps instead of 1 per mulss. 2693func x86ctx_emit_f32x4_dot(c: *X86Ctx, i: *Instr) -> i64 { 2694 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = a_ptr 2695 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // rcx = b_ptr 2696 out_str(c.o, " movups (%rax), %xmm0\n") // xmm0 = a[0..3] 2697 out_str(c.o, " movups (%rcx), %xmm1\n") // xmm1 = b[0..3] 2698 out_str(c.o, " mulps %xmm1, %xmm0\n") // xmm0 = 4 products (one instruction) 2699 out_str(c.o, " movups %xmm0, -16(%rsp)\n") // spill the 4 lanes to the red zone 2700 out_str(c.o, " movss -16(%rsp), %xmm0\n") // lane0 2701 out_str(c.o, " movss -12(%rsp), %xmm1\n") // lane1 2702 out_str(c.o, " addss %xmm1, %xmm0\n") // +lane1 2703 out_str(c.o, " movss -8(%rsp), %xmm1\n") // lane2 2704 out_str(c.o, " addss %xmm1, %xmm0\n") // +lane2 2705 out_str(c.o, " movss -4(%rsp), %xmm1\n") // lane3 2706 out_str(c.o, " addss %xmm1, %xmm0\n") // +lane3 = dot 2707 out_str(c.o, " movss %xmm0, -8(%rsp)\n") // store result f32 bits 2708 out_str(c.o, " movl -8(%rsp), %eax\n") // eax = f32 bits 2709 x86ctx_store_result(c, i.result, "rax" as *u8) 2710 return 0 2711} 2712 2713// PACKED f32x8 dot (AVX2 8-wide): op0,op1 -> 8 contiguous 4-byte f32 each. vmovups+vmulps do 8 lanes 2714// per instruction (2x the SSE width); vextractf128 folds hi4+lo4 -> xmm, vzeroupper clears the AVX-SSE 2715// transition penalty, then the PROVEN SSE scalar hsum. VEX bytes via .byte (proven nxasm_vex_kat 7/7; 2716// regs fixed ymm0/ymm1 + ptrs in rax/rcx so every ModRM is constant) -- no nxasm ymm-parser needed. 2717// __f32_i8dot32(a:*i8[32], b:*f32[32]) -> f32 = sum_{j<32} (sext(a[j])) * b[j]. 2718// The Q8_0/quantized dequant-dot lever: 32 int8 sign-extended + converted + 2719// multiplied by 32 f32, all SSE, unrolled x8 (4 lanes/iter), hsum once. New 2720// SSE ops (pmovsxbd/cvtdq2ps/movd/mulps/addps/xorps/movaps) emitted as .byte 2721// with FIXED registers (rax=a, rcx=b, xmm0 work, xmm1 A-lanes, xmm2 acc) -- 2722// no nxasm SSE-parser needed (the __f32x8_dot AVX-.byte precedent). .byte is 2723// DECIMAL (nxasm axc_emit_bytes = v*10+d). Bit-exact-safe: int8 in [-128,127] 2724// and their products are exact in f32; sum order = lane-parallel (4-wide) then 2725// hsum -- the CALLER (a Q8_0 dequant-dot) owns the block/scale order. 2726// __q5_unpack32(qhqs:*u8[20], out:*i8[32], consts:*u8[80]) -> 0. 2727// Unpacks a Q5_0 block (qh[0..4] u32 high-bits, qs[4..20] 16 nibble-bytes) 2728// into 32 signed int8 = (nibble | (qh_bit<<4)) - 16, in A-order (low 2729// nibbles+qh bits 0..15 -> out[0..15]; high nibbles+qh bits 16..31 -> 2730// out[16..31]). All SSE via .byte, fixed regs: rax=qhqs rcx=out rdx=consts; 2731// xmm0=qs xmm3=qh xmm4=c_0F xmm6=c_bitmask xmm7=c_10; xmm1=vals xmm2=qh-spread 2732// xmm5=pshuf mask. consts layout: [0]c_0F [16]pshuf_lo [32]pshuf_hi 2733// [48]bitmask [64]c_10 (each 16B). Caller builds consts ONCE. 2734func x86ctx_emit_q5unpack32(c: *X86Ctx, i: *Instr) -> i64 { 2735 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = qhqs 2736 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // rcx = out 2737 x86ctx_load_value_v(c, i.op2, "rdx" as *u8) // rdx = consts 2738 out_str(c.o, " .byte 243, 15, 111, 64, 4\n") // movdqu 4(%rax),%xmm0 (qs) 2739 out_str(c.o, " .byte 102, 15, 110, 24\n") // movd (%rax),%xmm3 (qh) 2740 out_str(c.o, " .byte 243, 15, 111, 34\n") // movdqu (%rdx),%xmm4 (c_0F) 2741 out_str(c.o, " .byte 243, 15, 111, 114, 48\n") // movdqu 48(%rdx),%xmm6 (bitmask) 2742 out_str(c.o, " .byte 243, 15, 111, 122, 64\n") // movdqu 64(%rdx),%xmm7 (c_10) 2743 // ---- LOW group -> out[0..15] ---- 2744 out_str(c.o, " .byte 102, 15, 111, 200\n") // movdqa %xmm0,%xmm1 2745 out_str(c.o, " .byte 102, 15, 219, 204\n") // pand %xmm4,%xmm1 (low nibbles) 2746 out_str(c.o, " .byte 243, 15, 111, 106, 16\n") // movdqu 16(%rdx),%xmm5 (pshuf_lo) 2747 out_str(c.o, " .byte 102, 15, 111, 211\n") // movdqa %xmm3,%xmm2 (qh) 2748 out_str(c.o, " .byte 102, 15, 56, 0, 213\n") // pshufb %xmm5,%xmm2 2749 out_str(c.o, " .byte 102, 15, 219, 214\n") // pand %xmm6,%xmm2 (& bitmask) 2750 out_str(c.o, " .byte 102, 15, 116, 214\n") // pcmpeqb %xmm6,%xmm2 (0xFF if set) 2751 out_str(c.o, " .byte 102, 15, 219, 215\n") // pand %xmm7,%xmm2 (& 0x10) 2752 out_str(c.o, " .byte 102, 15, 235, 202\n") // por %xmm2,%xmm1 (nibble|bit<<4) 2753 out_str(c.o, " .byte 102, 15, 248, 207\n") // psubb %xmm7,%xmm1 (- 16) 2754 out_str(c.o, " .byte 243, 15, 127, 9\n") // movdqu %xmm1,(%rcx) out[0..15] 2755 // ---- HIGH group -> out[16..31] ---- 2756 out_str(c.o, " .byte 102, 15, 111, 200\n") // movdqa %xmm0,%xmm1 2757 out_str(c.o, " .byte 102, 15, 113, 209, 4\n") // psrlw $4,%xmm1 2758 out_str(c.o, " .byte 102, 15, 219, 204\n") // pand %xmm4,%xmm1 (high nibbles) 2759 out_str(c.o, " .byte 243, 15, 111, 106, 32\n") // movdqu 32(%rdx),%xmm5 (pshuf_hi) 2760 out_str(c.o, " .byte 102, 15, 111, 211\n") // movdqa %xmm3,%xmm2 2761 out_str(c.o, " .byte 102, 15, 56, 0, 213\n") // pshufb %xmm5,%xmm2 2762 out_str(c.o, " .byte 102, 15, 219, 214\n") // pand %xmm6,%xmm2 2763 out_str(c.o, " .byte 102, 15, 116, 214\n") // pcmpeqb %xmm6,%xmm2 2764 out_str(c.o, " .byte 102, 15, 219, 215\n") // pand %xmm7,%xmm2 2765 out_str(c.o, " .byte 102, 15, 235, 202\n") // por %xmm2,%xmm1 2766 out_str(c.o, " .byte 102, 15, 248, 207\n") // psubb %xmm7,%xmm1 2767 out_str(c.o, " .byte 243, 15, 127, 73, 16\n") // movdqu %xmm1,16(%rcx) out[16..31] 2768 out_str(c.o, " movabsq $0, %rax\n") 2769 x86ctx_store_result(c, i.result, "rax" as *u8) 2770 return 0 2771} 2772 2773// __q4k_unpack32s(qs:*u8[32], out:*i16[64], scpack:i64) -> 0. AVX2, VEX .byte (DECIMAL, the 2774// __i16x16_madd precedent). One Q4_K sub-block pair: 32 packed nibble bytes -> 64 i16 lanes 2775// ALREADY multiplied by their 6-bit sub-block scale, so the consumer feeds vpmaddwd directly. 2776// out[0..31] = (qs[k] & 15) * sc_lo (low nibbles, lane k = byte k) 2777// out[32..63] = (qs[k] >> 4) * sc_hi (high nibbles) 2778// Fixed regs: rax=qs rcx=out rdx=scpack. ymm2 = 0x000F per word (built from all-ones >> 12, no 2779// memory constant), ymm3 = sc_lo broadcast, ymm4 = sc_hi broadcast; per 16-byte half: vpmovzxbw 2780// widens bytes to words, vpsrlw $4 yields the high nibble (a word holds one byte, so nothing 2781// crosses a lane), vpand the mask yields the low nibble, vpmullw scales, vmovups stores. 2782// Products <= 15*63 = 945 fit a lane exactly; bit-identical to the scalar spread+multiply it 2783// replaces (nx_nofloat_q4k_gate is the exact-reference proof). Clobbers ymm0-4 and rdx. 2784func x86ctx_emit_q4kunpack32s(c: *X86Ctx, i: *Instr) -> i64 { 2785 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = qs 2786 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // rcx = out 2787 x86ctx_load_value_v(c, i.op2, "rdx" as *u8) // rdx = sc_lo | sc_hi<<16 2788 out_str(c.o, " .byte 197, 237, 118, 210\n") // C5 ED 76 D2 vpcmpeqd %ymm2,%ymm2,%ymm2 (all ones) 2789 out_str(c.o, " .byte 197, 237, 113, 210, 12\n") // C5 ED 71 D2 0C vpsrlw $12,%ymm2,%ymm2 (0x000F per word) 2790 out_str(c.o, " .byte 197, 249, 110, 218\n") // C5 F9 6E DA vmovd %edx,%xmm3 (sc_lo in word 0) 2791 out_str(c.o, " .byte 196, 226, 125, 121, 219\n") // C4 E2 7D 79 DB vpbroadcastw %xmm3,%ymm3 2792 out_str(c.o, " .byte 72, 193, 234, 16\n") // 48 C1 EA 10 shrq $16,%rdx (sc_hi to word 0) 2793 out_str(c.o, " .byte 197, 249, 110, 226\n") // C5 F9 6E E2 vmovd %edx,%xmm4 2794 out_str(c.o, " .byte 196, 226, 125, 121, 228\n") // C4 E2 7D 79 E4 vpbroadcastw %xmm4,%ymm4 2795 // ---- bytes 0..15 -> out[0..15] (lo) and out[32..47] (hi) ---- 2796 out_str(c.o, " .byte 196, 226, 125, 48, 0\n") // C4 E2 7D 30 00 vpmovzxbw (%rax),%ymm0 2797 out_str(c.o, " .byte 197, 245, 113, 208, 4\n") // C5 F5 71 D0 04 vpsrlw $4,%ymm0,%ymm1 (high nibbles) 2798 out_str(c.o, " .byte 197, 253, 219, 194\n") // C5 FD DB C2 vpand %ymm2,%ymm0,%ymm0 (low nibbles) 2799 out_str(c.o, " .byte 197, 253, 213, 195\n") // C5 FD D5 C3 vpmullw %ymm3,%ymm0,%ymm0 (* sc_lo) 2800 out_str(c.o, " .byte 197, 245, 213, 204\n") // C5 F5 D5 CC vpmullw %ymm4,%ymm1,%ymm1 (* sc_hi) 2801 out_str(c.o, " .byte 197, 252, 17, 1\n") // C5 FC 11 01 vmovups %ymm0,(%rcx) 2802 out_str(c.o, " .byte 197, 252, 17, 73, 64\n") // C5 FC 11 49 40 vmovups %ymm1,64(%rcx) 2803 // ---- bytes 16..31 -> out[16..31] (lo) and out[48..63] (hi) ---- 2804 out_str(c.o, " .byte 196, 226, 125, 48, 64, 16\n") // C4 E2 7D 30 40 10 vpmovzxbw 16(%rax),%ymm0 2805 out_str(c.o, " .byte 197, 245, 113, 208, 4\n") // vpsrlw $4,%ymm0,%ymm1 2806 out_str(c.o, " .byte 197, 253, 219, 194\n") // vpand %ymm2,%ymm0,%ymm0 2807 out_str(c.o, " .byte 197, 253, 213, 195\n") // vpmullw %ymm3,%ymm0,%ymm0 2808 out_str(c.o, " .byte 197, 245, 213, 204\n") // vpmullw %ymm4,%ymm1,%ymm1 2809 out_str(c.o, " .byte 197, 252, 17, 65, 32\n") // C5 FC 11 41 20 vmovups %ymm0,32(%rcx) 2810 out_str(c.o, " .byte 197, 252, 17, 73, 96\n") // C5 FC 11 49 60 vmovups %ymm1,96(%rcx) 2811 out_str(c.o, " movabsq $0, %rax\n") 2812 x86ctx_store_result(c, i.result, "rax" as *u8) 2813 return 0 2814} 2815 2816// __q4k_sb_dot(sb:*u8[144], col:*i16[256], scpre:*i64[8], out:*i64[5]) -> 0. ONE WHOLE Q4_K SUPER-BLOCK. 2817// MEASURED reason (2026-09-02): with __q4k_unpack32s the serve profile still put 77 percent of a token in the 2818// fused GEMM at 3.25 ms per 7B call, and the kernel's serial time barely moved when its byte reads were removed -- 2819// the cost was the compiler's scalar code around four intrinsic calls per super-block, not the arithmetic. This 2820// intrinsic does the super-block in registers: the 12 scale bytes decoded with shifts (w0 = bytes 0..7, w1 = bytes 2821// 8..15), per sub-block pair the unpack+scale sequence, vpmaddwd straight from the activation lanes in memory, 2822// eight i32 lanes accumulated in ymm5 across all eight sub-blocks, and the dmin term (m_sub * scpre[sub]) summed 2823// in r10. out[0..3] = the accumulator lanes (caller: ds_hsum), out[4] = the dmin sum. 2824// Fixed regs: rax=sb rcx=col (advanced 128 B per group) rdx=scpre rsi=out; r8=w0 r9=w1 r10=msum r11=scpack 2825// rdi,rbx=temps (rbx is a callee-saved home register in this backend, so it is pushed and popped); 2826// ymm0/ymm1 work, ymm2 = 0x000F words, ymm3/ymm4 = scale broadcasts, ymm5 = accumulator. 2827// Lane bound: 32 products per lane per super-block, each <= 945*32767 -> < 2^31. Bit-identical to nx_q4k_dot_simd2 2828// (nx_q4k_simd2_gate proves it against the scalar row_col ruler on random real-shaped rows). 2829func x86ctx_emit_q4ksbdot(c: *X86Ctx, i: *Instr) -> i64 { 2830 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = sb 2831 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // rcx = col lanes of this super-block (512 B) 2832 x86ctx_load_value_v(c, i.op2, "rdx" as *u8) // rdx = scpre (8 x i64) 2833 x86ctx_load_value_v(c, i.op3, "rsi" as *u8) // rsi = out (5 x i64) 2834 out_str(c.o, " pushq %rbx\n") 2835 out_str(c.o, " .byte 197, 237, 118, 210\n") // vpcmpeqd %ymm2,%ymm2,%ymm2 2836 out_str(c.o, " .byte 197, 237, 113, 210, 12\n") // vpsrlw $12,%ymm2,%ymm2 (0x000F per word) 2837 out_str(c.o, " .byte 197, 213, 239, 237\n") // C5 D5 EF ED vpxor %ymm5,%ymm5,%ymm5 2838 out_str(c.o, " movq 4(%rax), %r8\n") // scale bytes 0..7 2839 out_str(c.o, " movq 12(%rax), %r9\n") // scale bytes 8..11 (+4 qs bytes never selected) 2840 out_str(c.o, " xorq %r10, %r10\n") 2841 var g: i64 = 0 2842 while g < 4 { 2843 let is0: i64 = g + g 2844 let is1: i64 = is0 + 1 2845 if g < 2 { 2846 // sub-blocks 0..3: sc = b[is] & 63, m = b[is+4] & 63 2847 out_str(c.o, " movq %r8, %r11\n") 2848 if is0 > 0 { out_str(c.o, " shrq $"); out_i64(c.o, is0 * 8); out_str(c.o, ", %r11\n") } 2849 out_str(c.o, " andq $63, %r11\n") 2850 out_str(c.o, " movq %r8, %rdi\n shrq $"); out_i64(c.o, (is0 + 4) * 8); out_str(c.o, ", %rdi\n andq $63, %rdi\n") 2851 out_str(c.o, " movq "); out_i64(c.o, is0 * 8); out_str(c.o, "(%rdx), %rbx\n imulq %rbx, %rdi\n addq %rdi, %r10\n") 2852 out_str(c.o, " movq %r8, %rdi\n shrq $"); out_i64(c.o, is1 * 8); out_str(c.o, ", %rdi\n andq $63, %rdi\n shlq $16, %rdi\n orq %rdi, %r11\n") 2853 out_str(c.o, " movq %r8, %rdi\n shrq $"); out_i64(c.o, (is1 + 4) * 8); out_str(c.o, ", %rdi\n andq $63, %rdi\n") 2854 out_str(c.o, " movq "); out_i64(c.o, is1 * 8); out_str(c.o, "(%rdx), %rbx\n imulq %rbx, %rdi\n addq %rdi, %r10\n") 2855 } else { 2856 // sub-blocks 4..7 (k = is-4): sc = ((b[k]>>6)&3)<<4 | (b[8+k] & 15), m = ((b[k+4]>>6)&3)<<4 | ((b[8+k]>>4) & 15) 2857 let k0: i64 = is0 - 4 2858 let k1: i64 = is1 - 4 2859 out_str(c.o, " movq %r8, %r11\n shrq $"); out_i64(c.o, k0 * 8 + 6); out_str(c.o, ", %r11\n andq $3, %r11\n shlq $4, %r11\n") 2860 out_str(c.o, " movq %r9, %rdi\n") 2861 if k0 > 0 { out_str(c.o, " shrq $"); out_i64(c.o, k0 * 8); out_str(c.o, ", %rdi\n") } 2862 out_str(c.o, " andq $15, %rdi\n orq %rdi, %r11\n") 2863 out_str(c.o, " movq %r8, %rdi\n shrq $"); out_i64(c.o, (k0 + 4) * 8 + 6); out_str(c.o, ", %rdi\n andq $3, %rdi\n shlq $4, %rdi\n") 2864 out_str(c.o, " movq %r9, %rbx\n shrq $"); out_i64(c.o, k0 * 8 + 4); out_str(c.o, ", %rbx\n andq $15, %rbx\n orq %rbx, %rdi\n") 2865 out_str(c.o, " movq "); out_i64(c.o, is0 * 8); out_str(c.o, "(%rdx), %rbx\n imulq %rbx, %rdi\n addq %rdi, %r10\n") 2866 out_str(c.o, " movq %r8, %rdi\n shrq $"); out_i64(c.o, k1 * 8 + 6); out_str(c.o, ", %rdi\n andq $3, %rdi\n shlq $4, %rdi\n") 2867 out_str(c.o, " movq %r9, %rbx\n shrq $"); out_i64(c.o, k1 * 8); out_str(c.o, ", %rbx\n andq $15, %rbx\n orq %rbx, %rdi\n shlq $16, %rdi\n orq %rdi, %r11\n") 2868 out_str(c.o, " movq %r8, %rdi\n shrq $"); out_i64(c.o, (k1 + 4) * 8 + 6); out_str(c.o, ", %rdi\n andq $3, %rdi\n shlq $4, %rdi\n") 2869 out_str(c.o, " movq %r9, %rbx\n shrq $"); out_i64(c.o, k1 * 8 + 4); out_str(c.o, ", %rbx\n andq $15, %rbx\n orq %rbx, %rdi\n") 2870 out_str(c.o, " movq "); out_i64(c.o, is1 * 8); out_str(c.o, "(%rdx), %rbx\n imulq %rbx, %rdi\n addq %rdi, %r10\n") 2871 } 2872 // scale broadcasts: ymm3 = sc_lo (r11 low word), ymm4 = sc_hi (r11 >> 16) 2873 out_str(c.o, " .byte 196, 193, 121, 110, 219\n") // C4 C1 79 6E DB vmovd %r11d,%xmm3 2874 out_str(c.o, " .byte 196, 226, 125, 121, 219\n") // vpbroadcastw %xmm3,%ymm3 2875 out_str(c.o, " shrq $16, %r11\n") 2876 out_str(c.o, " .byte 196, 193, 121, 110, 227\n") // C4 C1 79 6E E3 vmovd %r11d,%xmm4 2877 out_str(c.o, " .byte 196, 226, 125, 121, 228\n") // vpbroadcastw %xmm4,%ymm4 2878 // half 0: qs bytes 16+32g .. +15 -> lanes 0..15 of sub-block is0 (low nibbles, col +0) and is1 (high, col +64) 2879 out_str(c.o, " .byte 196, 226, 125, 48, 64, "); out_i64(c.o, 16 + 32 * g); out_str(c.o, "\n") // vpmovzxbw d8(%rax),%ymm0 2880 out_str(c.o, " .byte 197, 245, 113, 208, 4\n") // vpsrlw $4,%ymm0,%ymm1 2881 out_str(c.o, " .byte 197, 253, 219, 194\n") // vpand %ymm2,%ymm0,%ymm0 2882 out_str(c.o, " .byte 197, 253, 213, 195\n") // vpmullw %ymm3,%ymm0,%ymm0 2883 out_str(c.o, " .byte 197, 245, 213, 204\n") // vpmullw %ymm4,%ymm1,%ymm1 2884 out_str(c.o, " .byte 197, 253, 245, 65, 0\n") // C5 FD F5 41 00 vpmaddwd 0(%rcx),%ymm0,%ymm0 2885 out_str(c.o, " .byte 197, 213, 254, 232\n") // C5 D5 FE E8 vpaddd %ymm0,%ymm5,%ymm5 2886 out_str(c.o, " .byte 197, 245, 245, 73, 64\n") // C5 F5 F5 49 40 vpmaddwd 64(%rcx),%ymm1,%ymm1 2887 out_str(c.o, " .byte 197, 213, 254, 233\n") // C5 D5 FE E9 vpaddd %ymm1,%ymm5,%ymm5 2888 // half 1: qs bytes 32+32g .. +15 -> lanes 16..31 of is0 (col +32) and is1 (col +96) 2889 out_str(c.o, " .byte 196, 226, 125, 48, 64, "); out_i64(c.o, 32 + 32 * g); out_str(c.o, "\n") 2890 out_str(c.o, " .byte 197, 245, 113, 208, 4\n") 2891 out_str(c.o, " .byte 197, 253, 219, 194\n") 2892 out_str(c.o, " .byte 197, 253, 213, 195\n") 2893 out_str(c.o, " .byte 197, 245, 213, 204\n") 2894 out_str(c.o, " .byte 197, 253, 245, 65, 32\n") // vpmaddwd 32(%rcx),%ymm0,%ymm0 2895 out_str(c.o, " .byte 197, 213, 254, 232\n") 2896 out_str(c.o, " .byte 197, 245, 245, 73, 96\n") // vpmaddwd 96(%rcx),%ymm1,%ymm1 2897 out_str(c.o, " .byte 197, 213, 254, 233\n") 2898 out_str(c.o, " addq $128, %rcx\n") // next sub-block pair's 128 B of lanes 2899 g = g + 1 2900 } 2901 out_str(c.o, " .byte 197, 252, 17, 46\n") // C5 FC 11 2E vmovups %ymm5,(%rsi) acc lanes -> out[0..3] 2902 out_str(c.o, " movq %r10, 32(%rsi)\n") // dmin sum -> out[4] 2903 out_str(c.o, " popq %rbx\n") 2904 out_str(c.o, " movabsq $0, %rax\n") 2905 x86ctx_store_result(c, i.result, "rax" as *u8) 2906 return 0 2907} 2908 2909// __f32_i8dot32a(a:*i8[32], b:*f32[32]) -> f32 : AVX2 256-bit dequant-dot. 2910// 4 blocks of 8 lanes (vs the SSE version's 8 blocks of 4), and TWO 2911// accumulators (ymm4 blocks 0,2 ; ymm5 blocks 1,3) so the vaddps chains run 2912// in parallel instead of an 8-deep serial addps -- the gcc-proven codegen 2913// lever (SSE i8dot32 hit ~5 GB/s; gcc's vectorized code ~15-30). VEX .byte 2914// (DECIMAL; the __f32x8_dot precedent). Fixed regs: rax=a rcx=b ; ymm0 work 2915// (converted a), ymm1 (b), ymm4/ymm5 accs. NOT bit-identical to i8dot32 2916// (2-acc summation order); int8 products are exact in f32 so only the add 2917// ORDER differs -- argmax-robust, gated by nx_i8dot32a_kat + " Paris". 2918// __f32_i8fma32(a:*i8[32], b:*f32[32], d_bits:i64, acc:*f32[8]) -> 0. 2919// DEFERRED-HSUM block: acc[8] += d * (sext(a) . b), 8-lane AVX2, NO hsum. 2920// vfmadd231ps into a persistent 8-lane acc; the caller loops all k/32 blocks 2921// (broadcasting each block's scale d) then hsums the acc ONCE per output 2922// (__f32x8_hsum) -- kills 27/28 per-block hsums (the cold-forward matmul 2923// lever, 2026-07-10; the SSE/AVX2 per-32 hsum, not the dot width, was the 2924// bottleneck). regs: rax=a rcx=b rdx=d_bits rsi=acc ; ymm7=d broadcast, 2925// ymm6=acc, ymm0 work, ymm1 b. VEX .byte (decimal). 4 lanes-of-8 unrolled. 2926// unique loop label for the monolithic row kernel (fn name + result id), 2927// mirroring x86ctx_emit_clone_label. 2928func x86ctx_emit_q8row_label(c: *X86Ctx, rid: i64) -> i64 { 2929 out_str(c.o, ".Lq8row_") 2930 let name: *u8 = c.f.name_start as *u8 2931 if name != (0 as *u8) { out_str(c.o, name) } 2932 out_char(c.o, 0x5F) // '_' 2933 out_i64(c.o, rid) 2934 return 0 2935} 2936 2937// __f32_q8row_dot(qbuf_row:*u8, a_row:*f32, nblocks:i64) -> f32. 2938// MONOLITHIC Q8_0 row dot: acc = sum_b d_b * (sext(int8_b) . a_b), with the 2939// 8-lane ymm6 accumulator REGISTER-RESIDENT across the whole block loop (the 2940// deferred-hsum win the per-block __f32_i8fma32 couldn't get -- that pushed 2941// the acc through memory 28x/output). F16C vcvtph2ps decodes each block's 2942// f16 scale in ONE instruction. regs: rax=qbuf_row rcx=a_row rdx=nblocks ; 2943// ymm6=acc ymm7=d-broadcast ymm0/1 work. NOT bit-identical (8-lane+FMA 2944// order); argmax-robust. Requires AVX2+F16C (already assumed: vfmadd/vpmaddwd). 2945func x86ctx_emit_q8rowdot(c: *X86Ctx, i: *Instr) -> i64 { 2946 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // qbuf_row 2947 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // a_row 2948 x86ctx_load_value_v(c, i.op2, "rdx" as *u8) // nblocks 2949 // FOUR accumulators (ymm2/3/4/5), one per sub-block, to break the serial 2950 // vfmadd231ps chain (a single acc = 4*nblocks-deep = latency-bound). 2951 out_str(c.o, " .byte 197, 236, 87, 210\n") // vxorps %ymm2,%ymm2,%ymm2 2952 out_str(c.o, " .byte 197, 228, 87, 219\n") // vxorps %ymm3,%ymm3,%ymm3 2953 out_str(c.o, " .byte 197, 220, 87, 228\n") // vxorps %ymm4,%ymm4,%ymm4 2954 out_str(c.o, " .byte 197, 212, 87, 237\n") // vxorps %ymm5,%ymm5,%ymm5 2955 x86ctx_emit_q8row_label(c, i.result) 2956 out_str(c.o, ":\n") 2957 // f16 d at (%rax) -> broadcast into ymm7 (F16C, one instr). 2958 out_str(c.o, " .byte 196, 226, 121, 19, 56\n") // vcvtph2ps (%rax),%xmm7 (lane0=d) 2959 out_str(c.o, " .byte 196, 226, 125, 24, 255\n") // vbroadcastss %xmm7,%ymm7 2960 // 4 sub-blocks -> 4 accs: int8 at rax+2+j*8 ; f32 at rcx+j*32 ; accN += d*(int8.a) 2961 out_str(c.o, " .byte 196, 226, 125, 33, 64, 2\n"); out_str(c.o, " .byte 197, 252, 91, 192\n"); out_str(c.o, " .byte 197, 252, 16, 9\n"); out_str(c.o, " .byte 197, 252, 89, 193\n"); out_str(c.o, " .byte 196, 226, 125, 184, 215\n") 2962 out_str(c.o, " .byte 196, 226, 125, 33, 64, 10\n"); out_str(c.o, " .byte 197, 252, 91, 192\n"); out_str(c.o, " .byte 197, 252, 16, 73, 32\n"); out_str(c.o, " .byte 197, 252, 89, 193\n"); out_str(c.o, " .byte 196, 226, 125, 184, 223\n") 2963 out_str(c.o, " .byte 196, 226, 125, 33, 64, 18\n"); out_str(c.o, " .byte 197, 252, 91, 192\n"); out_str(c.o, " .byte 197, 252, 16, 73, 64\n"); out_str(c.o, " .byte 197, 252, 89, 193\n"); out_str(c.o, " .byte 196, 226, 125, 184, 231\n") 2964 out_str(c.o, " .byte 196, 226, 125, 33, 64, 26\n"); out_str(c.o, " .byte 197, 252, 91, 192\n"); out_str(c.o, " .byte 197, 252, 16, 73, 96\n"); out_str(c.o, " .byte 197, 252, 89, 193\n"); out_str(c.o, " .byte 196, 226, 125, 184, 239\n") 2965 out_str(c.o, " addq $34, %rax\n") // next block (34 bytes) 2966 out_str(c.o, " addq $128, %rcx\n") // next 32 f32 (128 bytes) 2967 out_str(c.o, " subq $1, %rdx\n") 2968 out_str(c.o, " jne ") 2969 x86ctx_emit_q8row_label(c, i.result) 2970 out_char(c.o, 0x0A) 2971 // combine 4 accs -> ymm2, then hsum. 2972 out_str(c.o, " .byte 197, 236, 88, 211\n") // vaddps %ymm3,%ymm2,%ymm2 2973 out_str(c.o, " .byte 197, 220, 88, 229\n") // vaddps %ymm5,%ymm4,%ymm4 2974 out_str(c.o, " .byte 197, 236, 88, 212\n") // vaddps %ymm4,%ymm2,%ymm2 2975 out_str(c.o, " .byte 197, 252, 40, 194\n") // vmovaps %ymm2,%ymm0 2976 out_str(c.o, " .byte 196, 227, 125, 25, 193, 1\n") // vextractf128 $1,%ymm0,%xmm1 2977 out_str(c.o, " .byte 197, 248, 119\n") // vzeroupper 2978 out_str(c.o, " addps %xmm1, %xmm0\n") 2979 out_str(c.o, " movups %xmm0, -16(%rsp)\n") 2980 out_str(c.o, " movss -16(%rsp), %xmm0\n") 2981 out_str(c.o, " movss -12(%rsp), %xmm1\n") 2982 out_str(c.o, " addss %xmm1, %xmm0\n") 2983 out_str(c.o, " movss -8(%rsp), %xmm1\n") 2984 out_str(c.o, " addss %xmm1, %xmm0\n") 2985 out_str(c.o, " movss -4(%rsp), %xmm1\n") 2986 out_str(c.o, " addss %xmm1, %xmm0\n") 2987 out_str(c.o, " movss %xmm0, -8(%rsp)\n") 2988 out_str(c.o, " movl -8(%rsp), %eax\n") 2989 x86ctx_store_result(c, i.result, "rax" as *u8) 2990 return 0 2991} 2992 2993func x86ctx_emit_i8fma32(c: *X86Ctx, i: *Instr) -> i64 { 2994 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // a 2995 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // b 2996 x86ctx_load_value_v(c, i.op2, "rdx" as *u8) // d_bits (f32 in low32) 2997 x86ctx_load_value_v(c, i.op3, "rsi" as *u8) // acc ptr 2998 out_str(c.o, " .byte 102, 15, 110, 250\n") // movd %edx,%xmm7 2999 out_str(c.o, " .byte 196, 226, 125, 24, 255\n") // vbroadcastss %xmm7,%ymm7 (d in 8 lanes) 3000 out_str(c.o, " .byte 197, 252, 16, 54\n") // vmovups (%rsi),%ymm6 (load acc) 3001 // 4 blocks of 8: products = int8.b ; acc += products * d 3002 out_str(c.o, " .byte 196, 226, 125, 33, 0\n"); out_str(c.o, " .byte 197, 252, 91, 192\n"); out_str(c.o, " .byte 197, 252, 16, 9\n"); out_str(c.o, " .byte 197, 252, 89, 193\n"); out_str(c.o, " .byte 196, 226, 125, 184, 247\n") 3003 out_str(c.o, " .byte 196, 226, 125, 33, 64, 8\n"); out_str(c.o, " .byte 197, 252, 91, 192\n"); out_str(c.o, " .byte 197, 252, 16, 73, 32\n"); out_str(c.o, " .byte 197, 252, 89, 193\n"); out_str(c.o, " .byte 196, 226, 125, 184, 247\n") 3004 out_str(c.o, " .byte 196, 226, 125, 33, 64, 16\n"); out_str(c.o, " .byte 197, 252, 91, 192\n"); out_str(c.o, " .byte 197, 252, 16, 73, 64\n"); out_str(c.o, " .byte 197, 252, 89, 193\n"); out_str(c.o, " .byte 196, 226, 125, 184, 247\n") 3005 out_str(c.o, " .byte 196, 226, 125, 33, 64, 24\n"); out_str(c.o, " .byte 197, 252, 91, 192\n"); out_str(c.o, " .byte 197, 252, 16, 73, 96\n"); out_str(c.o, " .byte 197, 252, 89, 193\n"); out_str(c.o, " .byte 196, 226, 125, 184, 247\n") 3006 out_str(c.o, " .byte 197, 252, 17, 54\n") // vmovups %ymm6,(%rsi) (store acc) 3007 out_str(c.o, " .byte 197, 248, 119\n") // vzeroupper 3008 out_str(c.o, " xorq %rax, %rax\n") 3009 x86ctx_store_result(c, i.result, "rax" as *u8) 3010 return 0 3011} 3012 3013func x86ctx_emit_i8dot32a(c: *X86Ctx, i: *Instr) -> i64 { 3014 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = a (i8 ptr) 3015 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // rcx = b (f32 ptr) 3016 out_str(c.o, " .byte 197, 220, 87, 228\n") // vxorps %ymm4,%ymm4,%ymm4 (acc0=0) 3017 out_str(c.o, " .byte 197, 212, 87, 237\n") // vxorps %ymm5,%ymm5,%ymm5 (acc1=0) 3018 // block j: vpmovsxbd (rax+j*8)->ymm0 ; vcvtdq2ps ; vmovups (rcx+j*32)->ymm1 ; 3019 // vmulps ymm1,ymm0,ymm0 ; vaddps ymm0,accN,accN. disp8: i8 j*8, f32 j*32. 3020 // -- block 0 -> acc0(ymm4) 3021 out_str(c.o, " .byte 196, 226, 125, 33, 0\n"); out_str(c.o, " .byte 197, 252, 91, 192\n"); out_str(c.o, " .byte 197, 252, 16, 9\n"); out_str(c.o, " .byte 197, 252, 89, 193\n"); out_str(c.o, " .byte 197, 220, 88, 224\n") 3022 // -- block 1 -> acc1(ymm5) 3023 out_str(c.o, " .byte 196, 226, 125, 33, 64, 8\n"); out_str(c.o, " .byte 197, 252, 91, 192\n"); out_str(c.o, " .byte 197, 252, 16, 73, 32\n"); out_str(c.o, " .byte 197, 252, 89, 193\n"); out_str(c.o, " .byte 197, 212, 88, 232\n") 3024 // -- block 2 -> acc0(ymm4) 3025 out_str(c.o, " .byte 196, 226, 125, 33, 64, 16\n"); out_str(c.o, " .byte 197, 252, 91, 192\n"); out_str(c.o, " .byte 197, 252, 16, 73, 64\n"); out_str(c.o, " .byte 197, 252, 89, 193\n"); out_str(c.o, " .byte 197, 220, 88, 224\n") 3026 // -- block 3 -> acc1(ymm5) 3027 out_str(c.o, " .byte 196, 226, 125, 33, 64, 24\n"); out_str(c.o, " .byte 197, 252, 91, 192\n"); out_str(c.o, " .byte 197, 252, 16, 73, 96\n"); out_str(c.o, " .byte 197, 252, 89, 193\n"); out_str(c.o, " .byte 197, 212, 88, 232\n") 3028 out_str(c.o, " .byte 197, 220, 88, 229\n") // vaddps %ymm5,%ymm4,%ymm4 (combine accs) 3029 out_str(c.o, " .byte 197, 252, 40, 196\n") // vmovaps %ymm4,%ymm0 (-> ymm0 for hsum) 3030 // hsum ymm0 -> eax : fold hi128+lo128 then SSE 4-lane hsum (the f32x8 tail). 3031 out_str(c.o, " .byte 196, 227, 125, 25, 193, 1\n") // vextractf128 $1,%ymm0,%xmm1 3032 out_str(c.o, " .byte 197, 248, 119\n") // vzeroupper 3033 out_str(c.o, " addps %xmm1, %xmm0\n") 3034 out_str(c.o, " movups %xmm0, -16(%rsp)\n") 3035 out_str(c.o, " movss -16(%rsp), %xmm0\n") 3036 out_str(c.o, " movss -12(%rsp), %xmm1\n") 3037 out_str(c.o, " addss %xmm1, %xmm0\n") 3038 out_str(c.o, " movss -8(%rsp), %xmm1\n") 3039 out_str(c.o, " addss %xmm1, %xmm0\n") 3040 out_str(c.o, " movss -4(%rsp), %xmm1\n") 3041 out_str(c.o, " addss %xmm1, %xmm0\n") 3042 out_str(c.o, " movss %xmm0, -8(%rsp)\n") 3043 out_str(c.o, " movl -8(%rsp), %eax\n") 3044 x86ctx_store_result(c, i.result, "rax" as *u8) 3045 return 0 3046} 3047 3048func x86ctx_emit_i8dot32(c: *X86Ctx, i: *Instr) -> i64 { 3049 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = a (i8 ptr) 3050 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // rcx = b (f32 ptr) 3051 out_str(c.o, " .byte 15, 87, 210\n") // xorps %xmm2,%xmm2 (acc=0) 3052 // 8 unrolled 4-lane blocks; i8 disp = b*4, f32 disp = b*16. 3053 // movd d(%rax),%xmm0 = 66 0F 6E 40 d ; pmovsxbd = 66 0F 38 21 C0 ; 3054 // cvtdq2ps = 0F 5B C0 ; movups d(%rcx),%xmm1 = 0F 10 49 d ; 3055 // mulps %xmm1,%xmm0 = 0F 59 C1 ; addps %xmm0,%xmm2 = 0F 58 D0 3056 out_str(c.o, " .byte 102, 15, 110, 64, 0\n"); out_str(c.o, " .byte 102, 15, 56, 33, 192\n"); out_str(c.o, " .byte 15, 91, 192\n"); out_str(c.o, " .byte 15, 16, 73, 0\n"); out_str(c.o, " .byte 15, 89, 193\n"); out_str(c.o, " .byte 15, 88, 208\n") 3057 out_str(c.o, " .byte 102, 15, 110, 64, 4\n"); out_str(c.o, " .byte 102, 15, 56, 33, 192\n"); out_str(c.o, " .byte 15, 91, 192\n"); out_str(c.o, " .byte 15, 16, 73, 16\n"); out_str(c.o, " .byte 15, 89, 193\n"); out_str(c.o, " .byte 15, 88, 208\n") 3058 out_str(c.o, " .byte 102, 15, 110, 64, 8\n"); out_str(c.o, " .byte 102, 15, 56, 33, 192\n"); out_str(c.o, " .byte 15, 91, 192\n"); out_str(c.o, " .byte 15, 16, 73, 32\n"); out_str(c.o, " .byte 15, 89, 193\n"); out_str(c.o, " .byte 15, 88, 208\n") 3059 out_str(c.o, " .byte 102, 15, 110, 64, 12\n"); out_str(c.o, " .byte 102, 15, 56, 33, 192\n"); out_str(c.o, " .byte 15, 91, 192\n"); out_str(c.o, " .byte 15, 16, 73, 48\n"); out_str(c.o, " .byte 15, 89, 193\n"); out_str(c.o, " .byte 15, 88, 208\n") 3060 out_str(c.o, " .byte 102, 15, 110, 64, 16\n"); out_str(c.o, " .byte 102, 15, 56, 33, 192\n"); out_str(c.o, " .byte 15, 91, 192\n"); out_str(c.o, " .byte 15, 16, 73, 64\n"); out_str(c.o, " .byte 15, 89, 193\n"); out_str(c.o, " .byte 15, 88, 208\n") 3061 out_str(c.o, " .byte 102, 15, 110, 64, 20\n"); out_str(c.o, " .byte 102, 15, 56, 33, 192\n"); out_str(c.o, " .byte 15, 91, 192\n"); out_str(c.o, " .byte 15, 16, 73, 80\n"); out_str(c.o, " .byte 15, 89, 193\n"); out_str(c.o, " .byte 15, 88, 208\n") 3062 out_str(c.o, " .byte 102, 15, 110, 64, 24\n"); out_str(c.o, " .byte 102, 15, 56, 33, 192\n"); out_str(c.o, " .byte 15, 91, 192\n"); out_str(c.o, " .byte 15, 16, 73, 96\n"); out_str(c.o, " .byte 15, 89, 193\n"); out_str(c.o, " .byte 15, 88, 208\n") 3063 out_str(c.o, " .byte 102, 15, 110, 64, 28\n"); out_str(c.o, " .byte 102, 15, 56, 33, 192\n"); out_str(c.o, " .byte 15, 91, 192\n"); out_str(c.o, " .byte 15, 16, 73, 112\n"); out_str(c.o, " .byte 15, 89, 193\n"); out_str(c.o, " .byte 15, 88, 208\n") 3064 out_str(c.o, " .byte 15, 40, 194\n") // movaps %xmm2,%xmm0 (acc -> xmm0 for the hsum) 3065 // hsum xmm0 -> eax (the __f32x4_dot tail, AT&T; nxasm-proven). 3066 out_str(c.o, " movups %xmm0, -16(%rsp)\n") 3067 out_str(c.o, " movss -16(%rsp), %xmm0\n") 3068 out_str(c.o, " movss -12(%rsp), %xmm1\n") 3069 out_str(c.o, " addss %xmm1, %xmm0\n") 3070 out_str(c.o, " movss -8(%rsp), %xmm1\n") 3071 out_str(c.o, " addss %xmm1, %xmm0\n") 3072 out_str(c.o, " movss -4(%rsp), %xmm1\n") 3073 out_str(c.o, " addss %xmm1, %xmm0\n") 3074 out_str(c.o, " movss %xmm0, -8(%rsp)\n") 3075 out_str(c.o, " movl -8(%rsp), %eax\n") 3076 x86ctx_store_result(c, i.result, "rax" as *u8) 3077 return 0 3078} 3079 3080func x86ctx_emit_f32x8_dot(c: *X86Ctx, i: *Instr) -> i64 { 3081 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = a_ptr (8 f32) 3082 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // rcx = b_ptr (8 f32) 3083 // nxasm .byte parses DECIMAL ONLY (axc_emit_bytes: v*10+d), so VEX bytes are emitted as decimal, 3084 // NOT 0x-hex (which would parse as just "0" and silently truncate the whole instruction -> SIGSEGV). 3085 out_str(c.o, " .byte 197, 252, 16, 0\n") // C5 FC 10 00 vmovups (%rax),%ymm0 3086 out_str(c.o, " .byte 197, 252, 16, 9\n") // C5 FC 10 09 vmovups (%rcx),%ymm1 3087 out_str(c.o, " .byte 197, 252, 89, 193\n") // C5 FC 59 C1 vmulps %ymm1,%ymm0,%ymm0 3088 out_str(c.o, " .byte 196, 227, 125, 25, 193, 1\n") // C4 E3 7D 19 C1 01 vextractf128 $1,%ymm0,%xmm1 3089 out_str(c.o, " .byte 197, 248, 119\n") // C5 F8 77 vzeroupper (clear AVX-SSE transition) 3090 out_str(c.o, " addps %xmm1, %xmm0\n") // xmm0 = lo4 + hi4 = 4 partial sums 3091 out_str(c.o, " movups %xmm0, -16(%rsp)\n") 3092 out_str(c.o, " movss -16(%rsp), %xmm0\n") 3093 out_str(c.o, " movss -12(%rsp), %xmm1\n") 3094 out_str(c.o, " addss %xmm1, %xmm0\n") 3095 out_str(c.o, " movss -8(%rsp), %xmm1\n") 3096 out_str(c.o, " addss %xmm1, %xmm0\n") 3097 out_str(c.o, " movss -4(%rsp), %xmm1\n") 3098 out_str(c.o, " addss %xmm1, %xmm0\n") 3099 out_str(c.o, " movss %xmm0, -8(%rsp)\n") 3100 out_str(c.o, " movl -8(%rsp), %eax\n") 3101 x86ctx_store_result(c, i.result, "rax" as *u8) 3102 return 0 3103} 3104 3105// FMA vector-accumulate (AVX2): *acc += a*b 8-wide FUSED (vfmadd231ps). op0=acc op1=a op2=b (pointers). 3106// Accumulator round-trips memory each call (L1-hot) but there is NO per-chunk hsum -- deferred to 3107// x86ctx_emit_f32x8_hsum, ONE hsum per dot. Pure AVX in the inner loop -> no AVX-SSE transition penalty. 3108func x86ctx_emit_f32x8_fma(c: *X86Ctx, i: *Instr) -> i64 { 3109 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = acc ptr 3110 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // rcx = a ptr 3111 x86ctx_load_value_v(c, i.op2, "rdx" as *u8) // rdx = b ptr 3112 out_str(c.o, " .byte 197, 252, 16, 0\n") // C5 FC 10 00 vmovups (%rax),%ymm0 (acc) 3113 out_str(c.o, " .byte 197, 252, 16, 9\n") // C5 FC 10 09 vmovups (%rcx),%ymm1 (a) 3114 out_str(c.o, " .byte 197, 252, 16, 18\n") // C5 FC 10 12 vmovups (%rdx),%ymm2 (b) 3115 out_str(c.o, " .byte 196, 226, 117, 184, 194\n") // C4 E2 75 B8 C2 vfmadd231ps %ymm2,%ymm1,%ymm0 3116 out_str(c.o, " .byte 197, 252, 17, 0\n") // C5 FC 11 00 vmovups %ymm0,(%rax) (store acc) 3117 x86ctx_store_result(c, i.result, "rax" as *u8) 3118 return 0 3119} 3120// horizontal sum of an 8-wide accumulator -> f32 scalar. op0 = acc ptr. vextractf128 folds hi4+lo4, SSE hsum. 3121func x86ctx_emit_f32x8_hsum(c: *X86Ctx, i: *Instr) -> i64 { 3122 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = acc ptr 3123 out_str(c.o, " .byte 197, 252, 16, 0\n") // C5 FC 10 00 vmovups (%rax),%ymm0 3124 out_str(c.o, " .byte 196, 227, 125, 25, 193, 1\n") // C4 E3 7D 19 C1 01 vextractf128 $1,%ymm0,%xmm1 3125 out_str(c.o, " .byte 197, 248, 119\n") // C5 F8 77 vzeroupper 3126 out_str(c.o, " addps %xmm1, %xmm0\n") 3127 out_str(c.o, " movups %xmm0, -16(%rsp)\n") 3128 out_str(c.o, " movss -16(%rsp), %xmm0\n") 3129 out_str(c.o, " movss -12(%rsp), %xmm1\n") 3130 out_str(c.o, " addss %xmm1, %xmm0\n") 3131 out_str(c.o, " movss -8(%rsp), %xmm1\n") 3132 out_str(c.o, " addss %xmm1, %xmm0\n") 3133 out_str(c.o, " movss -4(%rsp), %xmm1\n") 3134 out_str(c.o, " addss %xmm1, %xmm0\n") 3135 out_str(c.o, " movss %xmm0, -8(%rsp)\n") 3136 out_str(c.o, " movl -8(%rsp), %eax\n") 3137 x86ctx_store_result(c, i.result, "rax" as *u8) 3138 return 0 3139} 3140 3141// NO-FLOAT integer madd-accumulate (AVX2): *acc(i32x8) += vpmaddwd(a(i16x16), b(i16x16)). op0=acc op1=a 3142// op2=b. vpmaddwd does 16 SIGNED int16 multiplies + pairwise adds -> 8 int32; vpaddd accumulates into the 3143// int32 vector. EXACT + DETERMINISTIC (integer add is associative) -- the no-float compute lever. The 3144// final int64 hsum of the 8 int32 lanes is plain scalar code (caller), no overflow, no hsum intrinsic needed. 3145func x86ctx_emit_i16dot_label(c: *X86Ctx, rid: i64) -> i64 { 3146 out_str(c.o, ".Li16dot_") 3147 let name: *u8 = c.f.name_start as *u8 3148 if name != (0 as *u8) { out_str(c.o, name) } 3149 out_char(c.o, 0x5F) // '_' 3150 out_i64(c.o, rid) 3151 return 0 3152} 3153 3154// __i16_dot(a:*i16[n], b:*i16[n], n) -> i64 (R0r-b). ONE loop over the whole chunk with the int32x8 accumulator in 3155// %ymm0 -- one vmovups, one vpmaddwd straight from memory, one vpaddd per 16 lanes -- then a single store to the red 3156// zone and an int64 horizontal sum of the eight lanes (movslq each, so the sum is exact past int32). The per-madd 3157// builtin OP_I16X16_MADD loads and stores its accumulator on every call, which is the round trip that bounded the 3158// batched prefill kernel at 450 us per row. regs: rax=a rcx=b rdx=n ; ymm0=acc ymm1=work. n is a POSITIVE multiple 3159// of 16 by contract (a do-while, so n=0 would wrap); the caller bounds n so no int32 lane overflows. AVX2 only. 3160// __q8blk_i16dot(codes:*i8[32], x:*i16[32]) -> i64 (search R0s-b): one Q8_0 block's dot with i16 activation lanes, 3161// the int8 codes sign-extended IN REGISTER (vpmovsxbw) so the block-native kernel never stores an i16 copy of the 3162// weights: two 16-lane halves, each vpmaddwd against its 16 lanes of x, vpaddd, then the same widened int64 3163// horizontal sum __i16_dot uses. EXACT + deterministic: an int32 lane holds two products of at most 127*32767 per 3164// half, four after the add, far inside int32. regs: rax=codes rcx=x ; ymm1/ymm2 halves, ymm0 sum. AVX2 only. 3165// No loop, no counter; encodings mirror x86ctx_emit_i8dot32a (vpmovsx from a disp8 base) and x86ctx_emit_i16dot. 3166func x86ctx_emit_q8blkdot(c: *X86Ctx, i: *Instr) -> i64 { 3167 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = codes (int8 lanes) 3168 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // rcx = x (i16 lanes) 3169 out_str(c.o, " .byte 196, 226, 125, 32, 8\n") // C4 E2 7D 20 08 vpmovsxbw (%rax),%ymm1 (codes 0..15 -> i16) 3170 out_str(c.o, " .byte 197, 245, 245, 9\n") // C5 F5 F5 09 vpmaddwd (%rcx),%ymm1,%ymm1 (x 0..15) 3171 out_str(c.o, " .byte 196, 226, 125, 32, 80, 16\n") // C4 E2 7D 20 50 10 vpmovsxbw 16(%rax),%ymm2 (codes 16..31 -> i16) 3172 out_str(c.o, " .byte 197, 237, 245, 81, 32\n") // C5 ED F5 51 20 vpmaddwd 32(%rcx),%ymm2,%ymm2 (x 16..31) 3173 out_str(c.o, " .byte 197, 245, 254, 194\n") // C5 F5 FE C2 vpaddd %ymm2,%ymm1,%ymm0 3174 out_str(c.o, " .byte 197, 252, 17, 68, 36, 224\n") // C5 FC 11 44 24 E0 vmovups %ymm0,-32(%rsp) (red zone) 3175 out_str(c.o, " .byte 197, 248, 119\n") // vzeroupper 3176 out_str(c.o, " .byte 72, 99, 68, 36, 224\n") // movslq -32(%rsp),%rax lane 0 3177 out_str(c.o, " .byte 72, 99, 76, 36, 228\n") // movslq -28(%rsp),%rcx lane 1 3178 out_str(c.o, " .byte 72, 1, 200\n") // addq %rcx,%rax 3179 out_str(c.o, " .byte 72, 99, 76, 36, 232\n") // lane 2 3180 out_str(c.o, " .byte 72, 1, 200\n") 3181 out_str(c.o, " .byte 72, 99, 76, 36, 236\n") // lane 3 3182 out_str(c.o, " .byte 72, 1, 200\n") 3183 out_str(c.o, " .byte 72, 99, 76, 36, 240\n") // lane 4 3184 out_str(c.o, " .byte 72, 1, 200\n") 3185 out_str(c.o, " .byte 72, 99, 76, 36, 244\n") // lane 5 3186 out_str(c.o, " .byte 72, 1, 200\n") 3187 out_str(c.o, " .byte 72, 99, 76, 36, 248\n") // lane 6 3188 out_str(c.o, " .byte 72, 1, 200\n") 3189 out_str(c.o, " .byte 72, 99, 76, 36, 252\n") // lane 7 3190 out_str(c.o, " .byte 72, 1, 200\n") 3191 x86ctx_store_result(c, i.result, "rax" as *u8) 3192 return 0 3193} 3194 3195func x86ctx_emit_i16dot(c: *X86Ctx, i: *Instr) -> i64 { 3196 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = a (i16 lanes) 3197 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // rcx = b (i16 lanes) 3198 x86ctx_load_value_v(c, i.op2, "rdx" as *u8) // rdx = n lanes 3199 out_str(c.o, " .byte 197, 253, 239, 192\n") // C5 FD EF C0 vpxor %ymm0,%ymm0,%ymm0 (acc = 0) 3200 x86ctx_emit_i16dot_label(c, i.result) 3201 out_str(c.o, ":\n") 3202 out_str(c.o, " .byte 197, 252, 16, 8\n") // C5 FC 10 08 vmovups (%rax),%ymm1 (16 lanes of a) 3203 out_str(c.o, " .byte 197, 245, 245, 9\n") // C5 F5 F5 09 vpmaddwd (%rcx),%ymm1,%ymm1 (8 int32 pair sums) 3204 out_str(c.o, " .byte 197, 253, 254, 193\n") // C5 FD FE C1 vpaddd %ymm1,%ymm0,%ymm0 (acc +=) 3205 out_str(c.o, " addq $32, %rax\n") // next 16 lanes of a 3206 out_str(c.o, " addq $32, %rcx\n") // next 16 lanes of b 3207 out_str(c.o, " subq $16, %rdx\n") 3208 out_str(c.o, " jne ") 3209 x86ctx_emit_i16dot_label(c, i.result) 3210 out_char(c.o, 0x0A) 3211 out_str(c.o, " .byte 197, 252, 17, 68, 36, 224\n") // C5 FC 11 44 24 E0 vmovups %ymm0,-32(%rsp) (red zone) 3212 out_str(c.o, " .byte 197, 248, 119\n") // vzeroupper 3213 out_str(c.o, " .byte 72, 99, 68, 36, 224\n") // movslq -32(%rsp),%rax lane 0 3214 out_str(c.o, " .byte 72, 99, 76, 36, 228\n") // movslq -28(%rsp),%rcx lane 1 3215 out_str(c.o, " .byte 72, 1, 200\n") // addq %rcx,%rax 3216 out_str(c.o, " .byte 72, 99, 76, 36, 232\n") // lane 2 3217 out_str(c.o, " .byte 72, 1, 200\n") 3218 out_str(c.o, " .byte 72, 99, 76, 36, 236\n") // lane 3 3219 out_str(c.o, " .byte 72, 1, 200\n") 3220 out_str(c.o, " .byte 72, 99, 76, 36, 240\n") // lane 4 3221 out_str(c.o, " .byte 72, 1, 200\n") 3222 out_str(c.o, " .byte 72, 99, 76, 36, 244\n") // lane 5 3223 out_str(c.o, " .byte 72, 1, 200\n") 3224 out_str(c.o, " .byte 72, 99, 76, 36, 248\n") // lane 6 3225 out_str(c.o, " .byte 72, 1, 200\n") 3226 out_str(c.o, " .byte 72, 99, 76, 36, 252\n") // lane 7 3227 out_str(c.o, " .byte 72, 1, 200\n") 3228 x86ctx_store_result(c, i.result, "rax" as *u8) 3229 return 0 3230} 3231 3232func x86ctx_emit_i16x16_madd(c: *X86Ctx, i: *Instr) -> i64 { 3233 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = acc (i32x8) 3234 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // rcx = a (i16x16) 3235 x86ctx_load_value_v(c, i.op2, "rdx" as *u8) // rdx = b (i16x16) 3236 out_str(c.o, " .byte 197, 252, 16, 0\n") // C5 FC 10 00 vmovups (%rax),%ymm0 (acc) 3237 out_str(c.o, " .byte 197, 252, 16, 9\n") // C5 FC 10 09 vmovups (%rcx),%ymm1 (a) 3238 out_str(c.o, " .byte 197, 252, 16, 18\n") // C5 FC 10 12 vmovups (%rdx),%ymm2 (b) 3239 out_str(c.o, " .byte 197, 245, 245, 202\n") // C5 F5 F5 CA vpmaddwd %ymm2,%ymm1,%ymm1 (16 int16 -> 8 int32) 3240 out_str(c.o, " .byte 197, 253, 254, 193\n") // C5 FD FE C1 vpaddd %ymm1,%ymm0,%ymm0 (acc += ) 3241 out_str(c.o, " .byte 197, 252, 17, 0\n") // C5 FC 11 00 vmovups %ymm0,(%rax) (store acc) 3242 x86ctx_store_result(c, i.result, "rax" as *u8) 3243 return 0 3244} 3245 3246// AES-NI: encrypt the 16-byte block at op0 (state ptr) IN PLACE using the 11 expanded 3247// round keys (176B) at op1 (roundkeys ptr). State lives in %xmm0 across all 10 rounds 3248// (each round key streamed into %xmm1); ~hardware speed vs the ~0.7 MB/s software path. 3249func x86ctx_emit_aesni(c: *X86Ctx, i: *Instr) -> i64 { 3250 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = state ptr (in/out) 3251 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // rcx = round keys ptr 3252 out_str(c.o, " movdqu (%rax), %xmm0\n") 3253 out_str(c.o, " movdqu (%rcx), %xmm1\n") 3254 out_str(c.o, " pxor %xmm1, %xmm0\n") // AddRoundKey rk0 3255 out_str(c.o, " movdqu 16(%rcx), %xmm1\n"); out_str(c.o, " aesenc %xmm1, %xmm0\n") 3256 out_str(c.o, " movdqu 32(%rcx), %xmm1\n"); out_str(c.o, " aesenc %xmm1, %xmm0\n") 3257 out_str(c.o, " movdqu 48(%rcx), %xmm1\n"); out_str(c.o, " aesenc %xmm1, %xmm0\n") 3258 out_str(c.o, " movdqu 64(%rcx), %xmm1\n"); out_str(c.o, " aesenc %xmm1, %xmm0\n") 3259 out_str(c.o, " movdqu 80(%rcx), %xmm1\n"); out_str(c.o, " aesenc %xmm1, %xmm0\n") 3260 out_str(c.o, " movdqu 96(%rcx), %xmm1\n"); out_str(c.o, " aesenc %xmm1, %xmm0\n") 3261 out_str(c.o, " movdqu 112(%rcx), %xmm1\n"); out_str(c.o, " aesenc %xmm1, %xmm0\n") 3262 out_str(c.o, " movdqu 128(%rcx), %xmm1\n"); out_str(c.o, " aesenc %xmm1, %xmm0\n") 3263 out_str(c.o, " movdqu 144(%rcx), %xmm1\n"); out_str(c.o, " aesenc %xmm1, %xmm0\n") 3264 out_str(c.o, " movdqu 160(%rcx), %xmm1\n"); out_str(c.o, " aesenclast %xmm1, %xmm0\n") 3265 out_str(c.o, " movdqu %xmm0, (%rax)\n") // store encrypted block in place 3266 out_str(c.o, " xorq %rax, %rax\n") 3267 x86ctx_store_result(c, i.result, "rax" as *u8) 3268 return 0 3269} 3270 3271// Emit `palignr $imm,%xmmSRC,%xmmDST` (SRC,DST both 0..7) as raw bytes. palignr is 3272// 66 0F 3A 0F /r ib and is NOT a mnemonic the sovereign assembler recognises, so -- exactly 3273// like the AVX2 vpmaddwd path above emits VEX ops via .byte -- we encode it directly. For 3274// xmm0..xmm7 no REX is needed: ModRM(11,dst,src) = 192 | (dst<<3) | src. This is the ONLY 3275// SHA-NI instruction that needs .byte; all the others (movdqu/movdqa/pshufd/pshufb/paddd/ 3276// punpck*/sha256msg1/msg2/rnds2) are assembler mnemonics. 3277func x86ctx_emit_palignr(c: *X86Ctx, dst: i64, src: i64, imm: i64) -> i64 { 3278 let modrm: i64 = 192 + (dst * 8) + src 3279 out_str(c.o, " .byte 102, 15, 58, 15, ") 3280 out_i64(c.o, modrm) 3281 out_str(c.o, ", ") 3282 out_i64(c.o, imm) 3283 out_char(c.o, 0x0A) 3284 return 0 3285} 3286 3287// SHA-NI: one full SHA-256 block compression IN PLACE (the Intel SHA extension). op0=state ptr 3288// (8 contiguous u32 = working state a..h), op1=block ptr (64 raw big-endian message bytes), 3289// op2=K ptr (64 contiguous u32 round constants). Register plan mirrors the AES emitter: rax= 3290// state, rcx=block, rdx=K; r8 = scratch GPR for the byte-swap mask build. xmm layout: MSG=xmm0, 3291// STATE0=xmm1, STATE1=xmm2, MSG0..3=xmm3..6, TMP=xmm7, SHUF=xmm8, ABEF_SAVE=xmm9, CDGH_SAVE=xmm10. 3292// Operand order transcribed from the canonical Intel/Linux SHA-NI sequence; validated bit-exact 3293// against the software sha256_compress oracle (nx_shani_block_probe + NIST KAT). ~hardware speed. 3294func x86ctx_emit_sha256_ni(c: *X86Ctx, i: *Instr) -> i64 { 3295 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = state ptr (in/out) 3296 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // rcx = block ptr 3297 x86ctx_load_value_v(c, i.op2, "rdx" as *u8) // rdx = K table ptr (64 u32) 3298 // --- build the big-endian byte-swap mask 0x0c0d0e0f08090a0b0405060700010203 on the stack --- 3299 // (there is no RIP-relative movdqu in the assembler, so materialise via two 64-bit movabsq 3300 // halves + movq stores, then movdqu into xmm8. r8 is caller-saved scratch, not a home reg.) 3301 out_str(c.o, " subq $16, %rsp\n") 3302 out_str(c.o, " movabsq $289644378169868803, %r8\n") // 0x0405060700010203 (bytes 0..7) 3303 out_str(c.o, " movq %r8, (%rsp)\n") 3304 out_str(c.o, " movabsq $868365760874482187, %r8\n") // 0x0c0d0e0f08090a0b (bytes 8..15) 3305 out_str(c.o, " movq %r8, 8(%rsp)\n") 3306 out_str(c.o, " movdqu (%rsp), %xmm8\n") // SHUF mask 3307 out_str(c.o, " addq $16, %rsp\n") 3308 // --- load + arrange state (memory a b c d | e f g h -> STATE0=ABEF, STATE1=CDGH) --- 3309 out_str(c.o, " movdqu (%rax), %xmm1\n") // STATE0 = DCBA (a b c d) 3310 out_str(c.o, " movdqu 16(%rax), %xmm2\n") // STATE1 = HGFE (e f g h) 3311 out_str(c.o, " movdqa %xmm1, %xmm7\n") // TMP = STATE0 3312 out_str(c.o, " punpcklqdq %xmm2, %xmm1\n") // STATE0 = FEBA 3313 out_str(c.o, " punpckhqdq %xmm7, %xmm2\n") // STATE1 = DCHG 3314 out_str(c.o, " pshufd $27, %xmm1, %xmm1\n") // 0x1B: STATE0 = ABEF 3315 out_str(c.o, " pshufd $177, %xmm2, %xmm2\n") // 0xB1: STATE1 = CDGH 3316 out_str(c.o, " movdqa %xmm1, %xmm9\n") // ABEF_SAVE 3317 out_str(c.o, " movdqa %xmm2, %xmm10\n") // CDGH_SAVE 3318 // --- load message quads + byte-swap to big-endian --- 3319 out_str(c.o, " movdqu (%rcx), %xmm3\n"); out_str(c.o, " pshufb %xmm8, %xmm3\n") // W0..3 3320 out_str(c.o, " movdqu 16(%rcx), %xmm4\n"); out_str(c.o, " pshufb %xmm8, %xmm4\n") // W4..7 3321 out_str(c.o, " movdqu 32(%rcx), %xmm5\n"); out_str(c.o, " pshufb %xmm8, %xmm5\n") // W8..11 3322 out_str(c.o, " movdqu 48(%rcx), %xmm6\n"); out_str(c.o, " pshufb %xmm8, %xmm6\n") // W12..15 3323 // --- 16 quad-steps, uniform macro --- 3324 var q: i64 = 0 3325 while q < 16 { 3326 let m0: i64 = 3 + (q & 3) 3327 let m1: i64 = 3 + ((q + 1) & 3) 3328 let m2: i64 = 3 + ((q + 2) & 3) 3329 let m3: i64 = 3 + ((q + 3) & 3) 3330 let koff: i64 = q * 16 3331 // TMP = K[q..] ; TMP += m0 3332 out_str(c.o, " movdqu ") 3333 out_i64(c.o, koff) 3334 out_str(c.o, "(%rdx), %xmm7\n") 3335 x86ctx_emit_sha_paddd(c, 7, m0) // paddd %m0, %xmm7 (TMP = m0 + K) 3336 if q < 12 { 3337 x86ctx_emit_sha_msg1(c, m1, m0) // sha256msg1 %m1, %m0 3338 } 3339 x86ctx_emit_sha_movdqa(c, 0, 7) // movdqa %xmm7, %xmm0 (MSG = TMP) 3340 out_str(c.o, " sha256rnds2 %xmm1, %xmm2\n") // STATE1 = rnds2(STATE1,STATE0,MSG) 3341 out_str(c.o, " pshufd $14, %xmm7, %xmm0\n") // 0x0E: MSG = high 2 dwords of TMP 3342 out_str(c.o, " sha256rnds2 %xmm2, %xmm1\n") // STATE0 = rnds2(STATE0,STATE1,MSG) 3343 if q < 12 { 3344 x86ctx_emit_sha_movdqa(c, 7, m3) // movdqa %m3, %xmm7 (TMP = m3) 3345 x86ctx_emit_palignr(c, 7, m2, 4) // palignr $4, %m2, %xmm7 3346 x86ctx_emit_sha_paddd(c, m0, 7) // paddd %xmm7, %m0 (m0 += TMP) 3347 x86ctx_emit_sha_msg2(c, m3, m0) // sha256msg2 %m3, %m0 3348 } 3349 q = q + 1 3350 } 3351 // --- add saved state back --- 3352 out_str(c.o, " paddd %xmm9, %xmm1\n") // STATE0 += ABEF_SAVE 3353 out_str(c.o, " paddd %xmm10, %xmm2\n") // STATE1 += CDGH_SAVE 3354 // --- unshuffle + store (inverse of the arrange) --- 3355 out_str(c.o, " movdqa %xmm1, %xmm7\n") // TMP = STATE0 (ABEF) 3356 out_str(c.o, " punpcklqdq %xmm2, %xmm1\n") // STATE0 = GHEF 3357 out_str(c.o, " punpckhqdq %xmm7, %xmm2\n") // STATE1 = ABCD 3358 out_str(c.o, " pshufd $177, %xmm1, %xmm1\n") // 0xB1: STATE0 = HGFE 3359 out_str(c.o, " pshufd $27, %xmm2, %xmm2\n") // 0x1B: STATE1 = DCBA 3360 out_str(c.o, " movdqu %xmm2, (%rax)\n") // state[0..3] = a b c d 3361 out_str(c.o, " movdqu %xmm1, 16(%rax)\n") // state[4..7] = e f g h 3362 out_str(c.o, " xorq %rax, %rax\n") 3363 x86ctx_store_result(c, i.result, "rax" as *u8) 3364 return 0 3365} 3366 3367// Small helpers so the SHA-NI emitter can address xmm3..xmm10 by register NUMBER (the AES/madd 3368// paths only ever name fixed regs). Each formats one AT&T SSE mnemonic with numeric xmm operands. 3369func x86ctx_emit_sha_paddd(c: *X86Ctx, dst: i64, src: i64) -> i64 { 3370 out_str(c.o, " paddd %xmm"); out_i64(c.o, src) 3371 out_str(c.o, ", %xmm"); out_i64(c.o, dst); out_char(c.o, 0x0A) 3372 return 0 3373} 3374func x86ctx_emit_sha_movdqa(c: *X86Ctx, dst: i64, src: i64) -> i64 { 3375 out_str(c.o, " movdqa %xmm"); out_i64(c.o, src) 3376 out_str(c.o, ", %xmm"); out_i64(c.o, dst); out_char(c.o, 0x0A) 3377 return 0 3378} 3379func x86ctx_emit_sha_msg1(c: *X86Ctx, src: i64, dst: i64) -> i64 { 3380 out_str(c.o, " sha256msg1 %xmm"); out_i64(c.o, src) 3381 out_str(c.o, ", %xmm"); out_i64(c.o, dst); out_char(c.o, 0x0A) 3382 return 0 3383} 3384func x86ctx_emit_sha_msg2(c: *X86Ctx, src: i64, dst: i64) -> i64 { 3385 out_str(c.o, " sha256msg2 %xmm"); out_i64(c.o, src) 3386 out_str(c.o, ", %xmm"); out_i64(c.o, dst); out_char(c.o, 0x0A) 3387 return 0 3388} 3389 3390// Hardware CLMUL (PCLMULQDQ): carry-less-multiply a selected 64-bit half of *op0 by a 3391// selected half of *op1; the 128-bit product is written back to *op0 IN PLACE. imm is the 3392// PCLMULQDQ half-select emitted in DECIMAL (the sovereign assembler's $imm parser is decimal 3393// only): 0 (0x00)=op0.lo*op1.lo, 17 (0x11)=op0.hi*op1.hi, 16 (0x10)=op0.lo*op1.hi, 3394// 1 (0x01)=op0.hi*op1.lo. xmm0/xmm1 are outside the GPR allocator; rax/rcx are caller-saved 3395// scratch (same as the f32/AES paths). Dispatch sets G1_RAX_SLOT=-1 first to invalidate the 3396// rax-tracking peephole. This is the GHASH/GF(2^128) accelerator -- fast AES-GCM auth. 3397func x86ctx_emit_clmul(c: *X86Ctx, i: *Instr, imm: i64) -> i64 { 3398 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = p ptr (in/out: holds operand a) 3399 x86ctx_load_value_v(c, i.op1, "rcx" as *u8) // rcx = q ptr (operand b) 3400 out_str(c.o, " movdqu (%rax), %xmm0\n") // xmm0 = *p (src1) 3401 out_str(c.o, " movdqu (%rcx), %xmm1\n") // xmm1 = *q (src2) 3402 if imm == 0 { out_str(c.o, " pclmulqdq $0, %xmm1, %xmm0\n") } 3403 if imm == 17 { out_str(c.o, " pclmulqdq $17, %xmm1, %xmm0\n") } 3404 if imm == 16 { out_str(c.o, " pclmulqdq $16, %xmm1, %xmm0\n") } 3405 if imm == 1 { out_str(c.o, " pclmulqdq $1, %xmm1, %xmm0\n") } 3406 out_str(c.o, " movdqu %xmm0, (%rax)\n") // *p = 128-bit carry-less product 3407 out_str(c.o, " xorq %rax, %rax\n") 3408 x86ctx_store_result(c, i.result, "rax" as *u8) 3409 return 0 3410} 3411 3412// Fused 4x64-limb wide multiply (__mul256_wide): *dst(u64[8]) = *a(u64[4]) * *b(u64[4]). 3413// The ADX/BMI2 dual-carry-chain schoolbook kernel -- MULX (flags-free 64x64->128) feeds TWO 3414// independent carry chains: ADCX accumulates the low halves via CF, ADOX the high halves via OF, 3415// so both carries propagate in parallel with no software carry (the exact win the plain 4x64 mulq 3416// path lacked -- it paid +4% on software u64_lt carries). Register plan: rdx=b[j] (mulx implicit 3417// multiplier), rsi=a ptr, rdi=b ptr, r15=a[i] scratch, rax=mulx-lo/zero-source, rcx=mulx-hi, 3418// r8..r11+rbx+r12..r14 = the 8 result limbs r[0..7]. dst ptr is stashed on the stack. The 3419// callee-saved regs we use (rbx,r12-r15) are push/pop-balanced. Per-row carry SETTLE folds the 3420// CF/OF tails into the next-higher limb, which is still zero at that point (rows processed low->high), 3421// so a settle add of {0,1}+{0,1} into a zero limb never cascades. Correctness is difftest-gated 3422// bit-exact vs the proven 8x32 u256_mul_wide oracle; the software path stays the oracle/fallback. 3423func x86ctx_mul256_reg(k: i64) -> *u8 { 3424 if k == 0 { return "r8" as *u8 } 3425 if k == 1 { return "r9" as *u8 } 3426 if k == 2 { return "r10" as *u8 } 3427 if k == 3 { return "r11" as *u8 } 3428 if k == 4 { return "rbx" as *u8 } 3429 if k == 5 { return "r12" as *u8 } 3430 if k == 6 { return "r13" as *u8 } 3431 return "r14" as *u8 // k == 7 3432} 3433func x86ctx_emit_mul256_wide(c: *X86Ctx, i: *Instr) -> i64 { 3434 // Materialise the three pointers into caller-saved regs BEFORE clobbering the callee-saved homes. 3435 x86ctx_load_value_v(c, i.op0, "rax" as *u8) // rax = dst ptr 3436 x86ctx_load_value_v(c, i.op1, "rsi" as *u8) // rsi = a ptr 3437 x86ctx_load_value_v(c, i.op2, "rdi" as *u8) // rdi = b ptr 3438 // Save the callee-saved regs used as result limbs r[4..7] + the a[i] scratch (r15). 3439 out_str(c.o, " pushq %rbx\n") 3440 out_str(c.o, " pushq %r12\n") 3441 out_str(c.o, " pushq %r13\n") 3442 out_str(c.o, " pushq %r14\n") 3443 out_str(c.o, " pushq %r15\n") 3444 out_str(c.o, " pushq %rax\n") // stash dst ptr 3445 // Zero the 8 result limbs. 3446 out_str(c.o, " xorq %r8, %r8\n") 3447 out_str(c.o, " xorq %r9, %r9\n") 3448 out_str(c.o, " xorq %r10, %r10\n") 3449 out_str(c.o, " xorq %r11, %r11\n") 3450 out_str(c.o, " xorq %rbx, %rbx\n") 3451 out_str(c.o, " xorq %r12, %r12\n") 3452 out_str(c.o, " xorq %r13, %r13\n") 3453 out_str(c.o, " xorq %r14, %r14\n") 3454 var j: i64 = 0 3455 while j < 4 { 3456 out_str(c.o, " movq ") 3457 out_i64(c.o, j * 8) 3458 out_str(c.o, "(%rdi), %rdx\n") // rdx = b[j] (mulx implicit multiplier) 3459 out_str(c.o, " xorq %rax, %rax\n") // CF=0, OF=0 for both carry chains 3460 var ii: i64 = 0 3461 while ii < 4 { 3462 out_str(c.o, " movq ") 3463 out_i64(c.o, ii * 8) 3464 out_str(c.o, "(%rsi), %r15\n") // r15 = a[i] 3465 out_str(c.o, " mulx %r15, %rax, %rcx\n") // rcx:rax = b[j] * a[i] 3466 out_str(c.o, " adcx %rax, %") 3467 out_str(c.o, x86ctx_mul256_reg(j + ii)) 3468 out_str(c.o, "\n") // CF chain: r[j+ii] += lo 3469 out_str(c.o, " adox %rcx, %") 3470 out_str(c.o, x86ctx_mul256_reg(j + ii + 1)) 3471 out_str(c.o, "\n") // OF chain: r[j+ii+1] += hi 3472 ii = ii + 1 3473 } 3474 // Settle: rax=0 without disturbing flags, fold CF tail into r[j+4]; for j<3 push the 3475 // residual OF + CF carries into the still-zero limb r[j+5]. 3476 out_str(c.o, " movq $0, %rax\n") 3477 out_str(c.o, " adcx %rax, %") 3478 out_str(c.o, x86ctx_mul256_reg(j + 4)) 3479 out_str(c.o, "\n") 3480 if j < 3 { 3481 out_str(c.o, " adox %rax, %") 3482 out_str(c.o, x86ctx_mul256_reg(j + 5)) 3483 out_str(c.o, "\n") 3484 out_str(c.o, " adcx %rax, %") 3485 out_str(c.o, x86ctx_mul256_reg(j + 5)) 3486 out_str(c.o, "\n") 3487 } 3488 j = j + 1 3489 } 3490 // Reload dst ptr + store the 8 result limbs (before restoring the callee-saved homes). 3491 out_str(c.o, " popq %rax\n") 3492 var k: i64 = 0 3493 while k < 8 { 3494 out_str(c.o, " movq %") 3495 out_str(c.o, x86ctx_mul256_reg(k)) 3496 out_str(c.o, ", ") 3497 out_i64(c.o, k * 8) 3498 out_str(c.o, "(%rax)\n") 3499 k = k + 1 3500 } 3501 out_str(c.o, " popq %r15\n") 3502 out_str(c.o, " popq %r14\n") 3503 out_str(c.o, " popq %r13\n") 3504 out_str(c.o, " popq %r12\n") 3505 out_str(c.o, " popq %rbx\n") 3506 out_str(c.o, " xorq %rax, %rax\n") 3507 x86ctx_store_result(c, i.result, "rax" as *u8) 3508 return 0 3509} 3510 3511// ===== opcode dispatch ============================================ 3512 3513func x86ctx_emit_instr(c: *X86Ctx, i: *Instr) -> i64 { 3514 let op: i64 = i.op 3515 // Binops 3516 if op == OP_ADD { x86ctx_emit_binop(c, i); return 0 } 3517 if op == OP_SUB { x86ctx_emit_binop(c, i); return 0 } 3518 if op == OP_MUL { x86ctx_emit_binop(c, i); return 0 } 3519 if op == OP_UMULHI { x86ctx_emit_binop(c, i); return 0 } 3520 if op == OP_CRC32 { x86ctx_emit_binop(c, i); return 0 } 3521 if op == OP_PDEP { x86ctx_emit_binop(c, i); return 0 } 3522 if op == OP_PEXT { x86ctx_emit_binop(c, i); return 0 } 3523 if op == OP_DIV_S { x86ctx_emit_binop(c, i); return 0 } 3524 if op == OP_DIV_U { x86ctx_emit_binop(c, i); return 0 } 3525 if op == OP_REM_S { x86ctx_emit_binop(c, i); return 0 } 3526 if op == OP_REM_U { x86ctx_emit_binop(c, i); return 0 } 3527 if op == OP_AND { x86ctx_emit_binop(c, i); return 0 } 3528 if op == OP_OR { x86ctx_emit_binop(c, i); return 0 } 3529 if op == OP_XOR { x86ctx_emit_binop(c, i); return 0 } 3530 if op == OP_SHL { x86ctx_emit_binop(c, i); return 0 } 3531 if op == OP_SHR_S { x86ctx_emit_binop(c, i); return 0 } 3532 if op == OP_SHR_U { x86ctx_emit_binop(c, i); return 0 } 3533 if op == OP_ROTL64 { x86ctx_emit_binop(c, i); return 0 } 3534 if op == OP_ROTR64 { x86ctx_emit_binop(c, i); return 0 } 3535 // Unops 3536 if op == OP_NEG { x86ctx_emit_unop(c, i); return 0 } 3537 if op == OP_NOT { x86ctx_emit_unop(c, i); return 0 } 3538 if op == OP_TRUNC { x86ctx_emit_unop(c, i); return 0 } 3539 if op == OP_SEXT { x86ctx_emit_unop(c, i); return 0 } 3540 if op == OP_ZEXT { x86ctx_emit_unop(c, i); return 0 } 3541 if op == OP_BITCAST { x86ctx_emit_unop(c, i); return 0 } 3542 if op == OP_BSWAP64 { x86ctx_emit_unop(c, i); return 0 } 3543 if op == OP_POPCNT64 { x86ctx_emit_unop(c, i); return 0 } 3544 if op == OP_CLZ32 { x86ctx_emit_unop(c, i); return 0 } 3545 if op == OP_CTZ32 { x86ctx_emit_unop(c, i); return 0 } 3546 if op == OP_RDTSC { x86ctx_emit_unop(c, i); return 0 } 3547 // Atomics 3548 if op == OP_ATOMIC_LOAD_I64 { x86ctx_emit_atomic(c, i); return 0 } 3549 if op == OP_ATOMIC_STORE_I64 { x86ctx_emit_atomic(c, i); return 0 } 3550 if op == OP_ATOMIC_CAS_I64 { x86ctx_emit_atomic(c, i); return 0 } 3551 if op == OP_ATOMIC_FAA_I64 { x86ctx_emit_atomic(c, i); return 0 } 3552 if op == OP_ATOMIC_FENCE { x86ctx_emit_atomic(c, i); return 0 } 3553 if op == OP_ADC_ACC { G1_RAX_SLOT = 0 - 1; x86ctx_emit_adc_acc(c, i); return 0 } 3554 if op == OP_CPUID_EBX { G1_RAX_SLOT = 0 - 1; x86ctx_emit_cpuid_ebx(c, i); return 0 } 3555 if op == OP_THREAD_CLONE { x86ctx_emit_thread_clone(c, i); return 0 } 3556 // Address-of: emit the alloca slot ADDRESS (leaq) via the as-address 3557 // load path -- NOT the auto-loading as-value path that unops use. 3558 if op == OP_ADDR_OF { 3559 x86ctx_load_value(c, i.op0, "rax" as *u8) 3560 x86ctx_store_result(c, i.result, "rax" as *u8) 3561 return 0 3562 } 3563 // Compares 3564 if op == OP_EQ { x86ctx_emit_cmp(c, i); return 0 } 3565 if op == OP_NE { x86ctx_emit_cmp(c, i); return 0 } 3566 if op == OP_LT_S { x86ctx_emit_cmp(c, i); return 0 } 3567 if op == OP_LE_S { x86ctx_emit_cmp(c, i); return 0 } 3568 if op == OP_GT_S { x86ctx_emit_cmp(c, i); return 0 } 3569 if op == OP_GE_S { x86ctx_emit_cmp(c, i); return 0 } 3570 // Branches 3571 if op == OP_BR { x86ctx_emit_br(c, i); return 0 } 3572 if op == OP_BR_COND { x86ctx_emit_br_cond(c, i); return 0 } 3573 // Memory 3574 if op == OP_LOAD { x86ctx_emit_load(c, i); return 0 } 3575 if op == OP_STORE { x86ctx_emit_store(c, i); return 0 } 3576 if op == OP_GEP { x86ctx_emit_gep(c, i); return 0 } 3577 if op == OP_ALLOCA { 3578 // Address lazily materialised via load_value; nothing emitted here. 3579 return 0 3580 } 3581 // Control 3582 if op == OP_RETURN { x86ctx_emit_return(c, i); return 0 } 3583 if op == OP_CALL { G1_RAX_SLOT = 0 - 1; x86ctx_emit_call(c, i); return 0 } 3584 if op == OP_CALL_INDIRECT { G1_RAX_SLOT = 0 - 1; x86ctx_emit_call_indirect(c, i); return 0 } 3585 if op == OP_TAIL_CALL { G1_RAX_SLOT = 0 - 1; x86ctx_emit_tail_call(c, i); return 0 } 3586 if op == OP_SYSCALL { G1_RAX_SLOT = 0 - 1; x86ctx_emit_syscall(c, i); return 0 } 3587 // hardware float (SSE scalar) -- carried as i64 bit-patterns, clobbers rax/rcx/xmm. 3588 // x86ctx_emit_float picks f64 (movsd/addsd/...) vs f32 (movss/...) by precision. 3589 if op == OP_FADD { G1_RAX_SLOT = 0 - 1; x86ctx_emit_float(c, i); return 0 } 3590 if op == OP_FSUB { G1_RAX_SLOT = 0 - 1; x86ctx_emit_float(c, i); return 0 } 3591 if op == OP_FMUL { G1_RAX_SLOT = 0 - 1; x86ctx_emit_float(c, i); return 0 } 3592 if op == OP_FDIV { G1_RAX_SLOT = 0 - 1; x86ctx_emit_float(c, i); return 0 } 3593 if op == OP_FSQRT { G1_RAX_SLOT = 0 - 1; x86ctx_emit_float(c, i); return 0 } 3594 if op == OP_FCAST_I_TO_F { G1_RAX_SLOT = 0 - 1; x86ctx_emit_float(c, i); return 0 } 3595 if op == OP_FCAST_F_TO_I { G1_RAX_SLOT = 0 - 1; x86ctx_emit_float(c, i); return 0 } 3596 if op == OP_F32X4_DOT { G1_RAX_SLOT = 0 - 1; x86ctx_emit_f32x4_dot(c, i); return 0 } 3597 if op == OP_I8DOT32 { G1_RAX_SLOT = 0 - 1; x86ctx_emit_i8dot32(c, i); return 0 } 3598 if op == OP_I8DOT32A { G1_RAX_SLOT = 0 - 1; x86ctx_emit_i8dot32a(c, i); return 0 } 3599 if op == OP_I8FMA32 { G1_RAX_SLOT = 0 - 1; x86ctx_emit_i8fma32(c, i); return 0 } 3600 if op == OP_Q8ROWDOT { G1_RAX_SLOT = 0 - 1; x86ctx_emit_q8rowdot(c, i); return 0 } 3601 if op == OP_Q5UNPACK32 { G1_RAX_SLOT = 0 - 1; x86ctx_emit_q5unpack32(c, i); return 0 } 3602 if op == OP_Q4KUNPACK32S { G1_RAX_SLOT = 0 - 1; x86ctx_emit_q4kunpack32s(c, i); return 0 } 3603 if op == OP_Q4KSBDOT { G1_RAX_SLOT = 0 - 1; x86ctx_emit_q4ksbdot(c, i); return 0 } 3604 if op == OP_F32X8_DOT { G1_RAX_SLOT = 0 - 1; x86ctx_emit_f32x8_dot(c, i); return 0 } 3605 if op == OP_F32X8_FMA { G1_RAX_SLOT = 0 - 1; x86ctx_emit_f32x8_fma(c, i); return 0 } 3606 if op == OP_F32X8_HSUM { G1_RAX_SLOT = 0 - 1; x86ctx_emit_f32x8_hsum(c, i); return 0 } 3607 if op == OP_I16X16_MADD { G1_RAX_SLOT = 0 - 1; x86ctx_emit_i16x16_madd(c, i); return 0 } 3608 if op == OP_I16DOT { G1_RAX_SLOT = 0 - 1; x86ctx_emit_i16dot(c, i); return 0 } 3609 if op == OP_Q8BLKDOT { G1_RAX_SLOT = 0 - 1; x86ctx_emit_q8blkdot(c, i); return 0 } 3610 if op == OP_AES128_ENC_BLOCK { G1_RAX_SLOT = 0 - 1; x86ctx_emit_aesni(c, i); return 0 } 3611 if op == OP_SHA256_NI_BLOCK { G1_RAX_SLOT = 0 - 1; x86ctx_emit_sha256_ni(c, i); return 0 } 3612 if op == OP_MUL256_WIDE { G1_RAX_SLOT = 0 - 1; x86ctx_emit_mul256_wide(c, i); return 0 } 3613 if op == OP_CLMUL_LL { G1_RAX_SLOT = 0 - 1; x86ctx_emit_clmul(c, i, 0); return 0 } 3614 if op == OP_CLMUL_HH { G1_RAX_SLOT = 0 - 1; x86ctx_emit_clmul(c, i, 17); return 0 } 3615 if op == OP_CLMUL_LH { G1_RAX_SLOT = 0 - 1; x86ctx_emit_clmul(c, i, 16); return 0 } 3616 if op == OP_CLMUL_HL { G1_RAX_SLOT = 0 - 1; x86ctx_emit_clmul(c, i, 1); return 0 } 3617 if op == OP_COPY { x86ctx_emit_copy(c, i); return 0 } 3618 // Unhandled 3619 out_str(c.o, " # x86_64: opcode ") 3620 out_i64(c.o, op) 3621 out_str(c.o, " not yet wired (session 6 subset)\n") 3622 return 0 3623} 3624 3625// ===== block + function emission ================================== 3626// 3627// For each block: 3628// .L<fn>_bb<id>: 3629// <emit each instr> 3630// Param prologue is omitted in V1 (stack-machine layer assumes 3631// callers already used rdi..r9 directly; for VK_PARAM values we'd 3632// need to spill them into their slot on entry -- session 6b adds 3633// that). 3634 3635func x86ctx_emit_param_prologue(c: *X86Ctx) -> i64 { 3636 var p: i64 = 0 3637 let n: i64 = c.f.n_values 3638 var pi: i64 = 0 3639 while pi < n { 3640 let val: *Value = x86ctx_value_at(c.f, pi) 3641 if val.kind == VK_PARAM { 3642 let idx: i64 = val.param_index 3643 if idx >= 0 { 3644 if idx < 6 { 3645 let reg: *u8 = x86_arg_reg_name(idx) 3646 x86ctx_store_result(c, pi, reg) 3647 } 3648 // Stack params (idx >= 6): SysV ABI puts them at 3649 // 16(%rbp) + (idx-6)*8 (above saved RA + saved RBP). 3650 // Caller's x86ctx_emit_call pushes them in reverse; 3651 // we load them into %rax then store to the local 3652 // slot like the register-param case. 2026-05-20 fix 3653 // per Task #93: was silently dropping params 7+. 3654 if idx >= 6 { 3655 let stack_off: i64 = 16 + (idx - 6) * 8 3656 out_str(c.o, " movq ") 3657 out_i64(c.o, stack_off) 3658 out_str(c.o, "(%rbp), %rax\n") 3659 x86ctx_store_result(c, pi, "rax" as *u8) 3660 } 3661 } 3662 p = p + 1 3663 } 3664 pi = pi + 1 3665 } 3666 return p 3667} 3668 3669// DDR-006 rung 2: the LAST expanded line already announced by a .loc. A row per instruction would 3670// bloat .debug_line with no added information -- the line-program VM holds a line until told 3671// otherwise -- so we emit only on CHANGE, which is exactly what gcc and clang do. 3672static x86_last_loc_exp: i64 3673 3674func x86ctx_emit_block(c: *X86Ctx, bb: *BasicBlock) -> i64 { 3675 x86ctx_emit_bb_label(c, bb.id) 3676 out_str(c.o, ":\n") 3677 G1_RAX_SLOT = 0 - 1 // block boundary = control-flow join: rax unknown 3678 G1_PENDING_CC = 0 - 1 // G6: flags never cross a block boundary 3679 G1_PENDING_VAL = 0 - 1 3680 G24_SKIP = 0 3681 var i: *Instr = bb.head 3682 let BUDGET: i64 = 65536 3683 var iter: i64 = 0 3684 while i != (0 as *Instr) { 3685 if iter >= BUDGET { i = 0 as *Instr } 3686 if i != (0 as *Instr) { 3687 // DEBUG LINE INFO, rung 2 -- STATEMENT granularity (DDR-006, 2026-08-07). 3688 // The line rides OUT OF BAND, keyed by the instruction pointer: alloc_instr hands 3689 // instructions out of a contiguous per-function pool, so the pointer IS a dense 3690 // identity and struct Instr never had to grow (its own comments record that widening 3691 // it moved the stride twice and needed lockstep edits across four modules). 3692 if lm_debug_on() == 1 { 3693 let il_map: *LineMap = lm_get_active() 3694 if (il_map as i64) != 0 { 3695 let il_exp: i64 = lm_stmt_lookup(i as i64) 3696 if il_exp > 0 { 3697 if il_exp != x86_last_loc_exp { 3698 let il_sc: *i64 = sys_mmap(32) as *i64 3699 let il_fp: *i64 = il_sc 3700 let il_sp: *i64 = ((il_sc as i64) + 8) as *i64 3701 *il_fp = 0 3702 *il_sp = 0 3703 // Silent when the map cannot PROVE the location -- same rule as the 3704 // function-level emit. A wrong line sends a debugger to innocent code. 3705 if lm_lookup(il_map, il_exp, il_fp, il_sp) == 1 { 3706 out_str(c.o, " .loc ") 3707 out_i64(c.o, *il_fp + 1) 3708 out_str(c.o, " ") 3709 out_i64(c.o, *il_sp) 3710 out_str(c.o, "\n") 3711 x86_last_loc_exp = il_exp 3712 } 3713 } 3714 } 3715 } 3716 } 3717 if G24_SKIP > 0 { G24_SKIP = G24_SKIP - 1 } else { x86ctx_emit_instr(c, i) } 3718 i = i.next 3719 } 3720 iter = iter + 1 3721 } 3722 return 0 3723} 3724 3725func x86ctx_emit_function(f: *Function, o: *OutBuf) -> i64 { 3726 let name: *u8 = f.name_start as *u8 3727 sys_write(2, "fn=" as *u8, 3) 3728 if name != (0 as *u8) { sys_write(2, name, f.name_len) } 3729 sys_write(2, "\n" as *u8, 1) 3730 let c: *X86Ctx = x86ctx_init(f, o) 3731 G1_RAX_SLOT = 0 - 1 // fresh function: rax holds nothing known 3732 G1_PENDING_CC = 0 - 1 // G6: no pending compare (statics are BSS-zero 3733 G1_PENDING_VAL = 0 - 1 // and 0 is a real CC code -- must init here) 3734 3735 x86_emit_function_start(o, name) 3736 // DEBUG LINE INFO, rung 1 -- FUNCTION granularity (DDR-002, 2026-08-06). 3737 // Emits a gas-compatible `.loc <file> <line>` right after the function's label. 3738 // WHY HERE AND NOT IN THE COMPILER'S OWN TABLE: .debug_line maps ADDRESSES to lines, and 3739 // the compiler does not know addresses -- only the assembler does. So the compiler's whole 3740 // job is to say WHICH LINE this label belongs to and let nxasm pair it with the offset it 3741 // assigns. That split is forced by the format, which is why GCC and Clang both use it. 3742 // WHY FUNCTION AND NOT STATEMENT: the IR carries no line numbers at all (Tok does, Instr 3743 // does not), so statement granularity is a much larger arc. Function granularity needs only 3744 // what the parser already recorded, and it is what makes a backtrace name its frames. 3745 // BYTE-NEUTRAL TODAY: nxasm's directive dispatch is a flat if-chain with no else branch, so 3746 // an unrecognised directive is ignored. The equivalence gate can PROVE that -- identical 3747 // per-row ea/eb byte counts, not merely a GREEN verdict. 3748 // OPT-IN (-g). Default OFF so the emitted binary is byte-identical to a no-debug build -- 3749 // see the switch's note in nx_linemap.nx for why that default is load-bearing. 3750 // Fresh function: nothing has been announced yet, so the first statement must emit its .loc 3751 // even if it happens to share a line with the previous function's last statement. 3752 x86_last_loc_exp = 0 3753 var dl_map: *LineMap = 0 as *LineMap 3754 if lm_debug_on() == 1 { dl_map = lm_get_active() } 3755 if (dl_map as i64) != 0 { 3756 let dl_exp: i64 = lm_fn_lookup(dl_map, name, f.name_len) 3757 if dl_exp > 0 { 3758 let dl_sc: *i64 = sys_mmap(32) as *i64 3759 let dl_fp: *i64 = dl_sc 3760 let dl_sp: *i64 = ((dl_sc as i64) + 8) as *i64 3761 *dl_fp = 0 3762 *dl_sp = 0 3763 // Silent when the map cannot PROVE the location. A wrong line in debug info sends a 3764 // debugger to innocent code -- the same class as a caret under the wrong line. 3765 if lm_lookup(dl_map, dl_exp, dl_fp, dl_sp) == 1 { 3766 out_str(o, " .loc ") 3767 out_i64(o, *dl_fp + 1) 3768 out_str(o, " ") 3769 out_i64(o, *dl_sp) 3770 out_str(o, "\n") 3771 // Seed the change-tracker so the first statement on this same line does not emit a 3772 // second, identical .loc immediately after the function label. 3773 x86_last_loc_exp = dl_exp 3774 } 3775 } 3776 } 3777 x86_emit_prologue(o, c.frame_size) 3778 // G1 FIX-17: save callee-saved homes AFTER the prologue but BEFORE the param 3779 // prologue (a param homed in r12 must not be captured as the caller's r12). 3780 x86ctx_emit_cs_save(c) 3781 3782 // Spill params from arg-regs to their stack slots. 3783 x86ctx_emit_param_prologue(c) 3784 3785 // G5: load homed BIG constants into their callee-saved homes, once per 3786 // function (after cs_save -- the caller's register values are already 3787 // banked; before any block -- every use site reads the home). 3788 var g5cv: i64 = 0 3789 while g5cv < f.n_values { 3790 let g5cl: *ValueLoc = ((c.locs as i64) + g5cv * 16) as *ValueLoc 3791 if g5cl.kind == VL_REGISTER { 3792 let g5cval: *Value = x86ctx_value_at(f, g5cv) 3793 if g5cval.kind == VK_CONST_INT { 3794 x86_emit_movabsq(o, x86_home_reg_name(g5cl.idx), g5cval.const_int) 3795 } 3796 } 3797 g5cv = g5cv + 1 3798 } 3799 3800 // Emit each block. G12: track the NEXT emitted block's id so terminators 3801 // can elide jumps-to-fall-through (-1 for the last block = never elide). 3802 var b: i64 = 0 3803 while b < f.n_blocks { 3804 let bb: *BasicBlock = x86ctx_block_at(f, b) 3805 c.next_bb = 0 - 1 3806 if b + 1 < f.n_blocks { 3807 let nbb: *BasicBlock = x86ctx_block_at(f, b + 1) 3808 c.next_bb = nbb.id 3809 } 3810 x86ctx_emit_block(c, bb) 3811 b = b + 1 3812 } 3813 3814 // Defensive epilogue if the IR didn't terminate (shouldn't happen 3815 // for well-formed IR; harmless safety net). 3816 x86_emit_movabsq(o, "rax" as *u8, 0) 3817 x86ctx_emit_cs_restore(c) 3818 x86_emit_epilogue(o) 3819 x86_emit_function_end(o, name) 3820 return 0 3821} 3822 3823// ===== module-level globals dump (session 6b) ==================== 3824// 3825// Parallels nx_nxc.nx stage 3.5 (and riscv.c's globals dump). Each 3826// VK_GLOBAL Value references a Module.globals[id] entry; the asm 3827// emitted for the function body says `leaq .Lg<id>(%rip), %reg`, 3828// which is a forward reference. Without this section emitted at 3829// the end of the module, those leaq refs unresolved. 3830// 3831// Emits: 3832// .section .rodata 3833// .Lg0: .asciz "bytes..." 3834// .Lg1: .asciz "bytes..." 3835// ... 3836// .data (for writable globals) 3837// <name>: .quad ... or .byte ... 3838// .bss (for zero-init globals) 3839// .lcomm <name>, <len> 3840 3841// LN42 (2026-09-03): does this global carry any non-zero initializer byte? 3842func x86ctx_global_nonzero(g: *Global) -> i64 { 3843 if g.bytes == (0 as *u8) { return 0 } 3844 var i: i64 = 0 3845 while i < g.len { 3846 if g.bytes[i] != (0 as u8) { return 1 } 3847 i = i + 1 3848 } 3849 return 0 3850} 3851 3852func x86ctx_emit_module_globals(m: *Module, o: *OutBuf) -> i64 { 3853 if m == (0 as *Module) { return 0 } 3854 if m.n_globals <= 0 { return 0 } 3855 let g_base: i64 = m.globals as i64 3856 3857 // ---- LN42: WRITABLE NON-ZERO STATICS GO IN .data, AND THEY GO FIRST ------------------------- 3858 // Until now a writable static could only be ZERO-initialised: .rodata is read-only on the GNU 3859 // as/ld lane so a write SIGSEGVs there, and nxasm mapped .data to its ignore bucket, leaving 3860 // .lcomm (zero-fill, BSS) as the only both-lanes-writable form -- so a non-zero one was refused 3861 // by a deliberately invalid line. But nxasm's section-1 tail is ALREADY WRITABLE (that is exactly 3862 // what .lcomm allocates into, "a zero-filled MUTABLE slot ... appended to the data tail"), and 3863 // .data is writable on the GNU lane by definition. Teaching the assembler .data makes it the 3864 // both-lanes-correct emission this comment used to say did not exist. Three organs carried the 3865 // workaround by name (nx_lawpath, nx_rv64_fast, nx_toolsafety_lib) and a DUAL-TARGET module could 3866 // not declare anything at all -- which is how this was found. 3867 // 3868 // WHY FIRST, AND WHY PADDED: an i64 static must be 8-aligned. .lcomm aligns itself; a raw .byte 3869 // run does not, and the rodata string stream leaves the section cursor at an arbitrary offset. 3870 // Emitting this block BEFORE any string starts it at cursor 0 (aligned), and padding each entry 3871 // up to a multiple of 8 keeps every following entry aligned too. 3872 // 3873 // BYTE-IDENTICAL WHEN UNUSED: with no writable non-zero global the block is skipped entirely and 3874 // the stream below is exactly what it was -- checked by rebuilding, not asserted. 3875 var nzc: i64 = 0 3876 var nzi: i64 = 0 3877 while nzi < m.n_globals { 3878 let gz: *Global = (g_base + nzi * 80) as *Global 3879 if gz.zero_init == 0 { if gz.writable == 1 { if x86ctx_global_nonzero(gz) == 1 { nzc = nzc + 1 } } } 3880 nzi = nzi + 1 3881 } 3882 if nzc > 0 { 3883 out_str(o, " .data 3884") 3885 var nzk: i64 = 0 3886 while nzk < m.n_globals { 3887 let gk: *Global = (g_base + nzk * 80) as *Global 3888 if gk.zero_init == 0 { if gk.writable == 1 { if x86ctx_global_nonzero(gk) == 1 { 3889 out_str(o, ".Lg") 3890 out_i64(o, gk.id) 3891 out_str(o, ": 3892 .byte ") 3893 var nb: i64 = 0 3894 while nb < gk.len { 3895 out_i64(o, gk.bytes[nb]) 3896 out_str(o, ", ") 3897 nb = nb + 1 3898 } 3899 // ALWAYS pad to the NEXT 8-boundary -- when the length is already a multiple of 8 3900 // that is a full 8 bytes, deliberately: the content loop emits a trailing ", " after 3901 // every byte, so a zero-length pad would leave the line ending in a comma with no 3902 // operand, and a bare `.byte` is the empty-literal miscompile this file already 3903 // carries a standing witness for. The cost is at most 8 bytes per non-zero static and 3904 // it buys 8-alignment for every entry that follows. 3905 var pad: i64 = (8 - (gk.len & 7)) & 7 3906 if pad == 0 { pad = 8 } 3907 var pn: i64 = 0 3908 while pn < pad { 3909 out_i64(o, 0) 3910 if pn + 1 < pad { out_str(o, ", ") } 3911 pn = pn + 1 3912 } 3913 out_char(o, 0x0A) 3914 } } } 3915 nzk = nzk + 1 3916 } 3917 } 3918 3919 x86_emit_section_rodata(o) 3920 var i: i64 = 0 3921 while i < m.n_globals { 3922 // Stride 80 -- unified per nx_nxc.nx pool layout. Wrong 3923 // stride here caused SIGSEGV after all functions emitted 3924 // (session 9 bisect 2026-05-17). 3925 let g: *Global = (g_base + i * 80) as *Global 3926 3927 if g.zero_init == 0 { 3928 // X-G3 2026-07-15: WRITABLE data statics (ir_add_global_data -- 3929 // the 07-14 static-init parse change) must NOT land in .rodata: 3930 // on the GNU as/ld lane .rodata pages are read-only, so the 3931 // first static write SIGSEGVs (caught by the G2 gauntlet run, 3932 // ed25519 KAT nx_scratch_init writing .Lg0; error-7 write fault 3933 // at the rodata page). All-zero payloads emit as the exact 3934 // pre-regression `.lcomm` (BSS -- writable on BOTH lanes; nxasm 3935 // IGNORES `.data` sections wholesale (nxasm_x86.nx:810), so 3936 // .lcomm is the only both-lanes-writable form). A NONZERO-init 3937 // static has no both-lanes-correct emission yet -> emit a loud 3938 // invalid line so the build FAILS instead of silently zeroing 3939 // the initializer (no silent caps). Strings (writable=0) keep 3940 // the .rodata path byte-identical. 3941 if g.writable == 1 { 3942 // LN42: a NON-ZERO writable static was emitted into the .data block above; only the 3943 // all-zero ones reach .lcomm, and that path is byte-for-byte what it always was. 3944 if x86ctx_global_nonzero(g) == 0 { 3945 out_str(o, " .lcomm .Lg") 3946 out_i64(o, g.id) 3947 out_str(o, ", ") 3948 out_i64(o, g.len) 3949 out_char(o, 0x0A) 3950 } 3951 } 3952 if g.writable == 0 { 3953 // Anonymous globals always use .Lg<id>. Named globals 3954 // are queued for a follow-up; ingest CLI doesn't ship any. 3955 out_str(o, ".Lg") 3956 out_i64(o, g.id) 3957 out_str(o, ":\n .byte ") 3958 // Per nx_nxc.nx: use .byte listing rather than .asciz so 3959 // the assembler can't reinterpret any escape sequences. 3960 // Raw bytes round-trip safely. 3961 // Emit "<byte>, " per byte and then an UNCONDITIONAL terminating 0. 3962 // This produces exactly the same text as the previous 3963 // "join with ', '" + "append ', 0' when len > 0" form for every 3964 // NON-EMPTY literal -- but it also emits the terminator for the 3965 // EMPTY one, which the old shape did not. 3966 // 3967 // THE EMPTY-LITERAL MISCOMPILE (fixed here, 2026-07-25): with len == 0 3968 // the loop wrote nothing and the `len > 0` guard suppressed the 3969 // terminator, so the emitter produced a bare `.byte` with NO operands. 3970 // The label .Lg<id> then resolved to the FOLLOWING global's first byte, 3971 // making `""` silently ALIAS the next literal in the pool: the standing 3972 // witness runtime/nx_empty_lit_probe.nx measured strlen("") == 4 and 3973 // ("" as i64) == ("HOLD" as i64). Because pool layout shifts between 3974 // builds, the damage moved around -- nondeterminism ACROSS builds with 3975 // determinism WITHIN one binary was the signature. It corrupted a swarm 3976 // queue with "HOLD" and produced a false "seg_store corruption" verdict. 3977 var bi: i64 = 0 3978 while bi < g.len { 3979 out_i64(o, g.bytes[bi]) 3980 out_str(o, ", ") 3981 bi = bi + 1 3982 } 3983 out_i64(o, 0) 3984 out_char(o, 0x0A) 3985 } 3986 } 3987 if g.zero_init == 1 { 3988 out_str(o, " .lcomm .Lg") 3989 out_i64(o, g.id) 3990 out_str(o, ", ") 3991 out_i64(o, g.len) 3992 out_char(o, 0x0A) 3993 } 3994 3995 i = i + 1 3996 } 3997 return 0 3998} 3999 4000// ===== whole-module emission ====================================== 4001// 4002// Top-level: emits the standard _start trampoline + every function 4003// + the globals dump. The session-7 driver invokes this once per 4004// module to produce a complete .s file. 4005 4006func x86ctx_emit_module(m: *Module, o: *OutBuf) -> i64 { 4007 return x86ctx_emit_module_live(m, o, 0 as *u8) 4008} 4009 4010// Same as x86ctx_emit_module, restricted to the functions a reachability pass marked live. 4011// `live` is a per-slot byte map indexed like m.functions[] (see opt_module_dce_mark, nx_opt.nx); 4012// a NULL map means "emit everything" and is exactly the pre-B1 behaviour -- x86ctx_emit_module 4013// above is that call, so every existing caller is byte-for-byte unchanged. Dead functions are 4014// SKIPPED, never rewritten: the pool, its indices and every callee pointer stay as parsed. 4015// Globals are still dumped in full (a dead function's string literals cost .rodata bytes, not 4016// correctness; trimming them is a separate, measured rung). 4017func x86ctx_emit_module_live(m: *Module, o: *OutBuf, live: *u8) -> i64 { 4018 if m == (0 as *Module) { return 0 } 4019 4020 // _start trampoline. On Linux x86_64 process entry, the SysV ABI 4021 // for _start places (from sp): argc, argv[0..argc], NULL, envp... 4022 // To satisfy `main(argc: i64, argv: *i64)`'s SysV calling convention 4023 // we must MOVE argc into %rdi (1st arg) and pointer-to-argv into 4024 // %rsi (2nd arg) BEFORE calling main, then exit with main's 4025 // return value. 4026 // 4027 // Prior implementation called main with %rdi / %rsi unset, so 4028 // path-mode binaries that read argv[1] saw garbage and behaved as 4029 // though invoked with no args. Fixed 2026-05-21 during the 4030 // native-x86_64 self-host bootstrap diagnosis. 4031 4032 // DWARF FILE TABLE (DDR-002, 2026-08-06). The assembler is a SEPARATE PROCESS and never 4033 // sees the LineMap, so the .s has to carry the file names itself -- which is precisely why 4034 // gas grew `.file N "path"`. The `.loc` directives emitted per function index into this 4035 // table, so the two must agree on numbering: both are 1-based here. 4036 // FULL path on purpose: the basename is what a HUMAN wants in a diagnostic, but a debugger 4037 // has to FIND the source. DDR-001 kept the full path in the map for exactly this moment. 4038 // Byte-neutral today -- nxasm ignores unrecognised directives. 4039 // OPT-IN (-g), same switch as .loc -- the file table is useless without the .loc rows and 4040 // both must appear together or neither: an assembler that saw one and not the other would 4041 // have to guess, and a debug format is exactly where guessing is worst. 4042 var fl_map: *LineMap = 0 as *LineMap 4043 if lm_debug_on() == 1 { fl_map = lm_get_active() } 4044 if (fl_map as i64) != 0 { 4045 var fli: i64 = 0 4046 while fli < fl_map.n_files { 4047 let flp: *u8 = lm_file_path(fl_map, fli) 4048 if (flp as i64) != 0 { 4049 out_str(o, " .file ") 4050 out_i64(o, fli + 1) 4051 out_str(o, " \"") 4052 out_str(o, flp) 4053 out_str(o, "\"\n") 4054 } 4055 fli = fli + 1 4056 } 4057 } 4058 4059 out_str(o, " .text\n") 4060 out_str(o, " .globl _start\n") 4061 out_str(o, "_start:\n") 4062 out_str(o, " movq (%rsp), %rdi\n") // argc 4063 out_str(o, " leaq 8(%rsp), %rsi\n") // argv 4064 out_str(o, " call main\n") 4065 out_str(o, " movq %rax, %rdi\n") 4066 // exit_group, NOT exit (2026-07-07, threading live): nx threads 4067 // are CLONE_VM tasks with separate PIDs, so plain exit(60) after 4068 // main returns leaves live pool workers running -- they hold 4069 // stdout open and wedge any pipeline waiting for EOF (this hung 4070 // the build lane 22min via the shared-pool dispatcher). Return- 4071 // from-main must terminate the WHOLE thread group, same contract 4072 // as every threaded libc. Per-thread exit stays sys_exit(93->60) 4073 // in nx_thread_exit. 4074 x86_emit_movabsq(o, "rax" as *u8, NX_X64_SYS_EXIT_GROUP) 4075 x86_emit_syscall(o) 4076 4077 // Per-function emission (B1: only slots the live map keeps; NULL map keeps all). 4078 var i: i64 = 0 4079 while i < m.n_functions { 4080 let fn_base: i64 = m.functions as i64 4081 let f: *Function = (fn_base + i * 176) as *Function 4082 var emit_it: i64 = 1 4083 if live != (0 as *u8) { if live[i] == 0 { emit_it = 0 } } 4084 if emit_it == 1 { x86ctx_emit_function(f, o) } 4085 i = i + 1 4086 } 4087 4088 // Globals dump. 4089 x86ctx_emit_module_globals(m, o) 4090 4091 // GNU-stack note. 4092 x86_emit_gnu_stack_note(o) 4093 return 0 4094}