code wiki / (root) / nx_x86_64_ctx.nx

nx_x86_64_ctx.nx source

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