kit

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

asm.c (55290B)


      1 /* RV64 assembler — descriptor-table driven.
      2  *
      3  * Mnemonic → Rv64InsnDesc via rv64_asm_find; operand parsing dispatches
      4  * on the format kind. The descriptor's `match` field already carries
      5  * the funct3/funct7/opcode bits; the parser only needs to fill in the
      6  * register operands and immediate.
      7  *
      8  * Aliases (li, mv, ret, jr, j, nop, sext.w, beqz, bnez) are recognized
      9  * by their alias rows in the descriptor table and rewritten to the
     10  * canonical encoding here. Inline rv_* encoders in isa.h remain the
     11  * hot path for codegen; the assembler uses them to assemble the
     12  * machine word once it has the operand values. */
     13 
     14 #include "arch/riscv/asm.h"
     15 
     16 #include <string.h>
     17 
     18 #include "arch/riscv/isa.h"
     19 #include "arch/riscv/regs.h"
     20 #include "arch/riscv/rv64.h"
     21 #include "arch/riscv/variant.h"
     22 #include "asm/asm_helpers.h"
     23 #include "core/arena.h"
     24 #include "core/pool.h"
     25 #include "core/slice.h"
     26 #include "core/strbuf.h"
     27 #include "obj/obj.h"
     28 
     29 struct Rv64Asm {
     30   ArchAsm base;
     31   Compiler* c;
     32 
     33   /* Inline-asm bound state (set by rv64_inline_bind, cleared otherwise).
     34    * Operand indexing per GCC convention: 0..nout-1 are outputs, then
     35    * nout..nout+nin-1 are inputs. Templates address into this combined
     36    * list via %N / %zN / %aN / %w[name] / %x[name]. */
     37   const AsmConstraint* outs;
     38   Operand* out_ops;
     39   const AsmConstraint* ins;
     40   const Operand* in_ops;
     41   const Sym* clobbers;
     42   u32 nout;
     43   u32 nin;
     44   u32 nclob;
     45 };
     46 
     47 typedef struct Rv64Asm Rv64Asm;
     48 
     49 /* Relocation modifier on a 12-bit immediate offset (`%lo`/`%pcrel_lo`).
     50  * RV_MEMMOD_NONE means a plain numeric displacement in `disp`. */
     51 typedef enum RvMemMod {
     52   RV_MEMMOD_NONE = 0,
     53   RV_MEMMOD_LO,
     54   RV_MEMMOD_PCREL_LO,
     55 } RvMemMod;
     56 
     57 typedef struct Rv64Mem {
     58   i32 disp;
     59   u32 base;
     60   RvMemMod mod; /* reloc modifier on the offset, or RV_MEMMOD_NONE */
     61   ObjSymId sym; /* symbol when mod != NONE */
     62   i64 off;      /* addend when mod != NONE */
     63 } Rv64Mem;
     64 
     65 static int sym_to_cstr(AsmDriver* d, Sym s, char* out, size_t cap) {
     66   Slice sl = pool_slice(asm_driver_pool(d), s);
     67   if (!sl.s || sl.len >= cap) return 0;
     68   memcpy(out, sl.s, sl.len);
     69   out[sl.len] = '\0';
     70   return 1;
     71 }
     72 
     73 /* True if `s` begins with the NUL-terminated literal `pfx` (length-explicit).
     74  */
     75 static bool slice_has_prefix_cstr(Slice s, const char* pfx, size_t n) {
     76   return s.len >= n && memcmp(s.s, pfx, n) == 0;
     77 }
     78 
     79 static int rv_reg_from_name(AsmDriver* d, Sym s, u32* reg_out, int* fp_out) {
     80   char name[16];
     81   uint32_t dwarf = 0;
     82   if (!sym_to_cstr(d, s, name, sizeof name)) return 0;
     83   if (rv64_register_index(name, &dwarf) != 0) return 0;
     84   if (reg_out) *reg_out = dwarf & 31u;
     85   if (fp_out) *fp_out = dwarf >= 32u;
     86   return 1;
     87 }
     88 
     89 static u32 parse_reg(AsmDriver* d, int* fp_out) {
     90   AsmTok t = asm_driver_next(d);
     91   u32 r;
     92   if (t.kind != ASM_TOK_IDENT || !rv_reg_from_name(d, t.v.ident, &r, fp_out))
     93     asm_driver_panic(d, "rv64 asm: bad register");
     94   return r;
     95 }
     96 
     97 static u32 parse_xreg(AsmDriver* d) {
     98   int fp = 0;
     99   u32 r = parse_reg(d, &fp);
    100   if (fp) asm_driver_panic(d, "rv64 asm: expected integer register");
    101   return r;
    102 }
    103 
    104 static u32 parse_freg(AsmDriver* d) {
    105   int fp = 0;
    106   u32 r = parse_reg(d, &fp);
    107   if (!fp) asm_driver_panic(d, "rv64 asm: expected float register");
    108   return r;
    109 }
    110 
    111 static void expect_comma(AsmDriver* d) {
    112   if (!asm_driver_eat_comma(d)) asm_driver_panic(d, "rv64 asm: expected ','");
    113 }
    114 
    115 /* Parse a CSR operand: a standard CSR name (mstatus, mtvec, ...) or a bare
    116  * numeric expression. Returns the 12-bit CSR number. */
    117 static u32 parse_csr(AsmDriver* d) {
    118   AsmTok t = asm_driver_peek(d);
    119   if (t.kind == ASM_TOK_IDENT) {
    120     u16 num;
    121     if (rv64_csr_num_from_name(pool_slice(asm_driver_pool(d), t.v.ident),
    122                                &num)) {
    123       (void)asm_driver_next(d); /* consume the name */
    124       return (u32)num & 0xfffu;
    125     }
    126   }
    127   return (u32)asm_driver_parse_const(d) & 0xfffu;
    128 }
    129 
    130 /* Position of a `%mod(sym)` relocation operand: the 20-bit upper field of
    131  * lui/auipc, or a 12-bit I-type (addi/load) or S-type (store) immediate. */
    132 typedef enum RvModPos {
    133   RV_MODPOS_HI20,
    134   RV_MODPOS_LO_I,
    135   RV_MODPOS_LO_S,
    136 } RvModPos;
    137 
    138 /* Map a relocation-modifier name (`hi`, `lo`, `pcrel_hi`, `pcrel_lo`,
    139  * `got_pcrel_hi`) to the RelocKind appropriate for `pos`. Panics on a name
    140  * that is not valid at this operand position. */
    141 static RelocKind rv_mod_to_reloc(AsmDriver* d, Slice name, RvModPos pos) {
    142   if (pos == RV_MODPOS_HI20) {
    143     if (slice_eq_cstr(name, "hi")) return R_RV_HI20;
    144     if (slice_eq_cstr(name, "pcrel_hi")) return R_RV_PCREL_HI20;
    145     if (slice_eq_cstr(name, "got_pcrel_hi")) return R_RV_GOT_HI20;
    146   } else {
    147     int store = (pos == RV_MODPOS_LO_S);
    148     if (slice_eq_cstr(name, "lo")) return store ? R_RV_LO12_S : R_RV_LO12_I;
    149     if (slice_eq_cstr(name, "pcrel_lo"))
    150       return store ? R_RV_PCREL_LO12_S : R_RV_PCREL_LO12_I;
    151   }
    152   asm_driver_panic(d, "rv64 asm: relocation modifier not valid here");
    153 }
    154 
    155 /* If the next token is `%`, parse a `%mod(sym{+off})` relocation operand,
    156  * emit the relocation at the current emit position (where the about-to-be-
    157  * returned instruction word will land), and return 1. The caller encodes a
    158  * zero placeholder in the immediate field. Returns 0 if there is no modifier
    159  * (leaving the stream untouched for normal constant parsing). A leading `%`
    160  * is unambiguous here: modulo is infix and never starts an operand. */
    161 static int rv_parse_mod_reloc(AsmDriver* d, RvModPos pos, ObjSymId* sym_out,
    162                               i64* off_out, RelocKind* kind_out) {
    163   if (!asm_driver_tok_is_punct(asm_driver_peek(d), '%')) return 0;
    164   (void)asm_driver_next(d); /* eat '%' */
    165   AsmTok name = asm_driver_next(d);
    166   if (name.kind != ASM_TOK_IDENT)
    167     asm_driver_panic(d, "rv64 asm: expected relocation modifier name");
    168   Slice nm = pool_slice(asm_driver_pool(d), name.v.ident);
    169   asm_driver_expect_punct(d, '(', "'(' after relocation modifier");
    170   ObjSymId sym = OBJ_SYM_NONE;
    171   i64 off = 0;
    172   asm_driver_parse_sym_expr(d, &sym, &off);
    173   asm_driver_expect_punct(d, ')', "')' after %mod(sym)");
    174   RelocKind k = rv_mod_to_reloc(d, nm, pos);
    175   if (sym_out) *sym_out = sym;
    176   if (off_out) *off_out = off;
    177   if (kind_out) *kind_out = k;
    178   return 1;
    179 }
    180 
    181 /* Parse a RISC-V rounding-mode mnemonic (the comma is already consumed) into
    182  * its 3-bit funct3 value. cc -S emits this suffix on fcvt/fsqrt when the mode
    183  * isn't the default `dyn`, so the round-trip (and clang) re-encode the exact
    184  * mode rather than guessing a default. */
    185 static u32 rv_parse_rm_name(AsmDriver* d) {
    186   AsmTok t = asm_driver_next(d);
    187   Slice s;
    188   if (t.kind != ASM_TOK_IDENT)
    189     asm_driver_panic(d, "rv64 asm: expected rounding mode");
    190   s = pool_slice(asm_driver_pool(d), t.v.ident);
    191   if (slice_eq_cstr(s, "rne")) return 0u;
    192   if (slice_eq_cstr(s, "rtz")) return 1u;
    193   if (slice_eq_cstr(s, "rdn")) return 2u;
    194   if (slice_eq_cstr(s, "rup")) return 3u;
    195   if (slice_eq_cstr(s, "rmm")) return 4u;
    196   if (slice_eq_cstr(s, "dyn")) return 7u;
    197   asm_driver_panic(d, "rv64 asm: unknown rounding mode");
    198 }
    199 
    200 /* Emit a relocation for a U-type / I-type immediate `%mod(sym)` operand at
    201  * the current instruction position; returns 1 if one was present. */
    202 static int rv_emit_imm_mod_reloc(AsmDriver* d, RvModPos pos) {
    203   ObjSymId sym;
    204   i64 off;
    205   RelocKind k;
    206   if (!rv_parse_mod_reloc(d, pos, &sym, &off, &k)) return 0;
    207   MCEmitter* mc = asm_driver_mc(d);
    208   mc_emit_reloc_at(mc, mc->section_id, mc_pos(mc), k, sym, off, 0, 0);
    209   return 1;
    210 }
    211 
    212 static Rv64Mem parse_mem(AsmDriver* d) {
    213   Rv64Mem m;
    214   m.disp = 0;
    215   m.mod = RV_MEMMOD_NONE;
    216   m.sym = OBJ_SYM_NONE;
    217   m.off = 0;
    218   if (asm_driver_tok_is_punct(asm_driver_peek(d), '%')) {
    219     /* `%lo(sym)(base)` / `%pcrel_lo(label)(base)` — record the modifier; the
    220      * load/store caller emits the I- or S-type relocation. */
    221     ObjSymId sym;
    222     i64 off;
    223     RelocKind k;
    224     (void)rv_parse_mod_reloc(d, RV_MODPOS_LO_I, &sym, &off, &k);
    225     m.mod = (k == R_RV_PCREL_LO12_I) ? RV_MEMMOD_PCREL_LO : RV_MEMMOD_LO;
    226     m.sym = sym;
    227     m.off = off;
    228   } else {
    229     m.disp = (i32)asm_driver_parse_const(d);
    230   }
    231   asm_driver_expect_punct(d, '(', "'(' in rv64 memory operand");
    232   m.base = parse_xreg(d);
    233   asm_driver_expect_punct(d, ')', "')' in rv64 memory operand");
    234   return m;
    235 }
    236 
    237 /* Emit the I/S-type relocation recorded by parse_mem for a `%lo`/`%pcrel_lo`
    238  * memory offset, picking the S-type variant for stores. */
    239 static void rv_emit_mem_mod_reloc(AsmDriver* d, const Rv64Mem* m,
    240                                   int is_store) {
    241   if (m->mod == RV_MEMMOD_NONE) return;
    242   RelocKind k = (m->mod == RV_MEMMOD_PCREL_LO)
    243                     ? (is_store ? R_RV_PCREL_LO12_S : R_RV_PCREL_LO12_I)
    244                     : (is_store ? R_RV_LO12_S : R_RV_LO12_I);
    245   MCEmitter* mc = asm_driver_mc(d);
    246   mc_emit_reloc_at(mc, mc->section_id, mc_pos(mc), k, m->sym, m->off, 0, 0);
    247 }
    248 
    249 /* Fence pred/succ parser — accepts a string like "rw" / "iorw" / "0" /
    250  * a numeric literal. Returns the 4-bit mask: bit3=i, bit2=o, bit1=r,
    251  * bit0=w. */
    252 static u32 parse_fence_mask(AsmDriver* d) {
    253   AsmTok t = asm_driver_peek(d);
    254   if (t.kind == ASM_TOK_NUM) {
    255     (void)asm_driver_next(d);
    256     return (u32)asm_driver_parse_const(d) & 0xfu;
    257   }
    258   if (t.kind == ASM_TOK_IDENT) {
    259     char name[8];
    260     AsmTok tt = asm_driver_next(d);
    261     if (!sym_to_cstr(d, tt.v.ident, name, sizeof name))
    262       asm_driver_panic(d, "rv64 asm: bad fence mask");
    263     u32 mask = 0;
    264     for (const char* p = name; *p; ++p) {
    265       switch (*p) {
    266         case 'i':
    267           mask |= 8u;
    268           break;
    269         case 'o':
    270           mask |= 4u;
    271           break;
    272         case 'r':
    273           mask |= 2u;
    274           break;
    275         case 'w':
    276           mask |= 1u;
    277           break;
    278         default:
    279           asm_driver_panic(d, "rv64 asm: bad fence char");
    280       }
    281     }
    282     return mask;
    283   }
    284   asm_driver_panic(d, "rv64 asm: bad fence operand");
    285 }
    286 
    287 /* The XLEN variant for the assembly target. Reached off the AsmDriver's
    288  * Compiler so the stateless encoders can gate rv32-vs-rv64 behavior
    289  * (shamt width, addiw availability). */
    290 static const RiscvVariant* rv_asm_variant(AsmDriver* d) {
    291   return riscv_variant_for_kind(asm_driver_compiler(d)->target.arch);
    292 }
    293 
    294 /* Field overlay onto a descriptor's `match` word.
    295  *
    296  * For most formats the descriptor's match already pins opcode +
    297  * funct3 + funct7. We OR in the per-operand fields. For shift-imm and
    298  * AMO families the layouts diverge from the basic R/I templates — we
    299  * handle those explicitly below. */
    300 
    301 static u32 enc_r(u32 match, u32 rd, u32 rs1, u32 rs2) {
    302   return match | ((rs2 & 0x1fu) << 20) | ((rs1 & 0x1fu) << 15) |
    303          ((rd & 0x1fu) << 7);
    304 }
    305 static u32 enc_i(u32 match, u32 rd, u32 rs1, i32 imm12) {
    306   return match | (((u32)imm12 & 0xfffu) << 20) | ((rs1 & 0x1fu) << 15) |
    307          ((rd & 0x1fu) << 7);
    308 }
    309 static u32 enc_s(u32 match, u32 rs2, u32 rs1, i32 imm12) {
    310   return match | rv_imm_s(imm12) | ((rs2 & 0x1fu) << 20) |
    311          ((rs1 & 0x1fu) << 15);
    312 }
    313 static u32 enc_b(u32 match, u32 rs1, u32 rs2, i32 imm13) {
    314   return match | rv_imm_b(imm13) | ((rs2 & 0x1fu) << 20) |
    315          ((rs1 & 0x1fu) << 15);
    316 }
    317 static u32 enc_u(u32 match, u32 rd, u32 imm20) {
    318   return match | ((imm20 & 0xfffffu) << 12) | ((rd & 0x1fu) << 7);
    319 }
    320 static u32 enc_j(u32 match, u32 rd, i32 imm21) {
    321   return match | rv_imm_j(imm21) | ((rd & 0x1fu) << 7);
    322 }
    323 static u32 enc_r4(u32 match, u32 rd, u32 rs1, u32 rs2, u32 rs3, u32 rm) {
    324   return match | ((rs3 & 0x1fu) << 27) | ((rs2 & 0x1fu) << 20) |
    325          ((rs1 & 0x1fu) << 15) | ((rm & 0x7u) << 12) | ((rd & 0x1fu) << 7);
    326 }
    327 
    328 /* SLLI/SRLI/SRAI shift-imm. The shamt occupies bits 25:20 on rv64 (6-bit,
    329  * funct6 in match) but only bits 24:20 on rv32 (5-bit; bit 25 belongs to
    330  * funct7 and MUST stay 0, else the word reads as a different funct7). The
    331  * variant's shamt_bits drives the mask; an rv32 shamt >= 32 is rejected. */
    332 static u32 enc_ishift(AsmDriver* d, u32 match, u32 rd, u32 rs1, u32 shamt) {
    333   u32 shamt_bits = rv_asm_variant(d)->shamt_bits;
    334   u32 shamt_mask = (shamt_bits == 5u) ? 0x1fu : 0x3fu;
    335   if (shamt > shamt_mask)
    336     asm_driver_panic(d, "rv64 asm: shift amount out of range for target XLEN");
    337   return match | ((shamt & shamt_mask) << 20) | ((rs1 & 0x1fu) << 15) |
    338          ((rd & 0x1fu) << 7);
    339 }
    340 /* RV32 word shift-imm: shamt5 occupies bits 24:20 (funct7 already pinned). */
    341 static u32 enc_ishiftw(u32 match, u32 rd, u32 rs1, u32 shamt) {
    342   return match | ((shamt & 0x1fu) << 20) | ((rs1 & 0x1fu) << 15) |
    343          ((rd & 0x1fu) << 7);
    344 }
    345 /* AMO: aq/rl bits 26/25 — we accept them as optional .aq/.rl suffixes
    346  * on the mnemonic. For now mnemonics arrive bare. */
    347 static u32 enc_amo(u32 match, u32 aq, u32 rl, u32 rd, u32 rs1, u32 rs2) {
    348   return match | ((aq & 1u) << 26) | ((rl & 1u) << 25) | ((rs2 & 0x1fu) << 20) |
    349          ((rs1 & 0x1fu) << 15) | ((rd & 0x1fu) << 7);
    350 }
    351 
    352 static u32 c_reg3(AsmDriver* d, u32 r) {
    353   if (r < 8u || r > 15u)
    354     asm_driver_panic(d,
    355                      "rv64 asm: compressed register must be x8..x15/f8..f15");
    356   return r - 8u;
    357 }
    358 
    359 static u32 enc_c_ci(u32 match, u32 rd, i32 imm) {
    360   u32 u = (u32)imm & 0x3fu;
    361   return match | (((u >> 5) & 1u) << 12) | ((rd & 0x1fu) << 7) |
    362          ((u & 0x1fu) << 2);
    363 }
    364 
    365 static u32 enc_c_cr(u32 match, u32 rd_rs1, u32 rs2) {
    366   return match | ((rd_rs1 & 0x1fu) << 7) | ((rs2 & 0x1fu) << 2);
    367 }
    368 
    369 static u32 enc_c_addi16sp(u32 match, i32 imm) {
    370   u32 u = (u32)imm & 0x3ffu;
    371   return match | (((u >> 9) & 1u) << 12) | (((u >> 4) & 1u) << 6) |
    372          (((u >> 6) & 1u) << 5) | (((u >> 7) & 3u) << 3) |
    373          (((u >> 5) & 1u) << 2);
    374 }
    375 
    376 static u32 enc_c_addi4spn(u32 match, u32 rd3, u32 imm) {
    377   u32 enc = (((imm >> 4) & 3u) << 6) | (((imm >> 6) & 0xfu) << 2) |
    378             (((imm >> 2) & 1u) << 1) | ((imm >> 3) & 1u);
    379   return match | ((enc & 0xffu) << 5) | ((rd3 & 7u) << 2);
    380 }
    381 
    382 static u32 enc_c_lwld(u32 match, u32 rd3, u32 rs1_3, u32 off, int wide64) {
    383   if (wide64) {
    384     return match | (((off >> 3) & 7u) << 10) | ((rs1_3 & 7u) << 7) |
    385            (((off >> 6) & 3u) << 5) | ((rd3 & 7u) << 2);
    386   }
    387   return match | (((off >> 3) & 7u) << 10) | ((rs1_3 & 7u) << 7) |
    388          (((off >> 2) & 1u) << 6) | (((off >> 6) & 1u) << 5) |
    389          ((rd3 & 7u) << 2);
    390 }
    391 
    392 static u32 enc_c_swld(u32 match, u32 rs2_3, u32 rs1_3, u32 off, int wide64) {
    393   return enc_c_lwld(match, rs2_3, rs1_3, off, wide64);
    394 }
    395 
    396 static u32 enc_c_lwsp(u32 match, u32 rd, u32 off, int wide64) {
    397   if (wide64) {
    398     return match | (((off >> 5) & 1u) << 12) | ((rd & 0x1fu) << 7) |
    399            (((off >> 3) & 3u) << 5) | (((off >> 6) & 7u) << 2);
    400   }
    401   return match | (((off >> 5) & 1u) << 12) | ((rd & 0x1fu) << 7) |
    402          (((off >> 2) & 7u) << 4) | (((off >> 6) & 3u) << 2);
    403 }
    404 
    405 static u32 enc_c_swsp(u32 match, u32 rs2, u32 off, int wide64) {
    406   u32 imm6;
    407   if (wide64)
    408     imm6 = (((off >> 3) & 7u) << 3) | ((off >> 6) & 7u);
    409   else
    410     imm6 = (((off >> 2) & 0xfu) << 2) | ((off >> 6) & 3u);
    411   return match | ((imm6 & 0x3fu) << 7) | ((rs2 & 0x1fu) << 2);
    412 }
    413 
    414 static u32 enc_c_cb_imm(u32 match, u32 rs1_3, i32 imm) {
    415   u32 u = (u32)imm & 0x1ffu;
    416   return match | (((u >> 8) & 1u) << 12) | (((u >> 3) & 3u) << 10) |
    417          ((rs1_3 & 7u) << 7) | (((u >> 6) & 3u) << 5) | (((u >> 1) & 3u) << 3) |
    418          (((u >> 5) & 1u) << 2);
    419 }
    420 
    421 static u32 enc_c_cb_alu_imm(u32 match, u32 rd3, i32 imm) {
    422   u32 u = (u32)imm & 0x3fu;
    423   return match | (((u >> 5) & 1u) << 12) | ((rd3 & 7u) << 7) |
    424          ((u & 0x1fu) << 2);
    425 }
    426 
    427 static u32 enc_c_cj(u32 match, i32 imm) {
    428   u32 u = (u32)imm & 0xfffu;
    429   return match | (((u >> 11) & 1u) << 12) | (((u >> 4) & 1u) << 11) |
    430          (((u >> 8) & 3u) << 9) | (((u >> 10) & 1u) << 8) |
    431          (((u >> 6) & 1u) << 7) | (((u >> 7) & 1u) << 6) |
    432          (((u >> 1) & 7u) << 3) | (((u >> 5) & 1u) << 2);
    433 }
    434 
    435 /* Parse a branch/jump target operand. With a symbolic target (a label), emit
    436  * the relocation at the current position — which is exactly where the caller
    437  * is about to write this instruction word — and return 0 as the placeholder
    438  * immediate. With a bare constant, return it as the PC-relative byte
    439  * displacement (preserving the existing numeric-offset corpus behavior). */
    440 static i32 rv_reloc_target(AsmDriver* d, RelocKind kind) {
    441   ObjSymId sym = OBJ_SYM_NONE;
    442   i64 off = 0;
    443   asm_driver_parse_sym_expr(d, &sym, &off);
    444   if (sym != OBJ_SYM_NONE) {
    445     MCEmitter* mc = asm_driver_mc(d);
    446     mc_emit_reloc_at(mc, mc->section_id, mc_pos(mc), kind, sym, off, 0, 0);
    447     return 0;
    448   }
    449   return (i32)off;
    450 }
    451 
    452 /* Per-format parser — reads the operand list off the driver and returns
    453  * the encoded 32-bit word, given the matched descriptor. */
    454 static u32 assemble_one(AsmDriver* d, const Rv64InsnDesc* desc) {
    455   u32 m = desc->match;
    456   u32 rd = 0, rs1 = 0, rs2 = 0;
    457   i32 imm = 0;
    458   Rv64Mem mem;
    459 
    460   switch ((Rv64Format)desc->fmt) {
    461     case RV64_FMT_R:
    462       /* Two-operand aliases: snez/neg/negw — rd, rs (rs1=x0). */
    463       if (desc->flags & RV64_ASMFL_ALIAS) {
    464         rd = parse_xreg(d);
    465         expect_comma(d);
    466         rs2 = parse_xreg(d);
    467         return enc_r(m, rd, 0u, rs2);
    468       }
    469       rd = parse_xreg(d);
    470       expect_comma(d);
    471       rs1 = parse_xreg(d);
    472       expect_comma(d);
    473       rs2 = parse_xreg(d);
    474       return enc_r(m, rd, rs1, rs2);
    475 
    476     case RV64_FMT_R4: {
    477       u32 rs3;
    478       rd = parse_freg(d);
    479       expect_comma(d);
    480       rs1 = parse_freg(d);
    481       expect_comma(d);
    482       rs2 = parse_freg(d);
    483       expect_comma(d);
    484       rs3 = parse_freg(d);
    485       return enc_r4(m, rd, rs1, rs2, rs3, 0x7u);
    486     }
    487 
    488     case RV64_FMT_I:
    489       /* Aliases first. `li` is handled earlier by rv64_emit_pseudo (it may
    490        * need a multi-word expansion), so it never reaches here. */
    491       if (desc->flags & RV64_ASMFL_ALIAS) {
    492         if (slice_eq_cstr(desc->mnemonic, "mv")) {
    493           /* Standard two-operand `mv rd, rs` = `addi rd, rs, 0`. (A %pcrel_lo
    494            * low-half is emitted as the canonical `addi rd, rs, %pcrel_lo(L)`,
    495            * not a non-standard 3-operand `mv`, so it lands in the ADDI path
    496            * below — matching clang.) */
    497           rd = parse_xreg(d);
    498           expect_comma(d);
    499           rs1 = parse_xreg(d);
    500           return enc_i(m, rd, rs1, 0);
    501         }
    502         if (slice_eq_cstr(desc->mnemonic, "sext.w")) {
    503           rd = parse_xreg(d);
    504           expect_comma(d);
    505           rs1 = parse_xreg(d);
    506           return enc_i(m, rd, rs1, 0);
    507         }
    508         if (slice_eq_cstr(desc->mnemonic, "seqz") ||
    509             slice_eq_cstr(desc->mnemonic, "not")) {
    510           rd = parse_xreg(d);
    511           expect_comma(d);
    512           rs1 = parse_xreg(d);
    513           /* match already has imm12 + funct3 + op pinned. */
    514           return m | ((rs1 & 0x1fu) << 15) | ((rd & 0x1fu) << 7);
    515         }
    516       }
    517       rd = parse_xreg(d);
    518       expect_comma(d);
    519       rs1 = parse_xreg(d);
    520       expect_comma(d);
    521       /* `addi rd, rs1, %lo(sym)` / `%pcrel_lo(label)` → R_RV_LO12_I. */
    522       if (rv_emit_imm_mod_reloc(d, RV_MODPOS_LO_I)) return enc_i(m, rd, rs1, 0);
    523       imm = (i32)asm_driver_parse_const(d);
    524       return enc_i(m, rd, rs1, imm);
    525 
    526     case RV64_FMT_I_SHIFT:
    527       rd = parse_xreg(d);
    528       expect_comma(d);
    529       rs1 = parse_xreg(d);
    530       expect_comma(d);
    531       return enc_ishift(d, m, rd, rs1, (u32)asm_driver_parse_const(d));
    532 
    533     case RV64_FMT_I_SHIFTW:
    534       rd = parse_xreg(d);
    535       expect_comma(d);
    536       rs1 = parse_xreg(d);
    537       expect_comma(d);
    538       return enc_ishiftw(m, rd, rs1, (u32)asm_driver_parse_const(d));
    539 
    540     case RV64_FMT_U:
    541       rd = parse_xreg(d);
    542       expect_comma(d);
    543       /* `lui rd, %hi(sym)` → R_RV_HI20; `auipc rd, %pcrel_hi(sym)` →
    544        * R_RV_PCREL_HI20 (or %got_pcrel_hi → R_RV_GOT_HI20). */
    545       if (rv_emit_imm_mod_reloc(d, RV_MODPOS_HI20)) return enc_u(m, rd, 0);
    546       imm = (i32)asm_driver_parse_const(d);
    547       /* LUI/AUIPC immediate is the upper-20 value: the input is interpreted
    548        * as the literal 20-bit value (already shifted-out form). */
    549       return enc_u(m, rd, (u32)imm);
    550 
    551     case RV64_FMT_J:
    552       /* `j label` / `jal rd, label` accept a symbolic target (R_RV_JAL) or a
    553        * bare numeric displacement. */
    554       if ((desc->flags & RV64_ASMFL_ALIAS) &&
    555           slice_eq_cstr(desc->mnemonic, "j")) {
    556         return enc_j(m, 0u, rv_reloc_target(d, R_RV_JAL));
    557       }
    558       rd = parse_xreg(d);
    559       expect_comma(d);
    560       return enc_j(m, rd, rv_reloc_target(d, R_RV_JAL));
    561 
    562     case RV64_FMT_B:
    563       /* `beq rs1, rs2, label` (and beqz/bnez aliases) accept a symbolic target
    564        * (R_RV_BRANCH) or a bare numeric displacement. */
    565       if (desc->flags & RV64_ASMFL_ALIAS) {
    566         /* beqz / bnez: rs, off. */
    567         rs1 = parse_xreg(d);
    568         expect_comma(d);
    569         return enc_b(m, rs1, 0u, rv_reloc_target(d, R_RV_BRANCH));
    570       }
    571       rs1 = parse_xreg(d);
    572       expect_comma(d);
    573       rs2 = parse_xreg(d);
    574       expect_comma(d);
    575       return enc_b(m, rs1, rs2, rv_reloc_target(d, R_RV_BRANCH));
    576 
    577     case RV64_FMT_LOAD:
    578       rd = (desc->flags & RV64_ASMFL_FP) ? parse_freg(d) : parse_xreg(d);
    579       expect_comma(d);
    580       mem = parse_mem(d);
    581       rv_emit_mem_mod_reloc(d, &mem, /*is_store=*/0);
    582       return enc_i(m, rd, mem.base, mem.disp);
    583 
    584     case RV64_FMT_FP_LOAD:
    585       rd = parse_freg(d);
    586       expect_comma(d);
    587       mem = parse_mem(d);
    588       rv_emit_mem_mod_reloc(d, &mem, /*is_store=*/0);
    589       return enc_i(m, rd, mem.base, mem.disp);
    590 
    591     case RV64_FMT_STORE:
    592       rs2 = (desc->flags & RV64_ASMFL_FP) ? parse_freg(d) : parse_xreg(d);
    593       expect_comma(d);
    594       mem = parse_mem(d);
    595       rv_emit_mem_mod_reloc(d, &mem, /*is_store=*/1);
    596       return enc_s(m, rs2, mem.base, mem.disp);
    597 
    598     case RV64_FMT_FP_STORE:
    599       rs2 = parse_freg(d);
    600       expect_comma(d);
    601       mem = parse_mem(d);
    602       rv_emit_mem_mod_reloc(d, &mem, /*is_store=*/1);
    603       return enc_s(m, rs2, mem.base, mem.disp);
    604 
    605     case RV64_FMT_JALR:
    606       if ((desc->flags & RV64_ASMFL_ALIAS) &&
    607           slice_eq_cstr(desc->mnemonic, "jr")) {
    608         rs1 = parse_xreg(d);
    609         return enc_i(m, 0u, rs1, 0);
    610       }
    611       rd = parse_xreg(d);
    612       if (!asm_driver_eat_comma(d)) {
    613         if (slice_eq_cstr(desc->mnemonic, "jalr"))
    614           return enc_i(m, RV_RA, rd, 0);
    615         asm_driver_panic(d, "rv64 asm: expected ','");
    616       }
    617       /* Accept both `jalr rd, imm(rs1)` and `jalr rd, rs1, imm`. */
    618       {
    619         AsmTok t = asm_driver_peek(d);
    620         if (t.kind == ASM_TOK_IDENT) {
    621           /* register first → register form */
    622           rs1 = parse_xreg(d);
    623           if (asm_driver_eat_comma(d)) {
    624             imm = (i32)asm_driver_parse_const(d);
    625           } else {
    626             imm = 0;
    627           }
    628           return enc_i(m, rd, rs1, imm);
    629         }
    630       }
    631       mem = parse_mem(d);
    632       return enc_i(m, rd, mem.base, mem.disp);
    633 
    634     case RV64_FMT_FENCE: {
    635       u32 pred, succ;
    636       pred = parse_fence_mask(d);
    637       expect_comma(d);
    638       succ = parse_fence_mask(d);
    639       return m | (pred << 24) | (succ << 20);
    640     }
    641 
    642     case RV64_FMT_SYSTEM:
    643       /* No operands. nop/ret/ecall/ebreak. */
    644       return m;
    645 
    646     case RV64_FMT_FP_RM:
    647       rd = parse_freg(d);
    648       expect_comma(d);
    649       rs1 = parse_freg(d);
    650       expect_comma(d);
    651       rs2 = parse_freg(d);
    652       /* Use DYN(=7) rounding mode by default. */
    653       return enc_r(m | (0x7u << 12), rd, rs1, rs2);
    654 
    655     case RV64_FMT_FP_R:
    656       if (desc->flags & RV64_ASMFL_FP) {
    657         rd = parse_freg(d);
    658       } else {
    659         rd = parse_xreg(d);
    660       }
    661       expect_comma(d);
    662       rs1 = parse_freg(d);
    663       expect_comma(d);
    664       rs2 = parse_freg(d);
    665       return enc_r(m, rd, rs1, rs2);
    666 
    667     case RV64_FMT_FP_CVT:
    668       if (desc->flags & RV64_ASMFL_FP) {
    669         rd = parse_freg(d);
    670         expect_comma(d);
    671         /* Source: integer reg for fcvt.s.w etc (no FP flag would
    672          * indicate); but since we have ASMFL_FP set on dest, source may
    673          * be either. Disambiguate by mnemonic. */
    674         if (slice_has_prefix_cstr(desc->mnemonic, "fcvt.s.", 7) &&
    675             (desc->mnemonic.s[7] == 'w' || desc->mnemonic.s[7] == 'l')) {
    676           rs1 = parse_xreg(d);
    677         } else if (slice_has_prefix_cstr(desc->mnemonic, "fcvt.d.", 7) &&
    678                    (desc->mnemonic.s[7] == 'w' || desc->mnemonic.s[7] == 'l')) {
    679           rs1 = parse_xreg(d);
    680         } else if (slice_eq_cstr(desc->mnemonic, "fmv.w.x") ||
    681                    slice_eq_cstr(desc->mnemonic, "fmv.d.x")) {
    682           rs1 = parse_xreg(d);
    683         } else {
    684           rs1 = parse_freg(d);
    685         }
    686       } else {
    687         rd = parse_xreg(d);
    688         expect_comma(d);
    689         rs1 = parse_freg(d);
    690       }
    691       /* match encodes rs2 (type selector); OR in rd/rs1 and the rounding mode.
    692        * An explicit `, <rm>` suffix (cc -S emits it for non-default modes, and
    693        * clang/gas accept it) takes precedence; otherwise a bare conversion
    694        * mnemonic encodes the dynamic rounding mode (DYN=7), matching gas/clang
    695        * for hand-written assembly. (Codegen's C float->int truncation is RTZ,
    696        * but that path uses the rv_fcvt_* encoders directly and supplies its own
    697        * rm; the text assembler must follow the assembler convention.) fmv
    698        * bit-moves carry no rounding (rm=0). */
    699       {
    700         u32 funct7 = (m >> 25) & 0x7fu;
    701         u32 rm;
    702         if (asm_driver_eat_comma(d)) {
    703           rm = rv_parse_rm_name(d);
    704         } else {
    705           switch (funct7) {
    706             case 0x70: /* fmv.x.w */
    707             case 0x71: /* fmv.x.d */
    708             case 0x78: /* fmv.w.x */
    709             case 0x79: /* fmv.d.x */
    710               rm = 0x0u;
    711               break;
    712             default: /* fcvt families: DYN (explicit suffix overrides above) */
    713               rm = 0x7u;
    714               break;
    715           }
    716         }
    717         return m | (rm << 12) | ((rs1 & 0x1fu) << 15) | ((rd & 0x1fu) << 7);
    718       }
    719 
    720     case RV64_FMT_AMO:
    721       rd = parse_xreg(d);
    722       expect_comma(d);
    723       rs2 = parse_xreg(d);
    724       expect_comma(d);
    725       asm_driver_expect_punct(d, '(', "'(' in rv64 amo operand");
    726       rs1 = parse_xreg(d);
    727       asm_driver_expect_punct(d, ')', "')' in rv64 amo operand");
    728       return enc_amo(m, 0u, 0u, rd, rs1, rs2);
    729 
    730     case RV64_FMT_LR:
    731       rd = parse_xreg(d);
    732       expect_comma(d);
    733       asm_driver_expect_punct(d, '(', "'(' in rv64 lr operand");
    734       rs1 = parse_xreg(d);
    735       asm_driver_expect_punct(d, ')', "')' in rv64 lr operand");
    736       return enc_amo(m, 0u, 0u, rd, rs1, 0u);
    737 
    738     case RV64_FMT_CSR: {
    739       u32 csr;
    740       rd = parse_xreg(d);
    741       expect_comma(d);
    742       csr = parse_csr(d);
    743       expect_comma(d);
    744       rs1 = parse_xreg(d);
    745       return enc_i(m, rd, rs1, (i32)csr);
    746     }
    747 
    748     case RV64_FMT_CSRI: {
    749       u32 csr;
    750       rd = parse_xreg(d);
    751       expect_comma(d);
    752       csr = parse_csr(d);
    753       expect_comma(d);
    754       u32 uimm = (u32)asm_driver_parse_const(d) & 0x1fu;
    755       return enc_i(m, rd, uimm, (i32)csr);
    756     }
    757 
    758     case RV64_FMT_CSR_PSEUDO: {
    759       /* 2-operand CSR pseudos. The match word already pins funct3+opcode; we
    760        * supply x0 for the implicit rd or rs1 per the mnemonic. */
    761       u32 csr;
    762       if (slice_eq_cstr(desc->mnemonic, "csrr")) {
    763         /* csrr rd, csr = csrrs rd, csr, x0 */
    764         rd = parse_xreg(d);
    765         expect_comma(d);
    766         csr = parse_csr(d);
    767         return enc_i(m, rd, 0u, (i32)csr);
    768       }
    769       /* csrw/csrs/csrc csr, rs   and   csrwi/csrsi/csrci csr, uimm:
    770        * destination is x0, csr comes first. */
    771       csr = parse_csr(d);
    772       expect_comma(d);
    773       if (slice_eq_cstr(desc->mnemonic, "csrwi") ||
    774           slice_eq_cstr(desc->mnemonic, "csrsi") ||
    775           slice_eq_cstr(desc->mnemonic, "csrci")) {
    776         u32 uimm = (u32)asm_driver_parse_const(d) & 0x1fu;
    777         return enc_i(m, 0u, uimm, (i32)csr);
    778       }
    779       rs1 = parse_xreg(d);
    780       return enc_i(m, 0u, rs1, (i32)csr);
    781     }
    782 
    783     case RV64_FMT_CR:
    784       if (slice_eq_cstr(desc->mnemonic, "c.jr") ||
    785           slice_eq_cstr(desc->mnemonic, "c.jalr")) {
    786         rs1 = parse_xreg(d);
    787         return enc_c_cr(m, rs1, 0u);
    788       }
    789       rd = parse_xreg(d);
    790       expect_comma(d);
    791       rs2 = parse_xreg(d);
    792       return enc_c_cr(m, rd, rs2);
    793 
    794     case RV64_FMT_CI:
    795       if (slice_eq_cstr(desc->mnemonic, "c.lwsp") ||
    796           slice_eq_cstr(desc->mnemonic, "c.ldsp") ||
    797           slice_eq_cstr(desc->mnemonic, "c.fldsp")) {
    798         rd = slice_eq_cstr(desc->mnemonic, "c.fldsp") ? parse_freg(d)
    799                                                       : parse_xreg(d);
    800         expect_comma(d);
    801         mem = parse_mem(d);
    802         if (mem.base != RV_SP)
    803           asm_driver_panic(d, "rv64 asm: compressed stack load needs sp base");
    804         return enc_c_lwsp(m, rd, (u32)mem.disp,
    805                           !slice_eq_cstr(desc->mnemonic, "c.lwsp"));
    806       }
    807       rd = parse_xreg(d);
    808       expect_comma(d);
    809       imm = (i32)asm_driver_parse_const(d);
    810       if (slice_eq_cstr(desc->mnemonic, "c.lui") && ((u32)imm & 0xfffu) == 0)
    811         imm >>= 12;
    812       if (slice_eq_cstr(desc->mnemonic, "c.addi16sp")) {
    813         if (rd != RV_SP)
    814           asm_driver_panic(d, "rv64 asm: c.addi16sp needs sp destination");
    815         return enc_c_addi16sp(m, imm);
    816       }
    817       return enc_c_ci(m, rd, imm);
    818 
    819     case RV64_FMT_CSS:
    820       rs2 = (desc->flags & RV64_ASMFL_FP) ? parse_freg(d) : parse_xreg(d);
    821       expect_comma(d);
    822       mem = parse_mem(d);
    823       if (mem.base != RV_SP)
    824         asm_driver_panic(d, "rv64 asm: compressed stack store needs sp base");
    825       return enc_c_swsp(m, rs2, (u32)mem.disp,
    826                         !slice_eq_cstr(desc->mnemonic, "c.swsp"));
    827 
    828     case RV64_FMT_CIW:
    829       rd = parse_xreg(d);
    830       expect_comma(d);
    831       rs1 = parse_xreg(d);
    832       expect_comma(d);
    833       if (rs1 != RV_SP)
    834         asm_driver_panic(d, "rv64 asm: c.addi4spn needs sp source");
    835       imm = (i32)asm_driver_parse_const(d);
    836       return enc_c_addi4spn(m, c_reg3(d, rd), (u32)imm);
    837 
    838     case RV64_FMT_CL:
    839       rd = (desc->flags & RV64_ASMFL_FP) ? parse_freg(d) : parse_xreg(d);
    840       expect_comma(d);
    841       mem = parse_mem(d);
    842       return enc_c_lwld(m, c_reg3(d, rd), c_reg3(d, mem.base), (u32)mem.disp,
    843                         !slice_eq_cstr(desc->mnemonic, "c.lw"));
    844 
    845     case RV64_FMT_CS:
    846       rs2 = (desc->flags & RV64_ASMFL_FP) ? parse_freg(d) : parse_xreg(d);
    847       expect_comma(d);
    848       mem = parse_mem(d);
    849       return enc_c_swld(m, c_reg3(d, rs2), c_reg3(d, mem.base), (u32)mem.disp,
    850                         !slice_eq_cstr(desc->mnemonic, "c.sw"));
    851 
    852     case RV64_FMT_CA:
    853       rd = parse_xreg(d);
    854       expect_comma(d);
    855       rs2 = parse_xreg(d);
    856       return m | (c_reg3(d, rd) << 7) | (c_reg3(d, rs2) << 2);
    857 
    858     case RV64_FMT_CB:
    859       rs1 = parse_xreg(d);
    860       expect_comma(d);
    861       imm = (i32)asm_driver_parse_const(d);
    862       if (slice_eq_cstr(desc->mnemonic, "c.beqz") ||
    863           slice_eq_cstr(desc->mnemonic, "c.bnez")) {
    864         return enc_c_cb_imm(m, c_reg3(d, rs1), imm);
    865       }
    866       return enc_c_cb_alu_imm(m, c_reg3(d, rs1), imm);
    867 
    868     case RV64_FMT_CJ:
    869       imm = (i32)asm_driver_parse_const(d);
    870       return enc_c_cj(m, imm);
    871 
    872     case RV64_FMT_C_NONE:
    873       return m;
    874 
    875     default:
    876       asm_driver_panic(d, "rv64 asm: unsupported format");
    877   }
    878 }
    879 
    880 /* ============================================================
    881  * Multi-word pseudo-instruction expansion.
    882  *
    883  * call/tail/la/lla expand to a PC-relative AUIPC + (JALR | ADDI) pair;
    884  * `li` with a constant that does not fit a 12-bit signed immediate
    885  * expands to an LUI/ADDI(W)/SLLI chain (no relocations). Each 32-bit
    886  * word goes out through rv64_emit32 — the same path assemble_one's
    887  * single-word result uses — and relocations are attached via
    888  * mc_emit_reloc_at at the appropriate word offset. */
    889 
    890 /* 12-bit signed immediate range check for li short-circuit. */
    891 static bool rv_fits_i12(i64 v) { return v >= -2048 && v <= 2047; }
    892 
    893 /* Sign-extend the low 12 bits of v. */
    894 static i64 rv_sext12(i64 v) {
    895   return (i64)((((u64)v & 0xfffu) ^ 0x800u)) - 0x800;
    896 }
    897 
    898 /* Emit an AUIPC rd,0 + a R_RV_PCREL_HI20(sym) reloc, then create a local
    899  * `.LpcrelHi` anchor at the AUIPC offset and return that anchor symbol so
    900  * the paired low-half reloc can reference it. Mirrors native.c's
    901  * rv_emit_global_addr (the non-GOT branch). */
    902 static ObjSymId rv_emit_pcrel_hi(AsmDriver* d, u32 rd, ObjSymId sym,
    903                                  i64 addend) {
    904   MCEmitter* mc = asm_driver_mc(d);
    905   ObjBuilder* obj = asm_driver_ob(d);
    906   Compiler* c = asm_driver_compiler(d);
    907   u32 sec = mc->section_id;
    908   u32 ap = mc_pos(mc);
    909   rv64_emit32(mc, rv_auipc(rd, 0));
    910   mc_emit_reloc_at(mc, sec, ap, R_RV_PCREL_HI20, sym, addend, 0, 0);
    911   Sym an = pool_intern_slice(c->global, SLICE_LIT(".LpcrelHi"));
    912   return obj_symbol(obj, an, SB_LOCAL, SK_OBJ, sec, (u64)ap, 0);
    913 }
    914 
    915 /* call/tail: AUIPC <link>,0 + JALR <rd>,<link>,0 with one R_RV_CALL reloc
    916  * at the AUIPC. `link` is the register the AUIPC materializes into and the
    917  * JALR's base; `rd` is the JALR link-register (ra for call, zero for
    918  * tail). The linker patches both words from the single R_RV_CALL reloc. */
    919 static void rv_emit_call_pseudo(AsmDriver* d, u32 link, u32 rd) {
    920   MCEmitter* mc = asm_driver_mc(d);
    921   ObjSymId sym = OBJ_SYM_NONE;
    922   i64 off = 0;
    923   asm_driver_parse_sym_expr(d, &sym, &off);
    924   if (sym == OBJ_SYM_NONE)
    925     asm_driver_panic(d, "rv64 asm: call/tail target must be a symbol");
    926   u32 sec = mc->section_id;
    927   u32 ap = mc_pos(mc);
    928   rv64_emit32(mc, rv_auipc(link, 0));
    929   rv64_emit32(mc, rv_jalr(rd, link, 0));
    930   mc_emit_reloc_at(mc, sec, ap, R_RV_CALL, sym, off, 0, 0);
    931 }
    932 
    933 /* la/lla rd, sym: AUIPC rd,%pcrel_hi(sym) + ADDI rd,rd,%pcrel_lo(anchor).
    934  * kit's static Local-Exec model has no GOT, so `la` == `lla`. */
    935 static void rv_emit_la_pseudo(AsmDriver* d) {
    936   MCEmitter* mc = asm_driver_mc(d);
    937   u32 rd = parse_xreg(d);
    938   expect_comma(d);
    939   ObjSymId sym = OBJ_SYM_NONE;
    940   i64 off = 0;
    941   asm_driver_parse_sym_expr(d, &sym, &off);
    942   if (sym == OBJ_SYM_NONE)
    943     asm_driver_panic(d, "rv64 asm: la/lla target must be a symbol");
    944   ObjSymId anchor = rv_emit_pcrel_hi(d, rd, sym, off);
    945   u32 sec = mc->section_id;
    946   u32 lp = mc_pos(mc);
    947   rv64_emit32(mc, rv_addi(rd, rd, 0));
    948   mc_emit_reloc_at(mc, sec, lp, R_RV_PCREL_LO12_I, anchor, 0, 0, 0);
    949 }
    950 
    951 /* LUI immediate that sign-extends to a negative 32-bit value: bit 19 of
    952  * the 20-bit field is set, i.e. Hi20 >= 0x80000. */
    953 #define RV_LUI_HI20_SIGN 0x80000LL
    954 
    955 /* Materialize a constant into `rd` via the LLVM RISCVMatInt sequence: for
    956  * values fitting a signed 32-bit range, LUI + ADDI/ADDIW; otherwise a
    957  * recursive top-down hi20/lo12 split with SLLI shifts that absorb trailing
    958  * zeros. No relocations.
    959  *
    960  * On rv64, after an LUI the low-half add uses ADDIW only when the LUI value
    961  * is negative in 32-bit form (Hi20 >= RV_LUI_HI20_SIGN): there the add must
    962  * wrap in 32-bit arithmetic and re-sign-extend to land in range. When the
    963  * LUI value is non-negative in its low 32 bits, plain ADDI keeps the
    964  * 64-bit result correct (matching LLVM's generateInstSeqImpl).
    965  *
    966  * On rv32 there is no ADDIW and the GPR is 32 bits wide, so every constant
    967  * fits a LUI + ADDI pair (the add already wraps mod 2^32). The variant's
    968  * has_w_forms gates both the ADDIW use and the >32-bit recursion below. */
    969 static void rv_emit_li_value(MCEmitter* mc, const RiscvVariant* variant, u32 rd,
    970                              i64 val) {
    971   if (!variant->has_w_forms || (val >= -2147483648LL && val <= 2147483647LL)) {
    972     i64 hi20 = ((val + 0x800) >> 12) & 0xfffffLL;
    973     i64 lo12 = rv_sext12(val);
    974     if (hi20) rv64_emit32(mc, rv_lui(rd, (u32)hi20));
    975     if (lo12 || hi20 == 0) {
    976       u32 src = hi20 ? rd : (u32)RV_ZERO;
    977       if (variant->has_w_forms && hi20 >= RV_LUI_HI20_SIGN)
    978         rv64_emit32(mc, rv_addiw(rd, src, (i32)lo12));
    979       else
    980         rv64_emit32(mc, rv_addi(rd, src, (i32)lo12));
    981     }
    982     return;
    983   }
    984   /* >32-bit: split off the low 12 bits, recurse on the (shifted) high
    985    * part, then SLLI back and ADD the low bits. The subtraction is done in
    986    * unsigned space so it cannot signed-overflow at the int64 extremes
    987    * (e.g. val=INT64_MAX, lo12=-1); the result has its low 12 bits clear,
    988    * and the arithmetic right shift recovers the sign-extended high part. */
    989   i64 lo12 = rv_sext12(val);
    990   i64 hi = (i64)((u64)val - (u64)lo12) >> 12;
    991   u32 shift = 12;
    992   /* Absorb trailing zeros of the high part into the shift amount. */
    993   while ((hi & 1) == 0) {
    994     hi >>= 1;
    995     ++shift;
    996   }
    997   rv_emit_li_value(mc, variant, rd, hi);
    998   rv64_emit32(mc, rv_slli(rd, rd, shift));
    999   if (lo12) rv64_emit32(mc, rv_addi(rd, rd, (i32)lo12));
   1000 }
   1001 
   1002 /* Dispatch a multi-word pseudo. Returns true if it consumed the operands
   1003  * and emitted its expansion; false to fall through to the single-word
   1004  * path. `li` is handled here only when its immediate exceeds the 12-bit
   1005  * signed range the alias row encodes directly. */
   1006 static bool rv64_emit_pseudo(AsmDriver* d, const Rv64InsnDesc* desc) {
   1007   MCEmitter* mc = asm_driver_mc(d);
   1008   if (desc->fmt == RV64_FMT_PSEUDO) {
   1009     if (slice_eq_cstr(desc->mnemonic, "call")) {
   1010       rv_emit_call_pseudo(d, RV_RA, RV_RA);
   1011       return true;
   1012     }
   1013     if (slice_eq_cstr(desc->mnemonic, "tail")) {
   1014       /* Standard RISC-V `tail` materializes the address into t1 (x6). kit
   1015        * codegen uses t0 for its own tail-call temp, so a `cc -S`-fused
   1016        * `tail sym` re-assembles to t1 not t0 — execution-equivalent (both are
   1017        * caller-saved temps clobbered by the tail jump; cross-exec still
   1018        * matches), only the byte image differs on tail-call cases. Keeping the
   1019        * assembler's `tail` standard preserves clang/gas interop. */
   1020       rv_emit_call_pseudo(d, RV_T1, RV_ZERO);
   1021       return true;
   1022     }
   1023     /* la / lla — identical PC-relative expansion in kit. */
   1024     rv_emit_la_pseudo(d);
   1025     return true;
   1026   }
   1027   if ((desc->flags & RV64_ASMFL_ALIAS) && slice_eq_cstr(desc->mnemonic, "li")) {
   1028     /* Peek the immediate without consuming the destination register: the
   1029      * single-word alias path re-parses both. We commit to the multi-word
   1030      * path only for out-of-range constants, leaving the existing 12-bit
   1031      * fast path (and its golden behavior) untouched. */
   1032     u32 rd = parse_xreg(d);
   1033     expect_comma(d);
   1034     i64 imm = asm_driver_parse_const(d);
   1035     if (rv_fits_i12(imm)) {
   1036       rv64_emit32(mc, rv_addi(rd, RV_ZERO, (i32)imm));
   1037     } else {
   1038       rv_emit_li_value(mc, rv_asm_variant(d), rd, imm);
   1039     }
   1040     return true;
   1041   }
   1042   return false;
   1043 }
   1044 
   1045 static void rv64_arch_asm_insn(ArchAsm* base, AsmDriver* d, Sym mnemonic) {
   1046   MCEmitter* mc = asm_driver_mc(d);
   1047   const Rv64InsnDesc* desc;
   1048   u8 av = rv_asm_variant(d)->xlen == 32u ? (u8)RV_AV_RV32 : (u8)RV_AV_RV64;
   1049   (void)base;
   1050   (void)asm_driver_cur_section(d);
   1051   desc = rv64_asm_find(pool_slice(asm_driver_pool(d), mnemonic), av);
   1052   if (!desc)
   1053     asm_driver_panic(d, av == (u8)RV_AV_RV32
   1054                             ? "rv32 asm: unsupported instruction"
   1055                             : "rv64 asm: unsupported instruction");
   1056   if (rv64_emit_pseudo(d, desc)) return;
   1057   if (desc->flags & RV64_ASMFL_C16)
   1058     rv64_emit16(mc, assemble_one(d, desc));
   1059   else
   1060     rv64_emit32(mc, assemble_one(d, desc));
   1061 }
   1062 
   1063 static void rv64_arch_asm_destroy(ArchAsm* base) { (void)base; }
   1064 
   1065 /* ---- textual-assembly operand syntax (printer <-> parser) ----------------
   1066  *
   1067  * Inverse of the `.s` parsers above (rv_parse_mod_reloc / rv_reloc_target and
   1068  * the call/la pseudo expanders): how a relocated rv64 operand is spelled in
   1069  * `cc -S` so the same text re-assembles under kit-as. RISC-V uses the same
   1070  * `%hi`/`%lo`/`%pcrel_hi`/`%pcrel_lo` operator syntax on every object format,
   1071  * so `fmt` is unused. See ArchAsmOps and src/api/asm_emit.c. */
   1072 static int rv64_reloc_operand(u16 kind, KitObjFmt fmt, ArchRelocOperand* out) {
   1073   (void)fmt;
   1074   out->prefix = "";
   1075   out->suffix = "";
   1076   out->addend_bias = 0;
   1077   out->emit_anchor = 0;
   1078   out->ref_anchor = 0;
   1079   switch (kind) {
   1080     case R_RV_PCREL_HI20:
   1081       out->surg = ARCH_RELOC_SURG_TAIL;
   1082       out->prefix = "%pcrel_hi(";
   1083       out->suffix = ")";
   1084       out->emit_anchor = 1; /* define a unique anchor label at this AUIPC */
   1085       return 1;
   1086     case R_RV_GOT_HI20:
   1087       out->surg = ARCH_RELOC_SURG_TAIL;
   1088       out->prefix = "%got_pcrel_hi(";
   1089       out->suffix = ")";
   1090       out->emit_anchor = 1;
   1091       return 1;
   1092     case R_RV_PCREL_LO12_I:
   1093     case R_RV_PCREL_LO12_S:
   1094       out->surg = ARCH_RELOC_SURG_RV_LO12;
   1095       out->prefix = "%pcrel_lo(";
   1096       out->suffix = ")";
   1097       out->ref_anchor = 1; /* references the preceding AUIPC's anchor label */
   1098       return 1;
   1099     case R_RV_HI20:
   1100       out->surg = ARCH_RELOC_SURG_TAIL;
   1101       out->prefix = "%hi(";
   1102       out->suffix = ")";
   1103       return 1;
   1104     case R_RV_LO12_I:
   1105     case R_RV_LO12_S:
   1106       out->surg = ARCH_RELOC_SURG_RV_LO12;
   1107       out->prefix = "%lo(";
   1108       out->suffix = ")";
   1109       return 1;
   1110     case R_RV_BRANCH:
   1111     case R_RV_JAL:
   1112       out->surg = ARCH_RELOC_SURG_TAIL;
   1113       return 1;
   1114     default:
   1115       return 0; /* R_ABS*, R_RV_RVC_*, R_RV_RELAX, TLS, ... → keep numeric */
   1116   }
   1117 }
   1118 
   1119 /* Intra-section local branches whose target codegen resolved in place (no
   1120  * relocation): the disassembler renders the target numerically, so cc -S
   1121  * synthesizes a label there. `j`/`jal x0` are JAL aliases; the conditional
   1122  * branches are B-type. `call`/`tail` are excluded — they carry R_RV_CALL. */
   1123 static int rv64_is_local_branch(KitSlice m) {
   1124   if (m.len == 1 && m.s[0] == 'j') return 1;
   1125   if (m.len == 3 && memcmp(m.s, "jal", 3) == 0) return 1;
   1126   if (m.len == 3 && memcmp(m.s, "beq", 3) == 0) return 1;
   1127   if (m.len == 3 && memcmp(m.s, "bne", 3) == 0) return 1;
   1128   if (m.len == 3 && memcmp(m.s, "blt", 3) == 0) return 1;
   1129   if (m.len == 3 && memcmp(m.s, "bge", 3) == 0) return 1;
   1130   if (m.len == 4 && memcmp(m.s, "bltu", 4) == 0) return 1;
   1131   if (m.len == 4 && memcmp(m.s, "bgeu", 4) == 0) return 1;
   1132   if (m.len == 4 && memcmp(m.s, "beqz", 4) == 0) return 1;
   1133   if (m.len == 4 && memcmp(m.s, "bnez", 4) == 0) return 1;
   1134   if (m.len == 4 && memcmp(m.s, "blez", 4) == 0) return 1;
   1135   if (m.len == 4 && memcmp(m.s, "bgez", 4) == 0) return 1;
   1136   if (m.len == 4 && memcmp(m.s, "bltz", 4) == 0) return 1;
   1137   if (m.len == 4 && memcmp(m.s, "bgtz", 4) == 0) return 1;
   1138   if (m.len == 6 && memcmp(m.s, "c.beqz", 6) == 0) return 1;
   1139   if (m.len == 6 && memcmp(m.s, "c.bnez", 6) == 0) return 1;
   1140   if (m.len == 3 && memcmp(m.s, "c.j", 3) == 0) return 1;
   1141   return 0;
   1142 }
   1143 
   1144 /* R_RV_CALL fuses an AUIPC+JALR pair into a single `call`/`tail sym` pseudo
   1145  * (the canonical `.s` spelling the assembler re-expands to the same pair +
   1146  * reloc). The reloc sits on the AUIPC; the JALR partner carries no reloc. A
   1147  * tail call links into x0 (the JALR's rd is `zero`); a regular call links into
   1148  * ra. We read that from the partner JALR's disassembled text. */
   1149 static int rv64_reloc_call_pair(u16 kind, KitSlice pair_mnemonic,
   1150                                 KitSlice pair_ops, const char** mnemonic_out) {
   1151   if (kind != R_RV_CALL) return 0;
   1152   /* The partner JALR links into ra (regular call) or x0 (tail). The
   1153    * disassembler renders the x0-link, zero-immediate form as the `jr rs`
   1154    * alias, and the ra form as `jalr ra, 0(ra)`. So a `jr` partner is always a
   1155    * tail; a `jalr` partner is a tail iff its link register is `zero`. */
   1156   if (pair_mnemonic.len == 2 && memcmp(pair_mnemonic.s, "jr", 2) == 0) {
   1157     *mnemonic_out = "tail";
   1158     return 1;
   1159   }
   1160   if (pair_mnemonic.len == 4 && memcmp(pair_mnemonic.s, "jalr", 4) == 0) {
   1161     if (pair_ops.len >= 4 && memcmp(pair_ops.s, "zero", 4) == 0)
   1162       *mnemonic_out = "tail";
   1163     else
   1164       *mnemonic_out = "call";
   1165     return 1;
   1166   }
   1167   return 0;
   1168 }
   1169 
   1170 const ArchAsmOps rv64_asm_ops = {
   1171     .reloc_operand = rv64_reloc_operand,
   1172     .is_local_branch = rv64_is_local_branch,
   1173     .reloc_call_pair = rv64_reloc_call_pair,
   1174 };
   1175 
   1176 ArchAsm* rv64_arch_asm_new(Compiler* c) {
   1177   Rv64Asm* a = arena_new(c->tu, Rv64Asm);
   1178   memset(a, 0, sizeof *a);
   1179   a->base.insn = rv64_arch_asm_insn;
   1180   a->base.destroy = rv64_arch_asm_destroy;
   1181   a->c = c;
   1182   return &a->base;
   1183 }
   1184 
   1185 /* ============================================================
   1186  * Inline-asm template walker (parallel to aa64 asm.c §"inline-asm
   1187  * template walker"). The walker substitutes %N / %[name] / %% / %a%w%x
   1188  * placeholders into a per-line StrBuf, then re-lexes each line through
   1189  * rv64_arch_asm_insn for assembly. Statement separators recognised are
   1190  * '\n' and ';' (outside parens / quoted strings).
   1191  * ============================================================ */
   1192 
   1193 Rv64Asm* rv64_asm_open(Compiler* c) {
   1194   Rv64Asm* a = arena_new(c->tu, Rv64Asm);
   1195   memset(a, 0, sizeof *a);
   1196   a->base.insn = rv64_arch_asm_insn;
   1197   a->base.destroy = rv64_arch_asm_destroy;
   1198   a->c = c;
   1199   return a;
   1200 }
   1201 
   1202 void rv64_asm_close(Rv64Asm* a) { (void)a; }
   1203 
   1204 void rv64_inline_bind(Rv64Asm* a, const AsmConstraint* outs, u32 nout,
   1205                       Operand* out_ops, const AsmConstraint* ins, u32 nin,
   1206                       const Operand* in_ops, const Sym* clobbers, u32 nclob) {
   1207   a->outs = outs;
   1208   a->out_ops = out_ops;
   1209   a->ins = ins;
   1210   a->in_ops = in_ops;
   1211   a->clobbers = clobbers;
   1212   a->nout = nout;
   1213   a->nin = nin;
   1214   a->nclob = nclob;
   1215 }
   1216 
   1217 /* Per-line rendered buffer cap. Inline asm rarely emits more than a
   1218  * handful of insns per block; one substituted line fits comfortably.
   1219  * Truncation panics — the operator grammar should never grow a single
   1220  * line beyond this without a deliberate reason. */
   1221 #define RV64_INLINE_LINE_CAP 1024
   1222 
   1223 _Noreturn static void inline_panic(Rv64Asm* a, const char* msg) {
   1224   SrcLoc loc = {0, 0, 0};
   1225   compiler_panic(a->c, loc, "rv64 inline asm: %.*s",
   1226                  SLICE_ARG(slice_from_cstr(msg)));
   1227 }
   1228 
   1229 /* Render a 5-bit integer register number using its canonical psABI name. */
   1230 static void render_xreg(StrBuf* sb, u32 reg) {
   1231   const char* nm = rv64_register_name(reg & 0x1fu);
   1232   if (!nm) {
   1233     strbuf_putc(sb, 'x');
   1234     if ((reg & 0x1fu) >= 10u)
   1235       strbuf_putc(sb, (char)('0' + ((reg & 0x1fu) / 10u)));
   1236     strbuf_putc(sb, (char)('0' + ((reg & 0x1fu) % 10u)));
   1237     return;
   1238   }
   1239   strbuf_puts(sb, nm);
   1240 }
   1241 
   1242 /* Render an FP register by its canonical psABI name (e.g., fa0). */
   1243 static void render_freg(StrBuf* sb, u32 reg) {
   1244   const char* nm = rv64_register_name(32u + (reg & 0x1fu));
   1245   if (!nm) {
   1246     strbuf_putc(sb, 'f');
   1247     if ((reg & 0x1fu) >= 10u)
   1248       strbuf_putc(sb, (char)('0' + ((reg & 0x1fu) / 10u)));
   1249     strbuf_putc(sb, (char)('0' + ((reg & 0x1fu) % 10u)));
   1250     return;
   1251   }
   1252   strbuf_puts(sb, nm);
   1253 }
   1254 
   1255 /* Render a signed 64-bit integer. Inline asm immediates appear bare in
   1256  * RISC-V (no '#' prefix), matching the standalone .s parser. */
   1257 static void render_imm(StrBuf* sb, i64 v) { strbuf_put_i64(sb, v); }
   1258 
   1259 /* Render addressing form `disp(base)`. */
   1260 static void render_indirect(Rv64Asm* a, StrBuf* sb, Reg base, i32 ofs) {
   1261   (void)a;
   1262   if (ofs != 0)
   1263     strbuf_put_i64(sb, (i64)ofs);
   1264   else
   1265     strbuf_putc(sb, '0');
   1266   strbuf_putc(sb, '(');
   1267   render_xreg(sb, (u32)base);
   1268   strbuf_putc(sb, ')');
   1269 }
   1270 
   1271 /* Resolve operand index → render into sb. form:
   1272  *   0 = default (per-kind),
   1273  *   1 = %wN (width hint; on rv64 same as default xreg form),
   1274  *   2 = %xN (force 64-bit reg form — identical to default for rv64),
   1275  *   3 = %aN (memory addressing form).
   1276  *   4 = %zN (RISC-V GCC: emits "zero" if operand is imm 0, else reg). */
   1277 static void render_operand(Rv64Asm* a, StrBuf* sb, u32 idx, int form) {
   1278   u32 ntot = a->nout + a->nin;
   1279   if (idx >= ntot) inline_panic(a, "operand index out of range");
   1280   const Operand* op =
   1281       (idx < a->nout) ? &a->out_ops[idx] : &a->in_ops[idx - a->nout];
   1282   switch (form) {
   1283     case 1: /* %wN — accept any reg/imm; rv64 has no narrower spelling. */
   1284     case 2: /* %xN — same. */
   1285       if (op->kind == RV64_INLINE_OPK_REG) {
   1286         if (op->pad[0] == RV64_INLINE_OPCLS_FP)
   1287           render_freg(sb, (u32)op->v.local);
   1288         else
   1289           render_xreg(sb, (u32)op->v.local);
   1290         return;
   1291       }
   1292       if (op->kind == OPK_IMM) {
   1293         render_imm(sb, op->v.imm);
   1294         return;
   1295       }
   1296       inline_panic(a, "%w/%x on unsupported operand kind");
   1297     case 3: /* %aN — memory addressing form */
   1298       if (op->kind != OPK_INDIRECT) inline_panic(a, "%a on non-memory operand");
   1299       if (op->v.ind.index != CG_LOCAL_NONE)
   1300         inline_panic(a,
   1301                      "%a on indexed memory operand: rv64 inline asm "
   1302                      "requires base+disp only");
   1303       render_indirect(a, sb, (Reg)op->v.ind.base, op->v.ind.ofs);
   1304       return;
   1305     case 4: /* %zN — zero-or-reg */
   1306       if (op->kind == OPK_IMM && op->v.imm == 0) {
   1307         strbuf_puts(sb, "zero");
   1308         return;
   1309       }
   1310       if (op->kind == RV64_INLINE_OPK_REG) {
   1311         if (op->pad[0] == RV64_INLINE_OPCLS_FP)
   1312           render_freg(sb, (u32)op->v.local);
   1313         else
   1314           render_xreg(sb, (u32)op->v.local);
   1315         return;
   1316       }
   1317       inline_panic(a, "%z on unsupported operand kind");
   1318     default:
   1319       break;
   1320   }
   1321   switch (op->kind) {
   1322     case RV64_INLINE_OPK_REG:
   1323       if (op->pad[0] == RV64_INLINE_OPCLS_FP)
   1324         render_freg(sb, (u32)op->v.local);
   1325       else
   1326         render_xreg(sb, (u32)op->v.local);
   1327       return;
   1328     case OPK_IMM:
   1329       render_imm(sb, op->v.imm);
   1330       return;
   1331     case OPK_INDIRECT:
   1332       if (op->v.ind.index != CG_LOCAL_NONE)
   1333         inline_panic(a,
   1334                      "indexed memory operand in inline asm: rv64 requires "
   1335                      "base+disp only");
   1336       render_indirect(a, sb, (Reg)op->v.ind.base, op->v.ind.ofs);
   1337       return;
   1338     default:
   1339       inline_panic(a, "unsupported operand kind for %N");
   1340   }
   1341 }
   1342 
   1343 /* Resolve a `%[name]` operand by looking up `needle` against the
   1344  * constraint.name fields on the combined outs+ins list. Returns the
   1345  * combined index, or (u32)-1 on miss. */
   1346 static u32 lookup_named(Rv64Asm* a, Sym needle) {
   1347   for (u32 k = 0; k < a->nout; ++k) {
   1348     if (a->outs[k].name == needle) return k;
   1349   }
   1350   for (u32 k = 0; k < a->nin; ++k) {
   1351     if (a->ins[k].name == needle) return a->nout + k;
   1352   }
   1353   return (u32)-1;
   1354 }
   1355 
   1356 /* Lex one line of substituted asm and dispatch via rv64_arch_asm_insn. */
   1357 static void run_one_line(Rv64Asm* a, MCEmitter* mc, const char* text,
   1358                          size_t len) {
   1359   /* Skip blank lines. */
   1360   size_t i;
   1361   for (i = 0; i < len; ++i) {
   1362     if (text[i] != ' ' && text[i] != '\t') break;
   1363   }
   1364   if (i == len) return;
   1365 
   1366   AsmLexer* lx = asm_lex_open_mem(a->c, "<inline-asm>", text, len);
   1367   AsmDriver* d = asm_driver_open_inline(a->c, mc, lx);
   1368 
   1369   /* The first non-trivial token must be the mnemonic identifier. */
   1370   AsmTok t = asm_driver_peek(d);
   1371   while (t.kind == ASM_TOK_NEWLINE) {
   1372     (void)asm_driver_next(d);
   1373     t = asm_driver_peek(d);
   1374   }
   1375   if (t.kind == ASM_TOK_EOF) {
   1376     asm_driver_close_inline(d);
   1377     asm_lex_close(lx);
   1378     return;
   1379   }
   1380   if (t.kind != ASM_TOK_IDENT)
   1381     inline_panic(a, "expected mnemonic at start of inline asm line");
   1382   (void)asm_driver_next(d);
   1383   Sym mn = t.v.ident;
   1384   /* Compose `fcvt.s.w` etc. — rv64 has dotted mnemonics; the standalone
   1385    * lexer already strings them together as a single IDENT in most paths.
   1386    * Mirror the aa64 composite handling for safety. */
   1387   AsmTok dot = asm_driver_peek(d);
   1388   while (asm_driver_tok_is_punct(dot, '.')) {
   1389     (void)asm_driver_next(d);
   1390     AsmTok rest = asm_driver_next(d);
   1391     if (rest.kind != ASM_TOK_IDENT)
   1392       inline_panic(a, "composite mnemonic: expected ident after '.'");
   1393     Slice hsl = pool_slice(asm_driver_pool(d), mn);
   1394     Slice rsl = pool_slice(asm_driver_pool(d), rest.v.ident);
   1395     size_t hn = hsl.len, rn = rsl.len;
   1396     char buf[64];
   1397     if (hn + 1 + rn >= sizeof buf)
   1398       inline_panic(a, "composite mnemonic too long");
   1399     for (size_t k = 0; k < hn; ++k) buf[k] = hsl.s[k];
   1400     buf[hn] = '.';
   1401     for (size_t k = 0; k < rn; ++k) buf[hn + 1 + k] = rsl.s[k];
   1402     mn = pool_intern_slice(asm_driver_pool(d),
   1403                            (Slice){.s = buf, .len = hn + 1 + rn});
   1404     dot = asm_driver_peek(d);
   1405   }
   1406   rv64_arch_asm_insn(&a->base, d, mn);
   1407   asm_driver_close_inline(d);
   1408   asm_lex_close(lx);
   1409 }
   1410 
   1411 /* Substitute placeholders into one line's StrBuf, then dispatch. */
   1412 static void render_and_run_line(Rv64Asm* a, MCEmitter* mc, StrBuf* sb,
   1413                                 const char* start, const char* end) {
   1414   strbuf_reset(sb);
   1415   for (const char* p = start; p < end; ++p) {
   1416     char c = *p;
   1417     if (c != '%') {
   1418       strbuf_putc(sb, c);
   1419       continue;
   1420     }
   1421     /* Placeholder. */
   1422     if (p + 1 >= end) inline_panic(a, "trailing '%' in template");
   1423     char n = *(p + 1);
   1424     if (n == '%') {
   1425       strbuf_putc(sb, '%');
   1426       ++p;
   1427       continue;
   1428     }
   1429     if (n == '[') {
   1430       const char* nbeg = p + 2;
   1431       const char* nend = nbeg;
   1432       while (nend < end && *nend != ']') ++nend;
   1433       if (nend == end) inline_panic(a, "unterminated %[name]");
   1434       size_t nlen = (size_t)(nend - nbeg);
   1435       Sym needle =
   1436           pool_intern_slice(a->c->global, (Slice){.s = nbeg, .len = nlen});
   1437       u32 idx = lookup_named(a, needle);
   1438       if (idx == (u32)-1)
   1439         inline_panic(a, "%[name] does not match any constraint");
   1440       p = nend; /* loop's ++p steps past the ']' */
   1441       render_operand(a, sb, idx, 0);
   1442       continue;
   1443     }
   1444     int form = 0; /* 0=default, 1=w, 2=x, 3=a, 4=z */
   1445     if (n == 'w' || n == 'x' || n == 'a' || n == 'z') {
   1446       form = (n == 'w') ? 1 : (n == 'x') ? 2 : (n == 'a') ? 3 : 4;
   1447       ++p;
   1448       if (p + 1 >= end) inline_panic(a, "trailing '%' modifier in template");
   1449       n = *(p + 1);
   1450     }
   1451     if (n == '[') {
   1452       const char* nbeg = p + 2;
   1453       const char* nend = nbeg;
   1454       while (nend < end && *nend != ']') ++nend;
   1455       if (nend == end) inline_panic(a, "unterminated %[name]");
   1456       size_t nlen = (size_t)(nend - nbeg);
   1457       Sym needle =
   1458           pool_intern_slice(a->c->global, (Slice){.s = nbeg, .len = nlen});
   1459       u32 idx = lookup_named(a, needle);
   1460       if (idx == (u32)-1)
   1461         inline_panic(a, "%[name] does not match any constraint");
   1462       p = nend;
   1463       render_operand(a, sb, idx, form);
   1464       continue;
   1465     }
   1466     if (n < '0' || n > '9') inline_panic(a, "expected digit after '%'");
   1467     u32 idx = (u32)(n - '0');
   1468     ++p;
   1469     /* GCC syntax permits up to two digits (%0..%99). */
   1470     if (p + 1 < end && *(p + 1) >= '0' && *(p + 1) <= '9') {
   1471       idx = idx * 10 + (u32)(*(p + 1) - '0');
   1472       ++p;
   1473     }
   1474     render_operand(a, sb, idx, form);
   1475   }
   1476   if (sb->truncated) inline_panic(a, "inline asm line buffer overflow");
   1477   run_one_line(a, mc, strbuf_cstr(sb), strbuf_len(sb));
   1478 }
   1479 
   1480 void rv64_asm_run_template(Rv64Asm* a, MCEmitter* mc, const char* tmpl) {
   1481   if (!tmpl || !*tmpl) return;
   1482 
   1483   char buf[RV64_INLINE_LINE_CAP];
   1484   StrBuf sb;
   1485   strbuf_init(&sb, buf, sizeof buf);
   1486 
   1487   /* Walk tmpl, splitting on '\n' and ';'. Track paren depth and quote
   1488    * state so that a literal ';' inside `( ... )` (memory operand) or a
   1489    * quoted string is not mistaken for a statement separator. RISC-V uses
   1490    * `disp(base)` for memory, hence we track parens. */
   1491   const char* line_start = tmpl;
   1492   int paren = 0;
   1493   char quote = 0;
   1494   for (const char* p = tmpl;; ++p) {
   1495     char c = *p;
   1496     if (c == '\0') {
   1497       render_and_run_line(a, mc, &sb, line_start, p);
   1498       break;
   1499     }
   1500     if (quote) {
   1501       if (c == '\\' && *(p + 1)) {
   1502         ++p;
   1503         continue;
   1504       }
   1505       if (c == quote) quote = 0;
   1506       continue;
   1507     }
   1508     if (c == '"' || c == '\'') {
   1509       quote = c;
   1510       continue;
   1511     }
   1512     if (c == '(') {
   1513       ++paren;
   1514       continue;
   1515     }
   1516     if (c == ')') {
   1517       if (paren) --paren;
   1518       continue;
   1519     }
   1520     if (paren == 0 && (c == '\n' || c == ';')) {
   1521       render_and_run_line(a, mc, &sb, line_start, p);
   1522       line_start = p + 1;
   1523     }
   1524   }
   1525 }