code wiki / (root) / nxasm_x86.nx

nxasm_x86.nx source

↩ module page · 1299 lines · 75071 B

1// nxasm_x86.nx -- sovereign x86_64 assembler (AT&T .s text -> machine 2// code) in NishiLang. The x86_64 sibling of nxasm_v2.nx (RV64). 3// 4// Drives nxasm_x86_enc.nx. Two-pass: pass 1 records label addresses, 5// pass 2 emits bytes resolving call/jmp/jcc displacements. Parses the 6// exact surface nxc2 --target x86_64 emits: directives (.att_syntax/ 7// .text/.globl/.type/.size/.section -- all metadata, ignored), labels 8// (`name:` incl. `.Lxxx:`), and AT&T operands (%reg / $imm / 9// disp(%base) / symbol). Replaces GNU `as` on the output path; gas is 10// kept only as a byte-exact oracle (Wheeler/benchmark). 11// 12// genealogy_id: intel_sdm_vol2 + att_syntax + nxasm_v2_two_pass_spine 13// lineage_id: nishi_sovereign_x86_64_assembler_m3 14// license_tier: ORIGINAL 15 16import "nxasm_x86_enc.nx" 17 18// ---- operand record (7-i64 array used as a struct) ---- 19const OP_KIND: i64 = 0 20const OP_REG: i64 = 1 21const OP_IMM: i64 = 2 22const OP_BASE: i64 = 3 23const OP_DISP: i64 = 4 24const OP_SYMOFF: i64 = 5 25const OP_SYMLEN: i64 = 6 26const OP_INDEX: i64 = 7 // SIB index reg (-1 = none); op arrays are mmap(72)=9 slots 27const OP_SCALE: i64 = 8 // SIB scale value {1,2,4,8} 28const K_REG: i64 = 0 29const K_IMM: i64 = 1 30const K_MEM: i64 = 2 31const K_XMM: i64 = 3 // an xmm-named register operand (distinct from a GPR so movq can pick the GPR<->xmm SSE form) 32const K_SYM: i64 = 3 33const K_NONE: i64 = 4 34const K_IND: i64 = 5 35const K_RIP: i64 = 6 36 37// 65536: the rebuilt self-host compiler's .s carries 10,674 labels — the old cap of 4096 38// overflowed the label tables SILENTLY and segfaulted (found 2026-06-09 assembling nx_nxc). 39// The add-site now fail-louds at the cap instead of corrupting adjacent mmaps. 40const ASM_MAX_LABELS: i64 = 65536 41 42// ---- char classifiers ---- 43func axc_is_space(c: i64) -> i64 { 44 if c == 32 { return 1 } 45 if c == 9 { return 1 } 46 return 0 47} 48func axc_is_digit(c: i64) -> i64 { 49 if c >= 48 { if c <= 57 { return 1 } } 50 return 0 51} 52func axc_is_ident(c: i64) -> i64 { 53 if c >= 97 { if c <= 122 { return 1 } } // a-z 54 if c >= 65 { if c <= 90 { return 1 } } // A-Z 55 if c >= 48 { if c <= 57 { return 1 } } // 0-9 56 if c == 95 { return 1 } // _ 57 if c == 46 { return 1 } // . 58 return 0 59} 60 61// ---- length of a null-terminated literal ---- 62func axc_cstr_len(s: *u8) -> i64 { 63 var i: i64 = 0 64 while s[i] != 0 { i = i + 1 } 65 return i 66} 67 68// ---- token [off,off+len) in src equals literal lit ? ---- 69func axc_tok_is(src: *u8, off: i64, len: i64, lit: *u8) -> i64 { 70 let ll: i64 = axc_cstr_len(lit) 71 if len != ll { return 0 } 72 var i: i64 = 0 73 while i < len { 74 if (src[off + i] & 0xff) != (lit[i] & 0xff) { return 0 } 75 i = i + 1 76 } 77 return 1 78} 79 80// ---- two src-slices equal ? ---- 81func axc_slice_eq(src: *u8, a: i64, alen: i64, b: i64, blen: i64) -> i64 { 82 if alen != blen { return 0 } 83 var i: i64 = 0 84 while i < alen { 85 if (src[a + i] & 0xff) != (src[b + i] & 0xff) { return 0 } 86 i = i + 1 87 } 88 return 1 89} 90 91// ---- register name -> encoding number (-1 if unknown) ---- 92func axc_reg_num(src: *u8, off: i64, len: i64) -> i64 { 93 if axc_tok_is(src, off, len, "rax") { return 0 } 94 if axc_tok_is(src, off, len, "rcx") { return 1 } 95 if axc_tok_is(src, off, len, "rdx") { return 2 } 96 if axc_tok_is(src, off, len, "rbx") { return 3 } 97 if axc_tok_is(src, off, len, "rsp") { return 4 } 98 if axc_tok_is(src, off, len, "rbp") { return 5 } 99 if axc_tok_is(src, off, len, "rsi") { return 6 } 100 if axc_tok_is(src, off, len, "rdi") { return 7 } 101 if axc_tok_is(src, off, len, "r8") { return 8 } 102 if axc_tok_is(src, off, len, "r9") { return 9 } 103 if axc_tok_is(src, off, len, "r10") { return 10 } 104 if axc_tok_is(src, off, len, "r11") { return 11 } 105 if axc_tok_is(src, off, len, "r12") { return 12 } 106 if axc_tok_is(src, off, len, "r13") { return 13 } 107 if axc_tok_is(src, off, len, "r14") { return 14 } 108 if axc_tok_is(src, off, len, "r15") { return 15 } 109 // 32-bit registers (same numbers; width comes from the opcode -- 110 // needed by lzcntl/tzcntl, the compiler's clz32/ctz32 lowering) 111 if axc_tok_is(src, off, len, "eax") { return 0 } 112 if axc_tok_is(src, off, len, "ecx") { return 1 } 113 if axc_tok_is(src, off, len, "edx") { return 2 } 114 if axc_tok_is(src, off, len, "ebx") { return 3 } 115 if axc_tok_is(src, off, len, "esp") { return 4 } 116 if axc_tok_is(src, off, len, "ebp") { return 5 } 117 if axc_tok_is(src, off, len, "esi") { return 6 } 118 if axc_tok_is(src, off, len, "edi") { return 7 } 119 if axc_tok_is(src, off, len, "r8d") { return 8 } 120 if axc_tok_is(src, off, len, "r9d") { return 9 } 121 if axc_tok_is(src, off, len, "r10d") { return 10 } 122 if axc_tok_is(src, off, len, "r11d") { return 11 } 123 if axc_tok_is(src, off, len, "r12d") { return 12 } 124 if axc_tok_is(src, off, len, "r13d") { return 13 } 125 if axc_tok_is(src, off, len, "r14d") { return 14 } 126 if axc_tok_is(src, off, len, "r15d") { return 15 } 127 // byte registers (same numbers; width comes from the opcode) 128 if axc_tok_is(src, off, len, "al") { return 0 } 129 if axc_tok_is(src, off, len, "cl") { return 1 } 130 if axc_tok_is(src, off, len, "dl") { return 2 } 131 if axc_tok_is(src, off, len, "bl") { return 3 } 132 if axc_tok_is(src, off, len, "spl") { return 4 } 133 if axc_tok_is(src, off, len, "bpl") { return 5 } 134 if axc_tok_is(src, off, len, "sil") { return 6 } 135 if axc_tok_is(src, off, len, "dil") { return 7 } 136 // SSE/AVX xmm registers (0..15). Same index space; the SSE/SHA-NI mnemonics 137 // interpret the operand as an xmm -- GPR mnemonics never receive an xmm operand. 138 if axc_tok_is(src, off, len, "xmm0") { return 0 } 139 if axc_tok_is(src, off, len, "xmm1") { return 1 } 140 if axc_tok_is(src, off, len, "xmm2") { return 2 } 141 if axc_tok_is(src, off, len, "xmm3") { return 3 } 142 if axc_tok_is(src, off, len, "xmm4") { return 4 } 143 if axc_tok_is(src, off, len, "xmm5") { return 5 } 144 if axc_tok_is(src, off, len, "xmm6") { return 6 } 145 if axc_tok_is(src, off, len, "xmm7") { return 7 } 146 if axc_tok_is(src, off, len, "xmm8") { return 8 } 147 if axc_tok_is(src, off, len, "xmm9") { return 9 } 148 if axc_tok_is(src, off, len, "xmm10") { return 10 } 149 if axc_tok_is(src, off, len, "xmm11") { return 11 } 150 if axc_tok_is(src, off, len, "xmm12") { return 12 } 151 if axc_tok_is(src, off, len, "xmm13") { return 13 } 152 if axc_tok_is(src, off, len, "xmm14") { return 14 } 153 if axc_tok_is(src, off, len, "xmm15") { return 15 } 154 // AVX/AVX2 ymm registers (0..15; same index space, width from the VEX.L bit). 155 if axc_tok_is(src, off, len, "ymm0") { return 0 } 156 if axc_tok_is(src, off, len, "ymm1") { return 1 } 157 if axc_tok_is(src, off, len, "ymm2") { return 2 } 158 if axc_tok_is(src, off, len, "ymm3") { return 3 } 159 if axc_tok_is(src, off, len, "ymm4") { return 4 } 160 if axc_tok_is(src, off, len, "ymm5") { return 5 } 161 if axc_tok_is(src, off, len, "ymm6") { return 6 } 162 if axc_tok_is(src, off, len, "ymm7") { return 7 } 163 if axc_tok_is(src, off, len, "ymm8") { return 8 } 164 if axc_tok_is(src, off, len, "ymm9") { return 9 } 165 if axc_tok_is(src, off, len, "ymm10") { return 10 } 166 if axc_tok_is(src, off, len, "ymm11") { return 11 } 167 if axc_tok_is(src, off, len, "ymm12") { return 12 } 168 if axc_tok_is(src, off, len, "ymm13") { return 13 } 169 if axc_tok_is(src, off, len, "ymm14") { return 14 } 170 if axc_tok_is(src, off, len, "ymm15") { return 15 } 171 return -1 172} 173 174// ---- parse a signed decimal integer at pos (advances pos) ---- 175func axc_parse_int(src: *u8, pos: *i64, le: i64) -> i64 { 176 var p: i64 = pos[0] 177 var neg: i64 = 0 178 if p < le { if src[p] == 45 { neg = 1; p = p + 1 } } // '-' 179 var v: i64 = 0 180 var done: i64 = 0 181 while p < le && done == 0 { 182 let c: i64 = src[p] & 0xff 183 if axc_is_digit(c) == 1 { 184 v = v * 10 + (c - 48) 185 p = p + 1 186 } else { 187 done = 1 188 } 189 } 190 pos[0] = p 191 if neg == 1 { return 0 - v } 192 return v 193} 194 195// ---- parse one operand at pos into op (7-i64 array); advances pos ---- 196// Operand kinds: %reg | $imm | disp(%base) memory | bare symbol. 197func axc_parse_operand(src: *u8, pos: *i64, le: i64, op: *i64) -> i64 { 198 var p: i64 = pos[0] 199 while p < le { if axc_is_space(src[p]) == 1 { p = p + 1 } else { break } } 200 op[OP_KIND] = K_NONE 201 op[OP_INDEX] = 0 - 1 // SIB sentinel: no index unless a ',' is parsed below 202 if p >= le { pos[0] = p; return 0 } 203 let c: i64 = src[p] & 0xff 204 // register: %name 205 if c == 37 { // '%' 206 p = p + 1 207 let s: i64 = p 208 while p < le { if axc_is_ident(src[p]) == 1 { p = p + 1 } else { break } } 209 op[OP_KIND] = K_REG 210 op[OP_REG] = axc_reg_num(src, s, p - s) 211 // xmm-named register -> K_XMM (same 0..15 index; only movq's GPR<->xmm 212 // form needs to tell them apart -- SSE mnemonics read OP_REG directly). 213 if (p - s) >= 3 { if src[s] == (120 as u8) { if src[s+1] == (109 as u8) { if src[s+2] == (109 as u8) { op[OP_KIND] = K_XMM } } } } 214 pos[0] = p 215 return 0 216 } 217 // indirect: *%reg (call/jmp through a register) 218 if c == 42 { // '*' 219 p = p + 1 220 if p < le { if src[p] == 37 { p = p + 1 } } // '%' 221 let s2: i64 = p 222 while p < le { if axc_is_ident(src[p]) == 1 { p = p + 1 } else { break } } 223 op[OP_KIND] = K_IND 224 op[OP_REG] = axc_reg_num(src, s2, p - s2) 225 pos[0] = p 226 return 0 227 } 228 // immediate: $int 229 if c == 36 { // '$' 230 p = p + 1 231 pos[0] = p 232 let v: i64 = axc_parse_int(src, pos, le) 233 op[OP_KIND] = K_IMM 234 op[OP_IMM] = v 235 return 0 236 } 237 // memory: '(' or a signed displacement followed by '(' 238 var is_mem: i64 = 0 239 if c == 40 { is_mem = 1 } // '(' 240 if c == 45 { is_mem = 1 } // '-' 241 if axc_is_digit(c) == 1 { is_mem = 1 } 242 if is_mem == 1 { 243 var disp: i64 = 0 244 if c != 40 { 245 pos[0] = p 246 disp = axc_parse_int(src, pos, le) 247 p = pos[0] 248 } 249 op[OP_DISP] = disp 250 if p < le { if src[p] == 40 { p = p + 1 } } // '(' 251 if p < le { if src[p] == 37 { p = p + 1 } } // '%' 252 let bs: i64 = p 253 while p < le { if axc_is_ident(src[p]) == 1 { p = p + 1 } else { break } } 254 op[OP_BASE] = axc_reg_num(src, bs, p - bs) 255 // SIB: `(%base,%index,scale)` -- a ',' after the base opens the index+scale. 256 // Additive: no existing .s emits this form, so the disp(%base) path above is 257 // byte-identical for all current code. 258 if p < le { if src[p] == 44 { // ',' 259 p = p + 1 260 if p < le { if src[p] == 37 { p = p + 1 } } // '%' 261 let is0: i64 = p 262 while p < le { if axc_is_ident(src[p]) == 1 { p = p + 1 } else { break } } 263 op[OP_INDEX] = axc_reg_num(src, is0, p - is0) 264 if p < le { if src[p] == 44 { p = p + 1 } } // ',' before scale 265 var scv: i64 = 0 266 while p < le { if axc_is_digit(src[p] & 0xff) == 1 { scv = scv * 10 + ((src[p] & 0xff) - 48); p = p + 1 } else { break } } 267 op[OP_SCALE] = scv 268 } } 269 if p < le { if src[p] == 41 { p = p + 1 } } // ')' 270 op[OP_KIND] = K_MEM 271 pos[0] = p 272 return 0 273 } 274 // bare symbol (`call main`) OR RIP-relative `LABEL(%rip)` 275 let ss: i64 = p 276 while p < le { if axc_is_ident(src[p]) == 1 { p = p + 1 } else { break } } 277 op[OP_SYMOFF] = ss 278 op[OP_SYMLEN] = p - ss 279 // detect a (%rip) suffix -> RIP-relative memory operand 280 if p < le { 281 if src[p] == 40 { // '(' 282 var q: i64 = p + 1 283 if q < le { if src[q] == 37 { q = q + 1 } } // '%' 284 let rs: i64 = q 285 while q < le { if axc_is_ident(src[q]) == 1 { q = q + 1 } else { break } } 286 if axc_tok_is(src, rs, q - rs, "rip") == 1 { 287 if q < le { if src[q] == 41 { q = q + 1 } } // ')' 288 op[OP_KIND] = K_RIP 289 pos[0] = q 290 return 0 291 } 292 } 293 } 294 op[OP_KIND] = K_SYM 295 pos[0] = p 296 return 0 297} 298 299// ---- find a label's address by name slice (-1 if not found) ---- 300func axc_label_find(src: *u8, lab_off: *i64, lab_len: *i64, lab_addr: *i64, 301 n_lab: i64, name_off: i64, name_len: i64) -> i64 { 302 var k: i64 = 0 303 while k < n_lab { 304 if axc_slice_eq(src, lab_off[k], lab_len[k], name_off, name_len) == 1 { 305 return lab_addr[k] 306 } 307 k = k + 1 308 } 309 return -1 310} 311 312// ---- O(1) hashed label lookup (PERF FIX 2026-06-09) ---------------- 313// The linear axc_label_find above is O(n_lab) string-compares PER 314// REFERENCE; on compiler-scale .s (~75K lines, tens of thousands of 315// labels x as many jmp/jcc/call/lea refs) that is billions of byte 316// compares = minutes of assemble time (the C2-deploy stall). Same 317// disease as the LICM fixpoint: a linear scan inside a hot loop. 318// Open-addressing hash over label INDICES, built ONCE after pass 0; 319// first-insert-wins so duplicate names resolve to the FIRST definition, 320// byte-identical to the linear scan's first-match semantics. 321const ASM_LH_SIZE: i64 = 262144 // 2^18 = 4x ASM_MAX_LABELS load<=25% 322const ASM_LH_MASK: i64 = 262143 323 324func axc_lh_hash(src: *u8, off: i64, len: i64) -> i64 { 325 var h: i64 = 5381 326 var i: i64 = 0 327 while i < len { 328 h = h * 33 + (src[off + i] & 0xff) 329 i = i + 1 330 } 331 if h < 0 { h = 0 - h } 332 return h & ASM_LH_MASK 333} 334 335// Build the index over labels 0..n_lab-1. lh holds label indices, -1 = empty. 336func axc_lh_build(src: *u8, lab_off: *i64, lab_len: *i64, n_lab: i64, lh: *i64) -> i64 { 337 var i: i64 = 0 338 while i < ASM_LH_SIZE { lh[i] = 0 - 1; i = i + 1 } 339 var k: i64 = 0 340 while k < n_lab { 341 var slot: i64 = axc_lh_hash(src, lab_off[k], lab_len[k]) 342 var placed: i64 = 0 343 while placed == 0 { 344 let e: i64 = lh[slot] 345 if e < 0 { lh[slot] = k; placed = 1 } 346 else { 347 // duplicate name: keep the FIRST definition (linear-scan parity) 348 if axc_slice_eq(src, lab_off[e], lab_len[e], lab_off[k], lab_len[k]) == 1 { placed = 1 } 349 else { slot = (slot + 1) & ASM_LH_MASK } 350 } 351 } 352 k = k + 1 353 } 354 return 0 355} 356 357func axc_label_find_h(src: *u8, lab_off: *i64, lab_len: *i64, lab_addr: *i64, 358 lh: *i64, name_off: i64, name_len: i64) -> i64 { 359 var slot: i64 = axc_lh_hash(src, name_off, name_len) 360 var hops: i64 = 0 361 while hops < ASM_LH_SIZE { 362 let e: i64 = lh[slot] 363 if e < 0 { return -1 } 364 if axc_slice_eq(src, lab_off[e], lab_len[e], name_off, name_len) == 1 { 365 return lab_addr[e] 366 } 367 slot = (slot + 1) & ASM_LH_MASK 368 hops = hops + 1 369 } 370 return -1 371} 372 373// LOUD-fail resolve for pass 2 (PREVENT): silently encoding a -1 address 374// turns a missing definition into a base-minus-one pointer that segfaults 375// at RUNTIME far from the cause (2026-06-10: ignored .lcomm .Lg472 -> 376// write through 0x400077, a day-long hunt). An assembler must never emit 377// a reference it could not resolve. 378func axc_label_resolve(src: *u8, lab_off: *i64, lab_len: *i64, lab_addr: *i64, 379 lh: *i64, name_off: i64, name_len: i64) -> i64 { 380 let a: i64 = axc_label_find_h(src, lab_off, lab_len, lab_addr, lh, name_off, name_len) 381 if a < 0 { 382 sys_write(2, "nxasm_x86: UNDEFINED label: " as *u8, 28) 383 sys_write(2, ((src as i64) + name_off) as *u8, name_len) 384 sys_write(2, "\n" as *u8, 1) 385 sys_exit(102) 386 } 387 return a 388} 389 390// ---- shared ALU dispatch (reg,reg via MR opcode | imm,reg via ext) ---- 391func axc_alu(out: *u8, op0: *i64, op1: *i64, rr_op: i64, imm_ext: i64) -> i64 { 392 if op0[OP_KIND] == K_IMM { return x86_alu_imm(out, 0, imm_ext, op1[OP_REG], op0[OP_IMM]) } 393 return x86_alu_rr(out, 0, rr_op, op1[OP_REG], op0[OP_REG]) 394} 395 396// ---- jcc mnemonic -> condition code (-1 if not a jcc) ---- 397func axc_jcc_cc(src: *u8, off: i64, len: i64) -> i64 { 398 if axc_tok_is(src, off, len, "je") { return X86_CC_E } 399 if axc_tok_is(src, off, len, "jne") { return X86_CC_NE } 400 if axc_tok_is(src, off, len, "jl") { return X86_CC_L } 401 if axc_tok_is(src, off, len, "jle") { return X86_CC_LE } 402 if axc_tok_is(src, off, len, "jg") { return X86_CC_G } 403 if axc_tok_is(src, off, len, "jge") { return X86_CC_GE } 404 if axc_tok_is(src, off, len, "jo") { return X86_CC_O } 405 if axc_tok_is(src, off, len, "jno") { return X86_CC_NO } 406 return -1 407} 408 409// ---- setcc mnemonic -> condition code (-1 if not a setcc) ---- 410func axc_setcc_cc(src: *u8, off: i64, len: i64) -> i64 { 411 if axc_tok_is(src, off, len, "sete") { return X86_CC_E } 412 if axc_tok_is(src, off, len, "setne") { return X86_CC_NE } 413 if axc_tok_is(src, off, len, "setl") { return X86_CC_L } 414 if axc_tok_is(src, off, len, "setle") { return X86_CC_LE } 415 if axc_tok_is(src, off, len, "seto") { return X86_CC_O } 416 if axc_tok_is(src, off, len, "setno") { return X86_CC_NO } 417 if axc_tok_is(src, off, len, "setg") { return X86_CC_G } 418 if axc_tok_is(src, off, len, "setge") { return X86_CC_GE } 419 return -1 420} 421 422// ---- cmovcc mnemonic -> condition code (-1 if not a cmov). Full family so 423// the mapper never lags the compiler; S/NS are the G22 bias-select users. ---- 424func axc_cmovcc_cc(src: *u8, off: i64, len: i64) -> i64 { 425 if axc_tok_is(src, off, len, "cmove") { return X86_CC_E } 426 if axc_tok_is(src, off, len, "cmovne") { return X86_CC_NE } 427 if axc_tok_is(src, off, len, "cmovl") { return X86_CC_L } 428 if axc_tok_is(src, off, len, "cmovle") { return X86_CC_LE } 429 if axc_tok_is(src, off, len, "cmovg") { return X86_CC_G } 430 if axc_tok_is(src, off, len, "cmovge") { return X86_CC_GE } 431 if axc_tok_is(src, off, len, "cmovs") { return X86_CC_S } 432 if axc_tok_is(src, off, len, "cmovns") { return X86_CC_NS } 433 return -1 434} 435 436// ---- atomic reg,(%base) encoder: [REX.W .R .B] [0F] opcode ModRM(mod=00, reg, base). The compiler emits 437// atomics only against a simple (%r11) base with rax/rcx as the register, so mod=00 rm=base is correct (no 438// SIB/disp -- base is never rsp/rbp/r12/r13). xchgq=87, cmpxchgq=0F B1, xaddq=0F C1. The F0 lock prefix for 439// cmpxchg/xadd is prepended by the assemble loop's `lock` handling (xchg-with-memory is auto-locked). 440func x86_atomic_rm(out: *u8, is0f: i64, opcode: i64, reg: i64, base: i64) -> i64 { 441 var rex: i64 = 0x48 442 if reg >= 8 { rex = rex + 4 } 443 if base >= 8 { rex = rex + 1 } 444 var n: i64 = 0 445 out[n] = rex as u8; n = n + 1 446 if is0f == 1 { out[n] = 0x0f as u8; n = n + 1 } 447 out[n] = opcode as u8; n = n + 1 448 out[n] = ((reg & 7) * 8 + (base & 7)) as u8; n = n + 1 449 return n 450} 451// mfence = 0F AE F0 (full memory fence). 452func x86_mfence_enc(out: *u8) -> i64 { out[0] = 0x0f as u8; out[1] = 0xae as u8; out[2] = 0xf0 as u8; return 3 } 453 454// ---- encode ONE instruction into out[0..]; return byte length ---- 455// `cur` = this instruction's address (for rel32 resolution); labels 456// resolved only in pass 2 (rel-independent length means pass-1 rel=0 457// gives the correct length). Returns -1 on an unrecognized mnemonic. 458func axc_emit(out: *u8, src: *u8, mn: i64, mnl: i64, op0: *i64, op1: *i64, op2: *i64, 459 lab_off: *i64, lab_len: *i64, lab_addr: *i64, n_lab: i64, 460 lh: *i64, cur: i64, pass: i64) -> i64 { 461 // First-character dispatch: x86 AT&T mnemonics are partitioned by 462 // their leading byte so a typical instruction pays ~1-6 token 463 // comparisons instead of walking a ~30-long linear chain (the chain 464 // position was measured at ~47% of per-instruction cost). Every 465 // case body below is byte-for-byte the original; only the grouping 466 // changed. Unknown first byte -> -1 (unrecognized), same as before. 467 let c0: i64 = src[mn] & 0xff 468 if c0 == 97 { // 'a' 469 if axc_tok_is(src, mn, mnl, "addq") { return axc_alu(out, op0, op1, X86_OP_ADD, X86_EXT_ADD) } 470 if axc_tok_is(src, mn, mnl, "andq") { return axc_alu(out, op0, op1, X86_OP_AND, X86_EXT_AND) } 471 // scalar single-precision float (SSE): addss F3 0F 58 /r (AT&T src,dst -> ModRM reg=dst rm=src) 472 if axc_tok_is(src, mn, mnl, "addss") { return x86_sse_rr(out, 0, 0xf3, 0, 0x58, op1[OP_REG], op0[OP_REG]) } 473 // scalar DOUBLE-precision (f64): same opcodes as *ss with the F2 prefix (not F3). 474 if axc_tok_is(src, mn, mnl, "addsd") { return x86_sse_rr(out, 0, 0xf2, 0, 0x58, op1[OP_REG], op0[OP_REG]) } 475 // PACKED single-precision (4 x f32): addps 0F 58 -- same as addss WITHOUT the F3 prefix (the SIMD lever). 476 if axc_tok_is(src, mn, mnl, "addps") { return x86_sse_rr(out, 0, 0, 0, 0x58, op1[OP_REG], op0[OP_REG]) } 477 // AES-NI (66 0F 38 xx /r, reg-reg). Hardware AES rounds: aesenc/last + aesdec/last + aesimc. 478 if axc_tok_is(src, mn, mnl, "aesenclast") { return x86_sse_rr(out, 0, 0x66, 0x38, 0xdd, op1[OP_REG], op0[OP_REG]) } 479 if axc_tok_is(src, mn, mnl, "aesenc") { return x86_sse_rr(out, 0, 0x66, 0x38, 0xdc, op1[OP_REG], op0[OP_REG]) } 480 if axc_tok_is(src, mn, mnl, "aesdeclast") { return x86_sse_rr(out, 0, 0x66, 0x38, 0xdf, op1[OP_REG], op0[OP_REG]) } 481 if axc_tok_is(src, mn, mnl, "aesdec") { return x86_sse_rr(out, 0, 0x66, 0x38, 0xde, op1[OP_REG], op0[OP_REG]) } 482 if axc_tok_is(src, mn, mnl, "aesimc") { return x86_sse_rr(out, 0, 0x66, 0x38, 0xdb, op1[OP_REG], op0[OP_REG]) } 483 // ADX add-with-carry (2-operand %src,%dst -> ModRM reg=dst rm=src, like crc32q): 484 // adcx uses the CF chain (66 REX.W 0F38 F6), adox uses the OF chain (F3 REX.W 0F38 F6). 485 // The two independent carry chains let a schoolbook column accumulate both carries in 486 // parallel around flags-free MULX -- the P-256/bignum dual-carry field-mul kernel. 487 if axc_tok_is(src, mn, mnl, "adcx") { return x86_adcx_r64(out, 0, op1[OP_REG], op0[OP_REG]) } 488 if axc_tok_is(src, mn, mnl, "adox") { return x86_adox_r64(out, 0, op1[OP_REG], op0[OP_REG]) } 489 return -1 490 } 491 if c0 == 98 { // 'b' 492 if axc_tok_is(src, mn, mnl, "bswapq") { return x86_bswap(out, 0, op0[OP_REG]) } 493 return -1 494 } 495 if c0 == 99 { // 'c' 496 if axc_tok_is(src, mn, mnl, "cqo") { return x86_cqo(out, 0) } 497 if axc_tok_is(src, mn, mnl, "cqto") { return x86_cqo(out, 0) } 498 if axc_tok_is(src, mn, mnl, "cmpq") { return axc_alu(out, op0, op1, X86_OP_CMP, X86_EXT_CMP) } 499 // atomic compare-and-swap (lock prefix from the assemble loop): cmpxchgq %reg, (%base) = 0F B1 /r 500 if axc_tok_is(src, mn, mnl, "cmpxchgq") { return x86_atomic_rm(out, 1, 0xb1, op0[OP_REG], op1[OP_BASE]) } 501 // scalar single-precision float converts (SSE, REX.W for the r64 operand): cvtsi2ss F3 0F 2A 502 // (r64->xmm), cvttss2si F3 0F 2C (xmm->r64 truncate), cvtss2si F3 0F 2D (xmm->r64 round-to-nearest). 503 if axc_tok_is(src, mn, mnl, "cvtsi2ss") { return x86_sse_rr_w(out, 0, 0xf3, 0, 0x2a, op1[OP_REG], op0[OP_REG]) } 504 if axc_tok_is(src, mn, mnl, "cvttss2si") { return x86_sse_rr_w(out, 0, 0xf3, 0, 0x2c, op1[OP_REG], op0[OP_REG]) } 505 // f64 converts: cvtsi2sd F2 0F 2A (r64->xmm double), cvttsd2si F2 0F 2C (xmm double->r64 truncate). 506 if axc_tok_is(src, mn, mnl, "cvtsi2sd") { return x86_sse_rr_w(out, 0, 0xf2, 0, 0x2a, op1[OP_REG], op0[OP_REG]) } 507 if axc_tok_is(src, mn, mnl, "cvttsd2si") { return x86_sse_rr_w(out, 0, 0xf2, 0, 0x2c, op1[OP_REG], op0[OP_REG]) } 508 if axc_tok_is(src, mn, mnl, "cvtss2si") { return x86_sse_rr_w(out, 0, 0xf3, 0, 0x2d, op1[OP_REG], op0[OP_REG]) } 509 // crc32q %src,%dst -- SSE4.2 CRC-32C accumulate (F2 REX.W 0F 38 F1 /r). 510 // AT&T src,dst -> ModRM reg=dst (accumulator), rm=src (data word). 511 if axc_tok_is(src, mn, mnl, "crc32q") { return x86_crc32_r64(out, 0, op1[OP_REG], op0[OP_REG]) } 512 // cmovcc %src,%dst -- conditional move family (REX.W 0F 40+cc /r). 513 let mcc: i64 = axc_cmovcc_cc(src, mn, mnl) 514 if mcc >= 0 { return x86_cmovcc(out, 0, mcc, op1[OP_REG], op0[OP_REG]) } 515 if axc_tok_is(src, mn, mnl, "call") { 516 if op0[OP_KIND] == K_IND { return x86_call_indirect(out, 0, op0[OP_REG]) } 517 var rel: i64 = 0 518 if pass == 2 { rel = axc_label_resolve(src, lab_off, lab_len, lab_addr, lh, op0[OP_SYMOFF], op0[OP_SYMLEN]) - (cur + 5) } 519 return x86_call_rel32(out, 0, rel) 520 } 521 return -1 522 } 523 if c0 == 100 { // 'd' 524 if axc_tok_is(src, mn, mnl, "decq") { return x86_incdec(out, 0, 1, op0[OP_REG]) } 525 // scalar single-precision float: divss F3 0F 5E /r 526 if axc_tok_is(src, mn, mnl, "divss") { return x86_sse_rr(out, 0, 0xf3, 0, 0x5e, op1[OP_REG], op0[OP_REG]) } 527 if axc_tok_is(src, mn, mnl, "divsd") { return x86_sse_rr(out, 0, 0xf2, 0, 0x5e, op1[OP_REG], op0[OP_REG]) } 528 return -1 529 } 530 if c0 == 105 { // 'i' 531 if axc_tok_is(src, mn, mnl, "idivq") { return x86_idiv(out, 0, op0[OP_REG]) } 532 if axc_tok_is(src, mn, mnl, "imulq") { return x86_imul_rr(out, 0, op1[OP_REG], op0[OP_REG]) } 533 if axc_tok_is(src, mn, mnl, "incq") { return x86_incdec(out, 0, 0, op0[OP_REG]) } 534 return -1 535 } 536 if c0 == 106 { // 'j' 537 if axc_tok_is(src, mn, mnl, "jmp") { 538 if op0[OP_KIND] == K_IND { return x86_jmp_indirect(out, 0, op0[OP_REG]) } 539 var rel: i64 = 0 540 if pass == 2 { rel = axc_label_resolve(src, lab_off, lab_len, lab_addr, lh, op0[OP_SYMOFF], op0[OP_SYMLEN]) - (cur + 5) } 541 return x86_jmp_rel32(out, 0, rel) 542 } 543 let cc: i64 = axc_jcc_cc(src, mn, mnl) 544 if cc >= 0 { 545 var rel: i64 = 0 546 if pass == 2 { rel = axc_label_resolve(src, lab_off, lab_len, lab_addr, lh, op0[OP_SYMOFF], op0[OP_SYMLEN]) - (cur + 6) } 547 return x86_jcc_rel32(out, 0, cc, rel) 548 } 549 return -1 550 } 551 if c0 == 108 { // 'l' 552 if axc_tok_is(src, mn, mnl, "lzcntl") { return x86_lzcnt32(out, 0, op1[OP_REG], op0[OP_REG]) } 553 if axc_tok_is(src, mn, mnl, "leaq") { 554 if op0[OP_KIND] == K_RIP { 555 var d: i64 = 0 556 if pass == 2 { d = axc_label_resolve(src, lab_off, lab_len, lab_addr, lh, op0[OP_SYMOFF], op0[OP_SYMLEN]) - (cur + 7) } 557 return x86_lea_rip(out, 0, op1[OP_REG], d) 558 } 559 // SIB source d(%base,%index,scale) -> lea-strength form (mirrors the 560 // movq K_MEM SIB dispatch below). op0[OP_INDEX] >= 0 signals a scaled index. 561 if op0[OP_INDEX] >= 0 { return x86_lea_sib(out, 0, op1[OP_REG], op0[OP_BASE], op0[OP_INDEX], op0[OP_SCALE], op0[OP_DISP]) } 562 return x86_lea(out, 0, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) 563 } 564 return -1 565 } 566 if c0 == 109 { // 'm' 567 if axc_tok_is(src, mn, mnl, "movabsq") { return x86_movabs(out, 0, op1[OP_REG], op0[OP_IMM]) } 568 // mulq %reg -- UNSIGNED 64x64 -> rdx:rax (REX.W F7 /4). Single-operand, 569 // like idivq/negq; the compiler emits this for OP_UMULHI (__umulhi64). 570 if axc_tok_is(src, mn, mnl, "mulq") { return x86_mul(out, 0, op0[OP_REG]) } 571 if axc_tok_is(src, mn, mnl, "mfence") { return x86_mfence_enc(out) } 572 // mulx %src,%dstlo,%dsthi -- BMI2 flags-free 64x64->128 (src * implicit RDX; low->dstlo, 573 // high->dsthi; CF/OF untouched). AT&T 3-operand (src,dstlo,dsthi) -> op0=src op1=dstlo 574 // op2=dsthi; encoder x86_mulx_r64(dst_hi,dst_lo,src). VEX.NDD.LZ.F2.0F38.W1 F6 /r. 575 if axc_tok_is(src, mn, mnl, "mulx") { return x86_mulx_r64(out, 0, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 576 // SSE 128-bit moves (xmm,xmm reg-reg + xmm<->m128 load/store). 577 // AT&T src,dst: a memory SOURCE (op0=K_MEM) is a LOAD (opcode 0x6f, 578 // reg=dst xmm); a memory DEST (op1=K_MEM) is a STORE (opcode 0x7f, 579 // reg=src xmm). Same K_MEM branch shape proven by movq (lines below). 580 if axc_tok_is(src, mn, mnl, "movdqa") { 581 if op0[OP_KIND] == K_MEM { return x86_sse_mem(out, 0, 0x66, 0x6f, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) } 582 if op1[OP_KIND] == K_MEM { return x86_sse_mem(out, 0, 0x66, 0x7f, op0[OP_REG], op1[OP_BASE], op1[OP_DISP]) } 583 return x86_sse_rr(out, 0, 0x66, 0, 0x6f, op1[OP_REG], op0[OP_REG]) 584 } 585 if axc_tok_is(src, mn, mnl, "movdqu") { 586 if op0[OP_KIND] == K_MEM { return x86_sse_mem(out, 0, 0xf3, 0x6f, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) } 587 if op1[OP_KIND] == K_MEM { return x86_sse_mem(out, 0, 0xf3, 0x7f, op0[OP_REG], op1[OP_BASE], op1[OP_DISP]) } 588 return x86_sse_rr(out, 0, 0xf3, 0, 0x6f, op1[OP_REG], op0[OP_REG]) 589 } 590 // scalar single-precision float: mulss F3 0F 59 /r; movss F3 0F 10 (load/reg-reg) / 0F 11 (store). 591 // AT&T src,dst: memory SOURCE (op0=K_MEM) is a LOAD (reg=dst xmm); memory DEST (op1=K_MEM) is a STORE. 592 if axc_tok_is(src, mn, mnl, "mulss") { return x86_sse_rr(out, 0, 0xf3, 0, 0x59, op1[OP_REG], op0[OP_REG]) } 593 if axc_tok_is(src, mn, mnl, "mulsd") { return x86_sse_rr(out, 0, 0xf2, 0, 0x59, op1[OP_REG], op0[OP_REG]) } 594 // PACKED single-precision (4 x f32): mulps 0F 59 -- mulss without the F3 prefix (the SIMD compute lever). 595 if axc_tok_is(src, mn, mnl, "mulps") { return x86_sse_rr(out, 0, 0, 0, 0x59, op1[OP_REG], op0[OP_REG]) } 596 if axc_tok_is(src, mn, mnl, "movss") { 597 if op0[OP_KIND] == K_MEM { return x86_sse_mem(out, 0, 0xf3, 0x10, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) } 598 if op1[OP_KIND] == K_MEM { return x86_sse_mem(out, 0, 0xf3, 0x11, op0[OP_REG], op1[OP_BASE], op1[OP_DISP]) } 599 return x86_sse_rr(out, 0, 0xf3, 0, 0x10, op1[OP_REG], op0[OP_REG]) 600 } 601 // scalar double load/store/reg (SSE movsd F2 0F 10/11) -- the xmm-operand form, NOT the string op. 602 if axc_tok_is(src, mn, mnl, "movsd") { 603 if op0[OP_KIND] == K_MEM { return x86_sse_mem(out, 0, 0xf2, 0x10, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) } 604 if op1[OP_KIND] == K_MEM { return x86_sse_mem(out, 0, 0xf2, 0x11, op0[OP_REG], op1[OP_BASE], op1[OP_DISP]) } 605 return x86_sse_rr(out, 0, 0xf2, 0, 0x10, op1[OP_REG], op0[OP_REG]) 606 } 607 // PACKED load/store 4 x f32: movups 0F 10 (load/reg-reg) / 0F 11 (store) -- movss without F3, all 4 lanes. 608 if axc_tok_is(src, mn, mnl, "movups") { 609 if op0[OP_KIND] == K_MEM { return x86_sse_mem(out, 0, 0, 0x10, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) } 610 if op1[OP_KIND] == K_MEM { return x86_sse_mem(out, 0, 0, 0x11, op0[OP_REG], op1[OP_BASE], op1[OP_DISP]) } 611 return x86_sse_rr(out, 0, 0, 0, 0x10, op1[OP_REG], op0[OP_REG]) 612 } 613 614 if axc_tok_is(src, mn, mnl, "movq") { 615 // GPR<->xmm direct moves (SSE2 66 REX.W 0F 6E/7E) -- the f64 fast path 616 // that avoids the red-zone memory shuttle. reg field = the xmm operand 617 // in BOTH directions; the opcode encodes the direction. 618 if op1[OP_KIND] == K_XMM { if op0[OP_KIND] == K_REG { return x86_sse_rr_w(out, 0, 0x66, 0, 0x6e, op1[OP_REG], op0[OP_REG]) } } // movq %gpr,%xmm 619 if op0[OP_KIND] == K_XMM { if op1[OP_KIND] == K_REG { return x86_sse_rr_w(out, 0, 0x66, 0, 0x7e, op0[OP_REG], op1[OP_REG]) } } // movq %xmm,%gpr 620 if op0[OP_KIND] == K_IMM { return x86_mov_imm32(out, 0, op1[OP_REG], op0[OP_IMM]) } 621 if op0[OP_KIND] == K_MEM { if op0[OP_INDEX] >= 0 { return x86_mov_load_sib(out, 0, op1[OP_REG], op0[OP_BASE], op0[OP_INDEX], op0[OP_SCALE], op0[OP_DISP]) } } 622 if op1[OP_KIND] == K_MEM { if op1[OP_INDEX] >= 0 { return x86_mov_store_sib(out, 0, op1[OP_BASE], op1[OP_INDEX], op1[OP_SCALE], op1[OP_DISP], op0[OP_REG]) } } 623 if op0[OP_KIND] == K_MEM { return x86_mov_load(out, 0, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) } 624 if op1[OP_KIND] == K_MEM { return x86_mov_store(out, 0, op1[OP_BASE], op1[OP_DISP], op0[OP_REG]) } 625 return x86_mov_reg(out, 0, op1[OP_REG], op0[OP_REG]) 626 } 627 if axc_tok_is(src, mn, mnl, "movzbq") { 628 if op0[OP_KIND] == K_MEM { return x86_movzbq_mem(out, 0, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) } 629 return x86_movzbq(out, 0, op1[OP_REG], op0[OP_REG]) 630 } 631 // SIGN-extending subword loads + the movzwq zero-twin (2026-07-10 debt fix; encoders in nxasm_x86_enc) 632 if axc_tok_is(src, mn, mnl, "movsbq") { 633 if op0[OP_KIND] == K_MEM { return x86_movsbq_mem(out, 0, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) } 634 return x86_movsbq(out, 0, op1[OP_REG], op0[OP_REG]) 635 } 636 if axc_tok_is(src, mn, mnl, "movswq") { 637 if op0[OP_KIND] == K_MEM { return x86_movswq_mem(out, 0, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) } 638 return x86_movswq(out, 0, op1[OP_REG], op0[OP_REG]) 639 } 640 if axc_tok_is(src, mn, mnl, "movzwq") { 641 if op0[OP_KIND] == K_MEM { return x86_movzwq_mem(out, 0, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) } 642 return x86_movzwq(out, 0, op1[OP_REG], op0[OP_REG]) 643 } 644 if axc_tok_is(src, mn, mnl, "movslq") { 645 if op0[OP_KIND] == K_MEM { return x86_movslq_mem(out, 0, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) } 646 return x86_movslq(out, 0, op1[OP_REG], op0[OP_REG]) 647 } 648 if axc_tok_is(src, mn, mnl, "movb") { 649 if op0[OP_KIND] == K_IMM { return x86_movb_imm(out, 0, op1[OP_BASE], op1[OP_DISP], op0[OP_IMM]) } 650 return x86_movb_store(out, 0, op1[OP_BASE], op1[OP_DISP], op0[OP_REG]) 651 } 652 if axc_tok_is(src, mn, mnl, "movl") { 653 if op0[OP_KIND] == K_MEM { return x86_movl_load(out, 0, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) } 654 if op1[OP_KIND] == K_MEM { return x86_movl_store(out, 0, op1[OP_BASE], op1[OP_DISP], op0[OP_REG]) } 655 return x86_movl_rr(out, 0, op1[OP_REG], op0[OP_REG]) 656 } 657 return -1 658 } 659 if c0 == 110 { // 'n' 660 if axc_tok_is(src, mn, mnl, "negq") { return x86_neg(out, 0, op0[OP_REG]) } 661 if axc_tok_is(src, mn, mnl, "notq") { return x86_not(out, 0, op0[OP_REG]) } 662 return -1 663 } 664 if c0 == 111 { // 'o' 665 if axc_tok_is(src, mn, mnl, "orq") { return axc_alu(out, op0, op1, X86_OP_OR, X86_EXT_OR) } 666 return -1 667 } 668 if c0 == 112 { // 'p' 669 if axc_tok_is(src, mn, mnl, "pushq") { return x86_push(out, 0, op0[OP_REG]) } 670 if axc_tok_is(src, mn, mnl, "popq") { return x86_pop(out, 0, op0[OP_REG]) } 671 if axc_tok_is(src, mn, mnl, "popcntq") { return x86_popcnt(out, 0, op1[OP_REG], op0[OP_REG]) } 672 // BMI2 parallel bit deposit/extract (VEX 3-operand $src2,$src1,$dst -> 673 // dst=ModRM.reg, src1=VEX.vvvv, src2=ModRM.rm). VEX.LZ.F2/F3.0F38.W1 F5. 674 if axc_tok_is(src, mn, mnl, "pdep") { return x86_pdep_r64(out, 0, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 675 if axc_tok_is(src, mn, mnl, "pext") { return x86_pext_r64(out, 0, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 676 // SSE/SHA-256 packed ops (xmm,xmm; AT&T src,dst -> ModRM reg=dst rm=src) 677 if axc_tok_is(src, mn, mnl, "pshufb") { return x86_sse_rr(out, 0, 0x66, 0x38, 0x00, op1[OP_REG], op0[OP_REG]) } 678 if axc_tok_is(src, mn, mnl, "paddd") { return x86_sse_rr(out, 0, 0x66, 0, 0xfe, op1[OP_REG], op0[OP_REG]) } 679 if axc_tok_is(src, mn, mnl, "punpcklqdq") { return x86_sse_rr(out, 0, 0x66, 0, 0x6c, op1[OP_REG], op0[OP_REG]) } 680 if axc_tok_is(src, mn, mnl, "punpckhqdq") { return x86_sse_rr(out, 0, 0x66, 0, 0x6d, op1[OP_REG], op0[OP_REG]) } 681 if axc_tok_is(src, mn, mnl, "pxor") { return x86_sse_rr(out, 0, 0x66, 0, 0xef, op1[OP_REG], op0[OP_REG]) } 682 // packed-integer bitwise (xmm,xmm reg-reg; opcode twins of the proven pxor path) 683 if axc_tok_is(src, mn, mnl, "por") { return x86_sse_rr(out, 0, 0x66, 0, 0xeb, op1[OP_REG], op0[OP_REG]) } 684 if axc_tok_is(src, mn, mnl, "pand") { return x86_sse_rr(out, 0, 0x66, 0, 0xdb, op1[OP_REG], op0[OP_REG]) } 685 if axc_tok_is(src, mn, mnl, "pandn") { return x86_sse_rr(out, 0, 0x66, 0, 0xdf, op1[OP_REG], op0[OP_REG]) } 686 // packed add across element widths: b=8 w=16 d=32(exists) q=64 687 if axc_tok_is(src, mn, mnl, "paddb") { return x86_sse_rr(out, 0, 0x66, 0, 0xfc, op1[OP_REG], op0[OP_REG]) } 688 if axc_tok_is(src, mn, mnl, "paddw") { return x86_sse_rr(out, 0, 0x66, 0, 0xfd, op1[OP_REG], op0[OP_REG]) } 689 if axc_tok_is(src, mn, mnl, "paddq") { return x86_sse_rr(out, 0, 0x66, 0, 0xd4, op1[OP_REG], op0[OP_REG]) } 690 // packed sub across element widths 691 if axc_tok_is(src, mn, mnl, "psubb") { return x86_sse_rr(out, 0, 0x66, 0, 0xf8, op1[OP_REG], op0[OP_REG]) } 692 if axc_tok_is(src, mn, mnl, "psubw") { return x86_sse_rr(out, 0, 0x66, 0, 0xf9, op1[OP_REG], op0[OP_REG]) } 693 if axc_tok_is(src, mn, mnl, "psubd") { return x86_sse_rr(out, 0, 0x66, 0, 0xfa, op1[OP_REG], op0[OP_REG]) } 694 if axc_tok_is(src, mn, mnl, "psubq") { return x86_sse_rr(out, 0, 0x66, 0, 0xfb, op1[OP_REG], op0[OP_REG]) } 695 // packed shift by immediate ($imm,%xmm; ModRM.reg = /ext): the rotl/rotr legs 696 if axc_tok_is(src, mn, mnl, "pslld") { return x86_sse_shift_imm(out, 0, 0x72, 6, op1[OP_REG], op0[OP_IMM]) } 697 if axc_tok_is(src, mn, mnl, "psrld") { return x86_sse_shift_imm(out, 0, 0x72, 2, op1[OP_REG], op0[OP_IMM]) } 698 if axc_tok_is(src, mn, mnl, "psllq") { return x86_sse_shift_imm(out, 0, 0x73, 6, op1[OP_REG], op0[OP_IMM]) } 699 if axc_tok_is(src, mn, mnl, "psrlq") { return x86_sse_shift_imm(out, 0, 0x73, 2, op1[OP_REG], op0[OP_IMM]) } 700 // packed shuffle dwords/words (3-operand $imm,%src,%dst): 0F 70 /r ib (pfx selects d/lw/hw) 701 if axc_tok_is(src, mn, mnl, "pshufd") { return x86_sse_rri(out, 0, 0x66, 0x70, op2[OP_REG], op1[OP_REG], op0[OP_IMM]) } 702 if axc_tok_is(src, mn, mnl, "pshuflw") { return x86_sse_rri(out, 0, 0xf2, 0x70, op2[OP_REG], op1[OP_REG], op0[OP_IMM]) } 703 if axc_tok_is(src, mn, mnl, "pshufhw") { return x86_sse_rri(out, 0, 0xf3, 0x70, op2[OP_REG], op1[OP_REG], op0[OP_IMM]) } 704 // carry-less multiply (CLMUL): 66 0F 3A 44 /r ib (3-operand $imm,%src,%dst). 705 // x86_sse_rr emits 66 0F 3A 44 ModRM; the trailing imm8 (half-select) goes after. 706 // This is the GHASH/GF(2^128) core -- the fast path for AES-GCM authentication. 707 if axc_tok_is(src, mn, mnl, "pclmulqdq") { 708 let pn: i64 = x86_sse_rr(out, 0, 0x66, 0x3a, 0x44, op2[OP_REG], op1[OP_REG]) 709 out[pn] = (op0[OP_IMM] & 0xff) as u8 710 return pn + 1 711 } 712 // packed-int MULTIPLY (SSE2/SSE4.1) -- the compute core of ML/GEMM kernels 713 if axc_tok_is(src, mn, mnl, "pmulld") { return x86_sse_rr(out, 0, 0x66, 0x38, 0x40, op1[OP_REG], op0[OP_REG]) } 714 if axc_tok_is(src, mn, mnl, "pmuludq") { return x86_sse_rr(out, 0, 0x66, 0, 0xf4, op1[OP_REG], op0[OP_REG]) } 715 if axc_tok_is(src, mn, mnl, "pmulhw") { return x86_sse_rr(out, 0, 0x66, 0, 0xe5, op1[OP_REG], op0[OP_REG]) } 716 if axc_tok_is(src, mn, mnl, "pmullw") { return x86_sse_rr(out, 0, 0x66, 0, 0xd5, op1[OP_REG], op0[OP_REG]) } 717 if axc_tok_is(src, mn, mnl, "pmaddwd") { return x86_sse_rr(out, 0, 0x66, 0, 0xf5, op1[OP_REG], op0[OP_REG]) } 718 // packed COMPARE (eq/gt) and MIN/MAX 719 if axc_tok_is(src, mn, mnl, "pcmpeqd") { return x86_sse_rr(out, 0, 0x66, 0, 0x76, op1[OP_REG], op0[OP_REG]) } 720 if axc_tok_is(src, mn, mnl, "pcmpgtd") { return x86_sse_rr(out, 0, 0x66, 0, 0x66, op1[OP_REG], op0[OP_REG]) } 721 if axc_tok_is(src, mn, mnl, "pminsd") { return x86_sse_rr(out, 0, 0x66, 0x38, 0x39, op1[OP_REG], op0[OP_REG]) } 722 if axc_tok_is(src, mn, mnl, "pmaxsd") { return x86_sse_rr(out, 0, 0x66, 0x38, 0x3d, op1[OP_REG], op0[OP_REG]) } 723 if axc_tok_is(src, mn, mnl, "pminub") { return x86_sse_rr(out, 0, 0x66, 0, 0xda, op1[OP_REG], op0[OP_REG]) } 724 if axc_tok_is(src, mn, mnl, "pmaxub") { return x86_sse_rr(out, 0, 0x66, 0, 0xde, op1[OP_REG], op0[OP_REG]) } 725 // packed SATURATING add/sub (b/w) 726 if axc_tok_is(src, mn, mnl, "paddsb") { return x86_sse_rr(out, 0, 0x66, 0, 0xec, op1[OP_REG], op0[OP_REG]) } 727 if axc_tok_is(src, mn, mnl, "paddusb") { return x86_sse_rr(out, 0, 0x66, 0, 0xdc, op1[OP_REG], op0[OP_REG]) } 728 if axc_tok_is(src, mn, mnl, "psubsb") { return x86_sse_rr(out, 0, 0x66, 0, 0xe8, op1[OP_REG], op0[OP_REG]) } 729 if axc_tok_is(src, mn, mnl, "psubusb") { return x86_sse_rr(out, 0, 0x66, 0, 0xd8, op1[OP_REG], op0[OP_REG]) } 730 // PACK (narrow) + UNPACK (interleave) + widening convert 731 if axc_tok_is(src, mn, mnl, "packssdw") { return x86_sse_rr(out, 0, 0x66, 0, 0x6b, op1[OP_REG], op0[OP_REG]) } 732 if axc_tok_is(src, mn, mnl, "packuswb") { return x86_sse_rr(out, 0, 0x66, 0, 0x67, op1[OP_REG], op0[OP_REG]) } 733 if axc_tok_is(src, mn, mnl, "punpckldq") { return x86_sse_rr(out, 0, 0x66, 0, 0x62, op1[OP_REG], op0[OP_REG]) } 734 if axc_tok_is(src, mn, mnl, "punpckhdq") { return x86_sse_rr(out, 0, 0x66, 0, 0x6a, op1[OP_REG], op0[OP_REG]) } 735 if axc_tok_is(src, mn, mnl, "pmovzxbw") { return x86_sse_rr(out, 0, 0x66, 0x38, 0x30, op1[OP_REG], op0[OP_REG]) } 736 // absolute value, horizontal add, sum-of-abs-diff, byte-mask extract 737 if axc_tok_is(src, mn, mnl, "pabsd") { return x86_sse_rr(out, 0, 0x66, 0x38, 0x1e, op1[OP_REG], op0[OP_REG]) } 738 if axc_tok_is(src, mn, mnl, "phaddd") { return x86_sse_rr(out, 0, 0x66, 0x38, 0x02, op1[OP_REG], op0[OP_REG]) } 739 if axc_tok_is(src, mn, mnl, "psadbw") { return x86_sse_rr(out, 0, 0x66, 0, 0xf6, op1[OP_REG], op0[OP_REG]) } 740 if axc_tok_is(src, mn, mnl, "pmovmskb") { return x86_sse_rr(out, 0, 0x66, 0, 0xd7, op1[OP_REG], op0[OP_REG]) } 741 return -1 742 } 743 if c0 == 114 { // 'r' 744 if axc_tok_is(src, mn, mnl, "ret") { return x86_ret(out, 0) } 745 if axc_tok_is(src, mn, mnl, "rdtsc") { return x86_rdtsc(out, 0) } 746 if axc_tok_is(src, mn, mnl, "rolq") { 747 if op0[OP_KIND] == K_IMM { return x86_rot_imm(out, 0, 0, op1[OP_REG], op0[OP_IMM]) } 748 return x86_rot_cl(out, 0, 0, op1[OP_REG]) 749 } 750 if axc_tok_is(src, mn, mnl, "rorq") { 751 if op0[OP_KIND] == K_IMM { return x86_rot_imm(out, 0, 1, op1[OP_REG], op0[OP_IMM]) } 752 return x86_rot_cl(out, 0, 1, op1[OP_REG]) 753 } 754 return -1 755 } 756 if c0 == 115 { // 's' 757 if axc_tok_is(src, mn, mnl, "syscall") { return x86_syscall(out, 0) } 758 // scalar float subtract + sqrt: subss/subsd F3/F2 0F 5C; sqrtss/sqrtsd F3/F2 0F 51. 759 if axc_tok_is(src, mn, mnl, "subss") { return x86_sse_rr(out, 0, 0xf3, 0, 0x5c, op1[OP_REG], op0[OP_REG]) } 760 if axc_tok_is(src, mn, mnl, "subsd") { return x86_sse_rr(out, 0, 0xf2, 0, 0x5c, op1[OP_REG], op0[OP_REG]) } 761 if axc_tok_is(src, mn, mnl, "sqrtss") { return x86_sse_rr(out, 0, 0xf3, 0, 0x51, op1[OP_REG], op0[OP_REG]) } 762 if axc_tok_is(src, mn, mnl, "sqrtsd") { return x86_sse_rr(out, 0, 0xf2, 0, 0x51, op1[OP_REG], op0[OP_REG]) } 763 // PACKED single-precision shuffle: shufps 0F C6 /r ib (3-operand $imm,%src,%dst) -- the broadcast/horizontal-sum primitive for packed f32 matmul. 764 if axc_tok_is(src, mn, mnl, "shufps") { return x86_sse_rri(out, 0, 0, 0xc6, op2[OP_REG], op1[OP_REG], op0[OP_IMM]) } 765 // SHA-NI rounds (xmm,xmm; sha256rnds2 also uses xmm0 implicitly as the message) 766 if axc_tok_is(src, mn, mnl, "sha256rnds2") { return x86_sse_rr(out, 0, 0, 0x38, 0xcb, op1[OP_REG], op0[OP_REG]) } 767 if axc_tok_is(src, mn, mnl, "sha256msg1") { return x86_sse_rr(out, 0, 0, 0x38, 0xcc, op1[OP_REG], op0[OP_REG]) } 768 if axc_tok_is(src, mn, mnl, "sha256msg2") { return x86_sse_rr(out, 0, 0, 0x38, 0xcd, op1[OP_REG], op0[OP_REG]) } 769 if axc_tok_is(src, mn, mnl, "subq") { return axc_alu(out, op0, op1, X86_OP_SUB, X86_EXT_SUB) } 770 if axc_tok_is(src, mn, mnl, "shlq") { 771 if op0[OP_KIND] == K_IMM { return x86_shift_imm(out, 0, X86_EXT_SHL, op1[OP_REG], op0[OP_IMM]) } 772 return x86_shift_cl(out, 0, X86_EXT_SHL, op1[OP_REG]) 773 } 774 if axc_tok_is(src, mn, mnl, "shrq") { 775 if op0[OP_KIND] == K_IMM { return x86_shift_imm(out, 0, X86_EXT_SHR, op1[OP_REG], op0[OP_IMM]) } 776 return x86_shift_cl(out, 0, X86_EXT_SHR, op1[OP_REG]) 777 } 778 if axc_tok_is(src, mn, mnl, "sarq") { 779 if op0[OP_KIND] == K_IMM { return x86_shift_imm(out, 0, X86_EXT_SAR, op1[OP_REG], op0[OP_IMM]) } 780 return x86_shift_cl(out, 0, X86_EXT_SAR, op1[OP_REG]) 781 } 782 let sc: i64 = axc_setcc_cc(src, mn, mnl) 783 if sc >= 0 { return x86_setcc(out, 0, sc, op0[OP_REG]) } 784 return -1 785 } 786 if c0 == 116 { // 't' 787 if axc_tok_is(src, mn, mnl, "testq") { return x86_alu_rr(out, 0, X86_OP_TEST, op1[OP_REG], op0[OP_REG]) } 788 if axc_tok_is(src, mn, mnl, "tzcntl") { return x86_tzcnt32(out, 0, op1[OP_REG], op0[OP_REG]) } 789 return -1 790 } 791 if c0 == 118 { // 'v' -- AVX/AVX2 (VEX-encoded, 256-bit ymm) 792 // 256-bit unaligned load/store: VEX.256.F3.0F 6F(load)/7F(store). 793 if axc_tok_is(src, mn, mnl, "vmovdqu") { 794 if op0[OP_KIND] == K_MEM { return x86_vex_rm(out, 0, 2, 1, 0, 1, 0x6f, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) } 795 if op1[OP_KIND] == K_MEM { return x86_vex_rm(out, 0, 2, 1, 0, 1, 0x7f, op0[OP_REG], op1[OP_BASE], op1[OP_DISP]) } 796 return -1 797 } 798 // 256-bit packed-int 3-operand ALU: VEX.256.66.0F <opc> (%src2,%src1,%dst). 799 if axc_tok_is(src, mn, mnl, "vpaddd") { return x86_vex_rrr(out, 0, 1, 1, 0, 1, 0xfe, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 800 if axc_tok_is(src, mn, mnl, "vpaddq") { return x86_vex_rrr(out, 0, 1, 1, 0, 1, 0xd4, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 801 if axc_tok_is(src, mn, mnl, "vpsubd") { return x86_vex_rrr(out, 0, 1, 1, 0, 1, 0xfa, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 802 if axc_tok_is(src, mn, mnl, "vpsubq") { return x86_vex_rrr(out, 0, 1, 1, 0, 1, 0xfb, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 803 if axc_tok_is(src, mn, mnl, "vpxor") { return x86_vex_rrr(out, 0, 1, 1, 0, 1, 0xef, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 804 if axc_tok_is(src, mn, mnl, "vpand") { return x86_vex_rrr(out, 0, 1, 1, 0, 1, 0xdb, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 805 if axc_tok_is(src, mn, mnl, "vpandn") { return x86_vex_rrr(out, 0, 1, 1, 0, 1, 0xdf, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 806 if axc_tok_is(src, mn, mnl, "vpor") { return x86_vex_rrr(out, 0, 1, 1, 0, 1, 0xeb, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 807 if axc_tok_is(src, mn, mnl, "vpshufb"){ return x86_vex_rrr(out, 0, 1, 2, 0, 1, 0x00, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 808 // 128-bit broadcast to both ymm lanes (VEX.256.66.0F38.W0 5A /r mem) -- OpenSSL's key-smash + mask-reload primitive 809 if axc_tok_is(src, mn, mnl, "vbroadcasti128") { if op0[OP_KIND] == K_MEM { return x86_vex_rm(out, 0, 1, 2, 0, 1, 0x5a, op1[OP_REG], op0[OP_BASE], op0[OP_DISP]) } return -1 } 810 // 256-bit packed multiply / compare / minmax (3-operand reg; 0F38 map=2 where noted) 811 if axc_tok_is(src, mn, mnl, "vpmulld") { return x86_vex_rrr(out, 0, 1, 2, 0, 1, 0x40, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 812 if axc_tok_is(src, mn, mnl, "vpmuludq") { return x86_vex_rrr(out, 0, 1, 1, 0, 1, 0xf4, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 813 if axc_tok_is(src, mn, mnl, "vpmaddwd") { return x86_vex_rrr(out, 0, 1, 1, 0, 1, 0xf5, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 814 if axc_tok_is(src, mn, mnl, "vpcmpeqd") { return x86_vex_rrr(out, 0, 1, 1, 0, 1, 0x76, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 815 if axc_tok_is(src, mn, mnl, "vpcmpgtd") { return x86_vex_rrr(out, 0, 1, 1, 0, 1, 0x66, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 816 if axc_tok_is(src, mn, mnl, "vpminsd") { return x86_vex_rrr(out, 0, 1, 2, 0, 1, 0x39, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 817 if axc_tok_is(src, mn, mnl, "vpmaxsd") { return x86_vex_rrr(out, 0, 1, 2, 0, 1, 0x3d, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 818 if axc_tok_is(src, mn, mnl, "vpermd") { return x86_vex_rrr(out, 0, 1, 2, 0, 1, 0x36, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 819 if axc_tok_is(src, mn, mnl, "vpsllvd") { return x86_vex_rrr(out, 0, 1, 2, 0, 1, 0x47, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 820 if axc_tok_is(src, mn, mnl, "vpsrlvd") { return x86_vex_rrr(out, 0, 1, 2, 0, 1, 0x45, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 821 if axc_tok_is(src, mn, mnl, "vfmadd231ps") { return x86_vex_rrr(out, 0, 1, 2, 0, 1, 0xb8, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 822 if axc_tok_is(src, mn, mnl, "vmulps") { return x86_vex_rrr(out, 0, 0, 1, 0, 1, 0x59, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 823 if axc_tok_is(src, mn, mnl, "vaddps") { return x86_vex_rrr(out, 0, 0, 1, 0, 1, 0x58, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 824 if axc_tok_is(src, mn, mnl, "vsubps") { return x86_vex_rrr(out, 0, 0, 1, 0, 1, 0x5c, op2[OP_REG], op1[OP_REG], op0[OP_REG]) } 825 // 256-bit shift-by-immediate (NDD: $imm,%src,%dst) -- the AVX2 ARX rotate legs 826 if axc_tok_is(src, mn, mnl, "vpslld") { return x86_vex_shift_imm(out, 0, 1, 1, 1, 0x72, 6, op2[OP_REG], op1[OP_REG], op0[OP_IMM]) } 827 if axc_tok_is(src, mn, mnl, "vpsrld") { return x86_vex_shift_imm(out, 0, 1, 1, 1, 0x72, 2, op2[OP_REG], op1[OP_REG], op0[OP_IMM]) } 828 if axc_tok_is(src, mn, mnl, "vpsrad") { return x86_vex_shift_imm(out, 0, 1, 1, 1, 0x72, 4, op2[OP_REG], op1[OP_REG], op0[OP_IMM]) } 829 if axc_tok_is(src, mn, mnl, "vpsllq") { return x86_vex_shift_imm(out, 0, 1, 1, 1, 0x73, 6, op2[OP_REG], op1[OP_REG], op0[OP_IMM]) } 830 if axc_tok_is(src, mn, mnl, "vpsrlq") { return x86_vex_shift_imm(out, 0, 1, 1, 1, 0x73, 2, op2[OP_REG], op1[OP_REG], op0[OP_IMM]) } 831 // 256-bit per-128-lane dword shuffle (imm,src,dst) 832 if axc_tok_is(src, mn, mnl, "vpshufd") { return x86_vex_rmi(out, 0, 1, 1, 0, 1, 0x70, op2[OP_REG], op1[OP_REG], op0[OP_IMM]) } 833 return -1 834 } 835 if c0 == 120 { // 'x' 836 if axc_tok_is(src, mn, mnl, "xorq") { return x86_alu_rr(out, 0, X86_OP_XOR, op1[OP_REG], op0[OP_REG]) } 837 // atomic exchange (xchg-with-memory is auto-locked): xchgq %reg, (%base) = 87 /r 838 if axc_tok_is(src, mn, mnl, "xchgq") { return x86_atomic_rm(out, 0, 0x87, op0[OP_REG], op1[OP_BASE]) } 839 // atomic fetch-and-add (lock prefix from the assemble loop): xaddq %reg, (%base) = 0F C1 /r 840 if axc_tok_is(src, mn, mnl, "xaddq") { return x86_atomic_rm(out, 1, 0xc1, op0[OP_REG], op1[OP_BASE]) } 841 return -1 842 } 843 return -1 // unrecognized mnemonic (caller treats as fatal) 844} 845 846// ---- emit OR count comma-separated `.byte` values; returns count ---- 847// do_emit=1 writes each (low) byte to dest[dest_off + k]; do_emit=0 only counts. 848func axc_emit_bytes(src: *u8, start: i64, le: i64, dest: *u8, dest_off: i64, do_emit: i64) -> i64 { 849 var i: i64 = start 850 var cnt: i64 = 0 851 var run: i64 = 1 852 while i < le && run == 1 { 853 while i < le { 854 let c: i64 = src[i] & 0xff 855 if c == 32 { i = i + 1 } else { if c == 9 { i = i + 1 } else { if c == 44 { i = i + 1 } else { break } } } 856 } 857 if i >= le { run = 0 } else { 858 let c0: i64 = src[i] & 0xff 859 var isnum: i64 = 0 860 if c0 == 45 { isnum = 1 } 861 if axc_is_digit(c0) == 1 { isnum = 1 } 862 if isnum == 0 { run = 0 } else { 863 var neg: i64 = 0 864 if c0 == 45 { neg = 1; i = i + 1 } 865 var v: i64 = 0 866 while i < le { 867 let d: i64 = src[i] & 0xff 868 if axc_is_digit(d) == 1 { v = v * 10 + (d - 48); i = i + 1 } else { break } 869 } 870 if neg == 1 { v = 0 - v } 871 if do_emit == 1 { dest[dest_off + cnt] = v & 0xff } 872 cnt = cnt + 1 873 } 874 } 875 } 876 return cnt 877} 878 879// ---- DEBUG LINE ACCUMULATION (DDR-002 step 2, 2026-08-06) ------------------ 880// The compiler emits gas-style `.file N "path"` and `.loc N LINE` ONLY under -g, so the mere 881// PRESENCE of these directives in the .s IS the opt-in signal -- no flag has to be plumbed 882// through nx_sov_build_run, and a .s that never mentions them produces output byte-identical 883// to a build that never heard of DWARF. That is what keeps the MEASURED 168-byte minimal 884// static binary intact (DDR-002 section 6.1), and it is why the switch lives at the compiler. 885// 886// STATIC POINTERS, LAZILY MMAP'd -- never static ARRAYS. A BSS [N]i64 crashes a module like 887// this silently at startup (banked landmine); the pointer form is the proven shape. 888const AXC_MAX_LOC: i64 = 65536 889const AXC_MAX_DFILE: i64 = 512 890 891static axc_loc_off: *i64 // text offset this row applies to 892static axc_loc_file: *i64 // 1-based file index exactly as the .s wrote it 893static axc_loc_line: *i64 // source line 894static axc_n_loc: i64 895static axc_dfile_off: *i64 // offset into src of the file name's first byte 896static axc_dfile_len: *i64 897static axc_n_dfile: i64 898 899// ---- FUNCTION BOUNDS (DDR-009 step 1, 2026-08-07) -------------------------- 900// .debug_info needs DW_AT_low_pc / DW_AT_high_pc per subprogram, and ONLY the assembler knows 901// addresses -- the same constraint that forced the .loc split in DDR-002. 902// NOTHING IS EMITTED FROM THIS TABLE YET, DELIBERATELY. Landing the recorder alone is provably 903// byte-neutral and the equivalence gate can prove it; DDR-002 section 5 is explicit that nxasm 904// is the crown jewel below the compiler and changes belong in steps that each carry their own 905// proof. high_pc is NOT stored: it is the NEXT function's low_pc (or text end), derived at 906// emit time, because storing a value you can derive is a second source of truth that can drift. 907const AXC_MAX_FN: i64 = 16384 908 909static axc_fn_off: *i64 // offset into src of the function name's first byte 910static axc_fn_len: *i64 911static axc_fn_low: *i64 // text offset of the function's first byte = low_pc 912static axc_n_fn: i64 913 914func axc_dbg_init() -> i64 { 915 if (axc_loc_off as i64) != 0 { return 0 } 916 axc_loc_off = sys_mmap(8 * AXC_MAX_LOC) as *i64 917 axc_loc_file = sys_mmap(8 * AXC_MAX_LOC) as *i64 918 axc_loc_line = sys_mmap(8 * AXC_MAX_LOC) as *i64 919 axc_dfile_off = sys_mmap(8 * AXC_MAX_DFILE) as *i64 920 axc_dfile_len = sys_mmap(8 * AXC_MAX_DFILE) as *i64 921 axc_fn_off = sys_mmap(8 * AXC_MAX_FN) as *i64 922 axc_fn_len = sys_mmap(8 * AXC_MAX_FN) as *i64 923 axc_fn_low = sys_mmap(8 * AXC_MAX_FN) as *i64 924 return 0 925} 926 927// `.type NAME, @function` at text offset `off`. gas emits this immediately BEFORE the label, so 928// the current text cursor IS the function's first byte -- verified against the emitted .s, not 929// assumed. Recorded on the EMIT pass only, for the same reason as .loc: sizing-pass offsets are 930// provisional, and a bounds table built from provisional addresses points a debugger into the 931// middle of a neighbouring function. 932// REFUSES rather than truncating silently when full, and records the fact so a gate can see it. 933func axc_dbg_type(src: *u8, j: i64, le: i64, off: i64) -> i64 { 934 axc_dbg_init() 935 if axc_n_fn >= AXC_MAX_FN { return 0 } 936 var p: i64 = j 937 while p < le { if axc_is_space(src[p]) == 1 { p = p + 1 } else { break } } 938 let n0: i64 = p 939 while p < le { if axc_is_ident(src[p]) == 1 { p = p + 1 } else { break } } 940 let nlen: i64 = p - n0 941 if nlen <= 0 { return 0 } 942 // Only record rows that actually say @function -- .type is also used for objects, and a 943 // subprogram DIE built over a data symbol would hand a debugger a fictional frame. 944 var q: i64 = p 945 var isfn: i64 = 0 946 while q < le { 947 if (src[q] & 0xff) == 64 { 948 if q + 9 <= le { 949 if axc_tok_is(src, q, 9, "@function") == 1 { isfn = 1 } 950 } 951 q = le 952 } else { q = q + 1 } 953 } 954 if isfn == 0 { return 0 } 955 let n: i64 = axc_n_fn 956 axc_fn_off[n] = n0 957 axc_fn_len[n] = nlen 958 axc_fn_low[n] = off 959 axc_n_fn = n + 1 960 return 1 961} 962 963// `.loc <file> <line>` at text offset `off`. Recorded on the EMIT pass ONLY: on the sizing 964// pass the offsets are not final, and a line table built from provisional addresses points a 965// debugger at the wrong instructions -- which is worse than having no table at all. 966func axc_dbg_loc(src: *u8, j: i64, le: i64, off: i64, posbox: *i64) -> i64 { 967 axc_dbg_init() 968 if axc_n_loc >= AXC_MAX_LOC { return 0 } 969 var p: i64 = j 970 while p < le { if axc_is_space(src[p]) == 1 { p = p + 1 } else { break } } 971 posbox[0] = p 972 let fno: i64 = axc_parse_int(src, posbox, le) 973 p = posbox[0] 974 while p < le { if axc_is_space(src[p]) == 1 { p = p + 1 } else { break } } 975 posbox[0] = p 976 let lno: i64 = axc_parse_int(src, posbox, le) 977 if fno <= 0 { return 0 } 978 if lno <= 0 { return 0 } 979 let n: i64 = axc_n_loc 980 axc_loc_off[n] = off 981 axc_loc_file[n] = fno 982 axc_loc_line[n] = lno 983 axc_n_loc = n + 1 984 return 1 985} 986 987// `.file <n> "<path>"`. Stores the name's BOUNDS in src rather than copying it: the source 988// buffer outlives the assembly, and a copy is one more thing that can drift from the original. 989// Indexed BY THE NUMBER THE .s GAVE, so a gap or an out-of-order table cannot silently shift 990// every later file by one -- the classic off-by-one that makes debug info point at a sibling. 991func axc_dbg_file(src: *u8, j: i64, le: i64, posbox: *i64) -> i64 { 992 axc_dbg_init() 993 var p: i64 = j 994 while p < le { if axc_is_space(src[p]) == 1 { p = p + 1 } else { break } } 995 posbox[0] = p 996 let fno: i64 = axc_parse_int(src, posbox, le) 997 p = posbox[0] 998 while p < le { if axc_is_space(src[p]) == 1 { p = p + 1 } else { break } } 999 if p >= le { return 0 } 1000 if (src[p] & 0xff) != 34 { return 0 } // '"' 1001 p = p + 1 1002 let s0: i64 = p 1003 while p < le { if (src[p] & 0xff) == 34 { break } else { p = p + 1 } } 1004 if fno <= 0 { return 0 } 1005 let idx: i64 = fno - 1 1006 if idx >= AXC_MAX_DFILE { return 0 } 1007 axc_dfile_off[idx] = s0 1008 axc_dfile_len[idx] = p - s0 1009 if idx + 1 > axc_n_dfile { axc_n_dfile = idx + 1 } 1010 return 1 1011} 1012 1013// Report on STDERR, never stdout -- stdout IS the ELF. Length computed at runtime rather than 1014// hand-counted: nx_parse learned that lesson the expensive way (a miscounted literal silently 1015// truncated diagnostics twice), and the fix is to make the bug class unrepresentable. 1016func axc_dbg_puts(s: *u8) -> i64 { 1017 var n: i64 = 0 1018 while s[n] != (0 as u8) { n = n + 1 } 1019 sys_write(2, s, n) 1020 return 0 1021} 1022 1023func axc_dbg_num(v: i64) -> i64 { 1024 if v == 0 { axc_dbg_puts("0" as *u8); return 0 } 1025 let d: *u8 = sys_mmap(24) 1026 var m: i64 = v 1027 var k: i64 = 0 1028 while m > 0 { d[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 1029 let o: *u8 = sys_mmap(32) 1030 var i: i64 = 0 1031 while i < k { o[i] = d[k - 1 - i]; i = i + 1 } 1032 o[k] = 0 as u8 1033 axc_dbg_puts(o) 1034 return 0 1035} 1036 1037// SILENT when no debug directives were seen, so a normal build's output is unchanged in every 1038// channel -- stdout AND stderr. A tool that announces itself on every ordinary run trains its 1039// reader to stop looking. 1040func axc_dbg_report() -> i64 { 1041 if axc_n_loc <= 0 { return 0 } 1042 axc_dbg_puts("nxasm_x86: debug-line rows=" as *u8) 1043 axc_dbg_num(axc_n_loc) 1044 axc_dbg_puts(" files=" as *u8) 1045 axc_dbg_num(axc_n_dfile) 1046 axc_dbg_puts("\n" as *u8) 1047 return 0 1048} 1049 1050// ---- map a section-changing directive to a section id (-1 if none) ---- 1051// 0=.text, 1=the DATA TAIL (.rodata and .data both), 2=ignore (e.g. .note.GNU-stack, .bss). 1052// 1053// LN42 (2026-09-03): .data USED to fall into the ignore bucket, and that single fact is why a 1054// writable NON-ZERO static initializer had no emission on either lane -- the compiler could not put 1055// it in .rodata (read-only under GNU as/ld, so the first write SIGSEGVs) and could not put it in 1056// .data (dropped here), leaving .lcomm's zero-fill as the only both-lanes-writable form, so 1057// x86ctx_emit_module_globals refused the whole class by name. It was never a hard problem: THIS 1058// SECTION IS ALREADY WRITABLE. .lcomm allocates into exactly this tail and its own comment calls the 1059// result "a zero-filled MUTABLE slot ... appended to the data tail" -- code writes those slots at 1060// runtime and always has. So .data needs no new segment, no new permission and no ELF change; it 1061// needed to stop being ignored. Both the bare directive and the `.section .data` form map here. 1062func axc_apply_section(src: *u8, t0: i64, tlen: i64, j: i64, le: i64) -> i64 { 1063 if axc_tok_is(src, t0, tlen, ".text") == 1 { return 0 } 1064 if axc_tok_is(src, t0, tlen, ".data") == 1 { return 1 } 1065 if axc_tok_is(src, t0, tlen, ".section") == 1 { 1066 var p: i64 = j 1067 while p < le { if axc_is_space(src[p]) == 1 { p = p + 1 } else { break } } 1068 let s2: i64 = p 1069 while p < le { if axc_is_ident(src[p]) == 1 { p = p + 1 } else { break } } 1070 if axc_tok_is(src, s2, p - s2, ".rodata") == 1 { return 1 } 1071 if axc_tok_is(src, s2, p - s2, ".data") == 1 { return 1 } 1072 if axc_tok_is(src, s2, p - s2, ".text") == 1 { return 0 } 1073 return 2 1074 } 1075 return -1 1076} 1077 1078// ---- one pass over the source. pass 0 = size + record (section, local 1079// offset) per label; pass 1 = emit text then rodata into `out`. text 1080// occupies [0,text_size); rodata follows at [text_size, ...). Returns 1081// text_size (pass 0) or total image length (pass 1), or a negative error. 1082func axc_pass(src: *u8, n: i64, out: *u8, text_size: i64, pass: i64, 1083 lab_off: *i64, lab_len: *i64, lab_addr: *i64, lab_sec: *i64, n_lab_box: *i64, 1084 lh: *i64, op0: *i64, op1: *i64, op2: *i64, scratch: *u8, posbox: *i64) -> i64 { 1085 var section: i64 = 0 1086 var tcur: i64 = 0 1087 var rcur: i64 = 0 1088 var ls: i64 = 0 1089 while ls < n { 1090 var le: i64 = ls 1091 while le < n { if (src[le] & 0xff) == 10 { break } else { le = le + 1 } } 1092 var i: i64 = ls 1093 while i < le { if axc_is_space(src[i]) == 1 { i = i + 1 } else { break } } 1094 if i < le { 1095 let c0: i64 = src[i] & 0xff 1096 if c0 != 35 { // not '#' 1097 let t0: i64 = i 1098 var j: i64 = i 1099 while j < le { if axc_is_ident(src[j]) == 1 { j = j + 1 } else { break } } 1100 let tlen: i64 = j - t0 1101 var is_label: i64 = 0 1102 if j < le { if (src[j] & 0xff) == 58 { is_label = 1 } } // ':' 1103 if is_label == 1 { 1104 if pass == 0 { 1105 let nl: i64 = n_lab_box[0] 1106 if nl >= ASM_MAX_LABELS { 1107 // LOUD-fail (PREVENT): never overflow the label tables into adjacent mmaps 1108 sys_write(2, "nxasm_x86: label table FULL (ASM_MAX_LABELS) -- raise the cap\n" as *u8, 62) 1109 sys_exit(101) 1110 } 1111 lab_off[nl] = t0 1112 lab_len[nl] = tlen 1113 if section == 1 { lab_sec[nl] = 1; lab_addr[nl] = rcur } else { lab_sec[nl] = 0; lab_addr[nl] = tcur } 1114 n_lab_box[0] = nl + 1 1115 } 1116 } else { 1117 if (src[t0] & 0xff) == 46 { // '.' -> directive 1118 let ns: i64 = axc_apply_section(src, t0, tlen, j, le) 1119 if ns >= 0 { section = ns } 1120 if axc_tok_is(src, t0, tlen, ".byte") == 1 { 1121 if section == 0 { 1122 tcur = tcur + axc_emit_bytes(src, j, le, out, tcur, pass) 1123 } 1124 if section == 1 { 1125 rcur = rcur + axc_emit_bytes(src, j, le, out, text_size + rcur, pass) 1126 } 1127 } 1128 // DEBUG DIRECTIVES (DDR-002). Emit pass only -- see axc_dbg_loc. 1129 // `.loc` is recorded against the TEXT cursor because that is the address 1130 // a debugger will hand us back; rodata has no instructions to stop at. 1131 if axc_tok_is(src, t0, tlen, ".loc") == 1 { 1132 if pass == 1 { if section == 0 { axc_dbg_loc(src, j, le, tcur, posbox) } } 1133 } 1134 if axc_tok_is(src, t0, tlen, ".file") == 1 { 1135 if pass == 1 { axc_dbg_file(src, j, le, posbox) } 1136 } 1137 // FUNCTION BOUNDS (DDR-009 step 1). Records only -- emits nothing yet, so 1138 // this step must leave every produced ELF byte-identical, which the 1139 // equivalence gate proves rather than asserts. 1140 if axc_tok_is(src, t0, tlen, ".type") == 1 { 1141 if pass == 1 { if section == 0 { axc_dbg_type(src, j, le, tcur) } } 1142 } 1143 if axc_tok_is(src, t0, tlen, ".lcomm") == 1 { 1144 // .lcomm NAME, SIZE -- zero-filled MUTABLE slot (the 1145 // compiler emits these for zero-init globals). gas 1146 // allocates BSS; our single-image model appends to the 1147 // data tail, 8-aligned. Missing this directive left the 1148 // label undefined -> lea resolved -1 -> SIGSEGV 1149 // (NXASM-ENC-GAP root cause, TLS gate 2026-06-10). 1150 var lp: i64 = j 1151 while lp < le { if axc_is_space(src[lp]) == 1 { lp = lp + 1 } else { break } } 1152 let lcn: i64 = lp 1153 while lp < le { if axc_is_ident(src[lp]) == 1 { lp = lp + 1 } else { break } } 1154 let lcl: i64 = lp - lcn 1155 while lp < le { 1156 if (src[lp] & 0xff) == 44 { lp = lp + 1 } 1157 else { if axc_is_space(src[lp]) == 1 { lp = lp + 1 } else { break } } 1158 } 1159 posbox[0] = lp 1160 let lcsz: i64 = axc_parse_int(src, posbox, le) 1161 let lcpad: i64 = (8 - (rcur & 7)) & 7 1162 if pass == 0 { 1163 let nl2: i64 = n_lab_box[0] 1164 if nl2 >= ASM_MAX_LABELS { 1165 sys_write(2, "nxasm_x86: label table FULL (ASM_MAX_LABELS) -- raise the cap\n" as *u8, 62) 1166 sys_exit(101) 1167 } 1168 lab_off[nl2] = lcn 1169 lab_len[nl2] = lcl 1170 lab_sec[nl2] = 1 1171 lab_addr[nl2] = rcur + lcpad 1172 n_lab_box[0] = nl2 + 1 1173 } 1174 if pass == 1 { 1175 var lz: i64 = 0 1176 while lz < lcpad + lcsz { out[text_size + rcur + lz] = 0; lz = lz + 1 } 1177 } 1178 rcur = rcur + lcpad + lcsz 1179 } 1180 } else { // instruction 1181 op0[OP_KIND] = K_NONE 1182 op1[OP_KIND] = K_NONE 1183 op2[OP_KIND] = K_NONE 1184 // LOCK prefix: `lock <insn>` -- emit F0, then encode <insn> with a RE-EXTRACTED mnemonic 1185 // + operand start (so `lock cmpxchgq %rcx, (%r11)` reads cmpxchgq + its operands, not 1186 // "lock" as the mnemonic). Only the atomic RMW ops (cmpxchg/xadd) carry it. 1187 var emn: i64 = t0 1188 var emnl: i64 = tlen 1189 var estart: i64 = j 1190 var lock_pfx: i64 = 0 1191 if axc_tok_is(src, t0, tlen, "lock") == 1 { 1192 lock_pfx = 1 1193 var lq: i64 = j 1194 while lq < le { if axc_is_space(src[lq]) == 1 { lq = lq + 1 } else { break } } 1195 emn = lq 1196 while lq < le { if axc_is_ident(src[lq]) == 1 { lq = lq + 1 } else { break } } 1197 emnl = lq - emn 1198 estart = lq 1199 } 1200 var p: i64 = estart 1201 while p < le { if axc_is_space(src[p]) == 1 { p = p + 1 } else { break } } 1202 if p < le { 1203 posbox[0] = p 1204 axc_parse_operand(src, posbox, le, op0) 1205 p = posbox[0] 1206 while p < le { if axc_is_space(src[p]) == 1 { p = p + 1 } else { break } } 1207 if p < le { 1208 if (src[p] & 0xff) == 44 { // ',' -> op1 1209 p = p + 1 1210 while p < le { if axc_is_space(src[p]) == 1 { p = p + 1 } else { break } } 1211 posbox[0] = p 1212 axc_parse_operand(src, posbox, le, op1) 1213 p = posbox[0] 1214 while p < le { if axc_is_space(src[p]) == 1 { p = p + 1 } else { break } } 1215 if p < le { 1216 if (src[p] & 0xff) == 44 { // ',' -> op2 (3-operand: pshufd, VEX/EVEX) 1217 p = p + 1 1218 while p < le { if axc_is_space(src[p]) == 1 { p = p + 1 } else { break } } 1219 posbox[0] = p 1220 axc_parse_operand(src, posbox, le, op2) 1221 } 1222 } 1223 } 1224 } 1225 } 1226 var eoff: i64 = 0 1227 if lock_pfx == 1 { scratch[0] = 0xf0 as u8; eoff = 1 } 1228 let elen0: i64 = axc_emit(((scratch as i64) + eoff) as *u8, src, emn, emnl, op0, op1, op2, 1229 lab_off, lab_len, lab_addr, n_lab_box[0], lh, tcur + eoff, pass + 1) 1230 var elen: i64 = elen0 1231 if elen0 >= 0 { elen = elen0 + eoff } 1232 if elen < 0 { 1233 // LOUD-fail (PREVENT): a bare -1 cost a full bisect 1234 // per enc-gap (NXASM-ENC-GAP) -- name the line instead 1235 sys_write(2, "nxasm_x86: cannot encode: " as *u8, 26) 1236 sys_write(2, ((src as i64) + ls) as *u8, le - ls) 1237 sys_write(2, "\n" as *u8, 1) 1238 return 0 - 100 1239 } 1240 if pass == 1 { 1241 var kk: i64 = 0 1242 while kk < elen { out[tcur + kk] = scratch[kk]; kk = kk + 1 } 1243 } 1244 tcur = tcur + elen 1245 } 1246 } 1247 } 1248 } 1249 ls = le + 1 1250 } 1251 if pass == 0 { return tcur } 1252 return text_size + rcur 1253} 1254 1255// ---- assemble full AT&T source -> machine code in `out`. ---- 1256// Returns total image length (text + rodata) or a negative error; writes 1257// the `_start` entry offset to p_entry. text then rodata in one image. 1258func nxasm_x86_assemble(src: *u8, n: i64, out: *u8, out_cap: i64, p_entry: *i64) -> i64 { 1259 let lab_off: *i64 = sys_mmap(ASM_MAX_LABELS * 8) as *i64 1260 let lab_len: *i64 = sys_mmap(ASM_MAX_LABELS * 8) as *i64 1261 let lab_addr: *i64 = sys_mmap(ASM_MAX_LABELS * 8) as *i64 1262 let lab_sec: *i64 = sys_mmap(ASM_MAX_LABELS * 8) as *i64 1263 let op0: *i64 = sys_mmap(72) as *i64 // 9 slots: +OP_INDEX(7)/OP_SCALE(8) for SIB 1264 let op1: *i64 = sys_mmap(72) as *i64 1265 let op2: *i64 = sys_mmap(72) as *i64 1266 let scratch: *u8 = sys_mmap(64) 1267 let posbox: *i64 = sys_mmap(16) as *i64 1268 let n_lab_box: *i64 = sys_mmap(16) as *i64 1269 n_lab_box[0] = 0 1270 1271 // O(1) label-lookup index (see axc_lh_build); pass 0 never finds, so 1272 // an empty table is fine there -- built for real after finalize. 1273 let lh: *i64 = sys_mmap(ASM_LH_SIZE * 8) as *i64 1274 1275 // pass 0: sizes + label (section, local offset) 1276 let text_size: i64 = axc_pass(src, n, out, 0, 0, lab_off, lab_len, lab_addr, lab_sec, n_lab_box, 1277 lh, op0, op1, op2, scratch, posbox) 1278 if text_size < 0 { return text_size } 1279 let n_lab: i64 = n_lab_box[0] 1280 1281 // finalize: rodata label abs = local + text_size ; locate _start entry 1282 var entry: i64 = 0 1283 var k: i64 = 0 1284 while k < n_lab { 1285 if lab_sec[k] == 1 { lab_addr[k] = lab_addr[k] + text_size } 1286 if axc_tok_is(src, lab_off[k], lab_len[k], "_start") == 1 { entry = lab_addr[k] } 1287 k = k + 1 1288 } 1289 axc_lh_build(src, lab_off, lab_len, n_lab, lh) 1290 1291 // pass 1: emit (resolving call/jmp/jcc/lea-rip against finalized labels) 1292 let total: i64 = axc_pass(src, n, out, text_size, 1, lab_off, lab_len, lab_addr, lab_sec, n_lab_box, 1293 lh, op0, op1, op2, scratch, posbox) 1294 if total < 0 { return total } 1295 if total > out_cap { return 0 - 200 } 1296 axc_dbg_report() 1297 p_entry[0] = entry 1298 return total 1299}