kit

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

asm.c (42671B)


      1 /* ARM32 (Thumb-2) textual-assembler frontend.
      2  *
      3  * Implements the per-instruction `insn` hook the shared assembler driver
      4  * (src/asm/asm.c) calls once per source line. Operands are parsed off the
      5  * AsmDriver token stream and encoded via the isa.h inline encoders, emitting
      6  * each Thumb-2 instruction as little-endian half-words (hw1 first for the
      7  * 32-bit forms). Mirrors src/arch/riscv/asm.c (descriptor lookup + format
      8  * dispatch) and src/arch/aa64/asm.c (the ArchAsmOps printer-side seam).
      9  *
     10  * Mnemonic resolution: the driver composes dotted mnemonics (so `mov.w`,
     11  * `b.w`, `ldr.w` arrive whole). A condition-code suffix (`bne`, `beq.w`,
     12  * `moveq`) is stripped here to recover the base mnemonic + condition. The
     13  * `.syntax unified` / `.thumb` directives are consumed by the shared driver. */
     14 #include "arch/arm32/asm.h"
     15 
     16 #include <string.h>
     17 
     18 #include "arch/arm32/isa.h"
     19 #include "arch/arm32/regs.h"
     20 #include "arch/arch.h"
     21 #include "asm/asm_helpers.h"
     22 #include "asm/asm_lex.h"
     23 #include "cg/cgir.h"
     24 #include "core/arena.h"
     25 #include "core/pool.h"
     26 #include "core/slice.h"
     27 #include "core/strbuf.h"
     28 #include "obj/obj.h"
     29 
     30 struct Arm32Asm {
     31   ArchAsm base;
     32   Compiler* c;
     33 
     34   const AsmConstraint* outs;
     35   Operand* out_ops;
     36   const AsmConstraint* ins;
     37   const Operand* in_ops;
     38   const Sym* clobbers;
     39   u32 nout;
     40   u32 nin;
     41   u32 nclob;
     42 };
     43 
     44 /* ---- byte emit (LE half-words, hw1 first for 32-bit) ---- */
     45 static void emit_t16(AsmDriver* d, u16 hw) {
     46   u8 b[2] = {(u8)(hw & 0xffu), (u8)((hw >> 8) & 0xffu)};
     47   mc_emit_bytes(asm_driver_mc(d), b, sizeof b);
     48 }
     49 static void emit_t32(AsmDriver* d, u32 instr) {
     50   u32 hw1 = (instr >> 16) & 0xffffu, hw2 = instr & 0xffffu;
     51   u8 b[4] = {(u8)(hw1 & 0xffu), (u8)((hw1 >> 8) & 0xffu), (u8)(hw2 & 0xffu),
     52              (u8)((hw2 >> 8) & 0xffu)};
     53   mc_emit_bytes(asm_driver_mc(d), b, sizeof b);
     54 }
     55 
     56 /* Construct a Slice (KitSlice's first member is a union, so the brace-init form
     57  * needs nesting; this helper keeps the call sites clean). */
     58 static Slice arm_slice(const char* s, size_t len) {
     59   Slice sl;
     60   sl.s = s;
     61   sl.len = len;
     62   return sl;
     63 }
     64 
     65 Arm32Asm* arm32_asm_open(Compiler* c) {
     66   Arm32Asm* a = arena_new(c->tu, Arm32Asm);
     67   memset(a, 0, sizeof *a);
     68   a->base.insn = NULL;
     69   a->base.destroy = NULL;
     70   a->c = c;
     71   return a;
     72 }
     73 
     74 void arm32_asm_close(Arm32Asm* a) { (void)a; }
     75 
     76 void arm32_inline_bind(Arm32Asm* a, const AsmConstraint* outs, u32 nout,
     77                        Operand* out_ops, const AsmConstraint* ins, u32 nin,
     78                        const Operand* in_ops, const Sym* clobbers, u32 nclob) {
     79   a->outs = outs;
     80   a->out_ops = out_ops;
     81   a->ins = ins;
     82   a->in_ops = in_ops;
     83   a->clobbers = clobbers;
     84   a->nout = nout;
     85   a->nin = nin;
     86   a->nclob = nclob;
     87 }
     88 
     89 static void render_reg(StrBuf* sb, u32 r) {
     90   strbuf_putc(sb, 'r');
     91   if (r >= 10u) strbuf_putc(sb, (char)('0' + (r / 10u)));
     92   strbuf_putc(sb, (char)('0' + (r % 10u)));
     93 }
     94 
     95 static void render_imm(StrBuf* sb, i64 v) {
     96   strbuf_putc(sb, '#');
     97   strbuf_put_i64(sb, v);
     98 }
     99 
    100 static void render_indirect(StrBuf* sb, Reg base, i32 ofs) {
    101   strbuf_putc(sb, '[');
    102   render_reg(sb, (u32)base);
    103   if (ofs != 0) {
    104     strbuf_puts(sb, ", ");
    105     render_imm(sb, (i64)ofs);
    106   }
    107   strbuf_putc(sb, ']');
    108 }
    109 
    110 _Noreturn static void inline_panic(Arm32Asm* a, const char* msg) {
    111   SrcLoc loc = {0, 0, 0};
    112   compiler_panic(a->c, loc, "inline asm: %.*s",
    113                  SLICE_ARG(slice_from_cstr(msg)));
    114 }
    115 
    116 static u32 lookup_named(Arm32Asm* a, Sym needle) {
    117   for (u32 k = 0; k < a->nout; ++k) {
    118     if (a->outs[k].name == needle) return k;
    119   }
    120   for (u32 k = 0; k < a->nin; ++k) {
    121     if (a->ins[k].name == needle) return a->nout + k;
    122   }
    123   return (u32)-1;
    124 }
    125 
    126 static void render_operand(Arm32Asm* a, StrBuf* sb, u32 idx, int form) {
    127   u32 ntot = a->nout + a->nin;
    128   if (idx >= ntot) inline_panic(a, "operand index out of range");
    129   const Operand* op =
    130       (idx < a->nout) ? &a->out_ops[idx] : &a->in_ops[idx - a->nout];
    131   if (form == 3) {
    132     if (op->kind != OPK_INDIRECT) inline_panic(a, "%a on non-memory operand");
    133     if (op->v.ind.index != CG_LOCAL_NONE)
    134       inline_panic(a, "%a on indexed memory operand");
    135     render_indirect(sb, op->v.ind.base, op->v.ind.ofs);
    136     return;
    137   }
    138   if (form != 0) inline_panic(a, "unsupported ARM32 inline asm modifier");
    139   switch (op->kind) {
    140     case ARM32_INLINE_OPK_REG:
    141       if (op->pad[0] == ARM32_INLINE_OPCLS_FP)
    142         inline_panic(a, "arm32 inline asm has no FP register constraints");
    143       render_reg(sb, (u32)op->v.local);
    144       return;
    145     case OPK_IMM:
    146       render_imm(sb, op->v.imm);
    147       return;
    148     case OPK_INDIRECT:
    149       if (op->v.ind.index != CG_LOCAL_NONE)
    150         inline_panic(a, "indexed memory operand in inline asm");
    151       render_indirect(sb, op->v.ind.base, op->v.ind.ofs);
    152       return;
    153     default:
    154       inline_panic(a, "unsupported operand kind for %N");
    155   }
    156 }
    157 
    158 /* ---- operand parse helpers ---- */
    159 static int sym_to_cstr(AsmDriver* d, Sym s, char* out, size_t cap) {
    160   Slice sl = pool_slice(asm_driver_pool(d), s);
    161   if (!sl.s || sl.len >= cap) return 0;
    162   memcpy(out, sl.s, sl.len);
    163   out[sl.len] = '\0';
    164   return 1;
    165 }
    166 
    167 static u32 parse_reg(AsmDriver* d) {
    168   AsmTok t = asm_driver_next(d);
    169   char name[16];
    170   uint32_t idx = 0;
    171   if (t.kind != ASM_TOK_IDENT || !sym_to_cstr(d, t.v.ident, name, sizeof name) ||
    172       arm32_register_index(name, &idx) != 0 || idx > 15u)
    173     asm_driver_panic(d, "arm32 asm: bad core register");
    174   return idx;
    175 }
    176 
    177 static void expect_comma(AsmDriver* d) {
    178   if (!asm_driver_eat_comma(d)) asm_driver_panic(d, "arm32 asm: expected ','");
    179 }
    180 
    181 /* 1 if the next token is a core register name (used to disambiguate the
    182  * register vs modified-immediate data-processing forms by operand shape). */
    183 static int peek_is_reg(AsmDriver* d) {
    184   AsmTok t = asm_driver_peek(d);
    185   char name[16];
    186   uint32_t idx;
    187   if (t.kind != ASM_TOK_IDENT) return 0;
    188   if (!sym_to_cstr(d, t.v.ident, name, sizeof name)) return 0;
    189   return arm32_register_index(name, &idx) == 0 && idx <= 15u;
    190 }
    191 
    192 /* Parse a #-prefixed immediate constant. The `#` is optional (GNU as accepts
    193  * both); a bare expression is also accepted. */
    194 static i64 parse_imm(AsmDriver* d) {
    195   (void)asm_driver_eat_punct(d, '#');
    196   return asm_driver_parse_const(d);
    197 }
    198 
    199 /* A modified-immediate operand: encode via thumb_expand_imm_encode, panicking
    200  * if the value isn't representable (the assembler does not materialize). */
    201 static u32 parse_modimm(AsmDriver* d) {
    202   i64 v = parse_imm(d);
    203   u32 out12;
    204   if (!thumb_expand_imm_encode((u32)v, &out12))
    205     asm_driver_panic(d, "arm32 asm: immediate not a modified-immediate");
    206   return out12;
    207 }
    208 
    209 /* `[Rn]` / `[Rn, #imm]` — returns base in *base_out, signed displacement in
    210  * *disp_out. Pre/post-index and shifted-index modes are a follow-on. */
    211 static void parse_mem(AsmDriver* d, u32* base_out, i64* disp_out) {
    212   asm_driver_expect_punct(d, '[', "'[' in arm32 memory operand");
    213   *base_out = parse_reg(d);
    214   *disp_out = 0;
    215   if (asm_driver_eat_comma(d)) *disp_out = parse_imm(d);
    216   asm_driver_expect_punct(d, ']', "']' in arm32 memory operand");
    217 }
    218 
    219 /* Register list `{r0, r1, lr}` -> bitmask. */
    220 static u32 parse_reglist(AsmDriver* d) {
    221   u32 mask = 0;
    222   asm_driver_expect_punct(d, '{', "'{' in arm32 register list");
    223   for (;;) {
    224     u32 r = parse_reg(d);
    225     mask |= (1u << r);
    226     if (!asm_driver_eat_comma(d)) break;
    227   }
    228   asm_driver_expect_punct(d, '}', "'}' in arm32 register list");
    229   return mask;
    230 }
    231 
    232 /* `#:lower16:sym` / `#:upper16:sym` modifier -> 1 + reloc kind, sym, addend.
    233  * The leading `#` is optional; returns 0 if no `:modifier:` is present. */
    234 static int parse_movw_mod(AsmDriver* d, RelocKind* kind_out, ObjSymId* sym_out,
    235                           i64* off_out, int is_movt) {
    236   (void)asm_driver_eat_punct(d, '#');
    237   if (!asm_driver_tok_is_punct(asm_driver_peek(d), ':')) return 0;
    238   (void)asm_driver_next(d); /* ':' */
    239   {
    240     AsmTok name = asm_driver_next(d);
    241     Slice s;
    242     RelocKind k;
    243     if (name.kind != ASM_TOK_IDENT)
    244       asm_driver_panic(d, "arm32 asm: expected relocation modifier");
    245     s = pool_slice(asm_driver_pool(d), name.v.ident);
    246     if (slice_eq_cstr(s, "lower16"))
    247       k = R_ARM_THM_MOVW_ABS_NC;
    248     else if (slice_eq_cstr(s, "upper16"))
    249       k = R_ARM_THM_MOVT_ABS;
    250     else
    251       asm_driver_panic(d, "arm32 asm: unsupported relocation modifier");
    252     if ((k == R_ARM_THM_MOVT_ABS) != (is_movt != 0))
    253       asm_driver_panic(d, "arm32 asm: lower16/upper16 mismatched with movw/movt");
    254     asm_driver_expect_punct(d, ':', "':' closing relocation modifier");
    255     {
    256       ObjSymId sym = OBJ_SYM_NONE;
    257       i64 off = 0;
    258       asm_driver_parse_sym_expr(d, &sym, &off);
    259       *kind_out = k;
    260       *sym_out = sym;
    261       *off_out = off;
    262     }
    263     return 1;
    264   }
    265 }
    266 
    267 /* Optional shift `, lsl #n` etc. on a register operand; returns the shift type
    268  * (0..3) and amount, 0/0 if absent. */
    269 static u32 parse_opt_shift(AsmDriver* d, u32* amount_out) {
    270   Slice s;
    271   AsmTok t;
    272   *amount_out = 0;
    273   t = asm_driver_peek(d);
    274   if (t.kind != ASM_TOK_IDENT) return 0;
    275   s = pool_slice(asm_driver_pool(d), t.v.ident);
    276   u32 type;
    277   if (slice_eq_cstr(s, "lsl"))
    278     type = 0;
    279   else if (slice_eq_cstr(s, "lsr"))
    280     type = 1;
    281   else if (slice_eq_cstr(s, "asr"))
    282     type = 2;
    283   else if (slice_eq_cstr(s, "ror"))
    284     type = 3;
    285   else
    286     return 0;
    287   (void)asm_driver_next(d); /* the shift mnemonic */
    288   *amount_out = (u32)parse_imm(d);
    289   return type;
    290 }
    291 
    292 /* Barrier option: `sy` (full system) or a bare numeric. */
    293 static u32 parse_barrier_opt(AsmDriver* d) {
    294   AsmTok t = asm_driver_peek(d);
    295   if (t.kind == ASM_TOK_IDENT) {
    296     Slice s = pool_slice(asm_driver_pool(d), t.v.ident);
    297     if (slice_eq_cstr(s, "sy")) {
    298       (void)asm_driver_next(d);
    299       return 0xfu;
    300     }
    301   }
    302   if (asm_driver_at_eol(d)) return 0xfu; /* default SY */
    303   return (u32)asm_driver_parse_const(d) & 0xfu;
    304 }
    305 
    306 /* =====================================================================
    307  * Mnemonic resolution: strip a trailing condition-code suffix to recover the
    308  * base mnemonic + condition (e.g. `bne`->`b`/ne, `beq.w`->`b.w`/eq,
    309  * `moveq`->`mov`/eq). Returns the base descriptor and writes *cond_out
    310  * (ARM_CC_AL if unconditional). The condition is encoded only by the branch
    311  * formats; DP/etc. conditional forms require an enclosing IT block and the
    312  * assembler accepts the suffix but the IT instruction supplies the predicate.
    313  * ===================================================================== */
    314 static const Arm32InsnDesc* resolve_mnemonic(AsmDriver* d, Slice mn,
    315                                              u32* cond_out) {
    316   const Arm32InsnDesc* desc = arm32_asm_find(mn);
    317   *cond_out = ARM_CC_AL;
    318   if (desc) return desc;
    319   /* Try a trailing ".w" + 2-char cond (e.g. "bne.w" -> base "b.w"). */
    320   if (mn.len >= 4 && mn.s[mn.len - 2] == '.' && mn.s[mn.len - 1] == 'w') {
    321     int cv = arm32_cond_from_name(arm_slice(mn.s + mn.len - 4, 2));
    322     if (cv >= 0) {
    323       char buf[24];
    324       size_t base = mn.len - 4;
    325       if (base + 2 < sizeof buf) {
    326         memcpy(buf, mn.s, base);
    327         buf[base] = '.';
    328         buf[base + 1] = 'w';
    329         desc = arm32_asm_find(arm_slice(buf, base + 2));
    330         if (desc) {
    331           *cond_out = (u32)cv;
    332           return desc;
    333         }
    334       }
    335     }
    336   }
    337   /* Try a trailing 2-char cond (e.g. "bne" -> "b", "moveq" -> "mov"). */
    338   if (mn.len >= 2) {
    339     int cv = arm32_cond_from_name(arm_slice(mn.s + mn.len - 2, 2));
    340     if (cv >= 0) {
    341       desc = arm32_asm_find(arm_slice(mn.s, mn.len - 2));
    342       if (desc) {
    343         *cond_out = (u32)cv;
    344         return desc;
    345       }
    346     }
    347   }
    348   asm_driver_panic(d, "arm32 asm: unsupported instruction");
    349 }
    350 
    351 /* Encode the third operand of a general `add`/`sub rd, rn, <#imm | rm>` once rd
    352  * and rn are parsed and the separating comma consumed. There is no 16-bit
    353  * non-flag add/sub-immediate or 3-register form, so a register third operand
    354  * takes the 32-bit add.w/sub.w, and an immediate takes the densest of: the
    355  * 16-bit sp-adjust / add-from-sp forms, the 32-bit modified-immediate
    356  * (add.w/sub.w), then the 12-bit addw/subw. A negative immediate flips the
    357  * operation. The 16-bit hi-register `add rdn, rm` (2 operands) is handled at the
    358  * call site, not here. */
    359 static void arm_asm_addsub_third(AsmDriver* d, int is_sub, u32 rd, u32 rn) {
    360   i64 simm;
    361   u32 mag, out12;
    362   if (peek_is_reg(d)) {
    363     u32 rm = parse_reg(d);
    364     emit_t32(d, is_sub ? arm_sub_reg(rd, rn, rm) : arm_add_reg(rd, rn, rm));
    365     return;
    366   }
    367   simm = parse_imm(d);
    368   if (simm < 0) {
    369     is_sub = !is_sub;
    370     simm = -simm;
    371   }
    372   mag = (u32)simm;
    373   if (rd == 13u && rn == 13u && (mag & 3u) == 0u && (mag >> 2) <= 0x7fu) {
    374     emit_t16(d, (u16)((is_sub ? 0xb080u : 0xb000u) | ((mag >> 2) & 0x7fu)));
    375   } else if (!is_sub && rn == 13u && rd <= 7u && (mag & 3u) == 0u &&
    376              (mag >> 2) <= 0xffu) {
    377     emit_t16(d, (u16)(0xa800u | ((rd & 7u) << 8) | ((mag >> 2) & 0xffu)));
    378   } else if (thumb_expand_imm_encode(mag, &out12)) {
    379     emit_t32(d, arm_dp_imm(is_sub ? 13u : 8u, 0u, rd, rn, out12));
    380   } else if (mag <= 0xfffu) {
    381     emit_t32(d, is_sub ? arm_sub_imm12(rd, rn, mag) : arm_add_imm12(rd, rn, mag));
    382   } else {
    383     asm_driver_panic(d, "arm32 asm: add/sub immediate not encodable");
    384   }
    385 }
    386 
    387 /* Encode + emit one instruction for the matched descriptor. */
    388 static void assemble_one(AsmDriver* d, const Arm32InsnDesc* desc, u32 cond) {
    389   Slice mn = desc->mnemonic;
    390   switch ((Arm32Format)desc->fmt) {
    391     case ARM_FMT_NONE: {
    392       emit_t16(d, (u16)(desc->match & 0xffffu));
    393       return;
    394     }
    395     case ARM_FMT_DP_REG:
    396     case ARM_FMT_SHIFT_REG: {
    397       u32 rd = parse_reg(d);
    398       u32 rn, rm;
    399       expect_comma(d);
    400       rn = parse_reg(d);
    401       expect_comma(d);
    402       rm = parse_reg(d);
    403       /* hw1 = base|rn, hw2 = (rd<<8)|rm (shift-reg uses hw2 0xF000 base). */
    404       if ((Arm32Format)desc->fmt == ARM_FMT_SHIFT_REG)
    405         emit_t32(d, desc->match | (rn << 16) | (rd << 8) | rm);
    406       else
    407         emit_t32(d, desc->match | (rn << 16) | (rd << 8) | rm);
    408       return;
    409     }
    410     case ARM_FMT_DP_IMM: {
    411       /* The mnemonic table prefers the modified-immediate row, but `add.w r0,
    412        * r1, r2` etc. take a register third operand. Dispatch on shape: a
    413        * register operand emits the shifted-register form (hw1 0xEAxx; the
    414        * op4/S selector bits sit in the same positions as the imm form). */
    415       u32 rd = parse_reg(d), rn;
    416       expect_comma(d);
    417       rn = parse_reg(d);
    418       expect_comma(d);
    419       if (peek_is_reg(d)) {
    420         u32 rm = parse_reg(d);
    421         u32 base = (desc->match & ~(0xf000u << 16)) | (0xea00u << 16);
    422         emit_t32(d, base | (rn << 16) | (rd << 8) | rm);
    423       } else {
    424         u32 out12 = parse_modimm(d);
    425         u32 i = (out12 >> 11) & 1u, imm3 = (out12 >> 8) & 7u,
    426             imm8 = out12 & 0xffu;
    427         emit_t32(d, desc->match | (i << 26) | (rn << 16) | (imm3 << 12) |
    428                         (rd << 8) | imm8);
    429       }
    430       return;
    431     }
    432     case ARM_FMT_MOV_IMM: {
    433       /* `mov.w rd, #imm` (this row) or `mov.w rd, rm` (register form). The
    434        * register form is ORR rd, 1111, rm (hw1 0xEA4F for MOV / 0xEA6F MVN). */
    435       u32 rd = parse_reg(d);
    436       expect_comma(d);
    437       if (peek_is_reg(d)) {
    438         u32 rm = parse_reg(d);
    439         u32 mvn = slice_eq_cstr(mn, "mvn.w");
    440         emit_t32(d, arm_t32(mvn ? 0xea6fu : 0xea4fu, (rd << 8) | rm));
    441         return;
    442       }
    443       {
    444         u32 out12 = parse_modimm(d);
    445         u32 i = (out12 >> 11) & 1u, imm3 = (out12 >> 8) & 7u,
    446             imm8 = out12 & 0xffu;
    447         emit_t32(d, desc->match | (i << 26) | (imm3 << 12) | (rd << 8) | imm8);
    448       }
    449       return;
    450     }
    451     case ARM_FMT_MOV_REG: {
    452       u32 rd = parse_reg(d), rm;
    453       expect_comma(d);
    454       rm = parse_reg(d);
    455       emit_t32(d, desc->match | (rd << 8) | rm);
    456       return;
    457     }
    458     case ARM_FMT_CMP_REG: {
    459       u32 rn = parse_reg(d), rm;
    460       expect_comma(d);
    461       rm = parse_reg(d);
    462       emit_t32(d, desc->match | (rn << 16) | rm);
    463       return;
    464     }
    465     case ARM_FMT_CMP_IMM: {
    466       u32 rn = parse_reg(d), out12;
    467       expect_comma(d);
    468       if (peek_is_reg(d)) {
    469         u32 rm = parse_reg(d);
    470         u32 base = (desc->match & ~(0xf000u << 16)) | (0xea00u << 16);
    471         emit_t32(d, base | (rn << 16) | rm);
    472         return;
    473       }
    474       out12 = parse_modimm(d);
    475       {
    476         u32 i = (out12 >> 11) & 1u, imm3 = (out12 >> 8) & 7u,
    477             imm8 = out12 & 0xffu;
    478         emit_t32(d, desc->match | (i << 26) | (rn << 16) | (imm3 << 12) | imm8);
    479       }
    480       return;
    481     }
    482     case ARM_FMT_MOVW: {
    483       int is_movt = slice_eq_cstr(mn, "movt");
    484       u32 rd = parse_reg(d);
    485       RelocKind k;
    486       ObjSymId sym;
    487       i64 off;
    488       expect_comma(d);
    489       if (parse_movw_mod(d, &k, &sym, &off, is_movt)) {
    490         MCEmitter* mc = asm_driver_mc(d);
    491         u32 base = is_movt ? arm_movt(rd, 0) : arm_movw(rd, 0);
    492         mc_emit_reloc_at(mc, mc->section_id, mc_pos(mc), k, sym, off, 0, 0);
    493         emit_t32(d, base);
    494       } else {
    495         u32 imm16 = (u32)parse_imm(d) & 0xffffu;
    496         emit_t32(d, is_movt ? arm_movt(rd, imm16) : arm_movw(rd, imm16));
    497       }
    498       return;
    499     }
    500     case ARM_FMT_ADDW: {
    501       u32 rd = parse_reg(d), rn, imm12;
    502       expect_comma(d);
    503       rn = parse_reg(d);
    504       expect_comma(d);
    505       imm12 = (u32)parse_imm(d) & 0xfffu;
    506       emit_t32(d, slice_eq_cstr(mn, "subw") ? arm_sub_imm12(rd, rn, imm12)
    507                                             : arm_add_imm12(rd, rn, imm12));
    508       return;
    509     }
    510     case ARM_FMT_SHIFT_IMM: {
    511       u32 type = (desc->match >> 4) & 3u; /* hw2[5:4] */
    512       u32 rd = parse_reg(d), rm, sh;
    513       expect_comma(d);
    514       rm = parse_reg(d);
    515       expect_comma(d);
    516       sh = (u32)parse_imm(d);
    517       emit_t32(d, arm_shift_imm(type, rd, rm, sh));
    518       return;
    519     }
    520     case ARM_FMT_MUL: {
    521       u32 rd = parse_reg(d), rn, rm;
    522       expect_comma(d);
    523       rn = parse_reg(d);
    524       expect_comma(d);
    525       rm = parse_reg(d);
    526       emit_t32(d, arm_mul(rd, rn, rm));
    527       return;
    528     }
    529     case ARM_FMT_MLA: {
    530       u32 rd = parse_reg(d), rn, rm, ra;
    531       expect_comma(d);
    532       rn = parse_reg(d);
    533       expect_comma(d);
    534       rm = parse_reg(d);
    535       expect_comma(d);
    536       ra = parse_reg(d);
    537       emit_t32(d, slice_eq_cstr(mn, "mls") ? arm_mls(rd, rn, rm, ra)
    538                                            : arm_mla(rd, rn, rm, ra));
    539       return;
    540     }
    541     case ARM_FMT_DIV: {
    542       u32 rd = parse_reg(d), rn, rm;
    543       expect_comma(d);
    544       rn = parse_reg(d);
    545       expect_comma(d);
    546       rm = parse_reg(d);
    547       emit_t32(d, slice_eq_cstr(mn, "udiv") ? arm_udiv(rd, rn, rm)
    548                                             : arm_sdiv(rd, rn, rm));
    549       return;
    550     }
    551     case ARM_FMT_MULL: {
    552       u32 rdlo = parse_reg(d), rdhi, rn, rm;
    553       expect_comma(d);
    554       rdhi = parse_reg(d);
    555       expect_comma(d);
    556       rn = parse_reg(d);
    557       expect_comma(d);
    558       rm = parse_reg(d);
    559       emit_t32(d, slice_eq_cstr(mn, "smull") ? arm_smull(rdlo, rdhi, rn, rm)
    560                                              : arm_umull(rdlo, rdhi, rn, rm));
    561       return;
    562     }
    563     case ARM_FMT_EXT: {
    564       u32 rd = parse_reg(d), rm;
    565       expect_comma(d);
    566       rm = parse_reg(d);
    567       emit_t32(d, desc->match | (rd << 8) | rm);
    568       return;
    569     }
    570     case ARM_FMT_REV: {
    571       u32 rd = parse_reg(d), rm;
    572       expect_comma(d);
    573       rm = parse_reg(d);
    574       /* match has rm in hw1[3:0] and hw2[3:0]; both equal rm for REV/CLZ. */
    575       emit_t32(d, (desc->match & 0xfff0fff0u) | (rm << 16) | (rd << 8) | rm);
    576       return;
    577     }
    578     case ARM_FMT_BFX: {
    579       u32 rd = parse_reg(d), rn, lsb, width;
    580       expect_comma(d);
    581       rn = parse_reg(d);
    582       expect_comma(d);
    583       lsb = (u32)parse_imm(d);
    584       expect_comma(d);
    585       width = (u32)parse_imm(d);
    586       emit_t32(d, slice_eq_cstr(mn, "ubfx") ? arm_ubfx(rd, rn, lsb, width)
    587                                             : arm_sbfx(rd, rn, lsb, width));
    588       return;
    589     }
    590     case ARM_FMT_BFI: {
    591       u32 rd = parse_reg(d), rn, lsb, width;
    592       expect_comma(d);
    593       rn = parse_reg(d);
    594       expect_comma(d);
    595       lsb = (u32)parse_imm(d);
    596       expect_comma(d);
    597       width = (u32)parse_imm(d);
    598       emit_t32(d, arm_bfi(rd, rn, lsb, width));
    599       return;
    600     }
    601     case ARM_FMT_BFC: {
    602       u32 rd = parse_reg(d), lsb, width;
    603       expect_comma(d);
    604       lsb = (u32)parse_imm(d);
    605       expect_comma(d);
    606       width = (u32)parse_imm(d);
    607       emit_t32(d, arm_bfc(rd, lsb, width));
    608       return;
    609     }
    610     case ARM_FMT_SAT: {
    611       /* SSAT/USAT rd, #sat, rm (no-shift first cut; mnemonic picks signed vs
    612        * unsigned). */
    613       u32 rd = parse_reg(d), sat, rm;
    614       expect_comma(d);
    615       sat = (u32)parse_imm(d);
    616       expect_comma(d);
    617       rm = parse_reg(d);
    618       emit_t32(d, slice_eq_cstr(mn, "usat") ? arm_usat(rd, sat, rm)
    619                                             : arm_ssat(rd, sat, rm));
    620       return;
    621     }
    622     case ARM_FMT_QADD: {
    623       /* QADD/QSUB/QDADD/QDSUB rd, rm, rn (ARM operand order). */
    624       u32 rd = parse_reg(d), rm, rn;
    625       expect_comma(d);
    626       rm = parse_reg(d);
    627       expect_comma(d);
    628       rn = parse_reg(d);
    629       emit_t32(d, slice_eq_cstr(mn, "qsub")    ? arm_qsub(rd, rm, rn)
    630                   : slice_eq_cstr(mn, "qdadd") ? arm_qdadd(rd, rm, rn)
    631                   : slice_eq_cstr(mn, "qdsub") ? arm_qdsub(rd, rm, rn)
    632                                                : arm_qadd(rd, rm, rn));
    633       return;
    634     }
    635     case ARM_FMT_LDST_T3: {
    636       u32 rt = parse_reg(d), base;
    637       i64 disp;
    638       expect_comma(d);
    639       parse_mem(d, &base, &disp);
    640       if (disp < 0) {
    641         /* negative offset -> emit the T4 (±imm8) form. */
    642         u32 t4 = (desc->match & 0xfff00000u) - 0x00100000u; /* not used */
    643         (void)t4;
    644         asm_driver_panic(d, "arm32 asm: use the T4 (#-imm8) form for negative offsets");
    645       }
    646       emit_t32(d, (desc->match & 0xfff00000u) | (base << 16) | (rt << 12) |
    647                       ((u32)disp & 0xfffu));
    648       return;
    649     }
    650     case ARM_FMT_LDST_T4: {
    651       u32 rt = parse_reg(d), base;
    652       i64 disp;
    653       u32 add, imm8;
    654       expect_comma(d);
    655       parse_mem(d, &base, &disp);
    656       add = disp >= 0 ? 1u : 0u;
    657       imm8 = (u32)(disp < 0 ? -disp : disp) & 0xffu;
    658       emit_t32(d, (desc->match & 0xfff00000u) | (base << 16) | (rt << 12) |
    659                       0xc00u | (add << 9) | imm8);
    660       return;
    661     }
    662     case ARM_FMT_LDREX: {
    663       u32 rt = parse_reg(d), base;
    664       i64 disp = 0;
    665       expect_comma(d);
    666       parse_mem(d, &base, &disp);
    667       emit_t32(d, arm_ldrex(rt, base, (u32)(disp / 4)));
    668       return;
    669     }
    670     case ARM_FMT_STREX: {
    671       u32 rd = parse_reg(d), rt, base;
    672       i64 disp = 0;
    673       expect_comma(d);
    674       rt = parse_reg(d);
    675       expect_comma(d);
    676       parse_mem(d, &base, &disp);
    677       emit_t32(d, arm_strex(rd, rt, base, (u32)(disp / 4)));
    678       return;
    679     }
    680     case ARM_FMT_PUSHPOP: {
    681       u32 list = parse_reglist(d);
    682       emit_t32(d, slice_eq_cstr(mn, "pop.w") ? arm_pop_w(list)
    683                                              : arm_push_w(list));
    684       return;
    685     }
    686     case ARM_FMT_BARRIER: {
    687       u32 opt = parse_barrier_opt(d);
    688       emit_t32(d, (desc->match & 0xfffffff0u) | (opt & 0xfu));
    689       return;
    690     }
    691     case ARM_FMT_TB: {
    692       u32 rn, rm, sh;
    693       asm_driver_expect_punct(d, '[', "'[' in tbb/tbh operand");
    694       rn = parse_reg(d);
    695       expect_comma(d);
    696       rm = parse_reg(d);
    697       (void)parse_opt_shift(d, &sh); /* lsl #1 for tbh — implied by mnemonic */
    698       asm_driver_expect_punct(d, ']', "']' in tbb/tbh operand");
    699       emit_t32(d, slice_eq_cstr(mn, "tbh") ? arm_tbh(rn, rm) : arm_tbb(rn, rm));
    700       return;
    701     }
    702     case ARM_FMT_BL: {
    703       /* Placeholder MUST match codegen's arm_bl(): the THM_CALL reloc is REL and
    704        * the patcher preserves the field's J1/J2 (XOR-S) base, so an ad-hoc
    705        * placeholder whose J1/J2 imply a nonzero in-field offset corrupts the
    706        * applied target (a +0xC00000 skew on cross-object calls). */
    707       emit_t32(d, arm_bl());                   /* placeholder (== codegen) */
    708       /* reloc rides the BL we just emitted (offset = its start). */
    709       {
    710         MCEmitter* mc = asm_driver_mc(d);
    711         ObjSymId sym = OBJ_SYM_NONE;
    712         i64 off = 0;
    713         u32 pos = mc_pos(mc) - 4u;
    714         asm_driver_parse_sym_expr(d, &sym, &off);
    715         mc_emit_reloc_at(mc, mc->section_id, pos, R_ARM_THM_CALL, sym, off, 0,
    716                          0);
    717       }
    718       return;
    719     }
    720     case ARM_FMT_BRANCH_T4: {
    721       emit_t32(d, arm_b_w());
    722       {
    723         MCEmitter* mc = asm_driver_mc(d);
    724         ObjSymId sym = OBJ_SYM_NONE;
    725         i64 off = 0;
    726         u32 pos = mc_pos(mc) - 4u;
    727         asm_driver_parse_sym_expr(d, &sym, &off);
    728         mc_emit_reloc_at(mc, mc->section_id, pos, R_ARM_THM_JUMP24, sym, off, 0,
    729                          0);
    730       }
    731       return;
    732     }
    733     case ARM_FMT_BRANCH_T3: {
    734       emit_t32(d, arm_b_cond_w(cond));
    735       {
    736         MCEmitter* mc = asm_driver_mc(d);
    737         ObjSymId sym = OBJ_SYM_NONE;
    738         i64 off = 0;
    739         u32 pos = mc_pos(mc) - 4u;
    740         asm_driver_parse_sym_expr(d, &sym, &off);
    741         mc_emit_reloc_at(mc, mc->section_id, pos, R_ARM_THM_JUMP19, sym, off, 0,
    742                          0);
    743       }
    744       return;
    745     }
    746     case ARM_FMT_B16:
    747     case ARM_FMT_BCC16: {
    748       /* 16-bit branches: emit the 32-bit wide form so the relocation has the
    749        * full range (the disassembler still round-trips the wide encoding). */
    750       if (cond == ARM_CC_AL) {
    751         emit_t32(d, arm_b_w());
    752         {
    753           MCEmitter* mc = asm_driver_mc(d);
    754           ObjSymId sym = OBJ_SYM_NONE;
    755           i64 off = 0;
    756           u32 pos = mc_pos(mc) - 4u;
    757           asm_driver_parse_sym_expr(d, &sym, &off);
    758           mc_emit_reloc_at(mc, mc->section_id, pos, R_ARM_THM_JUMP24, sym, off,
    759                            0, 0);
    760         }
    761       } else {
    762         emit_t32(d, arm_b_cond_w(cond));
    763         {
    764           MCEmitter* mc = asm_driver_mc(d);
    765           ObjSymId sym = OBJ_SYM_NONE;
    766           i64 off = 0;
    767           u32 pos = mc_pos(mc) - 4u;
    768           asm_driver_parse_sym_expr(d, &sym, &off);
    769           mc_emit_reloc_at(mc, mc->section_id, pos, R_ARM_THM_JUMP19, sym, off,
    770                            0, 0);
    771         }
    772       }
    773       return;
    774     }
    775     case ARM_FMT_CBZ: {
    776       u32 rn = parse_reg(d);
    777       i64 imm;
    778       expect_comma(d);
    779       (void)asm_driver_eat_punct(d, '#');
    780       imm = asm_driver_parse_const(d);
    781       {
    782         /* imm is the PC-relative byte offset (must be +4..+130, even). The
    783          * encoded imm6 is (off-4)/2 half-words from the branch. */
    784         i64 hw = (imm - 4) / 2;
    785         u32 op = slice_eq_cstr(mn, "cbnz") ? 1u : 0u;
    786         if (imm < 4 || imm > 130 || (imm & 1))
    787           asm_driver_panic(d, "arm32 asm: cbz/cbnz target out of range");
    788         emit_t16(d, arm_cbz_raw(op, rn, (u32)hw));
    789       }
    790       return;
    791     }
    792     case ARM_FMT_IT: {
    793       /* `it<x><y><z> cc` — the suffix letters arrive folded into the mnemonic
    794        * (e.g. "itte"); we recover the mask from the suffix + the firstcond. */
    795       AsmTok t = asm_driver_next(d);
    796       Slice ccn;
    797       int fc;
    798       u32 mask;
    799       size_t nletters = mn.len - 2; /* letters after "it" */
    800       if (t.kind != ASM_TOK_IDENT)
    801         asm_driver_panic(d, "arm32 asm: IT expects a condition");
    802       ccn = pool_slice(asm_driver_pool(d), t.v.ident);
    803       fc = arm32_cond_from_name(ccn);
    804       if (fc < 0) asm_driver_panic(d, "arm32 asm: bad IT condition");
    805       /* Build the mask: bit3=1; for each of the up-to-3 suffix letters, bit
    806        * (3-k) is set to firstcond[0] for 't' (then) or its inverse for 'e'. */
    807       mask = 0x8u;
    808       {
    809         u32 then = (u32)fc & 1u;
    810         for (size_t k = 0; k < nletters && k < 3; ++k) {
    811           u32 bit = (mn.s[2 + k] == 't') ? then : (then ^ 1u);
    812           mask |= (bit << (3u - (u32)(k + 1)));
    813         }
    814         /* The lowest set bit position marks the block end. */
    815         mask |= (1u << (3u - (u32)nletters));
    816       }
    817       emit_t16(d, arm_it((u32)fc, mask));
    818       return;
    819     }
    820     case ARM_FMT_MOVHI16: {
    821       u32 rd = parse_reg(d), rm;
    822       expect_comma(d);
    823       /* `mov rd, #imm` selects the 32-bit mov.w (preserving mov's no-flags
    824        * semantics; conditionalizable inside an IT block as `moveq`/etc.). */
    825       if (!peek_is_reg(d)) {
    826         u32 out12;
    827         if (!thumb_expand_imm_encode((u32)parse_imm(d), &out12))
    828           asm_driver_panic(d, "arm32 asm: mov immediate not encodable");
    829         emit_t32(d, arm_mov_imm(rd, out12));
    830         return;
    831       }
    832       rm = parse_reg(d);
    833       emit_t16(d, arm_mov_hi(rd, rm));
    834       return;
    835     }
    836     case ARM_FMT_BX: {
    837       u32 rm = parse_reg(d);
    838       emit_t16(d, slice_eq_cstr(mn, "blx") ? arm_blx_reg(rm) : arm_bx(rm));
    839       return;
    840     }
    841     case ARM_FMT_BKPT: {
    842       u32 imm8 = (u32)parse_imm(d) & 0xffu;
    843       emit_t16(d, slice_eq_cstr(mn, "udf") ? arm_udf(imm8) : arm_bkpt(imm8));
    844       return;
    845     }
    846     case ARM_FMT_EXT16: {
    847       u32 rd = parse_reg(d), rm;
    848       expect_comma(d);
    849       rm = parse_reg(d);
    850       emit_t16(d, (u16)((desc->match & 0xffc0u) | ((rm & 7u) << 3) | (rd & 7u)));
    851       return;
    852     }
    853     case ARM_FMT_DPI8_16: {
    854       u32 rd = parse_reg(d), imm8;
    855       expect_comma(d);
    856       imm8 = (u32)parse_imm(d) & 0xffu;
    857       emit_t16(d, (u16)((desc->match & 0xf800u) | ((rd & 7u) << 8) | imm8));
    858       return;
    859     }
    860     case ARM_FMT_ADDSUB3_16: {
    861       u32 rd = parse_reg(d), rn, imm3;
    862       expect_comma(d);
    863       rn = parse_reg(d);
    864       expect_comma(d);
    865       imm3 = (u32)parse_imm(d) & 7u;
    866       emit_t16(d, (u16)((desc->match & 0xfe00u) | (imm3 << 6) | ((rn & 7u) << 3) |
    867                         (rd & 7u)));
    868       return;
    869     }
    870     case ARM_FMT_ADDSUBR_16: {
    871       u32 rd = parse_reg(d), rn, rm;
    872       expect_comma(d);
    873       rn = parse_reg(d);
    874       expect_comma(d);
    875       rm = parse_reg(d);
    876       emit_t16(d, (u16)((desc->match & 0xfe00u) | ((rm & 7u) << 6) |
    877                         ((rn & 7u) << 3) | (rd & 7u)));
    878       return;
    879     }
    880     case ARM_FMT_SHIFTI_16: {
    881       u32 rd = parse_reg(d), rm, imm5;
    882       expect_comma(d);
    883       rm = parse_reg(d);
    884       expect_comma(d);
    885       imm5 = (u32)parse_imm(d) & 0x1fu;
    886       emit_t16(d, (u16)((desc->match & 0xf800u) | (imm5 << 6) | ((rm & 7u) << 3) |
    887                         (rd & 7u)));
    888       return;
    889     }
    890     case ARM_FMT_ALU_16: {
    891       u32 rdn = parse_reg(d), rm;
    892       expect_comma(d);
    893       rm = parse_reg(d);
    894       if (slice_eq_cstr(mn, "muls")) {
    895         /* muls rdm, rn, rdm — skip the (redundant) third operand if present. */
    896         if (asm_driver_eat_comma(d)) (void)parse_reg(d);
    897       }
    898       emit_t16(d, (u16)((desc->match & 0xffc0u) | ((rm & 7u) << 3) | (rdn & 7u)));
    899       return;
    900     }
    901     case ARM_FMT_HIREG_16: {
    902       u32 rdn = parse_reg(d), rm;
    903       expect_comma(d);
    904       /* `cmp` resolves to this hi-register compare first; an immediate second
    905        * operand (`cmp rn, #imm`) selects the modified-immediate compare — the
    906        * 16-bit DPI8 form when rn<=r7 and imm8 fits, else the 32-bit cmp.w. */
    907       if (!peek_is_reg(d) && slice_eq_cstr(mn, "cmp")) {
    908         i64 imm = parse_imm(d);
    909         if (rdn <= 7u && imm >= 0 && imm <= 255)
    910           emit_t16(d, (u16)(0x2800u | (rdn << 8) | (u32)(imm & 0xffu)));
    911         else {
    912           u32 out12;
    913           if (!thumb_expand_imm_encode((u32)imm, &out12))
    914             asm_driver_panic(d, "arm32 asm: cmp immediate not encodable");
    915           emit_t32(d, arm_cmp_imm(rdn, out12));
    916         }
    917         return;
    918       }
    919       rm = parse_reg(d);
    920       /* A third operand means the general `add rd, rn, <#imm | rm>` (rd=rdn,
    921        * rn=rm): there is no 16-bit non-flag add-immediate or 3-register form, so
    922        * route it through the shared encoder (which also covers the sp-relative
    923        * forms). Without a third operand this is the 2-operand hi-register
    924        * `add rdn, rm` below; the old SP-only special case dropped every other
    925        * immediate. */
    926       if (slice_eq_cstr(mn, "add") && asm_driver_eat_comma(d)) {
    927         arm_asm_addsub_third(d, 0, rdn, rm);
    928         return;
    929       }
    930       emit_t16(d, (u16)((desc->match & 0xff00u) | (((rdn >> 3) & 1u) << 7) |
    931                         ((rm & 0xfu) << 3) | (rdn & 7u)));
    932       return;
    933     }
    934     case ARM_FMT_LDSTI5_16: {
    935       u32 rt = parse_reg(d), base;
    936       i64 disp;
    937       u32 op, scale, imm5;
    938       expect_comma(d);
    939       parse_mem(d, &base, &disp);
    940       op = (desc->match >> 11) & 0x1fu;
    941       scale = (op <= 0x0du) ? 4u : (op <= 0x0fu) ? 1u : 2u;
    942       /* A word str/ldr through SP uses the dedicated SP-relative T2 encoding
    943        * (0x9000/0x9800, imm8*4); the low-register T1 form here would truncate
    944        * sp(r13) to r5. */
    945       if (base == 13u && rt <= 7u &&
    946           (slice_eq_cstr(mn, "str") || slice_eq_cstr(mn, "ldr"))) {
    947         u32 sp_op = slice_eq_cstr(mn, "ldr") ? 0x9800u : 0x9000u;
    948         emit_t16(d, (u16)(sp_op | ((rt & 7u) << 8) | (((u32)disp / 4u) & 0xffu)));
    949         return;
    950       }
    951       /* The 16-bit T1 forms only encode low transfer + base registers and a
    952        * scaled imm5. A high register (e.g. saving sp/lr in the coroutine switch,
    953        * `str lr, [r0, #36]`), a high base, or an out-of-range / unscalable
    954        * displacement falls to the 32-bit T3 form (hw1 op|rn, hw2 rt<<12|imm12). */
    955       if (rt > 7u || base > 7u || disp < 0 || (u32)disp > 31u * scale ||
    956           (u32)disp % scale != 0u) {
    957         u32 t3 = (desc->match == 0x6000u)   ? 0xf8c0u   /* str  */
    958                  : (desc->match == 0x6800u) ? 0xf8d0u   /* ldr  */
    959                  : (desc->match == 0x7000u) ? 0xf880u   /* strb */
    960                  : (desc->match == 0x7800u) ? 0xf890u   /* ldrb */
    961                  : (desc->match == 0x8000u) ? 0xf8a0u   /* strh */
    962                                             : 0xf8b0u;  /* ldrh */
    963         if (disp < 0 || disp > 4095)
    964           asm_driver_panic(d, "arm32 asm: ldr/str offset out of T3 range");
    965         emit_t32(d, arm_t32(t3 | (base & 0xfu),
    966                             ((rt & 0xfu) << 12) | ((u32)disp & 0xfffu)));
    967         return;
    968       }
    969       imm5 = ((u32)disp / scale) & 0x1fu;
    970       emit_t16(d, (u16)((desc->match & 0xf800u) | (imm5 << 6) |
    971                         ((base & 7u) << 3) | (rt & 7u)));
    972       return;
    973     }
    974     case ARM_FMT_LDSTSP_16: {
    975       u32 rt = parse_reg(d), base;
    976       i64 disp;
    977       expect_comma(d);
    978       parse_mem(d, &base, &disp);
    979       emit_t16(d, (u16)((desc->match & 0xf800u) | ((rt & 7u) << 8) |
    980                         (((u32)disp / 4u) & 0xffu)));
    981       return;
    982     }
    983     case ARM_FMT_ADDSP_16: {
    984       u32 rd = parse_reg(d), imm8;
    985       expect_comma(d);
    986       (void)parse_reg(d); /* sp/pc — implied by the mnemonic/encoding */
    987       expect_comma(d);
    988       imm8 = ((u32)parse_imm(d) / 4u) & 0xffu;
    989       emit_t16(d, (u16)((desc->match & 0xf800u) | ((rd & 7u) << 8) | imm8));
    990       return;
    991     }
    992     case ARM_FMT_ADJSP_16: {
    993       /* Plain `sub` (and the unreachable plain-`add` row) land here. Parse the
    994        * real registers and route through the shared add/sub encoder, which picks
    995        * the 16-bit sp-adjust when rd==rn==sp. Accept the 2-operand shorthand
    996        * `sub rd, #imm` (== `sub rd, rd, #imm`); the old code assumed sp/sp and
    997        * silently mis-encoded `sub rN, rN, #imm` as `sub sp, sp, #...`. */
    998       int is_sub = slice_eq_cstr(mn, "sub");
    999       u32 rd = parse_reg(d), rn;
   1000       expect_comma(d);
   1001       if (peek_is_reg(d)) {
   1002         rn = parse_reg(d);
   1003         expect_comma(d);
   1004       } else {
   1005         rn = rd;
   1006       }
   1007       arm_asm_addsub_third(d, is_sub, rd, rn);
   1008       return;
   1009     }
   1010     case ARM_FMT_PUSHPOP_16: {
   1011       u32 list = parse_reglist(d);
   1012       u32 lo = list & 0xffu;
   1013       u32 extra = slice_eq_cstr(mn, "pop") ? ((list >> 15) & 1u)
   1014                                            : ((list >> 14) & 1u);
   1015       emit_t16(d, (u16)((desc->match & 0xfe00u) | (extra << 8) | lo));
   1016       return;
   1017     }
   1018   }
   1019   asm_driver_panic(d, "arm32 asm: unhandled instruction format");
   1020 }
   1021 
   1022 static void arm32_arch_asm_insn(ArchAsm* base, AsmDriver* d, Sym mnemonic) {
   1023   const Arm32InsnDesc* desc;
   1024   u32 cond = ARM_CC_AL;
   1025   (void)base;
   1026   (void)asm_driver_cur_section(d); /* ensure .text exists */
   1027   desc = resolve_mnemonic(d, pool_slice(asm_driver_pool(d), mnemonic), &cond);
   1028   assemble_one(d, desc, cond);
   1029 }
   1030 
   1031 void arm32_asm_insn(Arm32Asm* a, AsmDriver* d, Sym mnemonic) {
   1032   arm32_arch_asm_insn(&a->base, d, mnemonic);
   1033 }
   1034 
   1035 static void arm32_arch_asm_destroy(ArchAsm* base) { (void)base; }
   1036 
   1037 static void run_one_line(Arm32Asm* a, MCEmitter* mc, const char* text,
   1038                          size_t len) {
   1039   size_t i;
   1040   for (i = 0; i < len; ++i) {
   1041     if (text[i] != ' ' && text[i] != '\t') break;
   1042   }
   1043   if (i == len) return;
   1044 
   1045   AsmLexer* lx = asm_lex_open_mem(a->c, "<inline-asm>", text, len);
   1046   AsmDriver* d = asm_driver_open_inline(a->c, mc, lx);
   1047 
   1048   AsmTok t = asm_driver_peek(d);
   1049   while (t.kind == ASM_TOK_NEWLINE || t.kind == ASM_TOK_HASH) {
   1050     (void)asm_driver_next(d);
   1051     if (t.kind == ASM_TOK_HASH) {
   1052       while (!asm_driver_at_eol(d)) (void)asm_driver_next(d);
   1053     }
   1054     t = asm_driver_peek(d);
   1055   }
   1056   if (t.kind == ASM_TOK_EOF) {
   1057     asm_driver_close_inline(d);
   1058     asm_lex_close(lx);
   1059     return;
   1060   }
   1061   if (t.kind != ASM_TOK_IDENT)
   1062     inline_panic(a, "expected mnemonic at start of inline asm line");
   1063   (void)asm_driver_next(d);
   1064   Sym mn = t.v.ident;
   1065   AsmTok dot = asm_driver_peek(d);
   1066   while (asm_driver_tok_is_punct(dot, '.')) {
   1067     (void)asm_driver_next(d);
   1068     AsmTok rest = asm_driver_next(d);
   1069     if (rest.kind != ASM_TOK_IDENT)
   1070       inline_panic(a, "composite mnemonic: expected ident after '.'");
   1071     Slice hsl = pool_slice(asm_driver_pool(d), mn);
   1072     Slice rsl = pool_slice(asm_driver_pool(d), rest.v.ident);
   1073     char buf[64];
   1074     if (hsl.len + 1u + rsl.len >= sizeof buf)
   1075       inline_panic(a, "composite mnemonic too long");
   1076     memcpy(buf, hsl.s, hsl.len);
   1077     buf[hsl.len] = '.';
   1078     memcpy(buf + hsl.len + 1u, rsl.s, rsl.len);
   1079     mn = pool_intern_slice(asm_driver_pool(d),
   1080                            (Slice){.s = buf, .len = hsl.len + 1u + rsl.len});
   1081     dot = asm_driver_peek(d);
   1082   }
   1083   arm32_asm_insn(a, d, mn);
   1084   asm_driver_close_inline(d);
   1085   asm_lex_close(lx);
   1086 }
   1087 
   1088 static void render_and_run_line(Arm32Asm* a, MCEmitter* mc, StrBuf* sb,
   1089                                 const char* start, const char* end) {
   1090   strbuf_reset(sb);
   1091   for (const char* p = start; p < end; ++p) {
   1092     char c = *p;
   1093     if (c != '%') {
   1094       strbuf_putc(sb, c);
   1095       continue;
   1096     }
   1097     if (p + 1 >= end) inline_panic(a, "trailing '%' in template");
   1098     char n = *(p + 1);
   1099     if (n == '%') {
   1100       strbuf_putc(sb, '%');
   1101       ++p;
   1102       continue;
   1103     }
   1104     int form = 0;
   1105     if (n == 'a') {
   1106       form = 3;
   1107       ++p;
   1108       if (p + 1 >= end) inline_panic(a, "trailing '%' modifier in template");
   1109       n = *(p + 1);
   1110     }
   1111     if (n == '[') {
   1112       const char* nbeg = p + 2;
   1113       const char* nend = nbeg;
   1114       while (nend < end && *nend != ']') ++nend;
   1115       if (nend == end) inline_panic(a, "unterminated %[name]");
   1116       Sym needle = pool_intern_slice(
   1117           a->c->global, (Slice){.s = nbeg, .len = (size_t)(nend - nbeg)});
   1118       u32 idx = lookup_named(a, needle);
   1119       if (idx == (u32)-1)
   1120         inline_panic(a, "%[name] does not match any constraint");
   1121       p = nend;
   1122       render_operand(a, sb, idx, form);
   1123       continue;
   1124     }
   1125     if (n < '0' || n > '9') inline_panic(a, "expected digit after '%'");
   1126     u32 idx = (u32)(n - '0');
   1127     ++p;
   1128     if (p + 1 < end && *(p + 1) >= '0' && *(p + 1) <= '9') {
   1129       idx = idx * 10u + (u32)(*(p + 1) - '0');
   1130       ++p;
   1131     }
   1132     render_operand(a, sb, idx, form);
   1133   }
   1134   if (sb->truncated) inline_panic(a, "inline asm line buffer overflow");
   1135   run_one_line(a, mc, strbuf_cstr(sb), strbuf_len(sb));
   1136 }
   1137 
   1138 void arm32_asm_run_template(Arm32Asm* a, MCEmitter* mc, const char* tmpl) {
   1139   if (!tmpl || !*tmpl) return;
   1140 
   1141   char buf[512];
   1142   StrBuf sb;
   1143   strbuf_init(&sb, buf, sizeof buf);
   1144 
   1145   const char* line_start = tmpl;
   1146   int bracket = 0;
   1147   char quote = 0;
   1148   for (const char* p = tmpl;; ++p) {
   1149     char c = *p;
   1150     if (c == '\0') {
   1151       render_and_run_line(a, mc, &sb, line_start, p);
   1152       break;
   1153     }
   1154     if (quote) {
   1155       if (c == '\\' && *(p + 1)) {
   1156         ++p;
   1157         continue;
   1158       }
   1159       if (c == quote) quote = 0;
   1160       continue;
   1161     }
   1162     if (c == '"' || c == '\'') {
   1163       quote = c;
   1164       continue;
   1165     }
   1166     if (c == '[') {
   1167       ++bracket;
   1168       continue;
   1169     }
   1170     if (c == ']' && bracket > 0) {
   1171       --bracket;
   1172       continue;
   1173     }
   1174     if ((c == '\n' || c == ';') && bracket == 0) {
   1175       render_and_run_line(a, mc, &sb, line_start, p);
   1176       line_start = p + 1;
   1177     }
   1178   }
   1179 }
   1180 
   1181 /* ---- textual-assembly operand syntax (printer <-> parser seam) ----
   1182  * Inverse of the `.s` parsers above. ARM uses the GNU-as `:lower16:`/`:upper16:`
   1183  * prefix form for MOVW/MOVT symbol halves and bare numeric targets for branches
   1184  * (the symbolizer synthesizes labels for the local-branch set). */
   1185 static int arm32_reloc_operand(u16 kind, KitObjFmt fmt, ArchRelocOperand* out) {
   1186   (void)fmt;
   1187   out->prefix = "";
   1188   out->suffix = "";
   1189   out->addend_bias = 0;
   1190   out->emit_anchor = 0;
   1191   out->ref_anchor = 0;
   1192   switch (kind) {
   1193     case R_ARM_THM_MOVW_ABS_NC:
   1194       out->surg = ARCH_RELOC_SURG_TAIL;
   1195       out->prefix = "#:lower16:";
   1196       return 1;
   1197     case R_ARM_THM_MOVT_ABS:
   1198       out->surg = ARCH_RELOC_SURG_TAIL;
   1199       out->prefix = "#:upper16:";
   1200       return 1;
   1201     case R_ARM_THM_CALL:
   1202     case R_ARM_THM_JUMP24:
   1203     case R_ARM_THM_JUMP19:
   1204       out->surg = ARCH_RELOC_SURG_TAIL;
   1205       return 1;
   1206     default:
   1207       return 0; /* R_ABS32 / R_REL32 / TLS -> keep numeric */
   1208   }
   1209 }
   1210 
   1211 /* Intra-section local branches whose target the disassembler renders
   1212  * numerically; cc -S synthesizes a label there. Calls (bl) are excluded. */
   1213 static int arm32_is_local_branch(KitSlice m) {
   1214   if (m.len >= 1 && m.s[0] == 'b') {
   1215     if (slice_eq_cstr(m, "b") || slice_eq_cstr(m, "b.w")) return 1;
   1216     if (slice_eq_cstr(m, "cbz") || slice_eq_cstr(m, "cbnz")) return 1;
   1217     /* b<cond> / b<cond>.w */
   1218     {
   1219       size_t base = m.len;
   1220       if (m.len >= 3 && m.s[m.len - 2] == '.' && m.s[m.len - 1] == 'w')
   1221         base = m.len - 2;
   1222       if (base == 3) {
   1223         if (arm32_cond_from_name(arm_slice(m.s + 1, 2)) >= 0) return 1;
   1224       }
   1225     }
   1226   }
   1227   return 0;
   1228 }
   1229 
   1230 const ArchAsmOps arm32_asm_ops = {
   1231     .reloc_operand = arm32_reloc_operand,
   1232     .is_local_branch = arm32_is_local_branch,
   1233     /* ARM EABI: function symbols carry a Thumb (LSB) ISA-state bit; the
   1234      * standalone assembler's `.thumb_func` / `.size . - SYM` / label-define
   1235      * bookkeeping keys off this flag (see ArchAsmOps). */
   1236     .thumb_function_symbols = 1,
   1237 };
   1238 
   1239 ArchAsm* arm32_arch_asm_new(Compiler* c) {
   1240   Arm32Asm* a = arm32_asm_open(c);
   1241   a->base.insn = arm32_arch_asm_insn;
   1242   a->base.destroy = arm32_arch_asm_destroy;
   1243   return &a->base;
   1244 }