nxasm_x86_enc.nx source
↩ module page · 1173 lines · 49438 B
1// nxasm_x86_enc.nx -- x86_64 instruction encoder + minimal ELF writer,
2// written in NishiLang. The x86_64 sibling of asm_enc.nx (RV64).
3//
4// First brick of the SOVEREIGN x86_64 backend: replaces GNU `as` + `ld`
5// on the output path. Pure bit-manipulation -- no syscalls, no file
6// I/O here, so it stays architecture-neutral and the encoder KAT runs
7// under any lane. Live emit (write the ELF to a fd) is composed in the
8// test/driver via the syscall layer.
9//
10// Scope (this iteration): the exact constructs needed to emit a
11// runnable static ELF -- enough to prove the whole sovereign chain
12// end-to-end with an exit(N) binary. The full 29-mnemonic AT&T subset
13// nxc2 emits (movq/movabsq/add/sub/and/or/cmp/test/imul/idiv/cqo/shl/
14// sar/lea/call/jmp/jcc/setcc/movzbq/...) lands incrementally on this
15// same REX/ModRM/SIB foundation, each gated by a byte-exact KAT vs the
16// objdump oracle (Wheeler/benchmark reference only -- NEVER in our
17// output path).
18//
19// Encoding model (Intel SDM Vol.2):
20// REX prefix 0100 WRXB (0x40 base; W=64-bit, B=reg ext bit3)
21// ModRM mod(2) reg(3) rm(3)
22// immediates little-endian
23//
24// genealogy_id: intel_sdm_vol2 + amd_apm + osdev_elf (Wheeler refs)
25// lineage_id: nishi_sovereign_x86_64_backend_m1
26// license_tier: ORIGINAL
27
28// ---- x86_64 general registers (encoding numbers) ----
29const X86_RAX: i64 = 0
30const X86_RCX: i64 = 1
31const X86_RDX: i64 = 2
32const X86_RBX: i64 = 3
33const X86_RSP: i64 = 4
34const X86_RBP: i64 = 5
35const X86_RSI: i64 = 6
36const X86_RDI: i64 = 7
37const X86_R8: i64 = 8
38const X86_R9: i64 = 9
39const X86_R10: i64 = 10
40const X86_R11: i64 = 11
41const X86_R12: i64 = 12
42const X86_R13: i64 = 13
43const X86_R14: i64 = 14
44const X86_R15: i64 = 15
45
46// ---- ELF constants ----
47const EM_X86_64: i64 = 62
48const X86_BASE: i64 = 0x400000 // conventional static ET_EXEC base
49const X86_HDRLEN: i64 = 120 // 64 (ehdr) + 56 (one phdr)
50
51// ---- little-endian buffer writers (return new offset) ----
52func x86_put_u16le(out: *u8, o: i64, v: i64) -> i64 {
53 out[o] = v & 0xff
54 out[o + 1] = (v >> 8) & 0xff
55 return o + 2
56}
57
58func x86_put_u32le(out: *u8, o: i64, v: i64) -> i64 {
59 out[o] = v & 0xff
60 out[o + 1] = (v >> 8) & 0xff
61 out[o + 2] = (v >> 16) & 0xff
62 out[o + 3] = (v >> 24) & 0xff
63 return o + 4
64}
65
66func x86_put_u64le(out: *u8, o: i64, v: i64) -> i64 {
67 out[o] = v & 0xff
68 out[o + 1] = (v >> 8) & 0xff
69 out[o + 2] = (v >> 16) & 0xff
70 out[o + 3] = (v >> 24) & 0xff
71 out[o + 4] = (v >> 32) & 0xff
72 out[o + 5] = (v >> 40) & 0xff
73 out[o + 6] = (v >> 48) & 0xff
74 out[o + 7] = (v >> 56) & 0xff
75 return o + 8
76}
77
78// REX.W prefix, optionally extending reg (bit3 -> REX.B) and a second
79// reg field (bit3 -> REX.R). 0x48 = 0100 1000 (W set).
80func x86_rex_w(reg_field: i64, rm_field: i64) -> i64 {
81 var rex: i64 = 0x48
82 if (reg_field & 8) != 0 { rex = rex | 0x04 } // REX.R
83 if (rm_field & 8) != 0 { rex = rex | 0x01 } // REX.B
84 return rex
85}
86
87// ModRM byte: mod(2) | reg(3) | rm(3).
88func x86_modrm(mod: i64, reg: i64, rm: i64) -> i64 {
89 return ((mod & 3) << 6) | ((reg & 7) << 3) | (rm & 7)
90}
91
92// SSE/SHA-NI register-register: [pfx?] [REX?] 0F [esc?] opc ModRM(11,reg,rm).
93// pfx = 0x66/0xF3/0xF2 or 0 (none); esc = 0x38/0x3A or 0 (none). reg/rm = xmm 0..15.
94// REX (with R/B for xmm8-15) is emitted only when needed, AFTER any mandatory prefix.
95// This single primitive covers sha256rnds2/msg1/msg2 (esc=0x38), pshufb (66,0x38),
96// paddd/pxor/punpck*/movdqa/movdqu (66 or F3, no esc) -- the SHA-256 SHA-NI core.
97func x86_sse_rr(out: *u8, o: i64, pfx: i64, esc: i64, opc: i64, reg: i64, rm: i64) -> i64 {
98 var p: i64 = o
99 if pfx != 0 { out[p] = pfx & 0xff; p = p + 1 }
100 var rex: i64 = 0x40
101 var need: i64 = 0
102 if reg >= 8 { rex = rex | 4; need = 1 }
103 if rm >= 8 { rex = rex | 1; need = 1 }
104 if need == 1 { out[p] = rex & 0xff; p = p + 1 }
105 out[p] = 0x0f; p = p + 1
106 if esc != 0 { out[p] = esc & 0xff; p = p + 1 }
107 out[p] = opc & 0xff; p = p + 1
108 out[p] = x86_modrm(3, reg, rm); p = p + 1
109 return p
110}
111
112// SSE SCALAR reg-reg WITH REX.W: the GPR<->xmm converts (cvtsi2ss r64->xmm,
113// cvtss2si/cvttss2si xmm->r64) operate on a 64-bit GPR, so REX.W is mandatory
114// and REX is ALWAYS emitted (after any mandatory prefix, before 0F). Same
115// ModRM(11,reg,rm) shape as x86_sse_rr; reg/rm still extend to 8..15 via
116// REX.R/REX.B. This is the ONLY new encoder hardware f32 (R1) needs -- the
117// scalar-float ALU ops (addss/subss/mulss/divss/sqrtss) reuse x86_sse_rr.
118// ---- AVX/VEX encoders (the 8-wide packed lever toward physics) -----------------
119// 2-byte VEX (C5) reg-reg: 0F-map ops on ymm/xmm, NDS 3-operand form.
120// dst = ModRM.reg, src2 = ModRM.rm, src1 = VEX.vvvv (inverted). L=1 -> 256-bit ymm.
121// pp: 0=none(packed-single 0F), 1=66, 2=F3, 3=F2. R extends dst to 8..15; src2 must be 0..7
122// (2-byte VEX has no B bit -- use x86_vex3_rr for src2>=8). e.g. vmulps ymm0,ymm1,ymm2 = C5 F4 59 C2.
123func x86_vex2_rr(out: *u8, o: i64, opc: i64, dst: i64, src1: i64, src2: i64, l: i64, pp: i64) -> i64 {
124 var p: i64 = o
125 out[p] = 0xc5; p = p + 1
126 var rbit: i64 = 1
127 if dst >= 8 { rbit = 0 }
128 let vvvv: i64 = (15 - (src1 & 15)) & 15
129 out[p] = (((rbit & 1) << 7) | ((vvvv & 15) << 3) | ((l & 1) << 2) | (pp & 3)) & 0xff; p = p + 1
130 out[p] = opc & 0xff; p = p + 1
131 out[p] = x86_modrm(3, dst & 7, src2 & 7); p = p + 1
132 return p
133}
134// 3-byte VEX (C4) reg-reg: needed for the 0F38/0F3A maps (FMA) and src2>=8.
135// mmmmm: 1=0F, 2=0F38, 3=0F3A. w: VEX.W. e.g. vfmadd231ps ymm0,ymm1,ymm2 = C4 E2 75 B8 C2.
136func x86_vex3_rr(out: *u8, o: i64, mmmmm: i64, w: i64, opc: i64, dst: i64, src1: i64, src2: i64, l: i64, pp: i64) -> i64 {
137 var p: i64 = o
138 out[p] = 0xc4; p = p + 1
139 var rbit: i64 = 1
140 if dst >= 8 { rbit = 0 }
141 var bbit: i64 = 1
142 if src2 >= 8 { bbit = 0 }
143 out[p] = (((rbit & 1) << 7) | (1 << 6) | ((bbit & 1) << 5) | (mmmmm & 31)) & 0xff; p = p + 1
144 let vvvv: i64 = (15 - (src1 & 15)) & 15
145 out[p] = (((w & 1) << 7) | ((vvvv & 15) << 3) | ((l & 1) << 2) | (pp & 3)) & 0xff; p = p + 1
146 out[p] = opc & 0xff; p = p + 1
147 out[p] = x86_modrm(3, dst & 7, src2 & 7); p = p + 1
148 return p
149}
150func x86_sse_rr_w(out: *u8, o: i64, pfx: i64, esc: i64, opc: i64, reg: i64, rm: i64) -> i64 {
151 var p: i64 = o
152 if pfx != 0 { out[p] = pfx & 0xff; p = p + 1 }
153 var rex: i64 = 0x48
154 if reg >= 8 { rex = rex | 4 }
155 if rm >= 8 { rex = rex | 1 }
156 out[p] = rex & 0xff; p = p + 1
157 out[p] = 0x0f; p = p + 1
158 if esc != 0 { out[p] = esc & 0xff; p = p + 1 }
159 out[p] = opc & 0xff; p = p + 1
160 out[p] = x86_modrm(3, reg, rm); p = p + 1
161 return p
162}
163
164// SSE register<->MEMORY: [pfx?] [REX?] 0F opc ModRM(mem)+SIB+disp. The
165// memory twin of x86_sse_rr -- the follow-on named at nxasm_x86.nx:454.
166// Encodes xmm<->m128 load/store (movdqu/movdqa mem forms): reg = the xmm
167// (0..15) goes in ModRM.reg; base/disp is the `disp(%base)` operand, run
168// through the proven x86_mem_operand (ModRM mod/disp8/disp32 + rsp/r12 SIB).
169// REX.R extends the xmm (xmm8-15); REX.B extends the base (r8-15); both
170// emitted AFTER the mandatory 66/F3 prefix per Intel SDM Vol.2 prefix order.
171// This is the load/store leg every SIMD kernel needs to move vectors in and
172// out of memory (ChaCha state, FNet tiles); reg-reg packed ALU already exists.
173func x86_sse_mem(out: *u8, o: i64, pfx: i64, opc: i64, reg: i64, base: i64, disp: i64) -> i64 {
174 var p: i64 = o
175 if pfx != 0 { out[p] = pfx & 0xff; p = p + 1 }
176 var rex: i64 = 0x40
177 var need: i64 = 0
178 if reg >= 8 { rex = rex | 4; need = 1 } // REX.R (xmm8-15)
179 if base >= 8 { rex = rex | 1; need = 1 } // REX.B (r8-15 base)
180 if need == 1 { out[p] = rex & 0xff; p = p + 1 }
181 out[p] = 0x0f; p = p + 1
182 out[p] = opc & 0xff; p = p + 1
183 return x86_mem_operand(out, p, reg, base, disp)
184}
185
186// SSE packed shift-by-IMMEDIATE: 66 [REX.B?] 0F opc /ext ib. The ModRM.reg
187// field is the opcode EXTENSION (not a register): pslld=/6 psrld=/2 on opc
188// 0x72 (dword), psllq=/6 psrlq=/2 on opc 0x73 (qword). rm = the xmm being
189// shifted (REX.B extends it to xmm8-15; there is no REX.R since reg is /ext).
190// This is the rotate primitive: rotl32(x,n) = (pslld x,n) OR (psrld x,32-n),
191// the inner loop of ChaCha/BLAKE and every ARX cipher.
192func x86_sse_shift_imm(out: *u8, o: i64, opc: i64, ext: i64, xmm: i64, imm8: i64) -> i64 {
193 var p: i64 = o
194 out[p] = 0x66; p = p + 1
195 if xmm >= 8 { out[p] = 0x41; p = p + 1 } // REX.B extends rm (xmm8-15)
196 out[p] = 0x0f; p = p + 1
197 out[p] = opc & 0xff; p = p + 1
198 out[p] = x86_modrm(3, ext, xmm); p = p + 1
199 out[p] = imm8 & 0xff; p = p + 1
200 return p
201}
202
203// SSE reg-reg-with-IMMEDIATE: [pfx?] [REX?] 0F opc ModRM(11,reg,rm) ib.
204// x86_sse_rr + a trailing imm8. Covers pshufd (66 0F 70 /r ib), pshuflw
205// (F2), pshufhw (F3), pshufw, shufps, etc. -- the 3-operand `$imm,%src,%dst`
206// shuffles. reg = dst (ModRM.reg), rm = src (ModRM.rm), imm8 = the control.
207func x86_sse_rri(out: *u8, o: i64, pfx: i64, opc: i64, reg: i64, rm: i64, imm8: i64) -> i64 {
208 var p: i64 = o
209 if pfx != 0 { out[p] = pfx & 0xff; p = p + 1 }
210 var rex: i64 = 0x40
211 var need: i64 = 0
212 if reg >= 8 { rex = rex | 4; need = 1 }
213 if rm >= 8 { rex = rex | 1; need = 1 }
214 if need == 1 { out[p] = rex & 0xff; p = p + 1 }
215 out[p] = 0x0f; p = p + 1
216 out[p] = opc & 0xff; p = p + 1
217 out[p] = x86_modrm(3, reg, rm); p = p + 1
218 out[p] = imm8 & 0xff; p = p + 1
219 return p
220}
221
222// ---- AVX / AVX2 VEX-prefix encoders (256-bit ymm, 3-byte C4 form) ----
223// The 3-byte VEX always-correct form (encodes all 16 ymm + every map/pp/W);
224// the shorter 2-byte C5 form is a later size optimization. Intel SDM Vol.2:
225// C4 | R~X~B~ mmmmm | W vvvv~ L pp | opcode | ModRM [SIB][disp][imm]
226// R~/X~/B~ and vvvv~ are stored INVERTED. pp: 0=none 1=66 2=F3 3=F2.
227// mmmmm: 1=0F 2=0F38 3=0F3A. L: 0=128(xmm) 1=256(ymm).
228
229// 3-operand reg ALU `<op> %src2,%src1,%dst` (AT&T): dst=ModRM.reg, src2=ModRM.rm,
230// src1=VEX.vvvv. Covers vpaddd/vpsubd/vpxor/vpand/vpor/vpaddq/... (the AVX2 NDS form).
231func x86_vex_rrr(out: *u8, o: i64, pp: i64, mmmmm: i64, w: i64, vexL: i64, opc: i64, dst: i64, src1: i64, src2: i64) -> i64 {
232 var p: i64 = o
233 out[p] = 0xc4; p = p + 1
234 var b1: i64 = mmmmm & 0x1f
235 if dst < 8 { b1 = b1 | 0x80 } // R~ (inverted REX.R from ModRM.reg)
236 b1 = b1 | 0x40 // X~ = 1 (no index in reg form)
237 if src2 < 8 { b1 = b1 | 0x20 } // B~ (inverted REX.B from ModRM.rm)
238 out[p] = b1; p = p + 1
239 var b2: i64 = (((0 - 1) - src1) & 0xf) << 3 // vvvv~ = ~src1 (4-bit), 1st source
240 if w != 0 { b2 = b2 | 0x80 }
241 if vexL != 0 { b2 = b2 | 0x04 }
242 b2 = b2 | (pp & 3)
243 out[p] = b2; p = p + 1
244 out[p] = opc & 0xff; p = p + 1
245 out[p] = x86_modrm(3, dst, src2); p = p + 1
246 return p
247}
248
249// 2-operand reg<->MEMORY `<op>` with no 1st-source (vvvv=1111): vmovdqu load/store.
250// reg = ModRM.reg (the ymm), base/disp = the memory operand via x86_mem_operand.
251func x86_vex_rm(out: *u8, o: i64, pp: i64, mmmmm: i64, w: i64, vexL: i64, opc: i64, reg: i64, base: i64, disp: i64) -> i64 {
252 var p: i64 = o
253 out[p] = 0xc4; p = p + 1
254 var b1: i64 = mmmmm & 0x1f
255 if reg < 8 { b1 = b1 | 0x80 } // R~ from ModRM.reg
256 b1 = b1 | 0x40 // X~ = 1 (no index)
257 if base < 8 { b1 = b1 | 0x20 } // B~ from base
258 out[p] = b1; p = p + 1
259 var b2: i64 = 0x78 // vvvv~ = 1111 (unused), then OR L/pp/W
260 if w != 0 { b2 = b2 | 0x80 }
261 if vexL != 0 { b2 = b2 | 0x04 }
262 b2 = b2 | (pp & 3)
263 out[p] = b2; p = p + 1
264 out[p] = opc & 0xff; p = p + 1
265 return x86_mem_operand(out, p, reg, base, disp)
266}
267
268// VEX shift-by-IMMEDIATE, NDD (dst in VEX.vvvv): VEX 66.0F <opc> /ext ib. The
269// non-destructive 3-operand form `<op> $imm,%src,%dst`: ModRM.reg = /ext (opcode
270// extension, REX.R unused), ModRM.rm = src, VEX.vvvv = dst. vpslld=/6 vpsrld=/2
271// vpsrad=/4 on 0x72(d); vpsllq=/6 vpsrlq=/2 on 0x73(q). The AVX2 ARX rotate leg:
272// rotl(v,n) = vpslld $n,v,t ; vpsrld $32-n,v,v ; vpor t,v,v (no movdqa, VEX-native).
273func x86_vex_shift_imm(out: *u8, o: i64, pp: i64, mmmmm: i64, vexL: i64, opc: i64, ext: i64, dst: i64, src: i64, imm8: i64) -> i64 {
274 var p: i64 = o
275 out[p] = 0xc4; p = p + 1
276 var b1: i64 = mmmmm & 0x1f
277 b1 = b1 | 0x80 // R~ = 1 (ModRM.reg is the /ext)
278 b1 = b1 | 0x40 // X~ = 1
279 if src < 8 { b1 = b1 | 0x20 } // B~ from src (ModRM.rm)
280 out[p] = b1; p = p + 1
281 var b2: i64 = (((0 - 1) - dst) & 0xf) << 3 // vvvv~ = ~dst
282 if vexL != 0 { b2 = b2 | 0x04 }
283 b2 = b2 | (pp & 3)
284 out[p] = b2; p = p + 1
285 out[p] = opc & 0xff; p = p + 1
286 out[p] = x86_modrm(3, ext, src); p = p + 1
287 out[p] = imm8 & 0xff; p = p + 1
288 return p
289}
290
291// VEX reg<-rm with IMMEDIATE, no 1st-source (vvvv=1111): `<op> $imm,%src,%dst`.
292// reg=dst(ModRM.reg), rm=src(ModRM.rm), imm8. vpshufd (66.0F 70 /r ib), vpshuflw
293// (F2), vpshufhw (F3), vpermq/vpermpd (66.0F3A W1). The per-128-lane dword shuffle
294// (ChaCha diagonalize across 2 blocks at once on ymm).
295func x86_vex_rmi(out: *u8, o: i64, pp: i64, mmmmm: i64, w: i64, vexL: i64, opc: i64, dst: i64, src: i64, imm8: i64) -> i64 {
296 var p: i64 = o
297 out[p] = 0xc4; p = p + 1
298 var b1: i64 = mmmmm & 0x1f
299 if dst < 8 { b1 = b1 | 0x80 } // R~ from dst
300 b1 = b1 | 0x40 // X~ = 1
301 if src < 8 { b1 = b1 | 0x20 } // B~ from src
302 out[p] = b1; p = p + 1
303 var b2: i64 = 0x78 // vvvv~ = 1111
304 if w != 0 { b2 = b2 | 0x80 }
305 if vexL != 0 { b2 = b2 | 0x04 }
306 b2 = b2 | (pp & 3)
307 out[p] = b2; p = p + 1
308 out[p] = opc & 0xff; p = p + 1
309 out[p] = x86_modrm(3, dst, src); p = p + 1
310 out[p] = imm8 & 0xff; p = p + 1
311 return p
312}
313
314// ---- instruction encoders (write into out at o, return new offset) ----
315
316// movabsq $imm64, %reg -> REX.W B8+rd imm64
317func x86_movabs(out: *u8, o: i64, reg: i64, imm: i64) -> i64 {
318 var p: i64 = o
319 out[p] = 0x48 | ((reg >> 3) & 1) // REX.W (+REX.B if reg>=8)
320 p = p + 1
321 out[p] = 0xB8 + (reg & 7)
322 p = p + 1
323 return x86_put_u64le(out, p, imm)
324}
325
326// movq $imm32, %reg -> REX.W C7 /0 id (imm sign-extended to 64)
327func x86_mov_imm32(out: *u8, o: i64, reg: i64, imm: i64) -> i64 {
328 var p: i64 = o
329 out[p] = x86_rex_w(0, reg)
330 p = p + 1
331 out[p] = 0xC7
332 p = p + 1
333 out[p] = x86_modrm(3, 0, reg)
334 p = p + 1
335 return x86_put_u32le(out, p, imm)
336}
337
338// movq %src, %dst -> REX.W 89 /r (reg=src, rm=dst)
339func x86_mov_reg(out: *u8, o: i64, dst: i64, src: i64) -> i64 {
340 var p: i64 = o
341 out[p] = x86_rex_w(src, dst)
342 p = p + 1
343 out[p] = 0x89
344 p = p + 1
345 out[p] = x86_modrm(3, src, dst)
346 return p + 1
347}
348
349// syscall -> 0F 05
350func x86_syscall(out: *u8, o: i64) -> i64 {
351 out[o] = 0x0F
352 out[o + 1] = 0x05
353 return o + 2
354}
355
356// rdtsc -> 0F 31 (read time-stamp counter into edx:eax; no operands). Added 2026-07-06 so the sovereign
357// assembler can build the cycle-measurement organs (nx_rdtsc_test / nx_linkqual_cycles) -- previously only GNU
358// `as` could, which forced nx_engineer's rdtsc gate onto a /bin/bash .sh wrapper.
359func x86_rdtsc(out: *u8, o: i64) -> i64 {
360 out[o] = 0x0F
361 out[o + 1] = 0x31
362 return o + 2
363}
364
365// ret -> C3
366func x86_ret(out: *u8, o: i64) -> i64 {
367 out[o] = 0xC3
368 return o + 1
369}
370
371// pushq %reg -> (REX.B if reg>=8) 50+rd
372func x86_push(out: *u8, o: i64, reg: i64) -> i64 {
373 var p: i64 = o
374 if (reg & 8) != 0 { out[p] = 0x41; p = p + 1 }
375 out[p] = 0x50 + (reg & 7)
376 return p + 1
377}
378
379// popq %reg -> (REX.B if reg>=8) 58+rd
380func x86_pop(out: *u8, o: i64, reg: i64) -> i64 {
381 var p: i64 = o
382 if (reg & 8) != 0 { out[p] = 0x41; p = p + 1 }
383 out[p] = 0x58 + (reg & 7)
384 return p + 1
385}
386
387// ---- ALU reg,reg opcodes (MR form: REX.W <op> /r ; reg=src, rm=dst) ----
388const X86_OP_ADD: i64 = 0x01
389const X86_OP_SUB: i64 = 0x29
390const X86_OP_AND: i64 = 0x21
391const X86_OP_OR: i64 = 0x09
392const X86_OP_XOR: i64 = 0x31
393const X86_OP_CMP: i64 = 0x39
394const X86_OP_TEST: i64 = 0x85
395// ALU imm extension digits (REX.W 83 /ext ib | 81 /ext id)
396const X86_EXT_ADD: i64 = 0
397const X86_EXT_OR: i64 = 1
398const X86_EXT_AND: i64 = 4
399const X86_EXT_SUB: i64 = 5
400const X86_EXT_CMP: i64 = 7
401// shift extension digits (REX.W C1 /ext ib)
402const X86_EXT_SHL: i64 = 4
403const X86_EXT_SHR: i64 = 5
404const X86_EXT_SAR: i64 = 7
405// condition codes (jcc 0F 80+cc rel32 | setcc 0F 90+cc /0 | cmovcc 0F 40+cc /r)
406// LN18 (2026-09-01): the overflow pair -- OF=1 / OF=0 -- for the checked-arithmetic fuse (jo / jno).
407const X86_CC_O: i64 = 0
408const X86_CC_NO: i64 = 1
409const X86_CC_E: i64 = 4
410const X86_CC_NE: i64 = 5
411const X86_CC_L: i64 = 0xC
412const X86_CC_LE: i64 = 0xE
413const X86_CC_G: i64 = 0xF
414const X86_CC_GE: i64 = 0xD
415const X86_CC_S: i64 = 8 // SF=1 (negative) -- the G22 bias-select
416const X86_CC_NS: i64 = 9 // SF=0
417
418// ALU reg,reg: `<op>q %src,%dst` -> REX.W <opcode> ModRM(11 src dst)
419func x86_alu_rr(out: *u8, o: i64, opcode: i64, dst: i64, src: i64) -> i64 {
420 var p: i64 = o
421 out[p] = x86_rex_w(src, dst); p = p + 1
422 out[p] = opcode; p = p + 1
423 out[p] = x86_modrm(3, src, dst); p = p + 1
424 return p
425}
426
427// ALU imm,reg: `<op>q $imm,%dst`. imm8 form (83 /ext ib) when it fits a
428// signed byte, else imm32 form (81 /ext id) sign-extended to 64.
429func x86_alu_imm(out: *u8, o: i64, ext: i64, dst: i64, imm: i64) -> i64 {
430 var p: i64 = o
431 out[p] = x86_rex_w(0, dst); p = p + 1
432 var short: i64 = 0
433 if imm >= -128 { if imm <= 127 { short = 1 } }
434 if short == 1 {
435 out[p] = 0x83; p = p + 1
436 out[p] = x86_modrm(3, ext, dst); p = p + 1
437 out[p] = imm & 0xff; p = p + 1
438 return p
439 }
440 out[p] = 0x81; p = p + 1
441 out[p] = x86_modrm(3, ext, dst); p = p + 1
442 return x86_put_u32le(out, p, imm)
443}
444
445// Emit ModRM(+SIB+disp) for a `disp(%base)` memory operand with the
446// other operand in reg_field. mod=01(disp8) if disp fits a signed byte
447// else mod=10(disp32); base&7==4 (rsp/r12) requires the SIB byte 0x24.
448// (base==rbp with disp=0 still uses disp8=0, matching gas.)
449func x86_mem_operand(out: *u8, o: i64, reg_field: i64, base: i64, disp: i64) -> i64 {
450 var p: i64 = o
451 var mod: i64 = 2
452 if disp >= -128 { if disp <= 127 { mod = 1 } }
453 out[p] = x86_modrm(mod, reg_field, base & 7); p = p + 1
454 if (base & 7) == 4 { out[p] = 0x24; p = p + 1 } // SIB: scale0 index=none base=rsp
455 if mod == 1 { out[p] = disp & 0xff; return p + 1 }
456 return x86_put_u32le(out, p, disp)
457}
458
459// movq disp(%base),%dst (load) -> REX.W 8B /r
460func x86_mov_load(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 {
461 var p: i64 = o
462 out[p] = x86_rex_w(dst, base); p = p + 1
463 out[p] = 0x8B; p = p + 1
464 return x86_mem_operand(out, p, dst, base, disp)
465}
466
467// movq %src,disp(%base) (store) -> REX.W 89 /r
468func x86_mov_store(out: *u8, o: i64, base: i64, disp: i64, src: i64) -> i64 {
469 var p: i64 = o
470 out[p] = x86_rex_w(src, base); p = p + 1
471 out[p] = 0x89; p = p + 1
472 return x86_mem_operand(out, p, src, base, disp)
473}
474
475// ---- SIB (%base,%index,scale) addressing (2026-07-15, for array indexing) ----
476// REX.W + X(index) + R(reg) + B(base). The plain x86_rex_w has no X bit, so
477// indexed addressing needs this variant.
478func x86_rex_wx(reg_field: i64, index_field: i64, base_field: i64) -> i64 {
479 var rex: i64 = 0x48
480 if (reg_field & 8) != 0 { rex = rex | 0x04 } // REX.R
481 if (index_field & 8) != 0 { rex = rex | 0x02 } // REX.X
482 if (base_field & 8) != 0 { rex = rex | 0x01 } // REX.B
483 return rex
484}
485
486// scale VALUE {1,2,4,8} -> the 2-bit SIB scale field {0,1,2,3}.
487func x86_scale_log2(scale: i64) -> i64 {
488 if scale == 8 { return 3 }
489 if scale == 4 { return 2 }
490 if scale == 2 { return 1 }
491 return 0
492}
493
494// ModRM(rm=100 => SIB) + SIB(scale,index,base) + disp for (%base,%index,scale).
495// mod=00 (no disp) when disp==0 AND base&7 != 5 (rbp/r13 need disp8=0); else
496// disp8/disp32. index MUST NOT be rsp (4) -- the SIB index=100 means "none";
497// the compiler never selects rsp as an index, and the parser rejects it below.
498func x86_mem_operand_sib(out: *u8, o: i64, reg_field: i64, base: i64,
499 index: i64, scale: i64, disp: i64) -> i64 {
500 var p: i64 = o
501 var mod: i64 = 2
502 if disp >= -128 { if disp <= 127 { mod = 1 } }
503 if disp == 0 { if (base & 7) != 5 { mod = 0 } }
504 out[p] = x86_modrm(mod, reg_field, 4); p = p + 1 // rm=100 => SIB follows
505 out[p] = ((x86_scale_log2(scale) & 3) << 6) | ((index & 7) << 3) | (base & 7); p = p + 1
506 if mod == 0 { return p }
507 if mod == 1 { out[p] = disp & 0xff; return p + 1 }
508 return x86_put_u32le(out, p, disp)
509}
510
511// movq disp(%base,%index,scale),%dst (load) -> REX.WX 8B /r SIB
512func x86_mov_load_sib(out: *u8, o: i64, dst: i64, base: i64, index: i64, scale: i64, disp: i64) -> i64 {
513 var p: i64 = o
514 out[p] = x86_rex_wx(dst, index, base); p = p + 1
515 out[p] = 0x8B; p = p + 1
516 return x86_mem_operand_sib(out, p, dst, base, index, scale, disp)
517}
518
519// movq %src,disp(%base,%index,scale) (store) -> REX.WX 89 /r SIB
520func x86_mov_store_sib(out: *u8, o: i64, base: i64, index: i64, scale: i64, disp: i64, src: i64) -> i64 {
521 var p: i64 = o
522 out[p] = x86_rex_wx(src, index, base); p = p + 1
523 out[p] = 0x89; p = p + 1
524 return x86_mem_operand_sib(out, p, src, base, index, scale, disp)
525}
526
527// movl (32-bit): store 89 /r, load 8B /r -- NO REX.W; REX only for r8d+.
528// Emitted by the compiler for i32 struct-field stores (e.g. pollfd.fd).
529func x86_movl_rex_if(out: *u8, o: i64, reg: i64, rm: i64) -> i64 {
530 if ((reg | rm) & 8) == 0 { return o }
531 var rex: i64 = 0x40
532 if (reg & 8) != 0 { rex = rex | 0x04 }
533 if (rm & 8) != 0 { rex = rex | 0x01 }
534 out[o] = rex
535 return o + 1
536}
537func x86_movl_store(out: *u8, o: i64, base: i64, disp: i64, src: i64) -> i64 {
538 var p: i64 = x86_movl_rex_if(out, o, src, base)
539 out[p] = 0x89; p = p + 1
540 return x86_mem_operand(out, p, src, base, disp)
541}
542func x86_movl_load(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 {
543 var p: i64 = x86_movl_rex_if(out, o, dst, base)
544 out[p] = 0x8B; p = p + 1
545 return x86_mem_operand(out, p, dst, base, disp)
546}
547// movl %src,%dst (reg-reg) -> [REX] 89 /r mod=3
548func x86_movl_rr(out: *u8, o: i64, dst: i64, src: i64) -> i64 {
549 var p: i64 = x86_movl_rex_if(out, o, src, dst)
550 out[p] = 0x89; p = p + 1
551 out[p] = x86_modrm(3, src, dst); p = p + 1
552 return p
553}
554
555// leaq disp(%base),%dst -> REX.W 8D /r
556func x86_lea(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 {
557 var p: i64 = o
558 out[p] = x86_rex_w(dst, base); p = p + 1
559 out[p] = 0x8D; p = p + 1
560 return x86_mem_operand(out, p, dst, base, disp)
561}
562
563// leaq disp(%base,%index,scale),%dst -> REX.WRXB 8D /r SIB.
564// Same ModRM/SIB/disp shape as x86_mov_load_sib (8B), opcode 8D. The address
565// arithmetic (base + index*scale + disp) is COMPUTED into dst, no memory touch;
566// this is the lea-strength peephole target (c*n+d => leaq d(%n,%n,s) for
567// c in {2,3,5,9} with base==index==n).
568func x86_lea_sib(out: *u8, o: i64, dst: i64, base: i64, index: i64, scale: i64, disp: i64) -> i64 {
569 var p: i64 = o
570 out[p] = x86_rex_wx(dst, index, base); p = p + 1
571 out[p] = 0x8D; p = p + 1
572 return x86_mem_operand_sib(out, p, dst, base, index, scale, disp)
573}
574
575// leaq disp(%rip),%dst -> REX.W 8D /r, ModRM mod=00 reg=dst rm=101, disp32.
576// disp32 is relative to the END of this 7-byte instruction (RIP-relative).
577func x86_lea_rip(out: *u8, o: i64, dst: i64, disp32: i64) -> i64 {
578 var p: i64 = o
579 var rex: i64 = 0x48
580 if (dst & 8) != 0 { rex = rex | 0x04 } // REX.R
581 out[p] = rex; p = p + 1
582 out[p] = 0x8D; p = p + 1
583 out[p] = x86_modrm(0, dst, 5); p = p + 1 // mod=00 rm=101 => RIP-relative
584 return x86_put_u32le(out, p, disp32)
585}
586
587// imulq %src,%dst -> REX.W 0F AF /r (reg=dst, rm=src)
588func x86_imul_rr(out: *u8, o: i64, dst: i64, src: i64) -> i64 {
589 var p: i64 = o
590 out[p] = x86_rex_w(dst, src); p = p + 1
591 out[p] = 0x0F; p = p + 1
592 out[p] = 0xAF; p = p + 1
593 out[p] = x86_modrm(3, dst, src); p = p + 1
594 return p
595}
596
597// movzbq %src8,%dst -> REX.W 0F B6 /r (zero-extend byte to 64)
598func x86_movzbq(out: *u8, o: i64, dst: i64, src: i64) -> i64 {
599 var p: i64 = o
600 out[p] = x86_rex_w(dst, src); p = p + 1
601 out[p] = 0x0F; p = p + 1
602 out[p] = 0xB6; p = p + 1
603 out[p] = x86_modrm(3, dst, src); p = p + 1
604 return p
605}
606// movzbq disp(%base),%dst (MEMORY byte load, zero-extend) -> REX.W 0F B6 /r (twin of x86_mov_load)
607func x86_movzbq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 {
608 var p: i64 = o
609 out[p] = x86_rex_w(dst, base); p = p + 1
610 out[p] = 0x0F; p = p + 1
611 out[p] = 0xB6; p = p + 1
612 return x86_mem_operand(out, p, dst, base, disp)
613}
614
615// ---- SIGN-extending subword loads (2026-07-10, subword sign-extend debt fix) --------------------
616// The missing signed twins of movzbq/movzwq -- their ABSENCE is why the compiler backend went
617// zero-extend-only on every subword load (x86_emit_load_*_signed emitted mnemonics nxasm could not
618// assemble). Byte/word are the 0F BE / 0F BF opcode-siblings of movzbq's 0F B6; dword is MOVSXD
619// (REX.W 63 /r, no 0F prefix -- x86_mov_load's shape with a different opcode). movzwq (zero-word)
620// was ALSO missing and is added as the natural twin.
621// movsbq %src,%dst (reg-reg, sign-extend byte -> 64) -> REX.W 0F BE /r mod=3
622func x86_movsbq(out: *u8, o: i64, dst: i64, src: i64) -> i64 {
623 var p: i64 = o
624 out[p] = x86_rex_w(dst, src); p = p + 1
625 out[p] = 0x0F; p = p + 1
626 out[p] = 0xBE; p = p + 1
627 out[p] = x86_modrm(3, dst, src); p = p + 1
628 return p
629}
630// movsbq disp(%base),%dst (MEMORY byte load, SIGN-extend) -> REX.W 0F BE /r
631func x86_movsbq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 {
632 var p: i64 = o
633 out[p] = x86_rex_w(dst, base); p = p + 1
634 out[p] = 0x0F; p = p + 1
635 out[p] = 0xBE; p = p + 1
636 return x86_mem_operand(out, p, dst, base, disp)
637}
638// movswq %src,%dst (reg-reg, sign-extend word -> 64) -> REX.W 0F BF /r mod=3
639func x86_movswq(out: *u8, o: i64, dst: i64, src: i64) -> i64 {
640 var p: i64 = o
641 out[p] = x86_rex_w(dst, src); p = p + 1
642 out[p] = 0x0F; p = p + 1
643 out[p] = 0xBF; p = p + 1
644 out[p] = x86_modrm(3, dst, src); p = p + 1
645 return p
646}
647// movswq disp(%base),%dst (MEMORY word load, SIGN-extend) -> REX.W 0F BF /r
648func x86_movswq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 {
649 var p: i64 = o
650 out[p] = x86_rex_w(dst, base); p = p + 1
651 out[p] = 0x0F; p = p + 1
652 out[p] = 0xBF; p = p + 1
653 return x86_mem_operand(out, p, dst, base, disp)
654}
655// movzwq %src,%dst (reg-reg, ZERO-extend word -> 64) -> REX.W 0F B7 /r mod=3
656func x86_movzwq(out: *u8, o: i64, dst: i64, src: i64) -> i64 {
657 var p: i64 = o
658 out[p] = x86_rex_w(dst, src); p = p + 1
659 out[p] = 0x0F; p = p + 1
660 out[p] = 0xB7; p = p + 1
661 out[p] = x86_modrm(3, dst, src); p = p + 1
662 return p
663}
664// movzwq disp(%base),%dst (MEMORY word load, ZERO-extend) -> REX.W 0F B7 /r
665func x86_movzwq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 {
666 var p: i64 = o
667 out[p] = x86_rex_w(dst, base); p = p + 1
668 out[p] = 0x0F; p = p + 1
669 out[p] = 0xB7; p = p + 1
670 return x86_mem_operand(out, p, dst, base, disp)
671}
672// movslq %src,%dst (reg-reg MOVSXD, sign-extend dword -> 64) -> REX.W 63 /r mod=3
673func x86_movslq(out: *u8, o: i64, dst: i64, src: i64) -> i64 {
674 var p: i64 = o
675 out[p] = x86_rex_w(dst, src); p = p + 1
676 out[p] = 0x63; p = p + 1
677 out[p] = x86_modrm(3, dst, src); p = p + 1
678 return p
679}
680// movslq disp(%base),%dst (MEMORY dword load, SIGN-extend / MOVSXD) -> REX.W 63 /r
681func x86_movslq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 {
682 var p: i64 = o
683 out[p] = x86_rex_w(dst, base); p = p + 1
684 out[p] = 0x63; p = p + 1
685 return x86_mem_operand(out, p, dst, base, disp)
686}
687
688// cqo (sign-extend rax into rdx:rax for idiv) -> REX.W 99
689func x86_cqo(out: *u8, o: i64) -> i64 {
690 out[o] = 0x48; out[o + 1] = 0x99
691 return o + 2
692}
693
694// idivq %reg -> REX.W F7 /7
695func x86_idiv(out: *u8, o: i64, reg: i64) -> i64 {
696 var p: i64 = o
697 out[p] = x86_rex_w(0, reg); p = p + 1
698 out[p] = 0xF7; p = p + 1
699 out[p] = x86_modrm(3, 7, reg); p = p + 1
700 return p
701}
702
703// mulq %reg -> REX.W F7 /4 (UNSIGNED 64x64 -> rdx:rax = rax * reg).
704// Same F7 group as idiv (/7), neg (/3), not (/2); only the /digit differs.
705// REX.B (via x86_rex_w) extends reg to r8-r15. This is the __umulhi64
706// primitive (the rdx half) -- the wide-multiply core of every bignum/EC field-mul.
707func x86_mul(out: *u8, o: i64, reg: i64) -> i64 {
708 var p: i64 = o
709 out[p] = x86_rex_w(0, reg); p = p + 1
710 out[p] = 0xF7; p = p + 1
711 out[p] = x86_modrm(3, 4, reg); p = p + 1
712 return p
713}
714
715// crc32q %src,%dst -> F2 REX.W 0F 38 F1 /r (SSE4.2 CRC-32C/Castagnoli,
716// accumulate: dst = CRC32C(dst, src)). AT&T src,dst -> ModRM reg=dst (the
717// running CRC accumulator), rm=src (the 64-bit data word folded in). The
718// mandatory F2 prefix precedes REX.W (Intel SDM Vol.2 prefix order); REX.R
719// extends dst to r8-15, REX.B extends src. This is the hardware CRC-32C
720// primitive -- checksums, network-packet validation, hash-table fingerprints.
721func x86_crc32_r64(out: *u8, o: i64, dst: i64, src: i64) -> i64 {
722 var p: i64 = o
723 out[p] = 0xF2; p = p + 1
724 out[p] = x86_rex_w(dst, src); p = p + 1
725 out[p] = 0x0F; p = p + 1
726 out[p] = 0x38; p = p + 1
727 out[p] = 0xF1; p = p + 1
728 out[p] = x86_modrm(3, dst, src); p = p + 1
729 return p
730}
731
732// pdep %src2,%src1,%dst -> VEX.LZ.F2.0F38.W1 F5 /r (BMI2 parallel bit
733// DEPOSIT: scatter the low bits of src1 into the set-bit positions of the
734// mask src2, result in dst). AT&T src2,src1,dst -> dst=ModRM.reg,
735// src1=VEX.vvvv, src2=ModRM.rm. 3-byte VEX (0F38 map). Composes the proven
736// x86_vex3_rr encoder (mmmmm=2 for 0F38, W=1, pp=3 for F2, L=0). Deposit is
737// the varint-encode / bitboard-scatter / bit-interleave primitive.
738func x86_pdep_r64(out: *u8, o: i64, dst: i64, src1: i64, src2: i64) -> i64 {
739 return x86_vex3_rr(out, o, 2, 1, 0xF5, dst, src1, src2, 0, 3)
740}
741
742// pext %src2,%src1,%dst -> VEX.LZ.F3.0F38.W1 F5 /r (BMI2 parallel bit
743// EXTRACT: gather the src1 bits at the set-bit positions of the mask src2
744// down to the low bits of dst -- the inverse of pdep). Same operand layout
745// as pdep; only the mandatory prefix differs (pp=2 for F3). Extract is the
746// varint-decode / bitboard-gather / unicode-transcode / compression primitive.
747func x86_pext_r64(out: *u8, o: i64, dst: i64, src1: i64, src2: i64) -> i64 {
748 return x86_vex3_rr(out, o, 2, 1, 0xF5, dst, src1, src2, 0, 2)
749}
750
751// ---- BATCH 2: ADX/BMI2 wide-multiply dual-carry-chain (the P-256/bignum unlock) ----
752// mulx dst_hi, dst_lo, src -> VEX.NDD.LZ.F2.0F38.W1 F6 /r (BMI2 flags-free
753// unsigned 64x64->128 multiply: src * implicit RDX; low half -> dst_lo, high half
754// -> dst_hi; leaves CF/OF UNTOUCHED so it interleaves with two add-carry chains).
755// Intel SDM operand map (VEX.NDD): dst_hi = VEX.vvvv, dst_lo = ModRM.reg, src = ModRM.rm.
756// Composing x86_vex3_rr(mmmmm=2 [0F38], W=1, opc=0xF6, pp=3 [F2], L=0), whose params are
757// (dst->ModRM.reg, src1->VEX.vvvv, src2->ModRM.rm) -> pass dst_lo as dst, dst_hi as src1,
758// src as src2. This is THE instruction the shelved u256_mul_wide_4x64 waited for.
759func x86_mulx_r64(out: *u8, o: i64, dst_hi: i64, dst_lo: i64, src: i64) -> i64 {
760 // Intel SDM operand encoding: ModRM.reg = r64a = dst_HI, VEX.vvvv = r64b = dst_LO, ModRM.rm = src.
761 // x86_vex3_rr maps its (dst -> ModRM.reg, src1 -> VEX.vvvv), so pass dst_hi as dst, dst_lo as src1.
762 return x86_vex3_rr(out, o, 2, 1, 0xF6, dst_hi, dst_lo, src, 0, 3)
763}
764
765// adcx dst,src -> 66 REX.W 0F 38 F6 /r (ADX: dst = dst + src + CF, updates ONLY CF).
766// adox dst,src -> F3 REX.W 0F 38 F6 /r (ADX: dst = dst + src + OF, updates ONLY OF).
767// The two run INDEPENDENT carry chains (CF vs OF) so a schoolbook column can accumulate
768// both carries in parallel around flags-free MULX -> ~1 cyc/limb (OpenSSL nistp256 path).
769// Same legacy-prefix 0F38 shape as x86_crc32_r64; only the mandatory prefix + opcode differ.
770// AT&T dst,src -> ModRM.reg = dst (accumulator), ModRM.rm = src.
771func x86_adcx_r64(out: *u8, o: i64, dst: i64, src: i64) -> i64 {
772 var p: i64 = o
773 out[p] = 0x66; p = p + 1
774 out[p] = x86_rex_w(dst, src); p = p + 1
775 out[p] = 0x0F; p = p + 1
776 out[p] = 0x38; p = p + 1
777 out[p] = 0xF6; p = p + 1
778 out[p] = x86_modrm(3, dst, src); p = p + 1
779 return p
780}
781func x86_adox_r64(out: *u8, o: i64, dst: i64, src: i64) -> i64 {
782 var p: i64 = o
783 out[p] = 0xF3; p = p + 1
784 out[p] = x86_rex_w(dst, src); p = p + 1
785 out[p] = 0x0F; p = p + 1
786 out[p] = 0x38; p = p + 1
787 out[p] = 0xF6; p = p + 1
788 out[p] = x86_modrm(3, dst, src); p = p + 1
789 return p
790}
791
792// shift $imm8,%reg -> REX.W C1 /ext ib (ext = SHL/SHR/SAR)
793func x86_shift_imm(out: *u8, o: i64, ext: i64, reg: i64, imm8: i64) -> i64 {
794 var p: i64 = o
795 out[p] = x86_rex_w(0, reg); p = p + 1
796 out[p] = 0xC1; p = p + 1
797 out[p] = x86_modrm(3, ext, reg); p = p + 1
798 out[p] = imm8 & 0xff; p = p + 1
799 return p
800}
801
802// jmp rel32 -> E9 cd (deterministic always-rel32, per charter)
803func x86_jmp_rel32(out: *u8, o: i64, rel: i64) -> i64 {
804 out[o] = 0xE9
805 return x86_put_u32le(out, o + 1, rel)
806}
807
808// call rel32 -> E8 cd
809func x86_call_rel32(out: *u8, o: i64, rel: i64) -> i64 {
810 out[o] = 0xE8
811 return x86_put_u32le(out, o + 1, rel)
812}
813
814// jcc rel32 -> 0F 80+cc cd
815func x86_jcc_rel32(out: *u8, o: i64, cc: i64, rel: i64) -> i64 {
816 out[o] = 0x0F
817 out[o + 1] = 0x80 + cc
818 return x86_put_u32le(out, o + 2, rel)
819}
820
821// cmovcc %src,%dst -> REX.W 0F 40+cc /r (reg=dst, rm=src). Conditional move,
822// flag-READ-only (never writes flags) -- the G22 bias-via-cmov division form.
823// Same two-byte reg,reg shape as x86_imul_rr.
824func x86_cmovcc(out: *u8, o: i64, cc: i64, dst: i64, src: i64) -> i64 {
825 var p: i64 = o
826 out[p] = x86_rex_w(dst, src); p = p + 1
827 out[p] = 0x0F; p = p + 1
828 out[p] = 0x40 + cc; p = p + 1
829 out[p] = x86_modrm(3, dst, src); p = p + 1
830 return p
831}
832
833// setcc %reg8 -> [REX] 0F 90+cc /0 (REX needed for spl/bpl/sil/dil + r8b-r15b)
834func x86_setcc(out: *u8, o: i64, cc: i64, reg: i64) -> i64 {
835 var p: i64 = o
836 if reg >= 4 { out[p] = 0x40 | ((reg >> 3) & 1); p = p + 1 }
837 out[p] = 0x0F; p = p + 1
838 out[p] = 0x90 + cc; p = p + 1
839 out[p] = x86_modrm(3, 0, reg); p = p + 1
840 return p
841}
842
843// ---- M2 opcode tail: rotates / bit-scan / unary / byte / indirect ----
844// rotate $imm8,%reg -> REX.W C1 /(0=rol | 1=ror) ib (the __rotr64 class)
845func x86_rot_imm(out: *u8, o: i64, is_ror: i64, reg: i64, imm8: i64) -> i64 {
846 var p: i64 = o
847 out[p] = x86_rex_w(0, reg); p = p + 1
848 out[p] = 0xC1; p = p + 1
849 out[p] = x86_modrm(3, is_ror, reg); p = p + 1
850 out[p] = imm8 & 0xff; p = p + 1
851 return p
852}
853// rotate %cl,%reg -> REX.W D3 /(0|1)
854func x86_rot_cl(out: *u8, o: i64, is_ror: i64, reg: i64) -> i64 {
855 var p: i64 = o
856 out[p] = x86_rex_w(0, reg); p = p + 1
857 out[p] = 0xD3; p = p + 1
858 out[p] = x86_modrm(3, is_ror, reg); p = p + 1
859 return p
860}
861// shift %cl,%reg -> REX.W D3 /ext (ext = SHL=4 | SHR=5 | SAR=7) -- twin of x86_rot_cl
862func x86_shift_cl(out: *u8, o: i64, ext: i64, reg: i64) -> i64 {
863 var p: i64 = o
864 out[p] = x86_rex_w(0, reg); p = p + 1
865 out[p] = 0xD3; p = p + 1
866 out[p] = x86_modrm(3, ext, reg); p = p + 1
867 return p
868}
869// bswapq %reg -> REX.W 0F C8+rd
870func x86_bswap(out: *u8, o: i64, reg: i64) -> i64 {
871 var p: i64 = o
872 out[p] = 0x48 | ((reg >> 3) & 1); p = p + 1
873 out[p] = 0x0F; p = p + 1
874 out[p] = 0xC8 + (reg & 7); p = p + 1
875 return p
876}
877// popcntq %src,%dst -> F3 REX.W 0F B8 /r (F3 prefix precedes REX)
878func x86_popcnt(out: *u8, o: i64, dst: i64, src: i64) -> i64 {
879 var p: i64 = o
880 out[p] = 0xF3; p = p + 1
881 out[p] = x86_rex_w(dst, src); p = p + 1
882 out[p] = 0x0F; p = p + 1
883 out[p] = 0xB8; p = p + 1
884 out[p] = x86_modrm(3, dst, src); p = p + 1
885 return p
886}
887// lzcntl/tzcntl %src,%dst (32-bit) -> F3 [REX] 0F BD|BC /r
888// REX (no W) only when an extended register (r8d+) is involved -- the
889// compiler's clz32/ctz32 lowering emits the %eax,%eax form.
890func x86_cnt32(out: *u8, o: i64, opc: i64, dst: i64, src: i64) -> i64 {
891 var p: i64 = o
892 out[p] = 0xF3; p = p + 1
893 if ((dst | src) & 8) != 0 {
894 var rex: i64 = 0x40
895 if (dst & 8) != 0 { rex = rex | 0x04 } // REX.R
896 if (src & 8) != 0 { rex = rex | 0x01 } // REX.B
897 out[p] = rex; p = p + 1
898 }
899 out[p] = 0x0F; p = p + 1
900 out[p] = opc; p = p + 1
901 out[p] = x86_modrm(3, dst, src); p = p + 1
902 return p
903}
904func x86_lzcnt32(out: *u8, o: i64, dst: i64, src: i64) -> i64 { return x86_cnt32(out, o, 0xBD, dst, src) }
905func x86_tzcnt32(out: *u8, o: i64, dst: i64, src: i64) -> i64 { return x86_cnt32(out, o, 0xBC, dst, src) }
906// REX.W F7 /ext unary: neg=/3, not=/2
907func x86_unary_f7(out: *u8, o: i64, ext: i64, reg: i64) -> i64 {
908 var p: i64 = o
909 out[p] = x86_rex_w(0, reg); p = p + 1
910 out[p] = 0xF7; p = p + 1
911 out[p] = x86_modrm(3, ext, reg); p = p + 1
912 return p
913}
914func x86_neg(out: *u8, o: i64, reg: i64) -> i64 { return x86_unary_f7(out, o, 3, reg) }
915func x86_not(out: *u8, o: i64, reg: i64) -> i64 { return x86_unary_f7(out, o, 2, reg) }
916// REX.W FF /ext: inc=/0, dec=/1
917func x86_incdec(out: *u8, o: i64, ext: i64, reg: i64) -> i64 {
918 var p: i64 = o
919 out[p] = x86_rex_w(0, reg); p = p + 1
920 out[p] = 0xFF; p = p + 1
921 out[p] = x86_modrm(3, ext, reg); p = p + 1
922 return p
923}
924// movb $imm8,disp(%base) -> C6 /0 + mem + ib (byte op: no REX.W)
925func x86_movb_imm(out: *u8, o: i64, base: i64, disp: i64, imm8: i64) -> i64 {
926 var p: i64 = o
927 if (base & 8) != 0 { out[p] = 0x41; p = p + 1 } // REX.B for r8-15 base
928 out[p] = 0xC6; p = p + 1
929 p = x86_mem_operand(out, p, 0, base, disp)
930 out[p] = imm8 & 0xff; p = p + 1
931 return p
932}
933// movb $imm8, %reg8 -> [REX] B0+rd ib (feat row asm-movb-imm-reg, 2026-06-10:
934// previously this form fell into the imm->MEM branch reading garbage BASE/DISP =
935// silent wrong bytes -> segfaulting ELF. Template-analogy from movb_store's REX rule.)
936func x86_movb_imm_reg(out: *u8, o: i64, reg: i64, imm8: i64) -> i64 {
937 var p: i64 = o
938 var rex: i64 = 0
939 if reg >= 4 { rex = 0x40 } // spl/bpl/sil/dil need REX
940 if (reg & 8) != 0 { rex = rex | 0x41 } // REX.B for r8b-r15b
941 if rex != 0 { out[p] = rex; p = p + 1 }
942 out[p] = 0xB0 + (reg & 7); p = p + 1
943 out[p] = imm8 & 0xff; p = p + 1
944 return p
945}
946// movb %src8,disp(%base) -> [REX] 88 /r + mem
947func x86_movb_store(out: *u8, o: i64, base: i64, disp: i64, src: i64) -> i64 {
948 var p: i64 = o
949 var rex: i64 = 0
950 if src >= 4 { rex = 0x40 } // spl/bpl/sil/dil need REX
951 if (src & 8) != 0 { rex = rex | 0x04 } // REX.R
952 if (base & 8) != 0 { rex = rex | 0x40 | 0x01 } // REX.B
953 if rex != 0 { out[p] = rex; p = p + 1 }
954 out[p] = 0x88; p = p + 1
955 return x86_mem_operand(out, p, src, base, disp)
956}
957// call *%reg -> FF /2 ; jmp *%reg -> FF /4 (indirect, default 64-bit)
958func x86_call_indirect(out: *u8, o: i64, reg: i64) -> i64 {
959 var p: i64 = o
960 if (reg & 8) != 0 { out[p] = 0x41; p = p + 1 }
961 out[p] = 0xFF; p = p + 1
962 out[p] = x86_modrm(3, 2, reg); p = p + 1
963 return p
964}
965func x86_jmp_indirect(out: *u8, o: i64, reg: i64) -> i64 {
966 var p: i64 = o
967 if (reg & 8) != 0 { out[p] = 0x41; p = p + 1 }
968 out[p] = 0xFF; p = p + 1
969 out[p] = x86_modrm(3, 4, reg); p = p + 1
970 return p
971}
972
973// ---- minimal static x86_64 ELF (ET_EXEC, one R+X PT_LOAD) ----
974//
975// Layout: [0..64) ELF header, [64..120) program header, [120..) code.
976// One segment maps file offset 0 at vaddr X86_BASE (p_offset and
977// p_vaddr congruent mod p_align), entry = X86_BASE + X86_HDRLEN.
978// Returns total bytes written to `out` (X86_HDRLEN + code_len).
979func x86_build_elf_at(code: *u8, code_len: i64, entry_off: i64, out: *u8) -> i64 {
980 // e_ident
981 out[0] = 0x7F; out[1] = 0x45; out[2] = 0x4C; out[3] = 0x46 // \x7fELF
982 out[4] = 2 // EI_CLASS = ELFCLASS64
983 out[5] = 1 // EI_DATA = ELFDATA2LSB
984 out[6] = 1 // EI_VERSION
985 var i: i64 = 7
986 while i < 16 { out[i] = 0; i = i + 1 } // pad e_ident
987 x86_put_u16le(out, 16, 2) // e_type = ET_EXEC
988 x86_put_u16le(out, 18, EM_X86_64) // e_machine = EM_X86_64
989 x86_put_u32le(out, 20, 1) // e_version
990 let total: i64 = X86_HDRLEN + code_len
991 let entry: i64 = X86_BASE + X86_HDRLEN + entry_off
992 x86_put_u64le(out, 24, entry) // e_entry
993 x86_put_u64le(out, 32, 64) // e_phoff
994 x86_put_u64le(out, 40, 0) // e_shoff
995 x86_put_u32le(out, 48, 0) // e_flags
996 x86_put_u16le(out, 52, 64) // e_ehsize
997 x86_put_u16le(out, 54, 56) // e_phentsize
998 x86_put_u16le(out, 56, 1) // e_phnum
999 x86_put_u16le(out, 58, 0) // e_shentsize
1000 x86_put_u16le(out, 60, 0) // e_shnum
1001 x86_put_u16le(out, 62, 0) // e_shstrndx
1002 // program header (PT_LOAD, R+X)
1003 x86_put_u32le(out, 64, 1) // p_type = PT_LOAD
1004 // RWX: .lcomm mutable globals live in the same single segment as code.
1005 // W^X DEBT (named): split a second RW PT_LOAD for the data tail.
1006 x86_put_u32le(out, 68, 7) // p_flags = PF_R | PF_W | PF_X
1007 x86_put_u64le(out, 72, 0) // p_offset
1008 x86_put_u64le(out, 80, X86_BASE) // p_vaddr
1009 x86_put_u64le(out, 88, X86_BASE) // p_paddr
1010 x86_put_u64le(out, 96, total) // p_filesz
1011 x86_put_u64le(out, 104, total) // p_memsz
1012 x86_put_u64le(out, 112, 0x1000) // p_align
1013 // code
1014 var k: i64 = 0
1015 while k < code_len {
1016 out[X86_HDRLEN + k] = code[k]
1017 k = k + 1
1018 }
1019 return total
1020}
1021
1022// ---- DEBUG-AWARE VARIANT (DDR-002, 2026-08-07) -----------------------------
1023//
1024// Same image as x86_build_elf_at, plus a SECTION HEADER TABLE and a .debug_line section.
1025//
1026// WHY THIS IS SAFE TO ADD TO THE CROWN JEWEL: the SHT and .debug_line are appended PAST the
1027// PT_LOAD (p_filesz/p_memsz still cover exactly X86_HDRLEN + code_len), so the loader never reads
1028// a byte of them. An ELF with a wrong section table still executes correctly; only tools look at
1029// sections. Measured, not hoped: the layout was read out of this function before it was extended.
1030//
1031// WHY dbg_len == 0 DELEGATES: a build without -g emits no .file/.loc, so the assembler has no rows,
1032// so this returns the ORIGINAL bytes -- byte-identical to a toolchain that never heard of DWARF.
1033// That is what keeps the published 168-byte minimal static binary intact (DDR-002 section 6.1),
1034// and it is checkable: the equivalence gate must show ea == eb on every row.
1035const X86_SHT_ENTS: i64 = 4 // NULL, .text, .debug_line, .shstrtab
1036const X86_SHT_ENTSZ: i64 = 64
1037
1038// Copy a NUL-terminated literal into `out` at `off`, including its terminator.
1039// Returns bytes written. Length computed at runtime -- a hand-counted section-name table is the
1040// same bug class that has twice silently truncated diagnostics in this tree.
1041func x86_sh_str(out: *u8, off: i64, s: *u8) -> i64 {
1042 var i: i64 = 0
1043 while s[i] != (0 as u8) { out[off + i] = s[i]; i = i + 1 }
1044 out[off + i] = 0
1045 return i + 1
1046}
1047
1048func x86_sh_ent(out: *u8, at: i64, name: i64, styp: i64, flags: i64,
1049 addr: i64, soff: i64, ssz: i64, align: i64) -> i64 {
1050 var z: i64 = 0
1051 while z < X86_SHT_ENTSZ { out[at + z] = 0; z = z + 1 }
1052 x86_put_u32le(out, at + 0, name)
1053 x86_put_u32le(out, at + 4, styp)
1054 x86_put_u64le(out, at + 8, flags)
1055 x86_put_u64le(out, at + 16, addr)
1056 x86_put_u64le(out, at + 24, soff)
1057 x86_put_u64le(out, at + 32, ssz)
1058 x86_put_u64le(out, at + 48, align)
1059 return 0
1060}
1061
1062func x86_build_elf_dbg_at(code: *u8, code_len: i64, entry_off: i64, out: *u8,
1063 dbg: *u8, dbg_len: i64) -> i64 {
1064 let total: i64 = x86_build_elf_at(code, code_len, entry_off, out)
1065 if dbg_len <= 0 { return total }
1066
1067 // .debug_line immediately after the loaded image.
1068 let dbg_off: i64 = total
1069 var i: i64 = 0
1070 while i < dbg_len { out[dbg_off + i] = dbg[i]; i = i + 1 }
1071
1072 // .shstrtab: leading NUL, then the three names.
1073 let str_off: i64 = dbg_off + dbg_len
1074 var so: i64 = str_off
1075 out[so] = 0
1076 so = so + 1
1077 let n_text: i64 = so - str_off
1078 so = so + x86_sh_str(out, so, ".text" as *u8)
1079 let n_dbg: i64 = so - str_off
1080 so = so + x86_sh_str(out, so, ".debug_line" as *u8)
1081 let n_str: i64 = so - str_off
1082 so = so + x86_sh_str(out, so, ".shstrtab" as *u8)
1083 let str_len: i64 = so - str_off
1084
1085 // Section headers are 8-aligned by convention; pad explicitly rather than assume.
1086 var sht_off: i64 = so
1087 while (sht_off & 7) != 0 { out[sht_off] = 0; sht_off = sht_off + 1 }
1088
1089 x86_sh_ent(out, sht_off, 0, 0, 0, 0, 0, 0, 0)
1090 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ, n_text, 1, 6,
1091 X86_BASE + X86_HDRLEN, X86_HDRLEN, code_len, 16)
1092 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ*2, n_dbg, 1, 0, 0, dbg_off, dbg_len, 1)
1093 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ*3, n_str, 3, 0, 0, str_off, str_len, 1)
1094
1095 x86_put_u64le(out, 40, sht_off) // e_shoff
1096 x86_put_u16le(out, 58, X86_SHT_ENTSZ) // e_shentsize
1097 x86_put_u16le(out, 60, X86_SHT_ENTS) // e_shnum
1098 x86_put_u16le(out, 62, 3) // e_shstrndx -> .shstrtab
1099
1100 return sht_off + X86_SHT_ENTSZ * X86_SHT_ENTS
1101}
1102
1103// DDR-009 step 2: the SAME image plus THREE debug sections (.debug_line, .debug_info,
1104// .debug_abbrev). A SEPARATE function, not an arity change to x86_build_elf_dbg_at: that one has
1105// live callers and rule 19 says add, never widen underneath them. When info/abbrev are empty this
1106// DELEGATES to the 2-section builder, so a -g build with line info only stays byte-identical to
1107// what it produced yesterday -- the property nx_nxasm_neutral_gate exists to hold.
1108//
1109// All three sections live PAST the loaded image and are described only by the section header table,
1110// which no PT_LOAD covers. That is why this cannot change runtime behaviour: a loader never reads a
1111// byte of it. DDR-002 section 5 chose that placement deliberately.
1112func x86_build_elf_dbg3_at(code: *u8, code_len: i64, entry_off: i64, out: *u8,
1113 dline: *u8, dline_len: i64,
1114 dinfo: *u8, dinfo_len: i64,
1115 dabbr: *u8, dabbr_len: i64) -> i64 {
1116 if dinfo_len <= 0 { return x86_build_elf_dbg_at(code, code_len, entry_off, out, dline, dline_len) }
1117 if dabbr_len <= 0 { return x86_build_elf_dbg_at(code, code_len, entry_off, out, dline, dline_len) }
1118
1119 let total: i64 = x86_build_elf_at(code, code_len, entry_off, out)
1120
1121 let line_off: i64 = total
1122 var i: i64 = 0
1123 while i < dline_len { out[line_off + i] = dline[i]; i = i + 1 }
1124
1125 let info_off: i64 = line_off + dline_len
1126 i = 0
1127 while i < dinfo_len { out[info_off + i] = dinfo[i]; i = i + 1 }
1128
1129 let abbr_off: i64 = info_off + dinfo_len
1130 i = 0
1131 while i < dabbr_len { out[abbr_off + i] = dabbr[i]; i = i + 1 }
1132
1133 // .shstrtab: leading NUL then the five names, each offset captured as it is written so no
1134 // index is ever computed by hand.
1135 let str_off: i64 = abbr_off + dabbr_len
1136 var so: i64 = str_off
1137 out[so] = 0
1138 so = so + 1
1139 let n_text: i64 = so - str_off
1140 so = so + x86_sh_str(out, so, ".text" as *u8)
1141 let n_line: i64 = so - str_off
1142 so = so + x86_sh_str(out, so, ".debug_line" as *u8)
1143 let n_info: i64 = so - str_off
1144 so = so + x86_sh_str(out, so, ".debug_info" as *u8)
1145 let n_abbr: i64 = so - str_off
1146 so = so + x86_sh_str(out, so, ".debug_abbrev" as *u8)
1147 let n_str: i64 = so - str_off
1148 so = so + x86_sh_str(out, so, ".shstrtab" as *u8)
1149 let str_len: i64 = so - str_off
1150
1151 var sht_off: i64 = so
1152 while (sht_off & 7) != 0 { out[sht_off] = 0; sht_off = sht_off + 1 }
1153
1154 x86_sh_ent(out, sht_off, 0, 0, 0, 0, 0, 0, 0)
1155 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ, n_text, 1, 6,
1156 X86_BASE + X86_HDRLEN, X86_HDRLEN, code_len, 16)
1157 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ*2, n_line, 1, 0, 0, line_off, dline_len, 1)
1158 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ*3, n_info, 1, 0, 0, info_off, dinfo_len, 1)
1159 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ*4, n_abbr, 1, 0, 0, abbr_off, dabbr_len, 1)
1160 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ*5, n_str, 3, 0, 0, str_off, str_len, 1)
1161
1162 x86_put_u64le(out, 40, sht_off)
1163 x86_put_u16le(out, 58, X86_SHT_ENTSZ)
1164 x86_put_u16le(out, 60, 6) // e_shnum
1165 x86_put_u16le(out, 62, 5) // e_shstrndx -> .shstrtab
1166
1167 return sht_off + X86_SHT_ENTSZ * 6
1168}
1169
1170// Convenience wrapper: entry at the first code byte (entry_off = 0).
1171func x86_build_elf(code: *u8, code_len: i64, out: *u8) -> i64 {
1172 return x86_build_elf_at(code, code_len, 0, out)
1173}