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}