kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

asm.c (67514B)


      1 #include "arch/x64/asm.h"
      2 
      3 #include <string.h>
      4 
      5 #include "arch/x64/emit.h"
      6 #include "arch/x64/regs.h"
      7 #include "asm/asm_helpers.h"
      8 #include "core/arena.h"
      9 #include "core/pool.h"
     10 #include "core/slice.h"
     11 #include "core/strbuf.h"
     12 
     13 struct X64Asm {
     14   ArchAsm base;
     15   Compiler* c;
     16 
     17   const AsmConstraint* outs;
     18   Operand* out_ops;
     19   const AsmConstraint* ins;
     20   const Operand* in_ops;
     21   const Sym* clobbers;
     22   u32 nout;
     23   u32 nin;
     24   u32 nclob;
     25 };
     26 
     27 typedef enum X64AsmOperandKind {
     28   X64_ASM_OP_REG,
     29   X64_ASM_OP_XMM,
     30   X64_ASM_OP_IMM,
     31   X64_ASM_OP_MEM,
     32   X64_ASM_OP_IND_REG,
     33 } X64AsmOperandKind;
     34 
     35 typedef struct X64AsmOperand {
     36   u8 kind;
     37   u8 width;
     38   u8 reg;
     39   u8 base;
     40   u8 high8;
     41   u8 seg;
     42   u8 no_base;      /* MEM: segment-prefixed absolute, no base register */
     43   u8 index;        /* MEM SIB: index register (valid when has_index) */
     44   u8 scale;        /* MEM SIB: log2 of scale ∈ {0,1,2,3} → 1/2/4/8 */
     45   u8 has_index;    /* MEM: SIB index present */
     46   u8 rip_relative; /* MEM: bare (%rip)/disp(%rip) form */
     47   u8 has_reloc;    /* MEM: symbolic disp carries a relocation */
     48   u8 pad[3];
     49   i64 imm;
     50   i32 disp;
     51   RelocKind reloc_kind; /* MEM: reloc on the disp32 (PC32 / REX_GOTPCRELX) */
     52   ObjSymId reloc_sym;   /* MEM: relocated symbol */
     53   i64 reloc_off;        /* MEM: user addend on the symbol */
     54 } X64AsmOperand;
     55 
     56 static int x64_reg_from_name(AsmDriver* d, Sym s, u32* reg_out, u32* width_out,
     57                              u32* high8_out) {
     58   Slice sl = pool_slice(asm_driver_pool(d), s);
     59   char buf[16];
     60   u32 reg;
     61   u32 width = 8;
     62   Slice q;
     63   if (!sl.s || sl.len < 2 || sl.len >= sizeof buf) return 0;
     64   memcpy(buf, sl.s, sl.len);
     65   buf[sl.len] = '\0';
     66   q = slice_from_cstr(buf);
     67   if (slice_eq_cstr(q, "ah") || slice_eq_cstr(q, "ch") ||
     68       slice_eq_cstr(q, "dh") || slice_eq_cstr(q, "bh")) {
     69     static const u32 high_map[4] = {4u, 5u, 6u, 7u};
     70     const char* names = "acdb";
     71     for (u32 i = 0; i < 4u; ++i) {
     72       if (buf[0] == names[i]) {
     73         if (reg_out) *reg_out = high_map[i];
     74         if (width_out) *width_out = 1;
     75         if (high8_out) *high8_out = 1;
     76         return 1;
     77       }
     78     }
     79   }
     80   if (x64_register_hw_index(buf, &reg) != 0) return 0;
     81   if (reg > 15u) return 0;
     82   if (slice_eq_cstr(q, "al") || slice_eq_cstr(q, "cl") ||
     83       slice_eq_cstr(q, "dl") || slice_eq_cstr(q, "bl") ||
     84       slice_eq_cstr(q, "spl") || slice_eq_cstr(q, "bpl") ||
     85       slice_eq_cstr(q, "sil") || slice_eq_cstr(q, "dil") ||
     86       buf[sl.len - 1] == 'b') {
     87     width = 1;
     88   } else if (slice_eq_cstr(q, "ax") || slice_eq_cstr(q, "cx") ||
     89              slice_eq_cstr(q, "dx") || slice_eq_cstr(q, "bx") ||
     90              slice_eq_cstr(q, "sp") || slice_eq_cstr(q, "bp") ||
     91              slice_eq_cstr(q, "si") || slice_eq_cstr(q, "di") ||
     92              buf[sl.len - 1] == 'w') {
     93     width = 2;
     94   } else if (buf[sl.len - 1] == 'd' || buf[0] == 'e') {
     95     width = 4;
     96   }
     97   if (reg_out) *reg_out = reg;
     98   if (width_out) *width_out = width;
     99   if (high8_out) *high8_out = 0;
    100   return 1;
    101 }
    102 
    103 static int x64_xmm_from_name(AsmDriver* d, Sym s, u32* reg_out) {
    104   Slice sl = pool_slice(asm_driver_pool(d), s);
    105   const char* p = sl.s;
    106   size_t n = sl.len;
    107   u32 reg = 0;
    108   if (!p || n < 4 || n > 5) return 0;
    109   if (p[0] != 'x' || p[1] != 'm' || p[2] != 'm') return 0;
    110   for (size_t i = 3; i < n; ++i) {
    111     if (p[i] < '0' || p[i] > '9') return 0;
    112     reg = reg * 10u + (u32)(p[i] - '0');
    113   }
    114   if (reg > 15u) return 0;
    115   if (reg_out) *reg_out = reg;
    116   return 1;
    117 }
    118 
    119 static int x64_segment_prefix_from_name(AsmDriver* d, Sym s, u8* prefix_out) {
    120   Slice sl = pool_slice(asm_driver_pool(d), s);
    121   const char* p = sl.s;
    122   size_t n = sl.len;
    123   if (!p || n != 2) return 0;
    124   if (p[0] == 'f' && p[1] == 's') {
    125     if (prefix_out) *prefix_out = 0x64;
    126     return 1;
    127   }
    128   if (p[0] == 'g' && p[1] == 's') {
    129     if (prefix_out) *prefix_out = 0x65;
    130     return 1;
    131   }
    132   return 0;
    133 }
    134 
    135 static void expect_comma(AsmDriver* d);
    136 
    137 static u32 parse_reg(AsmDriver* d, u32* width_out, u32* high8_out) {
    138   AsmTok t;
    139   u32 reg;
    140   if (!asm_driver_eat_punct(d, '%'))
    141     asm_driver_panic(d, "x64 asm: expected register");
    142   t = asm_driver_next(d);
    143   if (t.kind != ASM_TOK_IDENT ||
    144       !x64_reg_from_name(d, t.v.ident, &reg, width_out, high8_out)) {
    145     asm_driver_panic(d, "x64 asm: bad register");
    146   }
    147   return reg;
    148 }
    149 
    150 /* True if the symbol names the instruction pointer ("rip"). */
    151 static int x64_ident_is_rip(AsmDriver* d, Sym s) {
    152   Slice sl = pool_slice(asm_driver_pool(d), s);
    153   return sl.s && sl.len == 3 && sl.s[0] == 'r' && sl.s[1] == 'i' &&
    154          sl.s[2] == 'p';
    155 }
    156 
    157 /* Convert a SIB scale literal (1/2/4/8) to its log2 (0/1/2/3). */
    158 static u32 x64_scale_to_log2(AsmDriver* d, i64 scale) {
    159   switch (scale) {
    160     case 1:
    161       return 0u;
    162     case 2:
    163       return 1u;
    164     case 4:
    165       return 2u;
    166     case 8:
    167       return 3u;
    168     default:
    169       asm_driver_panic(d, "x64 asm: memory scale must be 1, 2, 4, or 8");
    170   }
    171 }
    172 
    173 /* Parse the body of a memory operand once the leading '(' has been
    174  * consumed: '%base[,%index,scale])', '%rip)', or ',%index,scale)'.
    175  * Fills base/index/scale/has_index/rip_relative on `op` and eats the
    176  * closing ')'. */
    177 static void parse_mem_paren_body(AsmDriver* d, X64AsmOperand* op) {
    178   AsmTok t = asm_driver_peek(d);
    179   if (asm_driver_tok_is_punct(t, '%')) {
    180     /* Peek the register name to detect the RIP-relative form. */
    181     AsmTok ident;
    182     (void)asm_driver_next(d);
    183     ident = asm_driver_next(d);
    184     if (ident.kind != ASM_TOK_IDENT)
    185       asm_driver_panic(d, "x64 asm: bad register");
    186     if (x64_ident_is_rip(d, ident.v.ident)) {
    187       op->rip_relative = 1;
    188       asm_driver_expect_punct(d, ')', "')' in x64 memory operand");
    189       return;
    190     }
    191     {
    192       u32 reg = 0;
    193       if (!x64_reg_from_name(d, ident.v.ident, &reg, NULL, NULL))
    194         asm_driver_panic(d, "x64 asm: bad register");
    195       op->base = (u8)reg;
    196     }
    197     /* Optional ',%index,scale'. */
    198     if (asm_driver_eat_comma(d)) {
    199       op->index = (u8)parse_reg(d, NULL, NULL);
    200       op->has_index = 1;
    201       expect_comma(d);
    202       op->scale = (u8)x64_scale_to_log2(d, asm_driver_parse_const(d));
    203     }
    204     asm_driver_expect_punct(d, ')', "')' in x64 memory operand");
    205     return;
    206   }
    207   /* Index-only form: '(,%index,scale)' — base omitted. */
    208   if (asm_driver_eat_comma(d)) {
    209     op->no_base = 1;
    210     op->index = (u8)parse_reg(d, NULL, NULL);
    211     op->has_index = 1;
    212     expect_comma(d);
    213     op->scale = (u8)x64_scale_to_log2(d, asm_driver_parse_const(d));
    214     asm_driver_expect_punct(d, ')', "')' in x64 memory operand");
    215     return;
    216   }
    217   asm_driver_panic(d, "x64 asm: expected register in memory operand");
    218 }
    219 
    220 /* Consume an optional `@MOD` relocation suffix after a symbol and return the
    221  * RelocKind it selects, or `dflt` when no suffix is present. */
    222 static RelocKind x64_parse_reloc_suffix(AsmDriver* d, RelocKind dflt) {
    223   if (!asm_driver_tok_is_punct(asm_driver_peek(d), '@')) return dflt;
    224   (void)asm_driver_next(d); /* '@' */
    225   AsmTok n = asm_driver_next(d);
    226   if (n.kind != ASM_TOK_IDENT)
    227     asm_driver_panic(d, "x64 asm: expected relocation name after '@'");
    228   Slice s = pool_slice(asm_driver_pool(d), n.v.ident);
    229   if (slice_eq_cstr(s, "PLT")) return R_X64_PLT32;
    230   if (slice_eq_cstr(s, "GOTPCREL")) return R_X64_REX_GOTPCRELX;
    231   if (slice_eq_cstr(s, "GOTPCRELX")) return R_X64_GOTPCRELX;
    232   asm_driver_panic(d, "x64 asm: unsupported relocation suffix");
    233 }
    234 
    235 static X64AsmOperand parse_operand(AsmDriver* d) {
    236   X64AsmOperand op;
    237   AsmTok t;
    238   memset(&op, 0, sizeof op);
    239   t = asm_driver_peek(d);
    240   if (asm_driver_eat_punct(d, '*')) {
    241     op.kind = X64_ASM_OP_IND_REG;
    242     op.reg = (u8)parse_reg(d, NULL, NULL);
    243     return op;
    244   }
    245   if (asm_driver_eat_punct(d, '$')) {
    246     op.kind = X64_ASM_OP_IMM;
    247     op.imm = asm_driver_parse_const(d);
    248     return op;
    249   }
    250   if (asm_driver_tok_is_punct(t, '%')) {
    251     u32 width = 8;
    252     u32 high8 = 0;
    253     AsmTok ident;
    254     (void)asm_driver_next(d);
    255     ident = asm_driver_next(d);
    256     if (ident.kind != ASM_TOK_IDENT)
    257       asm_driver_panic(d, "x64 asm: bad register");
    258     if (x64_segment_prefix_from_name(d, ident.v.ident, &op.seg)) {
    259       asm_driver_expect_punct(d, ':', "':' after x64 segment register");
    260       op.kind = X64_ASM_OP_MEM;
    261       if (!asm_driver_tok_is_punct(asm_driver_peek(d), '('))
    262         op.disp = (i32)asm_driver_parse_const(d);
    263       if (asm_driver_eat_punct(d, '(')) {
    264         op.base = (u8)parse_reg(d, NULL, NULL);
    265         asm_driver_expect_punct(d, ')', "')' in x64 memory operand");
    266       } else {
    267         op.no_base = 1;
    268       }
    269       return op;
    270     }
    271     if (x64_xmm_from_name(d, ident.v.ident, &width)) {
    272       op.kind = X64_ASM_OP_XMM;
    273       op.reg = (u8)width;
    274       op.width = 16;
    275       return op;
    276     }
    277     {
    278       u32 reg = 0;
    279       if (!x64_reg_from_name(d, ident.v.ident, &reg, &width, &high8))
    280         asm_driver_panic(d, "x64 asm: bad register");
    281       op.kind = X64_ASM_OP_REG;
    282       op.reg = (u8)reg;
    283     }
    284     op.width = (u8)width;
    285     op.high8 = (u8)high8;
    286     return op;
    287   }
    288   op.kind = X64_ASM_OP_MEM;
    289   op.disp = 0;
    290   if (!asm_driver_tok_is_punct(t, '(')) {
    291     /* A symbolic displacement (`sym(%rip)`, `sym@GOTPCREL(%rip)`) becomes a
    292      * relocation; a numeric displacement stays literal. */
    293     if (asm_driver_peek(d).kind == ASM_TOK_IDENT) {
    294       asm_driver_parse_sym_expr(d, &op.reloc_sym, &op.reloc_off);
    295       op.reloc_kind = x64_parse_reloc_suffix(d, R_PC32);
    296       op.has_reloc = 1;
    297     } else {
    298       op.disp = (i32)asm_driver_parse_const(d);
    299     }
    300   }
    301   asm_driver_expect_punct(d, '(', "'(' in x64 memory operand");
    302   parse_mem_paren_body(d, &op);
    303   if (op.has_reloc && !op.rip_relative)
    304     asm_driver_panic(d,
    305                      "x64 asm: symbolic memory displacement requires (%rip)");
    306   return op;
    307 }
    308 
    309 /* Emit the relocation a symbolic `(%rip)` memory operand carries, if any. The
    310  * disp32 field is the last 4 bytes of the instruction except for an immediate
    311  * store, where `trailing` immediate bytes follow it. R_X86_64_PC32-style
    312  * relocs use addend (off - 4 - trailing) so S+A-P yields the rip-relative
    313  * displacement to the end of the instruction. */
    314 static void x64_emit_mem_reloc(AsmDriver* d, MCEmitter* mc,
    315                                const X64AsmOperand* m, u32 trailing) {
    316   if (!m->has_reloc) return;
    317   u32 disp_pos = mc_pos(mc) - 4u - trailing;
    318   mc_emit_reloc_at(mc, asm_driver_cur_section(d), disp_pos, m->reloc_kind,
    319                     m->reloc_sym, m->reloc_off - 4 - (i64)trailing, 1, 0);
    320 }
    321 
    322 static u32 x64_pack_rex_mem_operand(u8* out, int w, u32 reg,
    323                                     X64AsmOperand mem) {
    324   /* RIP-relative carries no base/index registers (rm=101, no SIB). */
    325   if (mem.rip_relative) return x64_pack_rex(out, w, reg, 0, 0u);
    326   /* SIB forms supply REX.X from the index register (and REX.B from base
    327    * unless the base is omitted in the index-only form). */
    328   if (mem.has_index)
    329     return x64_pack_rex(out, w, reg, mem.index, mem.no_base ? 0u : mem.base);
    330   return x64_pack_rex(out, w, reg, 0, mem.no_base ? 0u : mem.base);
    331 }
    332 
    333 static u32 x64_pack_mem_operand(u8* out, u32 reg, X64AsmOperand mem) {
    334   if (mem.rip_relative) return x64_pack_mem_rip(out, reg, mem.disp);
    335   if (mem.has_index) {
    336     /* Index-only form (no base): mod=00 with SIB.base=101 → disp32. */
    337     if (mem.no_base) {
    338       out[0] = x64_modrm(0u, reg, X64_MODRM_RM_SIB);
    339       out[1] = x64_sib(mem.scale, mem.index, X64_SIB_NO_BASE);
    340       return 2u + x64_put_u32le(out + 2, (u32)mem.disp);
    341     }
    342     return x64_pack_mem_sib(out, reg, mem.base, mem.index, mem.scale, mem.disp);
    343   }
    344   if (mem.no_base) {
    345     out[0] = x64_modrm(0u, reg, X64_MODRM_RM_SIB);
    346     out[1] = x64_sib(0u, X64_SIB_NO_INDEX, X64_SIB_NO_BASE);
    347     return 2u + x64_put_u32le(out + 2, (u32)mem.disp);
    348   }
    349   return x64_pack_mem(out, reg, mem.base, mem.disp);
    350 }
    351 
    352 /* reg ← mem with an explicit single-byte opcode (e.g. 0x8B MOV, 0x8D LEA).
    353  * Routes the full memory-operand variety (plain / SIB / RIP / segment)
    354  * through the shared pack helpers. */
    355 static void emit_reg_mem_operand(AsmDriver* d, MCEmitter* mc, u32 size, u8 opc,
    356                                  u32 dst, X64AsmOperand src) {
    357   u8 buf[16];
    358   u32 n = 0;
    359   if (size == 2u) buf[n++] = X64_OPSIZE_PFX;
    360   if (src.seg) buf[n++] = src.seg;
    361   n += x64_pack_rex_mem_operand(buf + n, size == 8u, dst, src);
    362   buf[n++] = opc;
    363   n += x64_pack_mem_operand(buf + n, dst, src);
    364   mc_emit_bytes(mc, buf, n);
    365   x64_emit_mem_reloc(d, mc, &src, 0);
    366 }
    367 
    368 static void emit_mov_load_operand(AsmDriver* d, MCEmitter* mc, u32 size,
    369                                   u32 dst, X64AsmOperand src) {
    370   emit_reg_mem_operand(d, mc, size, X64_OPC_MOV_R_RM, dst, src);
    371 }
    372 
    373 /* reg → mem store with an explicit reg-to-r/m opcode.  Used by MOV
    374  * (0x89/0x88) and the ALU /r stores (ADD 0x01, OR 0x09, AND 0x21,
    375  * SUB 0x29, XOR 0x31, CMP 0x39).  The register operand occupies the
    376  * ModR/M reg field; the memory operand the r/m field. */
    377 static void emit_reg_store_operand(AsmDriver* d, MCEmitter* mc, u32 size,
    378                                    u8 opc, u32 src, X64AsmOperand dst,
    379                                    int force_rex) {
    380   u8 buf[16];
    381   u32 n = 0;
    382   if (size == 2u) buf[n++] = X64_OPSIZE_PFX;
    383   if (dst.seg) buf[n++] = dst.seg;
    384   if (force_rex)
    385     n += x64_pack_rex_force(buf + n, size == 8u, src, 0,
    386                             dst.no_base ? 0u : dst.base);
    387   else
    388     n += x64_pack_rex_mem_operand(buf + n, size == 8u, src, dst);
    389   buf[n++] = opc;
    390   n += x64_pack_mem_operand(buf + n, src, dst);
    391   mc_emit_bytes(mc, buf, n);
    392   x64_emit_mem_reloc(d, mc, &dst, 0);
    393 }
    394 
    395 static void emit_mov_store_operand(AsmDriver* d, MCEmitter* mc, u32 size,
    396                                    u32 src, X64AsmOperand dst, int force_rex) {
    397   emit_reg_store_operand(d, mc, size,
    398                          size == 1u ? X64_OPC_MOV_RM_R8 : X64_OPC_MOV_RM_R, src,
    399                          dst, force_rex);
    400 }
    401 
    402 /* imm → mem store via a group-1 /digit opcode (group-1 ALU 0x80/0x81/0x83,
    403  * or MOV C6/C7).  `opc8`/`opc32` select the 8-bit-immediate vs
    404  * 32-bit-immediate (sign-extended) opcode; pass equal values when the
    405  * encoding has no imm8 short form (e.g. MOV).  `imm8` forces the short
    406  * form when the immediate fits. */
    407 static void emit_rm_imm_store_operand(AsmDriver* d, MCEmitter* mc, u32 size,
    408                                       u8 opc8, u8 opc32, u32 sub,
    409                                       X64AsmOperand dst, i64 imm,
    410                                       int allow_i8) {
    411   u8 buf[16];
    412   u32 n = 0;
    413   int use_i8 = allow_i8 && imm_fits_i8(imm);
    414   if (!use_i8 && !imm_fits_i32(imm) && size != 1u)
    415     asm_driver_panic(d, "x64 asm: immediate out of range");
    416   if (size == 2u) buf[n++] = X64_OPSIZE_PFX;
    417   if (dst.seg) buf[n++] = dst.seg;
    418   n += x64_pack_rex_mem_operand(buf + n, size == 8u, 0, dst);
    419   buf[n++] = use_i8 ? opc8 : opc32;
    420   n += x64_pack_mem_operand(buf + n, sub, dst);
    421   u32 trailing;
    422   if (size == 1u) {
    423     buf[n++] = (u8)imm;
    424     trailing = 1u;
    425   } else if (use_i8) {
    426     buf[n++] = (u8)(i8)imm;
    427     trailing = 1u;
    428   } else if (size == 2u) {
    429     /* 16-bit operand size: a 2-byte immediate (under the 0x66 prefix). */
    430     u16 v = (u16)imm;
    431     buf[n++] = (u8)v;
    432     buf[n++] = (u8)(v >> 8);
    433     trailing = 2u;
    434   } else {
    435     n += x64_put_u32le(buf + n, (u32)(i32)imm);
    436     trailing = 4u;
    437   }
    438   mc_emit_bytes(mc, buf, n);
    439   x64_emit_mem_reloc(d, mc, &dst, trailing);
    440 }
    441 
    442 static void expect_comma(AsmDriver* d) {
    443   if (!asm_driver_eat_comma(d)) asm_driver_panic(d, "x64 asm: expected ','");
    444 }
    445 
    446 static void emit_indirect_branch(MCEmitter* mc, u32 sub, u32 reg) {
    447   u8 op = 0xff;
    448   emit_rex(mc, 0, 0, 0, reg);
    449   mc_emit_bytes(mc, &op, 1);
    450   {
    451     u8 mr = modrm(3u, sub, reg);
    452     mc_emit_bytes(mc, &mr, 1);
    453   }
    454 }
    455 
    456 static void emit_packed(MCEmitter* mc, const u8* bytes, u32 n) {
    457   mc_emit_bytes(mc, bytes, n);
    458 }
    459 
    460 static int byte_reg_needs_rex(const X64AsmOperand* op) {
    461   return op && !op->high8 && op->reg >= 4u;
    462 }
    463 
    464 static void reject_high8_with_rex(AsmDriver* d, const X64AsmOperand* a,
    465                                   const X64AsmOperand* b) {
    466   if ((a && a->high8 && byte_reg_needs_rex(b)) ||
    467       (b && b->high8 && byte_reg_needs_rex(a))) {
    468     asm_driver_panic(d, "x64 asm: high-byte register cannot use REX");
    469   }
    470 }
    471 
    472 static __attribute__((unused)) void emit_movb_rr_operand(AsmDriver* d,
    473                                                          MCEmitter* mc,
    474                                                          X64AsmOperand dst,
    475                                                          X64AsmOperand src) {
    476   u8 ob = 0x88;
    477   reject_high8_with_rex(d, &dst, &src);
    478   if (byte_reg_needs_rex(&dst) || byte_reg_needs_rex(&src))
    479     emit_rex_force(mc, 0, src.reg, 0, dst.reg);
    480   else
    481     emit_rex(mc, 0, src.reg, 0, dst.reg);
    482   mc_emit_bytes(mc, &ob, 1);
    483   {
    484     u8 mr = modrm(3u, src.reg, dst.reg);
    485     mc_emit_bytes(mc, &mr, 1);
    486   }
    487 }
    488 
    489 static __attribute__((unused)) void emit_movb_store_operand(AsmDriver* d,
    490                                                             MCEmitter* mc,
    491                                                             X64AsmOperand src,
    492                                                             X64AsmOperand dst) {
    493   if (src.high8) {
    494     u8 ob = 0x88;
    495     if (dst.no_base || dst.base >= 8u)
    496       asm_driver_panic(d, "x64 asm: high-byte register cannot use REX");
    497     if (dst.seg) mc_emit_bytes(mc, &dst.seg, 1);
    498     mc_emit_bytes(mc, &ob, 1);
    499     emit_mem_operand(mc, src.reg, dst.base, dst.disp);
    500     return;
    501   }
    502   emit_mov_store_operand(d, mc, 1, src.reg, dst, 1);
    503 }
    504 
    505 static __attribute__((unused)) void emit_rm_imm(AsmDriver* d, MCEmitter* mc,
    506                                                 u32 width, u8 opc, u32 sub,
    507                                                 X64AsmOperand dst, i32 imm,
    508                                                 int imm32) {
    509   u8 buf[16];
    510   u32 n = 0;
    511   if (dst.kind != X64_ASM_OP_REG && dst.kind != X64_ASM_OP_MEM)
    512     asm_driver_panic(d, "x64 asm: expected register or memory destination");
    513   if (width == 2u) buf[n++] = X64_OPSIZE_PFX;
    514   if (dst.kind == X64_ASM_OP_REG) {
    515     n += x64_pack_rex(buf + n, width == 8u, 0, 0, dst.reg);
    516     buf[n++] = opc;
    517     buf[n++] = x64_modrm(3u, sub, dst.reg);
    518   } else {
    519     n += x64_pack_rex(buf + n, width == 8u, 0, 0, dst.base);
    520     buf[n++] = opc;
    521     n += x64_pack_mem(buf + n, sub, dst.base, dst.disp);
    522   }
    523   if (imm32)
    524     n += x64_put_u32le(buf + n, (u32)imm);
    525   else
    526     buf[n++] = (u8)(i8)imm;
    527   emit_packed(mc, buf, n);
    528 }
    529 
    530 static __attribute__((unused)) void emit_rm_op(AsmDriver* d, MCEmitter* mc,
    531                                                u32 width, u8 opc, u32 sub,
    532                                                X64AsmOperand dst) {
    533   u8 buf[16];
    534   u32 n = 0;
    535   if (dst.kind != X64_ASM_OP_REG && dst.kind != X64_ASM_OP_MEM)
    536     asm_driver_panic(d, "x64 asm: expected register or memory operand");
    537   if (width == 2u) buf[n++] = X64_OPSIZE_PFX;
    538   if (dst.kind == X64_ASM_OP_REG) {
    539     n += x64_pack_rex(buf + n, width == 8u, 0, 0, dst.reg);
    540     buf[n++] = opc;
    541     buf[n++] = x64_modrm(3u, sub, dst.reg);
    542   } else {
    543     n += x64_pack_rex(buf + n, width == 8u, 0, 0, dst.base);
    544     buf[n++] = opc;
    545     n += x64_pack_mem(buf + n, sub, dst.base, dst.disp);
    546   }
    547   emit_packed(mc, buf, n);
    548 }
    549 
    550 static __attribute__((unused)) void emit_reg_rm_twobyte(
    551     AsmDriver* d, MCEmitter* mc, u32 width, u8 opcode2, u32 dst,
    552     X64AsmOperand src, int force_rex, u8 prefix) {
    553   u8 buf[16];
    554   u32 n = 0;
    555   if (src.kind != X64_ASM_OP_REG && src.kind != X64_ASM_OP_MEM)
    556     asm_driver_panic(d, "x64 asm: expected register or memory source");
    557   if (prefix) buf[n++] = prefix;
    558   if (src.kind == X64_ASM_OP_REG) {
    559     if (force_rex)
    560       n += x64_pack_rex_force(buf + n, width == 8u, dst, 0, src.reg);
    561     else
    562       n += x64_pack_rex(buf + n, width == 8u, dst, 0, src.reg);
    563     buf[n++] = X64_OPC_TWOBYTE;
    564     buf[n++] = opcode2;
    565     buf[n++] = x64_modrm(3u, dst, src.reg);
    566   } else {
    567     /* Route the full memory-operand variety (plain / SIB-indexed / RIP /
    568      * segment) through the shared pack helpers so a SIB index register is
    569      * preserved (e.g. `movzbl (%rcx,%rsi,1), %edx`). */
    570     if (src.seg) buf[n++] = src.seg;
    571     n += x64_pack_rex_mem_operand(buf + n, width == 8u, dst, src);
    572     buf[n++] = X64_OPC_TWOBYTE;
    573     buf[n++] = opcode2;
    574     n += x64_pack_mem_operand(buf + n, dst, src);
    575   }
    576   emit_packed(mc, buf, n);
    577   if (src.kind == X64_ASM_OP_MEM) x64_emit_mem_reloc(d, mc, &src, 0);
    578 }
    579 
    580 /* ====================================================================
    581  * Descriptor-driven mnemonic dispatch.
    582  *
    583  * The disassembler's `x64_insn_table` (src/arch/x64/isa.c) lists every
    584  * encoding kit emits with its X64Format.  We reuse the SAME table for
    585  * the assembler: linear-scan to find the row whose mnemonic matches the
    586  * user's AT&T spelling (after stripping the size suffix b/w/l/q), then
    587  * dispatch to a per-format parser that consumes the operands and calls
    588  * the existing `emit_*` helpers in emit.c.
    589  *
    590  * The width comes from the suffix (or the row's width flags); per-format
    591  * parsers receive it via a small X64ParseCtx so they can pick the right
    592  * emit overload (e.g., MOV r,r at 32 vs 64 bits).
    593  *
    594  * Note: a single mnemonic ("mov") has multiple table rows for different
    595  * formats (MOV_RI, ALU_RR, MOV_RM_LOAD).  We return the FIRST row that
    596  * matches the mnemonic + width filter; per-format parsers that need a
    597  * different row (e.g., MOV imm→reg uses MOV_RI but our scan may have
    598  * returned ALU_RR first) fall through to operand-kind dispatch and
    599  * select the correct emit helper directly.  Phase 1 of this refactor
    600  * only exercises the mnemonics asm.c handled before; richer disambiguation
    601  * lands in follow-ups. */
    602 
    603 #define X64_SFX_NONE 0u
    604 #define X64_SFX_B 1u
    605 #define X64_SFX_W 2u
    606 #define X64_SFX_L 4u
    607 #define X64_SFX_Q 8u
    608 
    609 typedef struct X64MnInfo {
    610   char base[16]; /* stripped mnemonic (table-spelling) */
    611   u32 base_len;
    612   u32 width; /* X64_SFX_* — 0 if mnemonic carries no size letter */
    613   u32 cc;    /* condition nibble for jcc/cmovcc/setcc, or 16 if none */
    614 } X64MnInfo;
    615 
    616 /* Parse the user-supplied mnemonic into (root, width, cc).  Handles:
    617  *   - trailing size letter (b/w/l/q) when the table mnemonic has none
    618  *   - jXX → ("j", cc)
    619  *   - cmovXX[q|l|w|b] → ("cmov", cc, width)
    620  *   - setXX → ("set", cc)
    621  *   - exact-match mnemonics carried verbatim (movslq, movzbl, ud2, ...) */
    622 static int parse_mnemonic(const char* s, size_t n, X64MnInfo* out) {
    623   static const struct {
    624     const char* name;
    625     u8 cc;
    626   } kCC[] = {
    627       /* Two-letter codes first so e.g. "ne" beats "n" if we ever add it. */
    628       {"ae", X64_CC_AE}, {"be", X64_CC_BE}, {"ge", X64_CC_GE},
    629       {"le", X64_CC_LE}, {"ne", X64_CC_NE}, {"no", X64_CC_NO},
    630       {"np", X64_CC_NP}, {"ns", X64_CC_NS}, {"a", X64_CC_A},
    631       {"b", X64_CC_B},   {"e", X64_CC_E},   {"g", X64_CC_G},
    632       {"l", X64_CC_L},   {"o", X64_CC_O},   {"p", X64_CC_P},
    633       {"s", X64_CC_S},
    634   };
    635   out->base_len = 0;
    636   out->width = X64_SFX_NONE;
    637   out->cc = 16u;
    638   if (n == 0 || n >= sizeof out->base) return 0;
    639   memcpy(out->base, s, n);
    640   out->base[n] = '\0';
    641 
    642   /* Exact-match mnemonics that carry their own width letters or are
    643    * already canonical table spellings. */
    644   if (n >= 6 && memcmp(s, "movslq", 6) == 0) {
    645     memcpy(out->base, "movslq", 6);
    646     out->base_len = 6;
    647     out->width = X64_SFX_Q;
    648     return 1;
    649   }
    650   if (n >= 6 && (memcmp(s, "movzbl", 6) == 0 || memcmp(s, "movzwl", 6) == 0 ||
    651                  memcmp(s, "movsbl", 6) == 0 || memcmp(s, "movswl", 6) == 0 ||
    652                  memcmp(s, "movzbq", 6) == 0 || memcmp(s, "movzwq", 6) == 0 ||
    653                  memcmp(s, "movsbq", 6) == 0 || memcmp(s, "movswq", 6) == 0)) {
    654     memcpy(out->base, s, 6);
    655     out->base_len = 6;
    656     return 1;
    657   }
    658   if (n == 3 && memcmp(s, "ud2", 3) == 0) {
    659     out->base_len = 3;
    660     return 1;
    661   }
    662   if (n == 3 && memcmp(s, "nop", 3) == 0) {
    663     out->base_len = 3;
    664     return 1;
    665   }
    666   if (n == 3 && memcmp(s, "ret", 3) == 0) {
    667     out->base_len = 3;
    668     return 1;
    669   }
    670   /* "syscall" ends in 'l' — return early so the generic size-suffix
    671    * stripper below does not mistake it for a movl-style width letter. */
    672   if (n == 7 && memcmp(s, "syscall", 7) == 0) {
    673     out->base_len = 7;
    674     return 1;
    675   }
    676 
    677   /* Indirect-branch spellings carry an explicit 'q' suffix that must be
    678    * preserved — the BR_RM rows in the table are keyed on "jmpq"/"callq". */
    679   if (n == 4 && memcmp(s, "call", 4) == 0) {
    680     memcpy(out->base, "callq", 5);
    681     out->base[5] = '\0';
    682     out->base_len = 5;
    683     out->width = X64_SFX_Q;
    684     return 1;
    685   }
    686   if (n == 4 && memcmp(s, "jmpq", 4) == 0) {
    687     out->base_len = 4;
    688     out->width = X64_SFX_Q;
    689     return 1;
    690   }
    691   if (n == 5 && memcmp(s, "callq", 5) == 0) {
    692     out->base_len = 5;
    693     out->width = X64_SFX_Q;
    694     return 1;
    695   }
    696 
    697   /* CMOVcc: cmov<cc>[suffix].  Strip optional trailing q/l/w/b first. */
    698   if (n >= 5 && memcmp(s, "cmov", 4) == 0) {
    699     size_t after = 4;
    700     size_t tail = n;
    701     char last = s[n - 1];
    702     if (last == 'b' || last == 'w' || last == 'l' || last == 'q') {
    703       out->width = (last == 'b')   ? X64_SFX_B
    704                    : (last == 'w') ? X64_SFX_W
    705                    : (last == 'l') ? X64_SFX_L
    706                                    : X64_SFX_Q;
    707       tail = n - 1;
    708     }
    709     if (tail > after) {
    710       Slice cc = {{s + after}, tail - after};
    711       for (size_t i = 0; i < sizeof kCC / sizeof kCC[0]; ++i) {
    712         if (slice_eq_cstr(cc, kCC[i].name)) {
    713           out->cc = kCC[i].cc;
    714           memcpy(out->base, "cmov", 4);
    715           out->base[4] = '\0';
    716           out->base_len = 4;
    717           return 1;
    718         }
    719       }
    720     }
    721   }
    722 
    723   /* SETcc: set<cc>. */
    724   if (n > 3 && memcmp(s, "set", 3) == 0) {
    725     Slice cc = {{s + 3}, n - 3};
    726     for (size_t i = 0; i < sizeof kCC / sizeof kCC[0]; ++i) {
    727       if (slice_eq_cstr(cc, kCC[i].name)) {
    728         out->cc = kCC[i].cc;
    729         memcpy(out->base, "set", 3);
    730         out->base[3] = '\0';
    731         out->base_len = 3;
    732         return 1;
    733       }
    734     }
    735   }
    736 
    737   /* Jcc: j<cc> — but NOT "jmp" / "jmpq" (handled above). */
    738   if (n > 1 && s[0] == 'j' && !(n >= 3 && s[1] == 'm' && s[2] == 'p')) {
    739     Slice cc = {{s + 1}, n - 1};
    740     for (size_t i = 0; i < sizeof kCC / sizeof kCC[0]; ++i) {
    741       if (slice_eq_cstr(cc, kCC[i].name)) {
    742         out->cc = kCC[i].cc;
    743         out->base[0] = 'j';
    744         out->base[1] = '\0';
    745         out->base_len = 1;
    746         return 1;
    747       }
    748     }
    749   }
    750 
    751   /* Generic: strip trailing size letter b/w/l/q. */
    752   {
    753     char last = s[n - 1];
    754     if (last == 'b' || last == 'w' || last == 'l' || last == 'q') {
    755       out->width = (last == 'b')   ? X64_SFX_B
    756                    : (last == 'w') ? X64_SFX_W
    757                    : (last == 'l') ? X64_SFX_L
    758                                    : X64_SFX_Q;
    759       out->base_len = (u32)(n - 1);
    760       out->base[out->base_len] = '\0';
    761       return 1;
    762     }
    763   }
    764 
    765   out->base_len = (u32)n;
    766   return 1;
    767 }
    768 
    769 /* Width implied by a descriptor row, given the mnemonic's parsed width. */
    770 static u32 row_implied_width(const X64InsnDesc* d) {
    771   if (d->flags & X64_ASMFL_FORCE_W64) return 8u;
    772   if (d->flags & X64_ASMFL_BYTE) return 1u;
    773   if (d->flags & X64_ASMFL_W16) return 2u;
    774   if (d->flags & X64_ASMFL_W_FROM_REX) return 0u; /* any */
    775   if (d->leg_pfx == X64_PFX_66) return 2u;
    776   return 0u; /* any */
    777 }
    778 
    779 /* Linear scan for the first table row whose mnemonic matches `info->base`
    780  * AND whose width filter is compatible.  Returns NULL on miss. */
    781 static const X64InsnDesc* find_mnemonic_row(const X64MnInfo* info) {
    782   u32 want_w = info->width; /* 0 = any */
    783   Slice base = {{info->base}, info->base_len};
    784   for (u32 i = 0; i < x64_insn_table_n; ++i) {
    785     const X64InsnDesc* d = &x64_insn_table[i];
    786     if (!slice_eq(d->mnemonic, base)) continue;
    787     if (want_w != 0) {
    788       u32 rw = row_implied_width(d);
    789       if (rw != 0 && rw != want_w) continue;
    790     }
    791     return d;
    792   }
    793   return NULL;
    794 }
    795 
    796 /* Per-format parse context. */
    797 typedef struct X64ParseCtx {
    798   AsmDriver* d;
    799   MCEmitter* mc;
    800   const X64InsnDesc* desc;
    801   u32 width; /* 1/2/4/8 — derived from suffix or row */
    802   u32 cc;    /* condition nibble (jcc/cmovcc/setcc); 16 if unused */
    803 } X64ParseCtx;
    804 
    805 /* w-bit for emit_rex / emit_alu_rr / emit_mov_rr etc. */
    806 static int width_to_w(u32 w) { return w == 8u ? 1 : 0; }
    807 
    808 /* ---- per-format parsers ---- */
    809 
    810 static void parse_nullary(X64ParseCtx* p) {
    811   /* nop / ret / ud2 / leave / cltd / cqto. */
    812   u8 buf[4];
    813   u32 n = 0;
    814   if (p->desc->leg_pfx) buf[n++] = p->desc->leg_pfx;
    815   if (p->desc->rex_w_req == X64_W_REQ_1) buf[n++] = X64_REX_BASE | X64_REX_W;
    816   for (u32 i = 0; i < p->desc->opc_len; ++i) buf[n++] = p->desc->opc[i];
    817   if (p->desc->opc_len >= 1u) {
    818     mc_emit_bytes(p->mc, buf, n);
    819     return;
    820   }
    821   asm_driver_panic(p->d, "x64 asm: nullary form not implemented");
    822 }
    823 
    824 static void parse_br_rm(X64ParseCtx* p) {
    825   /* jmpq *%reg or callq *%reg.  /digit picks sub (2 = call, 4 = jmp). */
    826   X64AsmOperand op = parse_operand(p->d);
    827   if (op.kind != X64_ASM_OP_IND_REG)
    828     asm_driver_panic(p->d, "x64 asm: indirect branch form");
    829   emit_indirect_branch(p->mc, p->desc->modrm_reg, op.reg);
    830 }
    831 
    832 /* Look up the ALU_RM_IMM8 row for a given mnemonic root; the /digit
    833  * picks the operation (0=add, 1=or, 4=and, 5=sub, 6=xor, 7=cmp). */
    834 static const X64InsnDesc* find_alu_imm_row(Slice root) {
    835   for (u32 i = 0; i < x64_insn_table_n; ++i) {
    836     const X64InsnDesc* d = &x64_insn_table[i];
    837     if (d->fmt != X64_FMT_ALU_RM_IMM8) continue;
    838     if (!slice_eq(d->mnemonic, root)) continue;
    839     return d;
    840   }
    841   return NULL;
    842 }
    843 
    844 static void parse_alu_rr(X64ParseCtx* p) {
    845   /* op src, dst  in AT&T.  Row's opc[0] is the ALU opcode (0x01/0x09/...
    846    * 0x31/0x85/0x89).  The byte/16-bit forms are handled by the
    847    * existing emit.c helpers for w=0/w=1 + size suffix; here phase-1
    848    * supports only the regular 32/64 forms used by the prior asm.c. */
    849   X64AsmOperand src;
    850   X64AsmOperand dst;
    851   src = parse_operand(p->d);
    852   expect_comma(p->d);
    853   dst = parse_operand(p->d);
    854 
    855   /* Immediate source → not an ALU_RR encoding.  Redirect to the
    856    * ALU_RM_IMM row for this mnemonic. */
    857   if (src.kind == X64_ASM_OP_IMM &&
    858       (dst.kind == X64_ASM_OP_REG || dst.kind == X64_ASM_OP_MEM)) {
    859     const X64InsnDesc* imm_row = find_alu_imm_row(p->desc->mnemonic);
    860     if (!imm_row) asm_driver_panic(p->d, "x64 asm: no alu-imm row");
    861     if (dst.kind == X64_ASM_OP_MEM) {
    862       emit_rm_imm_store_operand(p->d, p->mc, p->width, X64_OPC_ALU_IMM8,
    863                                 X64_OPC_ALU_IMM32, imm_row->modrm_reg, dst,
    864                                 src.imm, 1);
    865       return;
    866     }
    867     /* Stack-pointer adjustments (`add/sub $imm, %rsp`, 64-bit) always use the
    868      * imm32 form in codegen — the prologue and alloca patch a fixed-width
    869      * placeholder, so they never shrink to imm8 even for a small frame. Match
    870      * that here so `cc -S | as` reproduces codegen's bytes exactly; %rsp is a
    871      * reserved register, so codegen never emits an imm8 ALU op against it. */
    872     if (dst.reg == X64_RSP && p->width == 8u && imm_fits_i32(src.imm))
    873       emit_alu_imm32(p->mc, 1, imm_row->modrm_reg, dst.reg, (i32)src.imm);
    874     else if (imm_fits_i8(src.imm))
    875       emit_alu_imm8(p->mc, width_to_w(p->width), imm_row->modrm_reg, dst.reg,
    876                     (i8)src.imm);
    877     else if (imm_fits_i32(src.imm))
    878       emit_alu_imm32(p->mc, width_to_w(p->width), imm_row->modrm_reg, dst.reg,
    879                      (i32)src.imm);
    880     else
    881       asm_driver_panic(p->d, "x64 asm: alu-imm out of range");
    882     return;
    883   }
    884 
    885   if (src.kind == X64_ASM_OP_REG && dst.kind == X64_ASM_OP_REG) {
    886     u8 op = p->desc->opc[0];
    887     if (p->width == 2u) {
    888       u8 pfx = X64_OPSIZE_PFX;
    889       mc_emit_bytes(p->mc, &pfx, 1);
    890     }
    891     if (op == 0x89u) {
    892       /* MOV r/m, r — phase-1 keeps the existing helper. */
    893       emit_mov_rr(p->mc, width_to_w(p->width), dst.reg, src.reg);
    894       return;
    895     }
    896     if (op == 0x88u) {
    897       emit_movb_rr_operand(p->d, p->mc, dst, src);
    898       return;
    899     }
    900     /* xor/test/and/... — emit_alu_rr handles the generic shape. */
    901     emit_alu_rr(p->mc, width_to_w(p->width), op, dst.reg, src.reg);
    902     return;
    903   }
    904   /* MOV r, r/m goes through MOV_RM_LOAD; MOV r, m goes through
    905    * MOV_RM_LOAD (load) or ALU_RR with mem dst (store).  We handle the
    906    * store side here only when the mnemonic is "mov" (opc 0x89). */
    907   if (p->desc->opc[0] == 0x89u && src.kind == X64_ASM_OP_REG &&
    908       dst.kind == X64_ASM_OP_MEM) {
    909     if (p->width == 1u)
    910       emit_movb_store_operand(p->d, p->mc, src, dst);
    911     else
    912       emit_mov_store_operand(p->d, p->mc, p->width, src.reg, dst, 0);
    913     return;
    914   }
    915   if (p->desc->opc[0] == 0x89u && src.kind == X64_ASM_OP_MEM &&
    916       dst.kind == X64_ASM_OP_REG) {
    917     emit_mov_load_operand(p->d, p->mc, p->width, dst.reg, src);
    918     return;
    919   }
    920   /* ALU reg → mem store (add/or/and/sub/xor/cmp %reg, mem): the reg-to-r/m
    921    * /r opcode (opc[0]) with a memory ModR/M.  The byte form clears the
    922    * opcode's W bit (e.g. ADD r/m,r 0x01 → r/m8,r8 0x00). */
    923   if (src.kind == X64_ASM_OP_REG && dst.kind == X64_ASM_OP_MEM) {
    924     u8 op = p->width == 1u ? (u8)(p->desc->opc[0] & ~1u) : p->desc->opc[0];
    925     emit_reg_store_operand(p->d, p->mc, p->width, op, src.reg, dst,
    926                            p->width == 1u && byte_reg_needs_rex(&src));
    927     return;
    928   }
    929   asm_driver_panic(p->d, "x64 asm: unsupported alu_rr form");
    930 }
    931 
    932 static void parse_mov_ri(X64ParseCtx* p) {
    933   X64AsmOperand src;
    934   X64AsmOperand dst;
    935   src = parse_operand(p->d);
    936   expect_comma(p->d);
    937   dst = parse_operand(p->d);
    938   if (src.kind != X64_ASM_OP_IMM ||
    939       (dst.kind != X64_ASM_OP_REG && dst.kind != X64_ASM_OP_MEM))
    940     asm_driver_panic(p->d, "x64 asm: mov-imm form");
    941   /* MOV $imm → mem: C6 /0 (byte) or C7 /0 (32/64 sign-extended imm32). */
    942   if (dst.kind == X64_ASM_OP_MEM) {
    943     if (p->width != 8u && !imm_fits_i32(src.imm))
    944       asm_driver_panic(p->d, "x64 asm: mov immediate out of range");
    945     emit_rm_imm_store_operand(
    946         p->d, p->mc, p->width, X64_OPC_MOV_RM_IMM8,
    947         p->width == 1u ? X64_OPC_MOV_RM_IMM8 : X64_OPC_MOV_RM_IMM32,
    948         X64_MOV_RM_IMM_SUB, dst, src.imm, 0);
    949     return;
    950   }
    951   if (p->width != 4u && p->width != 8u)
    952     asm_driver_panic(p->d, "x64 asm: mov imm only supports l/q forms");
    953   x64_emit_load_imm(p->mc, p->width == 8u ? 1 : 0, dst.reg, src.imm);
    954 }
    955 
    956 static void parse_mov_rm_load(X64ParseCtx* p) {
    957   /* MOV r, r/m (0x8B) or LEA r, m (0x8D).  AT&T order is src, dst.
    958    * Phase-1 covers reg-reg, reg←mem (load) and lea. */
    959   X64AsmOperand src;
    960   X64AsmOperand dst;
    961   src = parse_operand(p->d);
    962   expect_comma(p->d);
    963   dst = parse_operand(p->d);
    964   if (p->desc->opc[0] == 0x8Du) {
    965     if (src.kind != X64_ASM_OP_MEM || dst.kind != X64_ASM_OP_REG)
    966       asm_driver_panic(p->d, "x64 asm: lea form");
    967     emit_reg_mem_operand(p->d, p->mc, p->width, X64_OPC_LEA, dst.reg, src);
    968     return;
    969   }
    970   if (src.kind == X64_ASM_OP_MEM && dst.kind == X64_ASM_OP_REG) {
    971     emit_mov_load_operand(p->d, p->mc, p->width, dst.reg, src);
    972     return;
    973   }
    974   if (src.kind == X64_ASM_OP_REG && dst.kind == X64_ASM_OP_REG) {
    975     if (p->width == 2u) {
    976       u8 pfx = X64_OPSIZE_PFX;
    977       mc_emit_bytes(p->mc, &pfx, 1);
    978     }
    979     emit_mov_rr(p->mc, width_to_w(p->width), dst.reg, src.reg);
    980     return;
    981   }
    982   asm_driver_panic(p->d, "x64 asm: mov-load form");
    983 }
    984 
    985 static void parse_movsxd(X64ParseCtx* p) {
    986   X64AsmOperand src;
    987   X64AsmOperand dst;
    988   src = parse_operand(p->d);
    989   expect_comma(p->d);
    990   dst = parse_operand(p->d);
    991   if (dst.kind != X64_ASM_OP_REG)
    992     asm_driver_panic(p->d, "x64 asm: movslq form");
    993   if (src.kind == X64_ASM_OP_REG) {
    994     emit_extend_rr(p->mc, 1, 1, 4, dst.reg, src.reg);
    995   } else if (src.kind == X64_ASM_OP_MEM) {
    996     u8 buf[16];
    997     u32 n = x64_mov_rm_load_pack((X64MovRMLoad){.w = 1,
    998                                                 .opc0 = X64_OPC_MOVSXD,
    999                                                 .dst = dst.reg,
   1000                                                 .base = src.base,
   1001                                                 .disp = src.disp},
   1002                                  buf);
   1003     emit_packed(p->mc, buf, n);
   1004   } else {
   1005     asm_driver_panic(p->d, "x64 asm: movslq source");
   1006   }
   1007 }
   1008 
   1009 static void parse_alu_rm_imm(X64ParseCtx* p) {
   1010   X64AsmOperand src;
   1011   X64AsmOperand dst;
   1012   src = parse_operand(p->d);
   1013   expect_comma(p->d);
   1014   dst = parse_operand(p->d);
   1015   if (src.kind != X64_ASM_OP_IMM || dst.kind != X64_ASM_OP_REG)
   1016     asm_driver_panic(p->d, "x64 asm: alu-imm form");
   1017   if (imm_fits_i8(src.imm))
   1018     emit_alu_imm8(p->mc, width_to_w(p->width), p->desc->modrm_reg, dst.reg,
   1019                   (i8)src.imm);
   1020   else if (imm_fits_i32(src.imm))
   1021     emit_alu_imm32(p->mc, width_to_w(p->width), p->desc->modrm_reg, dst.reg,
   1022                    (i32)src.imm);
   1023   else
   1024     asm_driver_panic(p->d, "x64 asm: alu-imm out of range");
   1025 }
   1026 
   1027 static void parse_cmovcc(X64ParseCtx* p) {
   1028   X64AsmOperand src;
   1029   X64AsmOperand dst;
   1030   src = parse_operand(p->d);
   1031   expect_comma(p->d);
   1032   dst = parse_operand(p->d);
   1033   if (src.kind != X64_ASM_OP_REG || dst.kind != X64_ASM_OP_REG)
   1034     asm_driver_panic(p->d, "x64 asm: cmovcc form");
   1035   {
   1036     u8 op[2] = {0x0f, (u8)(0x40u | (p->cc & 0xfu))};
   1037     if (p->width == 2u) {
   1038       u8 pfx = X64_OPSIZE_PFX;
   1039       mc_emit_bytes(p->mc, &pfx, 1);
   1040     }
   1041     emit_rex(p->mc, width_to_w(p->width), dst.reg, 0, src.reg);
   1042     mc_emit_bytes(p->mc, op, 2);
   1043     emit_rm_reg(p->mc, dst.reg, src.reg);
   1044   }
   1045 }
   1046 
   1047 static void parse_push_pop(X64ParseCtx* p) {
   1048   X64AsmOperand op = parse_operand(p->d);
   1049   u8 base = p->desc->opc[0];
   1050   u8 ob;
   1051   if (op.kind != X64_ASM_OP_REG)
   1052     asm_driver_panic(p->d, "x64 asm: push/pop register");
   1053   emit_rex(p->mc, 0, 0, 0, op.reg);
   1054   ob = (u8)(base | (op.reg & 7u));
   1055   mc_emit_bytes(p->mc, &ob, 1);
   1056 }
   1057 
   1058 static void parse_movzx_movsx(X64ParseCtx* p) {
   1059   X64AsmOperand src = parse_operand(p->d);
   1060   X64AsmOperand dst;
   1061   expect_comma(p->d);
   1062   dst = parse_operand(p->d);
   1063   if (dst.kind != X64_ASM_OP_REG)
   1064     asm_driver_panic(p->d, "x64 asm: movx dst register");
   1065   /* REX.W follows the destination register width: `movsbq …, %rcx` (64-bit)
   1066    * needs REX.W; `movsbl …, %ecx` (32-bit) does not. The disassembler spells
   1067    * the q/l form from REX.W, so honoring dst width here round-trips it. */
   1068   emit_reg_rm_twobyte(
   1069       p->d, p->mc, dst.width == 8u ? 8u : 4u, p->desc->opc[1], dst.reg, src,
   1070       p->desc->opc[1] == X64_OPC_MOVZX_B || p->desc->opc[1] == X64_OPC_MOVSX_B,
   1071       0);
   1072 }
   1073 
   1074 static void parse_imul_rr(X64ParseCtx* p) {
   1075   X64AsmOperand src = parse_operand(p->d);
   1076   X64AsmOperand dst;
   1077   if (src.kind == X64_ASM_OP_IMM) {
   1078     X64AsmOperand real_src;
   1079     expect_comma(p->d);
   1080     real_src = parse_operand(p->d);
   1081     expect_comma(p->d);
   1082     dst = parse_operand(p->d);
   1083     if (dst.kind != X64_ASM_OP_REG)
   1084       asm_driver_panic(p->d, "x64 asm: imul dst register");
   1085     if (real_src.kind == X64_ASM_OP_REG) {
   1086       if (imm_fits_i8(src.imm))
   1087         emit_imul_imm8(p->mc, width_to_w(p->width), dst.reg, real_src.reg,
   1088                        (i8)src.imm);
   1089       else if (imm_fits_i32(src.imm))
   1090         emit_imul_imm32(p->mc, width_to_w(p->width), dst.reg, real_src.reg,
   1091                         (i32)src.imm);
   1092       else
   1093         asm_driver_panic(p->d, "x64 asm: imul imm out of range");
   1094       return;
   1095     }
   1096     if (real_src.kind == X64_ASM_OP_MEM) {
   1097       u8 buf[16];
   1098       u32 n = 0;
   1099       int imm32 = !imm_fits_i8(src.imm);
   1100       if (imm32 && !imm_fits_i32(src.imm))
   1101         asm_driver_panic(p->d, "x64 asm: imul imm out of range");
   1102       n += x64_pack_rex(buf + n, width_to_w(p->width), dst.reg, 0,
   1103                         real_src.base);
   1104       buf[n++] = imm32 ? X64_OPC_IMUL_IMM32 : X64_OPC_IMUL_IMM8;
   1105       n += x64_pack_mem(buf + n, dst.reg, real_src.base, real_src.disp);
   1106       if (imm32)
   1107         n += x64_put_u32le(buf + n, (u32)(i32)src.imm);
   1108       else
   1109         buf[n++] = (u8)(i8)src.imm;
   1110       emit_packed(p->mc, buf, n);
   1111       return;
   1112     }
   1113     asm_driver_panic(p->d, "x64 asm: imul source");
   1114   }
   1115   expect_comma(p->d);
   1116   dst = parse_operand(p->d);
   1117   if (dst.kind != X64_ASM_OP_REG)
   1118     asm_driver_panic(p->d, "x64 asm: imul dst register");
   1119   emit_reg_rm_twobyte(p->d, p->mc, p->width, X64_OPC_IMUL_2B, dst.reg, src, 0,
   1120                       0);
   1121 }
   1122 
   1123 static void parse_imul_rri(X64ParseCtx* p) {
   1124   X64AsmOperand imm = parse_operand(p->d);
   1125   X64AsmOperand src;
   1126   X64AsmOperand dst;
   1127   if (imm.kind != X64_ASM_OP_IMM) asm_driver_panic(p->d, "x64 asm: imul imm");
   1128   expect_comma(p->d);
   1129   src = parse_operand(p->d);
   1130   expect_comma(p->d);
   1131   dst = parse_operand(p->d);
   1132   if (dst.kind != X64_ASM_OP_REG)
   1133     asm_driver_panic(p->d, "x64 asm: imul dst register");
   1134   if (src.kind == X64_ASM_OP_REG) {
   1135     if (p->desc->opc[0] == X64_OPC_IMUL_IMM8 || imm_fits_i8(imm.imm))
   1136       emit_imul_imm8(p->mc, width_to_w(p->width), dst.reg, src.reg,
   1137                      (i8)imm.imm);
   1138     else if (imm_fits_i32(imm.imm))
   1139       emit_imul_imm32(p->mc, width_to_w(p->width), dst.reg, src.reg,
   1140                       (i32)imm.imm);
   1141     else
   1142       asm_driver_panic(p->d, "x64 asm: imul imm out of range");
   1143     return;
   1144   }
   1145   if (src.kind == X64_ASM_OP_MEM) {
   1146     u8 buf[16];
   1147     u32 n = 0;
   1148     int imm32 = !(p->desc->opc[0] == X64_OPC_IMUL_IMM8 || imm_fits_i8(imm.imm));
   1149     if (imm32 && !imm_fits_i32(imm.imm))
   1150       asm_driver_panic(p->d, "x64 asm: imul imm out of range");
   1151     n += x64_pack_rex(buf + n, width_to_w(p->width), dst.reg, 0, src.base);
   1152     buf[n++] = imm32 ? X64_OPC_IMUL_IMM32 : X64_OPC_IMUL_IMM8;
   1153     n += x64_pack_mem(buf + n, dst.reg, src.base, src.disp);
   1154     if (imm32)
   1155       n += x64_put_u32le(buf + n, (u32)(i32)imm.imm);
   1156     else
   1157       buf[n++] = (u8)(i8)imm.imm;
   1158     emit_packed(p->mc, buf, n);
   1159     return;
   1160   }
   1161   asm_driver_panic(p->d, "x64 asm: imul source");
   1162 }
   1163 
   1164 static void parse_f7_rm(X64ParseCtx* p) {
   1165   X64AsmOperand op = parse_operand(p->d);
   1166   emit_rm_op(p->d, p->mc, p->width, X64_OPC_F7, p->desc->modrm_reg, op);
   1167 }
   1168 
   1169 static void parse_shift(X64ParseCtx* p) {
   1170   X64AsmOperand src = parse_operand(p->d);
   1171   X64AsmOperand dst;
   1172   expect_comma(p->d);
   1173   dst = parse_operand(p->d);
   1174   if (src.kind == X64_ASM_OP_REG && src.reg == X64_RCX && src.width == 1u) {
   1175     emit_rm_op(p->d, p->mc, p->width, X64_OPC_SHIFT_CL, p->desc->modrm_reg,
   1176                dst);
   1177     return;
   1178   }
   1179   if (src.kind != X64_ASM_OP_IMM) asm_driver_panic(p->d, "x64 asm: shift imm");
   1180   emit_rm_imm(p->d, p->mc, p->width, X64_OPC_SHIFT_IMM, p->desc->modrm_reg, dst,
   1181               (i32)src.imm, 0);
   1182 }
   1183 
   1184 static void parse_rel32_branch(X64ParseCtx* p) {
   1185   ObjSymId sym = OBJ_SYM_NONE;
   1186   i64 off = 0;
   1187   u32 disp_pos;
   1188   if (p->desc->fmt == X64_FMT_JCC_REL32) {
   1189     u8 op[2] = {0x0f, (u8)(0x80u | (p->cc & 0xfu))};
   1190     mc_emit_bytes(p->mc, op, 2);
   1191   } else {
   1192     u8 op = (p->desc->fmt == X64_FMT_CALL_REL32) ? X64_OPC_CALL_REL32
   1193                                                  : X64_OPC_JMP_REL32;
   1194     mc_emit_bytes(p->mc, &op, 1);
   1195   }
   1196   disp_pos = mc_pos(p->mc);
   1197   emit_u32le(p->mc, 0);
   1198   asm_driver_parse_sym_expr(p->d, &sym, &off);
   1199   if (sym == OBJ_SYM_NONE)
   1200     asm_driver_panic(p->d, "x64 asm: symbolic branch target required");
   1201   /* A `@PLT` suffix forces the PLT32 reloc (the default for `call`); plain
   1202    * `jmp sym` uses PC32. */
   1203   RelocKind dflt = p->desc->fmt == X64_FMT_CALL_REL32 ? R_X64_PLT32 : R_PC32;
   1204   RelocKind k = x64_parse_reloc_suffix(p->d, dflt);
   1205   if (k != R_X64_PLT32 && k != R_PC32)
   1206     asm_driver_panic(p->d, "x64 asm: only @PLT is valid on a branch target");
   1207   mc_emit_reloc_at(p->mc, asm_driver_cur_section(p->d), disp_pos, k, sym,
   1208                        off - 4, 1, 0);
   1209 }
   1210 
   1211 static void parse_setcc(X64ParseCtx* p) {
   1212   X64AsmOperand dst = parse_operand(p->d);
   1213   if (dst.kind == X64_ASM_OP_REG) {
   1214     if (dst.high8) {
   1215       u8 op[2] = {0x0f, (u8)(0x90u | (p->cc & 0xfu))};
   1216       mc_emit_bytes(p->mc, op, 2);
   1217       emit_rm_reg(p->mc, 0, dst.reg);
   1218     } else {
   1219       emit_setcc(p->mc, p->cc, dst.reg);
   1220     }
   1221     return;
   1222   }
   1223   if (dst.kind == X64_ASM_OP_MEM) {
   1224     u8 buf[16];
   1225     u32 n = x64_pack_rex(buf, 0, 0, 0, dst.base);
   1226     buf[n++] = X64_OPC_TWOBYTE;
   1227     buf[n++] = (u8)(X64_OPC_SETCC_BASE | (p->cc & 0xfu));
   1228     n += x64_pack_mem(buf + n, 0, dst.base, dst.disp);
   1229     emit_packed(p->mc, buf, n);
   1230     return;
   1231   }
   1232   asm_driver_panic(p->d, "x64 asm: setcc destination");
   1233 }
   1234 
   1235 static void parse_sse_rr(X64ParseCtx* p) {
   1236   X64AsmOperand src = parse_operand(p->d);
   1237   X64AsmOperand dst;
   1238   int cvt_to_int = p->desc->opc[1] == 0x2cu;
   1239   int cvt_from_int = p->desc->opc[1] == 0x2au;
   1240   expect_comma(p->d);
   1241   dst = parse_operand(p->d);
   1242   if (cvt_to_int) {
   1243     /* cvttsd2si/cvttss2si XMM/m -> GPR: REX.W follows the GPR destination
   1244      * width (`%rdx` = 64-bit, `%edx` = 32-bit), not the mnemonic — these rows
   1245      * carry no size suffix. */
   1246     if (dst.kind != X64_ASM_OP_REG)
   1247       asm_driver_panic(p->d, "x64 asm: cvtt dst register");
   1248     u32 gpr_w = dst.width == 8u ? 8u : 4u;
   1249     if (src.kind == X64_ASM_OP_XMM)
   1250       emit_sse_rr_w(p->mc, p->desc->leg_pfx, p->desc->opc[1], width_to_w(gpr_w),
   1251                     dst.reg, src.reg);
   1252     else if (src.kind == X64_ASM_OP_MEM)
   1253       emit_reg_rm_twobyte(p->d, p->mc, gpr_w, p->desc->opc[1], dst.reg, src, 0,
   1254                           p->desc->leg_pfx);
   1255     else
   1256       asm_driver_panic(p->d, "x64 asm: cvtt source");
   1257     return;
   1258   }
   1259   if (cvt_from_int) {
   1260     /* cvtsi2sd/cvtsi2ss GPR/m -> XMM: REX.W follows the GPR source width. */
   1261     if (dst.kind != X64_ASM_OP_XMM)
   1262       asm_driver_panic(p->d, "x64 asm: cvtsi dst xmm");
   1263     if (src.kind == X64_ASM_OP_REG) {
   1264       u32 gpr_w = src.width == 8u ? 8u : 4u;
   1265       emit_sse_rr_w(p->mc, p->desc->leg_pfx, p->desc->opc[1], width_to_w(gpr_w),
   1266                     dst.reg, src.reg);
   1267     } else if (src.kind == X64_ASM_OP_MEM)
   1268       emit_sse_load(p->mc, p->desc->leg_pfx, p->desc->opc[1], dst.reg, src.base,
   1269                     src.disp);
   1270     else
   1271       asm_driver_panic(p->d, "x64 asm: cvtsi source");
   1272     return;
   1273   }
   1274   if (dst.kind == X64_ASM_OP_MEM && src.kind == X64_ASM_OP_XMM &&
   1275       p->desc->opc[1] == 0x10u &&
   1276       (slice_eq_cstr(p->desc->mnemonic, "movsd") ||
   1277        slice_eq_cstr(p->desc->mnemonic, "movss"))) {
   1278     emit_sse_store(p->mc, p->desc->leg_pfx, 0x11, src.reg, dst.base, dst.disp);
   1279     return;
   1280   }
   1281   if (dst.kind == X64_ASM_OP_MEM && src.kind == X64_ASM_OP_XMM &&
   1282       p->desc->opc[1] == 0x28u && slice_eq_cstr(p->desc->mnemonic, "movaps")) {
   1283     emit_sse_store(p->mc, p->desc->leg_pfx, 0x29, src.reg, dst.base, dst.disp);
   1284     return;
   1285   }
   1286   if (dst.kind != X64_ASM_OP_XMM)
   1287     asm_driver_panic(p->d, "x64 asm: sse dst xmm");
   1288   if (src.kind == X64_ASM_OP_XMM)
   1289     emit_sse_rr(p->mc, p->desc->leg_pfx, p->desc->opc[1], dst.reg, src.reg);
   1290   else if (src.kind == X64_ASM_OP_MEM)
   1291     emit_sse_load(p->mc, p->desc->leg_pfx, p->desc->opc[1], dst.reg, src.base,
   1292                   src.disp);
   1293   else
   1294     asm_driver_panic(p->d, "x64 asm: sse source");
   1295 }
   1296 
   1297 static void parse_bswap(X64ParseCtx* p) {
   1298   X64AsmOperand reg = parse_operand(p->d);
   1299   u8 op[2];
   1300   if (reg.kind != X64_ASM_OP_REG) asm_driver_panic(p->d, "x64 asm: bswap reg");
   1301   emit_rex(p->mc, width_to_w(p->width), 0, 0, reg.reg);
   1302   op[0] = 0x0f;
   1303   op[1] = (u8)(0xc8u | (reg.reg & 7u));
   1304   mc_emit_bytes(p->mc, op, 2);
   1305 }
   1306 
   1307 static void parse_bs_popcnt(X64ParseCtx* p) {
   1308   X64AsmOperand src = parse_operand(p->d);
   1309   X64AsmOperand dst;
   1310   expect_comma(p->d);
   1311   dst = parse_operand(p->d);
   1312   if (dst.kind != X64_ASM_OP_REG)
   1313     asm_driver_panic(p->d, "x64 asm: bit-scan dst register");
   1314   emit_reg_rm_twobyte(p->d, p->mc, p->width, p->desc->opc[1], dst.reg, src, 0,
   1315                       p->desc->leg_pfx);
   1316 }
   1317 
   1318 static void parse_atomic(X64ParseCtx* p) {
   1319   X64AsmOperand src = parse_operand(p->d);
   1320   X64AsmOperand dst;
   1321   u8 buf[16];
   1322   u32 n = 0;
   1323   expect_comma(p->d);
   1324   dst = parse_operand(p->d);
   1325   if (src.kind != X64_ASM_OP_REG ||
   1326       (dst.kind != X64_ASM_OP_REG && dst.kind != X64_ASM_OP_MEM))
   1327     asm_driver_panic(p->d, "x64 asm: atomic form");
   1328   n += x64_pack_rex(buf + n, width_to_w(p->width), src.reg, 0,
   1329                     dst.kind == X64_ASM_OP_REG ? dst.reg : dst.base);
   1330   if (p->desc->opc_len == 2) {
   1331     buf[n++] = X64_OPC_TWOBYTE;
   1332     buf[n++] = p->desc->opc[1];
   1333   } else {
   1334     buf[n++] = p->desc->opc[0];
   1335   }
   1336   if (dst.kind == X64_ASM_OP_REG)
   1337     buf[n++] = x64_modrm(3u, src.reg, dst.reg);
   1338   else
   1339     n += x64_pack_mem(buf + n, src.reg, dst.base, dst.disp);
   1340   emit_packed(p->mc, buf, n);
   1341 }
   1342 
   1343 static void parse_nop_multi(X64ParseCtx* p) {
   1344   u8 nop6[6] = {X64_NOP6_BYTE0, X64_NOP6_BYTE1, X64_NOP6_BYTE2,
   1345                 X64_NOP6_BYTE3, X64_NOP6_BYTE4, X64_NOP6_BYTE5};
   1346   mc_emit_bytes(p->mc, nop6, sizeof nop6);
   1347 }
   1348 
   1349 static void parse_and_emit_for_format(X64ParseCtx* p) {
   1350   switch ((X64Format)p->desc->fmt) {
   1351     case X64_FMT_NULLARY:
   1352       parse_nullary(p);
   1353       return;
   1354     case X64_FMT_NOP_MULTI:
   1355       parse_nop_multi(p);
   1356       return;
   1357     case X64_FMT_PUSH_POP:
   1358       parse_push_pop(p);
   1359       return;
   1360     case X64_FMT_BR_RM:
   1361       parse_br_rm(p);
   1362       return;
   1363     case X64_FMT_ALU_RR:
   1364       parse_alu_rr(p);
   1365       return;
   1366     case X64_FMT_MOV_RI:
   1367       parse_mov_ri(p);
   1368       return;
   1369     case X64_FMT_MOV_RM_LOAD:
   1370       parse_mov_rm_load(p);
   1371       return;
   1372     case X64_FMT_MOVZX_MOVSX:
   1373       parse_movzx_movsx(p);
   1374       return;
   1375     case X64_FMT_MOVSXD:
   1376       parse_movsxd(p);
   1377       return;
   1378     case X64_FMT_ALU_RM_IMM8:
   1379     case X64_FMT_ALU_RM_IMM32:
   1380       parse_alu_rm_imm(p);
   1381       return;
   1382     case X64_FMT_CMOVCC_RR:
   1383       parse_cmovcc(p);
   1384       return;
   1385     case X64_FMT_IMUL_RR:
   1386       parse_imul_rr(p);
   1387       return;
   1388     case X64_FMT_IMUL_RRI:
   1389       parse_imul_rri(p);
   1390       return;
   1391     case X64_FMT_F7_RM:
   1392       parse_f7_rm(p);
   1393       return;
   1394     case X64_FMT_SHIFT_IMM:
   1395     case X64_FMT_SHIFT_CL:
   1396       parse_shift(p);
   1397       return;
   1398     case X64_FMT_JCC_REL32:
   1399     case X64_FMT_JMP_REL32:
   1400     case X64_FMT_CALL_REL32:
   1401       parse_rel32_branch(p);
   1402       return;
   1403     case X64_FMT_SETCC_RM:
   1404       parse_setcc(p);
   1405       return;
   1406     case X64_FMT_SSE_RR:
   1407     case X64_FMT_SSE_LOAD:
   1408     case X64_FMT_SSE_STORE:
   1409       parse_sse_rr(p);
   1410       return;
   1411     case X64_FMT_BSWAP:
   1412       parse_bswap(p);
   1413       return;
   1414     case X64_FMT_BS:
   1415     case X64_FMT_POPCNT:
   1416       parse_bs_popcnt(p);
   1417       return;
   1418     case X64_FMT_XADD_MEM:
   1419     case X64_FMT_XCHG_MEM:
   1420     case X64_FMT_CMPXCHG_MEM:
   1421       parse_atomic(p);
   1422       return;
   1423     default:
   1424       asm_driver_panic(p->d, "x64 asm: format not implemented");
   1425   }
   1426 }
   1427 
   1428 /* Width letter (b/w/l/q) → width in bytes.  Falls back to row-implied
   1429  * width if the suffix is absent. */
   1430 static u32 width_from_info(const X64MnInfo* info, const X64InsnDesc* d) {
   1431   if (info->width != 0) return info->width;
   1432   {
   1433     u32 rw = row_implied_width(d);
   1434     return rw ? rw : 4u;
   1435   }
   1436 }
   1437 
   1438 static void x64_arch_asm_insn(ArchAsm* base, AsmDriver* d, Sym mnemonic) {
   1439   X64Asm* a = (X64Asm*)base;
   1440   MCEmitter* mc = asm_driver_mc(d);
   1441   Slice mnsl = pool_slice(asm_driver_pool(d), mnemonic);
   1442   const char* p = mnsl.s;
   1443   size_t n = mnsl.len;
   1444   X64MnInfo info;
   1445   const X64InsnDesc* desc;
   1446   X64ParseCtx ctx;
   1447   (void)a;
   1448   (void)asm_driver_cur_section(d);
   1449 
   1450   if (!p || !parse_mnemonic(p, n, &info))
   1451     asm_driver_panic(d, "x64 asm: bad mnemonic");
   1452 
   1453   if (n == 4 && memcmp(p, "lock", 4) == 0) {
   1454     AsmTok next;
   1455     u8 pfx = 0xf0;
   1456     mc_emit_bytes(mc, &pfx, 1);
   1457     next = asm_driver_next(d);
   1458     if (next.kind != ASM_TOK_IDENT)
   1459       asm_driver_panic(d, "x64 asm: lock requires an instruction");
   1460     x64_arch_asm_insn(base, d, next.v.ident);
   1461     return;
   1462   }
   1463 
   1464   /* Special case: imm→reg "mov" still spelled "movl"/"movq" but the
   1465    * generic scan returns ALU_RR (0x89) first.  When we see a "$"
   1466    * immediate as the first operand, we want MOV_RI instead.  Defer this
   1467    * disambiguation to parse_alu_rr would force pre-parsing operands;
   1468    * simpler is to special-case MOV here. */
   1469   if (info.base_len == 3 && memcmp(info.base, "mov", 3) == 0) {
   1470     /* Peek for leading '$' → immediate form. */
   1471     AsmTok t = asm_driver_peek(d);
   1472     if (asm_driver_tok_is_punct(t, '$')) {
   1473       /* Find the MOV_RI row. */
   1474       for (u32 i = 0; i < x64_insn_table_n; ++i) {
   1475         const X64InsnDesc* dr = &x64_insn_table[i];
   1476         if (dr->fmt == X64_FMT_MOV_RI && slice_eq_cstr(dr->mnemonic, "mov")) {
   1477           ctx.d = d;
   1478           ctx.mc = mc;
   1479           ctx.desc = dr;
   1480           ctx.width = info.width ? info.width : 4u;
   1481           ctx.cc = info.cc;
   1482           parse_mov_ri(&ctx);
   1483           return;
   1484         }
   1485       }
   1486     }
   1487     /* For mov reg,mem and mov mem,reg we need MOV_RM_LOAD (0x8B) for
   1488      * the load side.  Easiest: pre-parse src; if mem and dst is reg →
   1489      * MOV_RM_LOAD.  Doing so re-uses the AT&T parser cleanly. */
   1490     {
   1491       X64AsmOperand src = parse_operand(d);
   1492       expect_comma(d);
   1493       {
   1494         X64AsmOperand dst = parse_operand(d);
   1495         u32 w = info.width ? info.width : 4u;
   1496         if (src.kind == X64_ASM_OP_REG && dst.kind == X64_ASM_OP_REG) {
   1497           if (w == 1u) {
   1498             /* MOV r/m8, r8 — opcode 0x88. */
   1499             emit_movb_rr_operand(d, mc, dst, src);
   1500             return;
   1501           }
   1502           if (w == 2u) {
   1503             u8 pfx = X64_OPSIZE_PFX;
   1504             mc_emit_bytes(mc, &pfx, 1);
   1505           }
   1506           emit_mov_rr(mc, width_to_w(w), dst.reg, src.reg);
   1507           return;
   1508         }
   1509         if (src.kind == X64_ASM_OP_REG && dst.kind == X64_ASM_OP_MEM) {
   1510           if (w == 1u)
   1511             emit_movb_store_operand(d, mc, src, dst);
   1512           else
   1513             emit_mov_store_operand(d, mc, w, src.reg, dst, 0);
   1514           return;
   1515         }
   1516         if (src.kind == X64_ASM_OP_MEM && dst.kind == X64_ASM_OP_REG) {
   1517           emit_mov_load_operand(d, mc, w, dst.reg, src);
   1518           return;
   1519         }
   1520         /* movd/movq between a GPR and an XMM register: 66 [REX.W] 0F 6E (to
   1521          * xmm) / 7E (to gpr). The xmm is always the ModRM.reg field, the gpr
   1522          * the r/m; movq sets REX.W (w==8), movd does not (w==4). */
   1523         if ((src.kind == X64_ASM_OP_REG && dst.kind == X64_ASM_OP_XMM) ||
   1524             (src.kind == X64_ASM_OP_XMM && dst.kind == X64_ASM_OP_REG)) {
   1525           int to_xmm = (dst.kind == X64_ASM_OP_XMM);
   1526           u32 xmm = to_xmm ? dst.reg : src.reg;
   1527           u32 gpr = to_xmm ? src.reg : dst.reg;
   1528           emit_sse_rr_w(mc, X64_OPSIZE_PFX, to_xmm ? 0x6Eu : 0x7Eu,
   1529                         width_to_w(w), xmm, gpr);
   1530           return;
   1531         }
   1532         asm_driver_panic(d, "x64 asm: mov form");
   1533       }
   1534     }
   1535   }
   1536 
   1537   desc = find_mnemonic_row(&info);
   1538   if (!desc) asm_driver_panic(d, "x64 asm: unknown mnemonic");
   1539 
   1540   /* If the user wrote an indirect branch (`*%reg`), prefer the BR_RM row
   1541    * over the rel32 row that may sort first in the table. */
   1542   if (desc->fmt == X64_FMT_CALL_REL32 || desc->fmt == X64_FMT_JMP_REL32) {
   1543     AsmTok t = asm_driver_peek(d);
   1544     if (asm_driver_tok_is_punct(t, '*')) {
   1545       for (u32 i = 0; i < x64_insn_table_n; ++i) {
   1546         const X64InsnDesc* dr = &x64_insn_table[i];
   1547         if (dr->fmt != X64_FMT_BR_RM) continue;
   1548         if (!slice_eq(dr->mnemonic, (Slice){{info.base}, info.base_len}))
   1549           continue;
   1550         desc = dr;
   1551         break;
   1552       }
   1553     }
   1554   }
   1555 
   1556   ctx.d = d;
   1557   ctx.mc = mc;
   1558   ctx.desc = desc;
   1559   ctx.width = width_from_info(&info, desc);
   1560   ctx.cc = info.cc;
   1561   parse_and_emit_for_format(&ctx);
   1562 }
   1563 
   1564 static void x64_arch_asm_destroy(ArchAsm* base) {
   1565   x64_asm_close((X64Asm*)base);
   1566 }
   1567 
   1568 X64Asm* x64_asm_open(Compiler* c) {
   1569   X64Asm* a = arena_new(c->tu, X64Asm);
   1570   memset(a, 0, sizeof *a);
   1571   a->base.insn = x64_arch_asm_insn;
   1572   a->base.destroy = x64_arch_asm_destroy;
   1573   a->c = c;
   1574   return a;
   1575 }
   1576 
   1577 void x64_asm_close(X64Asm* a) { (void)a; }
   1578 
   1579 /* ---- cc -S symbolization hooks (printer <-> this parser) ------------------
   1580  *
   1581  * Inverse of the operand-syntax this parser accepts (parse_rel32_branch,
   1582  * x64_parse_reloc_suffix): how the printer spells a relocated x64 operand so it
   1583  * re-assembles. x64 relocs store addend-4 (rel32 bias), so addend_bias=4 makes
   1584  * the printed offset the symbol offset. R_PC32 covers BOTH a branch target and
   1585  * a RIP-relative lea/mov, so surgery is chosen from the operand text by the
   1586  * printer (an `(%rip)` operand uses RIP surgery); we just supply the modifier.
   1587  * Calls (R_X64_PLT32) print as a bare symbol — both kit-as (call default) and
   1588  * clang resolve a same-TU callee, so execution matches regardless of the exact
   1589  * reloc kind each assembler picks. */
   1590 static int x64_reloc_operand(u16 kind, KitObjFmt fmt, ArchRelocOperand* out) {
   1591   const char* suffix;
   1592   (void)fmt; /* x64 cc -S cross-targets ELF; one spelling */
   1593   switch (kind) {
   1594     case R_PC32:      /* jmp/jcc target, or RIP-relative lea/mov */
   1595     case R_X64_PLT32: /* call target -> bare symbol */
   1596       suffix = "";
   1597       break;
   1598     case R_X64_GOTPCREL:
   1599     case R_X64_GOTPCRELX:
   1600     case R_X64_REX_GOTPCRELX:
   1601       suffix = "@GOTPCREL"; /* RIP-relative GOT load */
   1602       break;
   1603     default:
   1604       return 0; /* data (R_ABS*) via emit_data_range; TLS/etc. unsymbolized */
   1605   }
   1606   out->surg =
   1607       ARCH_RELOC_SURG_TAIL; /* promoted to RIP by the printer if (%rip) */
   1608   out->prefix = "";
   1609   out->suffix = suffix;
   1610   out->addend_bias = 4;
   1611   return 1;
   1612 }
   1613 
   1614 /* Intra-section local branches whose target codegen resolved in place (no
   1615  * relocation): jmp and the Jcc family. Excludes call (always relocated) and
   1616  * indirect/register-form jumps (no numeric target to relabel). */
   1617 static int x64_is_local_branch(KitSlice m) {
   1618   static const char* const br[] = {
   1619       "jmp",
   1620       "jo",
   1621       "jno",
   1622       "jb",
   1623       "jae",
   1624       "je",
   1625       "jne",
   1626       "jbe",
   1627       "ja",
   1628       "js",
   1629       "jns",
   1630       "jp",
   1631       "jnp",
   1632       "jl",
   1633       "jge",
   1634       "jle",
   1635       "jg",
   1636       /* aliases the disassembler may not emit but harmless to accept */
   1637       "jz",
   1638       "jnz",
   1639       "jc",
   1640       "jnc",
   1641   };
   1642   u32 i;
   1643   for (i = 0; i < sizeof br / sizeof br[0]; ++i) {
   1644     size_t n = strlen(br[i]);
   1645     if (m.len == (u32)n && memcmp(m.s, br[i], n) == 0) return 1;
   1646   }
   1647   return 0;
   1648 }
   1649 
   1650 const ArchAsmOps x64_asm_ops = {
   1651     .reloc_operand = x64_reloc_operand,
   1652     .is_local_branch = x64_is_local_branch,
   1653 };
   1654 
   1655 ArchAsm* x64_arch_asm_new(Compiler* c) { return &x64_asm_open(c)->base; }
   1656 
   1657 void x64_inline_bind(X64Asm* a, const AsmConstraint* outs, u32 nout,
   1658                      Operand* out_ops, const AsmConstraint* ins, u32 nin,
   1659                      const Operand* in_ops, const Sym* clobbers, u32 nclob) {
   1660   a->outs = outs;
   1661   a->out_ops = out_ops;
   1662   a->ins = ins;
   1663   a->in_ops = in_ops;
   1664   a->clobbers = clobbers;
   1665   a->nout = nout;
   1666   a->nin = nin;
   1667   a->nclob = nclob;
   1668 }
   1669 
   1670 #define X64_INLINE_LINE_CAP 1024
   1671 
   1672 _Noreturn static void inline_panic(X64Asm* a, const char* msg) {
   1673   SrcLoc loc = {0, 0, 0};
   1674   compiler_panic(a->c, loc, "x64 inline asm: %.*s",
   1675                  SLICE_ARG(slice_from_cstr(msg)));
   1676 }
   1677 
   1678 /* Width selector for x64_reg_spelling: matches the operand-modifier
   1679  * forms recognised by the template walker. */
   1680 #define X64_REG_WIDTH_64 0
   1681 #define X64_REG_WIDTH_32 1
   1682 #define X64_REG_WIDTH_8 2
   1683 #define X64_REG_WIDTH_16 3
   1684 #define X64_REG_WIDTH_H8 4
   1685 
   1686 static void render_xmm(StrBuf* sb, u32 reg) {
   1687   strbuf_putc(sb, '%');
   1688   strbuf_puts(sb, "xmm");
   1689   reg &= 15u;
   1690   if (reg >= 10u) strbuf_putc(sb, (char)('0' + (reg / 10u)));
   1691   strbuf_putc(sb, (char)('0' + (reg % 10u)));
   1692 }
   1693 
   1694 static const char* x64_reg_spelling(u32 reg, int width) {
   1695   static const char* r64[16] = {
   1696       "rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi",
   1697       "r8",  "r9",  "r10", "r11", "r12", "r13", "r14", "r15",
   1698   };
   1699   static const char* r32[16] = {
   1700       "eax", "ecx", "edx",  "ebx",  "esp",  "ebp",  "esi",  "edi",
   1701       "r8d", "r9d", "r10d", "r11d", "r12d", "r13d", "r14d", "r15d",
   1702   };
   1703   static const char* r8[16] = {
   1704       "al",  "cl",  "dl",   "bl",   "spl",  "bpl",  "sil",  "dil",
   1705       "r8b", "r9b", "r10b", "r11b", "r12b", "r13b", "r14b", "r15b",
   1706   };
   1707   static const char* r16[16] = {
   1708       "ax",  "cx",  "dx",   "bx",   "sp",   "bp",   "si",   "di",
   1709       "r8w", "r9w", "r10w", "r11w", "r12w", "r13w", "r14w", "r15w",
   1710   };
   1711   static const char* rh8[4] = {"ah", "ch", "dh", "bh"};
   1712   if (width == X64_REG_WIDTH_H8) return reg < 4u ? rh8[reg] : NULL;
   1713   if (width == X64_REG_WIDTH_16) return r16[reg & 15u];
   1714   if (width == X64_REG_WIDTH_8) return r8[reg & 15u];
   1715   if (width == X64_REG_WIDTH_32) return r32[reg & 15u];
   1716   return r64[reg & 15u];
   1717 }
   1718 
   1719 static int x64_type_prefers_32(KitCgTypeId type) {
   1720   if (type == 0) return 0;
   1721   return !type_is_64(type);
   1722 }
   1723 
   1724 static void render_reg(StrBuf* sb, u32 reg, int width) {
   1725   const char* name = x64_reg_spelling(reg, width);
   1726   strbuf_putc(sb, '%');
   1727   if (name) strbuf_puts(sb, name);
   1728 }
   1729 
   1730 static void render_imm(StrBuf* sb, i64 v) {
   1731   strbuf_putc(sb, '$');
   1732   strbuf_put_i64(sb, v);
   1733 }
   1734 
   1735 static void render_indirect(StrBuf* sb, Reg base, i32 ofs) {
   1736   if (ofs) strbuf_put_i64(sb, (i64)ofs);
   1737   strbuf_putc(sb, '(');
   1738   render_reg(sb, (u32)base, X64_REG_WIDTH_64);
   1739   strbuf_putc(sb, ')');
   1740 }
   1741 
   1742 /* Operand-modifier forms used by the template walker. */
   1743 #define X64_FORM_DEFAULT 0
   1744 #define X64_FORM_W 1 /* %w — 16-bit */
   1745 #define X64_FORM_X 2 /* %x — 64-bit */
   1746 #define X64_FORM_A 3 /* %a — address / memory */
   1747 #define X64_FORM_B 4 /* %b — 8-bit (byte) register */
   1748 #define X64_FORM_K 5 /* %k — 32-bit */
   1749 #define X64_FORM_H 6 /* %h — high 8-bit register (a/c/d/b only) */
   1750 
   1751 static char x64_size_suffix_for_operand(X64Asm* a, u32 idx) {
   1752   u32 ntot = a->nout + a->nin;
   1753   const Operand* op;
   1754   u32 size;
   1755   if (idx >= ntot) inline_panic(a, "operand index out of range");
   1756   op = (idx < a->nout) ? &a->out_ops[idx] : &a->in_ops[idx - a->nout];
   1757   if (op->type)
   1758     size = type_byte_size(op->type);
   1759   else if (op->kind == OPK_IMM)
   1760     size = 4;
   1761   else
   1762     size = 8;
   1763   switch (size) {
   1764     case 1:
   1765       return 'b';
   1766     case 2:
   1767       return 'w';
   1768     case 4:
   1769       return 'l';
   1770     case 8:
   1771       return 'q';
   1772     default:
   1773       inline_panic(a, "%z requires a scalar 1/2/4/8-byte operand");
   1774   }
   1775 }
   1776 
   1777 static void render_operand(X64Asm* a, StrBuf* sb, u32 idx, int form) {
   1778   u32 ntot = a->nout + a->nin;
   1779   const Operand* op;
   1780   if (idx >= ntot) inline_panic(a, "operand index out of range");
   1781   op = (idx < a->nout) ? &a->out_ops[idx] : &a->in_ops[idx - a->nout];
   1782   if (form == X64_FORM_A) {
   1783     if (op->kind != OPK_INDIRECT) inline_panic(a, "%a on non-memory operand");
   1784     if (op->v.ind.index != REG_NONE)
   1785       inline_panic(a, "inline asm: indexed addressing not supported");
   1786     render_indirect(sb, op->v.ind.base, op->v.ind.ofs);
   1787     return;
   1788   }
   1789   if ((form == X64_FORM_B || form == X64_FORM_H) &&
   1790       op->kind != X64_INLINE_OPK_REG) {
   1791     inline_panic(a, "byte-register modifier requires a register operand");
   1792   }
   1793   if (op->kind == X64_INLINE_OPK_REG) {
   1794     int width;
   1795     if (op->pad[0] == X64_INLINE_OPCLS_FP) {
   1796       render_xmm(sb, (u32)op->v.local);
   1797       return;
   1798     }
   1799     if (form == X64_FORM_B)
   1800       width = X64_REG_WIDTH_8;
   1801     else if (form == X64_FORM_H) {
   1802       if (op->v.local > X64_RBX) {
   1803         inline_panic(a, "%h modifier requires ax/cx/dx/bx register");
   1804       }
   1805       width = X64_REG_WIDTH_H8;
   1806     } else if (form == X64_FORM_W)
   1807       width = X64_REG_WIDTH_16;
   1808     else if (form == X64_FORM_K)
   1809       width = X64_REG_WIDTH_32;
   1810     else if (form == X64_FORM_X)
   1811       width = X64_REG_WIDTH_64;
   1812     else
   1813       width =
   1814           x64_type_prefers_32(op->type) ? X64_REG_WIDTH_32 : X64_REG_WIDTH_64;
   1815     render_reg(sb, (u32)op->v.local, width);
   1816     return;
   1817   }
   1818   if (op->kind == OPK_IMM) {
   1819     render_imm(sb, op->v.imm);
   1820     return;
   1821   }
   1822   if (op->kind == OPK_INDIRECT) {
   1823     if (op->v.ind.index != REG_NONE)
   1824       inline_panic(a, "inline asm: indexed addressing not supported");
   1825     render_indirect(sb, op->v.ind.base, op->v.ind.ofs);
   1826     return;
   1827   }
   1828   inline_panic(a, "unsupported operand kind");
   1829 }
   1830 
   1831 static u32 find_named_operand(X64Asm* a, const char* name, size_t len) {
   1832   Sym needle = pool_intern_slice(a->c->global, (Slice){.s = name, .len = len});
   1833   u32 i;
   1834   for (i = 0; i < a->nout; ++i) {
   1835     if (a->outs[i].name == needle) return i;
   1836   }
   1837   for (i = 0; i < a->nin; ++i) {
   1838     if (a->ins[i].name == needle) return a->nout + i;
   1839   }
   1840   inline_panic(a, "%[name] does not match any constraint");
   1841 }
   1842 
   1843 static void run_one_line(X64Asm* a, MCEmitter* mc, const char* text,
   1844                          size_t len) {
   1845   size_t i;
   1846   AsmLexer* lx;
   1847   AsmDriver* d;
   1848   AsmTok t;
   1849   for (i = 0; i < len; ++i) {
   1850     if (text[i] != ' ' && text[i] != '\t') break;
   1851   }
   1852   if (i == len) return;
   1853   lx = asm_lex_open_mem(a->c, "<inline-asm>", text, len);
   1854   d = asm_driver_open_inline(a->c, mc, lx);
   1855   t = asm_driver_peek(d);
   1856   while (t.kind == ASM_TOK_NEWLINE || t.kind == ASM_TOK_HASH) {
   1857     (void)asm_driver_next(d);
   1858     if (t.kind == ASM_TOK_HASH) {
   1859       while (!asm_driver_at_eol(d)) (void)asm_driver_next(d);
   1860     }
   1861     t = asm_driver_peek(d);
   1862   }
   1863   if (t.kind == ASM_TOK_EOF) {
   1864     asm_driver_close_inline(d);
   1865     asm_lex_close(lx);
   1866     return;
   1867   }
   1868   if (t.kind != ASM_TOK_IDENT) inline_panic(a, "expected mnemonic");
   1869   (void)asm_driver_next(d);
   1870   x64_arch_asm_insn(&a->base, d, t.v.ident);
   1871   asm_driver_close_inline(d);
   1872   asm_lex_close(lx);
   1873 }
   1874 
   1875 static void render_and_run_line(X64Asm* a, MCEmitter* mc, StrBuf* sb,
   1876                                 const char* start, const char* end) {
   1877   strbuf_reset(sb);
   1878   for (const char* p = start; p < end; ++p) {
   1879     char c = *p;
   1880     char n;
   1881     int form = 0;
   1882     if (c != '%') {
   1883       strbuf_putc(sb, c);
   1884       continue;
   1885     }
   1886     if (p + 1 >= end) inline_panic(a, "trailing '%' in template");
   1887     n = *(p + 1);
   1888     if (n == '%') {
   1889       strbuf_putc(sb, '%');
   1890       ++p;
   1891       continue;
   1892     }
   1893     if (n == 'w' || n == 'x' || n == 'a' || n == 'b' || n == 'k' || n == 'h' ||
   1894         n == 'z') {
   1895       form = (n == 'w')   ? X64_FORM_W
   1896              : (n == 'x') ? X64_FORM_X
   1897              : (n == 'a') ? X64_FORM_A
   1898              : (n == 'b') ? X64_FORM_B
   1899              : (n == 'k') ? X64_FORM_K
   1900              : (n == 'h') ? X64_FORM_H
   1901                           : -1;
   1902       ++p;
   1903       if (p + 1 >= end) inline_panic(a, "trailing '%' modifier");
   1904       n = *(p + 1);
   1905     }
   1906     if (n == '[') {
   1907       const char* nbeg = p + 2;
   1908       const char* nend = nbeg;
   1909       u32 idx;
   1910       while (nend < end && *nend != ']') ++nend;
   1911       if (nend == end) inline_panic(a, "unterminated %[name]");
   1912       idx = find_named_operand(a, nbeg, (size_t)(nend - nbeg));
   1913       p = nend;
   1914       if (form == -1)
   1915         strbuf_putc(sb, x64_size_suffix_for_operand(a, idx));
   1916       else
   1917         render_operand(a, sb, idx, form);
   1918       continue;
   1919     }
   1920     if (n < '0' || n > '9') inline_panic(a, "expected digit after '%'");
   1921     {
   1922       u32 idx = (u32)(n - '0');
   1923       ++p;
   1924       if (p + 1 < end && *(p + 1) >= '0' && *(p + 1) <= '9') {
   1925         idx = idx * 10u + (u32)(*(p + 1) - '0');
   1926         ++p;
   1927       }
   1928       if (form == -1)
   1929         strbuf_putc(sb, x64_size_suffix_for_operand(a, idx));
   1930       else
   1931         render_operand(a, sb, idx, form);
   1932     }
   1933   }
   1934   if (sb->truncated) inline_panic(a, "inline asm line buffer overflow");
   1935   run_one_line(a, mc, strbuf_cstr(sb), strbuf_len(sb));
   1936 }
   1937 
   1938 void x64_asm_run_template(X64Asm* a, MCEmitter* mc, const char* tmpl) {
   1939   char buf[X64_INLINE_LINE_CAP];
   1940   StrBuf sb;
   1941   const char* line_start;
   1942   int bracket = 0;
   1943   char quote = 0;
   1944   if (!tmpl || !*tmpl) return;
   1945   strbuf_init(&sb, buf, sizeof buf);
   1946   line_start = tmpl;
   1947   for (const char* p = tmpl;; ++p) {
   1948     char c = *p;
   1949     if (c == '\0') {
   1950       render_and_run_line(a, mc, &sb, line_start, p);
   1951       break;
   1952     }
   1953     if (quote) {
   1954       if (c == '\\' && *(p + 1)) {
   1955         ++p;
   1956         continue;
   1957       }
   1958       if (c == quote) quote = 0;
   1959       continue;
   1960     }
   1961     if (c == '"' || c == '\'') {
   1962       quote = c;
   1963       continue;
   1964     }
   1965     if (c == '[') {
   1966       ++bracket;
   1967       continue;
   1968     }
   1969     if (c == ']') {
   1970       if (bracket) --bracket;
   1971       continue;
   1972     }
   1973     if (bracket == 0 && (c == '\n' || c == ';')) {
   1974       render_and_run_line(a, mc, &sb, line_start, p);
   1975       line_start = p + 1;
   1976     }
   1977   }
   1978 }