kit

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

asm.c (57362B)


      1 /* GNU-as compatible assembler driver — arch-agnostic.
      2  *
      3  * Reads tokens from an AsmLexer, dispatches directives, manages labels and
      4  * section state, and forwards mnemonic lines to the per-arch instruction
      5  * parser.  Output goes through MCEmitter against an ObjBuilder.
      6  *
      7  * AsmLexer quirks worked around here:
      8  *   - `#` is both the immediate marker in asm and the token used for
      9  *     preprocessed-assembler line markers.
     10  *     `#` at BOL is a cpp linemarker → skip to next newline; elsewhere
     11  *     the per-arch parser treats it as the immediate prefix.
     12  *   - composite mnemonics (`b.eq`, `b.ne`, ...) arrive as IDENT '.' IDENT
     13  *     and are reassembled before dispatch.
     14  *   - `.text` etc. arrive as PUNCT('.') + IDENT and are stitched here.
     15  *
     16  * Symbol bookkeeping: a Sym→ObjSymId map records the symbols introduced
     17  * by labels, `.globl`, and operand references so a forward reference
     18  * (`b foo` before `foo:`) shares one symbol with its later definition.
     19  * A second Sym→AsmEqu map carries `.set`/`.equ` constants. */
     20 
     21 #include "asm/asm.h"
     22 
     23 #include <stdarg.h>
     24 #include <string.h>
     25 
     26 #include "arch/arch.h"
     27 #include "asm/asm_helpers.h"
     28 #include "asm/asm_lex.h"
     29 #include "core/arena.h"
     30 #include "core/hashmap.h"
     31 #include "core/heap.h"
     32 #include "core/pool.h"
     33 #include "core/slice.h"
     34 #include "core/vec.h"
     35 #include "obj/obj.h"
     36 #include "obj/reloc_apply.h"
     37 
     38 HASHMAP_DEFINE(SymSecMap, Sym, ObjSecId, hash_u32);
     39 HASHMAP_DEFINE(SymSymMap, Sym, ObjSymId, hash_u32);
     40 HASHMAP_DEFINE(SymU8Map, Sym, u8, hash_u32);
     41 
     42 typedef struct AsmEqu {
     43   i64 value;
     44   ObjSymId sym; /* nonzero when value is `sym + offset` */
     45   u8 has_sym;
     46   u8 pad[3];
     47 } AsmEqu;
     48 HASHMAP_DEFINE(SymEquMap, Sym, AsmEqu, hash_u32);
     49 
     50 /* A `sym1 - sym2` data difference (e.g. `.quad _end - _start`) recorded at
     51  * emit time and resolved after the parse, once every label is defined. The
     52  * field at <section,offset> already holds the net constant addend. */
     53 typedef struct AsmDiff {
     54   ObjSecId section; /* fixup site section */
     55   u32 offset;       /* fixup site offset within `section` */
     56   u32 width;        /* 1/2/4/8 — the data width */
     57   ObjSymId minuend;    /* sym1 */
     58   ObjSymId subtrahend; /* sym2 */
     59   i64 addend;          /* net constant addend already written into the field */
     60   SrcLoc loc;          /* the directive's location, for late diagnostics */
     61 } AsmDiff;
     62 
     63 struct AsmDriver {
     64   Compiler* c;
     65   AsmLexer* lex;
     66   MCEmitter* mc;
     67   ObjBuilder* ob;
     68   Pool* pool;
     69   Heap* heap;
     70 
     71   AsmTok cur;
     72   int has_cur;
     73 
     74   /* OBJ_SEC_NONE until first emit / explicit `.text` etc. */
     75   ObjSecId cur_sec;
     76 
     77   SymSecMap sec_map;
     78   SymSymMap sym_map;
     79   SymU8Map thumb_func_map;
     80   SymEquMap equ_map;
     81 
     82   /* Deferred `sym1 - sym2` data differences (resolve_sym_diffs). */
     83   AsmDiff* diffs;
     84   u32 ndiffs;
     85   u32 diffs_cap;
     86 
     87   Sym n_text, n_data, n_rodata, n_bss;
     88   u8 pending_thumb_func;
     89 
     90   ArchAsm* arch_asm;
     91 };
     92 
     93 /* ---- token plumbing ---- */
     94 
     95 static AsmTok d_peek(AsmDriver* d) {
     96   if (!d->has_cur) {
     97     d->cur = asm_lex_next(d->lex);
     98     d->has_cur = 1;
     99   }
    100   return d->cur;
    101 }
    102 
    103 static AsmTok d_next(AsmDriver* d) {
    104   AsmTok t = d_peek(d);
    105   d->has_cur = 0;
    106   return t;
    107 }
    108 
    109 static int d_is_eol(AsmDriver* d) {
    110   AsmTok t = d_peek(d);
    111   return t.kind == ASM_TOK_NEWLINE || t.kind == ASM_TOK_EOF;
    112 }
    113 
    114 static void d_skip_to_eol(AsmDriver* d) {
    115   while (!d_is_eol(d)) (void)d_next(d);
    116 }
    117 
    118 static void d_eat_eol(AsmDriver* d) {
    119   AsmTok t = d_peek(d);
    120   if (t.kind == ASM_TOK_NEWLINE) (void)d_next(d);
    121 }
    122 
    123 static SrcLoc d_loc(AsmDriver* d) {
    124   if (d->has_cur) return d->cur.loc;
    125   return asm_lex_loc(d->lex);
    126 }
    127 
    128 _Noreturn static void d_panicf(AsmDriver* d, const char* fmt, ...) {
    129   va_list ap;
    130   va_start(ap, fmt);
    131   compiler_panicv(d->c, d_loc(d), fmt, ap);
    132   /* unreachable; va_end omitted because compiler_panicv is _Noreturn */
    133 }
    134 
    135 /* ---- spelling helpers ---- */
    136 
    137 static const char* asm_str(AsmDriver* d, Sym s, size_t* nout) {
    138   Slice sl = pool_slice(d->pool, s);
    139   if (nout) *nout = sl.len;
    140   return sl.s;
    141 }
    142 
    143 static int sym_eq(AsmDriver* d, Sym s, const char* lit) {
    144   size_t n = 0;
    145   const char* p = asm_str(d, s, &n);
    146   size_t i;
    147   if (!p) return 0;
    148   for (i = 0; i < n; ++i) {
    149     if (!lit[i] || p[i] != lit[i]) return 0;
    150   }
    151   return lit[n] == '\0';
    152 }
    153 
    154 static int starts_with(AsmDriver* d, Sym s, const char* prefix) {
    155   size_t n = 0;
    156   const char* p = asm_str(d, s, &n);
    157   size_t i;
    158   if (!p) return 0;
    159   for (i = 0; prefix[i]; ++i) {
    160     if (i >= n || p[i] != prefix[i]) return 0;
    161   }
    162   return 1;
    163 }
    164 
    165 /* ---- section management ---- */
    166 
    167 static ObjSecId ensure_section_ex(AsmDriver* d, Sym name, SecKind kind, u16 sem,
    168                                   u16 flags, u32 align) {
    169   ObjSecId* hit = SymSecMap_get(&d->sec_map, name);
    170   ObjSecId id;
    171   if (hit) return *hit;
    172   id = obj_section_ex(d->ob, name, kind, sem, flags, align, 0, OBJ_SEC_NONE, 0);
    173   SymSecMap_set(&d->sec_map, name, id);
    174   return id;
    175 }
    176 
    177 static ObjSecId ensure_section(AsmDriver* d, Sym name, SecKind kind, u16 flags,
    178                                u32 align) {
    179   /* A .bss section is NOBITS: it stores no bytes, only a size. Create it that
    180    * way (codegen does the same via obj_section_ex) so the ELF emitter writes
    181    * SHT_NOBITS and `.zero`/labels track bss_size, not a byte buffer — matching
    182    * `cc -c` so the round-tripped object isn't a writable-but-loaded .bss. */
    183   return ensure_section_ex(d, name, kind,
    184                            kind == SEC_BSS ? SSEM_NOBITS : SSEM_PROGBITS, flags,
    185                            align);
    186 }
    187 
    188 static void set_section(AsmDriver* d, Sym name, SecKind kind, u16 flags,
    189                         u32 align) {
    190   ObjSecId id = ensure_section(d, name, kind, flags, align);
    191   d->cur_sec = id;
    192   mc_set_section(d->mc, id);
    193 }
    194 
    195 /* ---- symbol management ---- */
    196 
    197 static ObjSymId intern_sym(AsmDriver* d, Sym name) {
    198   ObjSymId* hit = SymSymMap_get(&d->sym_map, name);
    199   if (hit) return *hit;
    200   ObjSymId id = obj_symbol_find(d->ob, name);
    201   if (id == OBJ_SYM_NONE) {
    202     id = obj_symbol_ex(d->ob, name, SB_LOCAL, SV_DEFAULT, SK_NOTYPE,
    203                        OBJ_SEC_NONE, 0, 0, 0);
    204   }
    205   SymSymMap_set(&d->sym_map, name, id);
    206   return id;
    207 }
    208 
    209 static ObjSym* sym_mut(AsmDriver* d, ObjSymId id) {
    210   /* obj.h gives us a const view via obj_symbol_get; the underlying
    211    * record lives in the builder's arena and is safe to mutate
    212    * pre-finalize.  Wrapping the cast keeps the const-stripping in
    213    * one place. */
    214   return (ObjSym*)obj_symbol_get(d->ob, id);
    215 }
    216 
    217 /* 1 if the active arch tags function symbols with a Thumb (LSB) ISA-state
    218  * bit. Replaces the former `target.arch == KIT_ARCH_ARM_32` identity check:
    219  * the behavior now hangs off the arch's ArchAsmOps vtable (set only on arm32),
    220  * so generic assembler code stops switching on arch identity. */
    221 static int asm_thumb_func_syms(const AsmDriver* d) {
    222   const ArchImpl* a = d->c ? arch_for_compiler(d->c) : NULL;
    223   return a && a->asm_ops && a->asm_ops->thumb_function_symbols;
    224 }
    225 
    226 static int asm_sym_is_thumb_func(AsmDriver* d, Sym name) {
    227   u8* hit = SymU8Map_get(&d->thumb_func_map, name);
    228   return hit && *hit;
    229 }
    230 
    231 static void mark_thumb_func(AsmDriver* d, Sym name) {
    232   ObjSymId id;
    233   ObjSym* s;
    234   if (!asm_thumb_func_syms(d)) return;
    235   (void)SymU8Map_set(&d->thumb_func_map, name, 1u);
    236   id = intern_sym(d, name);
    237   s = sym_mut(d, id);
    238   if (!s) return;
    239   s->kind = (u16)SK_FUNC;
    240   if (s->section_id != OBJ_SEC_NONE) s->value |= 1u;
    241 }
    242 
    243 /* GNU `as` makes any symbol that is referenced but neither defined nor
    244  * declared `.local` an undefined *global* (a local UNDEF is meaningless
    245  * in ELF and won't pull a member out of an archive at link time).
    246  * intern_sym mints every new symbol SB_LOCAL/SK_NOTYPE, so after the
    247  * parse we promote the ones that stayed undefined to global SK_UNDEF.
    248  * Defined locals (labels), `.local` decls, and absolute/common symbols
    249  * are left untouched. */
    250 static void promote_undef_externs(AsmDriver* d) {
    251   ObjSymIter* it = obj_symiter_new(d->ob);
    252   ObjSymEntry e;
    253   while (obj_symiter_next(it, &e)) {
    254     /* The iterator visits tombstoned slots too (see obj.h). Deferred
    255      * anonymous const-data / jump-table symbols (obj_symbol_defer) sit as
    256      * LOCAL/SK_OBJ/no-section tombstones until opt_whole_module_finalize
    257      * materializes them — which, when a file-scope `asm` block (e.g. a
    258      * FreeBSD header's `.symver`) replays before that finalize step, is
    259      * *after* this pass runs. Promoting them here would resurface them as
    260      * defined GLOBALs and collide at link (`duplicate definition of global
    261      * symbol '.Lkit_ro.0'`), so skip tombstones like every other consumer. */
    262     if (e.sym->removed) continue;
    263     if (e.sym->section_id != OBJ_SEC_NONE) continue; /* defined here */
    264     if (e.sym->bind != SB_LOCAL) continue;
    265     if (e.sym->kind == SK_ABS || e.sym->kind == SK_COMMON) continue;
    266     obj_symbol_set_bind(d->ob, e.id, SB_GLOBAL);
    267     sym_mut(d, e.id)->kind = (u16)SK_UNDEF;
    268   }
    269   obj_symiter_free(it);
    270 }
    271 
    272 /* ---- expression evaluator (constants + sym ± const) ---- */
    273 
    274 typedef struct AsmExpr {
    275   ObjSymId sym;  /* minuend, or the sole symbol */
    276   ObjSymId sym2; /* subtrahend: nonzero => value is `(sym - sym2) + const` */
    277   i64 value;
    278   u8 is_here; /* the location-counter token `.` (no sym, no value yet) */
    279   u8 pcrel;   /* `sym - .`: emit a PC-relative data reloc instead of absolute */
    280 } AsmExpr;
    281 
    282 static AsmExpr expr_c(i64 v) {
    283   AsmExpr e = {OBJ_SYM_NONE, OBJ_SYM_NONE, v, 0, 0};
    284   return e;
    285 }
    286 static AsmExpr expr_s(ObjSymId s, i64 v) {
    287   AsmExpr e = {s, OBJ_SYM_NONE, v, 0, 0};
    288   return e;
    289 }
    290 static AsmExpr expr_here(void) {
    291   AsmExpr e = {OBJ_SYM_NONE, OBJ_SYM_NONE, 0, 1, 0};
    292   return e;
    293 }
    294 
    295 static int tok_is_punct(AsmTok t, u32 p) {
    296   return t.kind == ASM_TOK_PUNCT && t.v.punct == p;
    297 }
    298 
    299 static i64 lit_to_i64(AsmDriver* d, Sym spelling) {
    300   size_t n = 0;
    301   const char* p = asm_str(d, spelling, &n);
    302   u64 v = 0;
    303   int base = 10;
    304   size_t i = 0;
    305   if (!p || !n) return 0;
    306   if (n >= 2 && p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) {
    307     base = 16;
    308     i = 2;
    309   } else if (n >= 2 && p[0] == '0' && (p[1] == 'b' || p[1] == 'B')) {
    310     base = 2;
    311     i = 2;
    312   } else if (n >= 1 && p[0] == '0') {
    313     base = 8;
    314     i = 1;
    315   }
    316   for (; i < n; ++i) {
    317     char c = p[i];
    318     u32 dv;
    319     if (c == 'u' || c == 'U' || c == 'l' || c == 'L') break;
    320     if (c >= '0' && c <= '9')
    321       dv = (u32)(c - '0');
    322     else if (c >= 'a' && c <= 'f')
    323       dv = 10 + (u32)(c - 'a');
    324     else if (c >= 'A' && c <= 'F')
    325       dv = 10 + (u32)(c - 'A');
    326     else
    327       d_panicf(d, "asm: bad digit in integer literal");
    328     if (dv >= (u32)base) d_panicf(d, "asm: digit out of base");
    329     v = v * (u64)base + dv;
    330   }
    331   return (i64)v;
    332 }
    333 
    334 static AsmExpr parse_expr(AsmDriver*);
    335 static AsmExpr parse_unary(AsmDriver*);
    336 
    337 static AsmExpr parse_primary(AsmDriver* d) {
    338   AsmTok t = d_peek(d);
    339   if (t.kind == ASM_TOK_NUM) {
    340     (void)d_next(d);
    341     return expr_c(lit_to_i64(d, t.spelling));
    342   }
    343   if (t.kind == ASM_TOK_IDENT) {
    344     (void)d_next(d);
    345     AsmEqu* eq = SymEquMap_get(&d->equ_map, t.v.ident);
    346     if (eq) {
    347       if (eq->has_sym) return expr_s(eq->sym, eq->value);
    348       return expr_c(eq->value);
    349     }
    350     return expr_s(intern_sym(d, t.v.ident), 0);
    351   }
    352   if (tok_is_punct(t, '(')) {
    353     (void)d_next(d);
    354     AsmExpr e = parse_expr(d);
    355     AsmTok cl = d_peek(d);
    356     if (!tok_is_punct(cl, ')')) d_panicf(d, "asm: expected ')'");
    357     (void)d_next(d);
    358     return e;
    359   }
    360   /* Lone `.` is the location counter (used in `sym - .` PC-relative data). */
    361   if (tok_is_punct(t, '.')) {
    362     (void)d_next(d);
    363     return expr_here();
    364   }
    365   d_panicf(d, "asm: expected expression");
    366 }
    367 
    368 static AsmExpr parse_unary(AsmDriver* d) {
    369   AsmTok t = d_peek(d);
    370   if (tok_is_punct(t, '-')) {
    371     (void)d_next(d);
    372     AsmExpr e = parse_unary(d);
    373     if (e.sym) d_panicf(d, "asm: unary '-' on symbol");
    374     /* Unsigned negate so `$-9223372036854775808` (negating INT64_MIN) is
    375      * well-defined 2's-complement, not signed-overflow UB. */
    376     return expr_c((i64)(0u - (u64)e.value));
    377   }
    378   if (tok_is_punct(t, '+')) {
    379     (void)d_next(d);
    380     return parse_unary(d);
    381   }
    382   if (tok_is_punct(t, '~')) {
    383     (void)d_next(d);
    384     AsmExpr e = parse_unary(d);
    385     if (e.sym) d_panicf(d, "asm: unary '~' on symbol");
    386     return expr_c(~e.value);
    387   }
    388   return parse_primary(d);
    389 }
    390 
    391 static AsmExpr parse_mul(AsmDriver* d) {
    392   AsmExpr a = parse_unary(d);
    393   for (;;) {
    394     AsmTok t = d_peek(d);
    395     if (!tok_is_punct(t, '*') && !tok_is_punct(t, '/') && !tok_is_punct(t, '%'))
    396       return a;
    397     u32 op = t.v.punct;
    398     (void)d_next(d);
    399     AsmExpr b = parse_unary(d);
    400     if (a.sym || b.sym || a.is_here || b.is_here)
    401       d_panicf(d, "asm: '*/%%' on symbolic operand");
    402     if (op == '*')
    403       a.value *= b.value;
    404     else if (op == '/') {
    405       if (!b.value) d_panicf(d, "asm: division by zero");
    406       a.value /= b.value;
    407     } else {
    408       if (!b.value) d_panicf(d, "asm: modulo by zero");
    409       a.value %= b.value;
    410     }
    411   }
    412 }
    413 
    414 static AsmExpr parse_add(AsmDriver* d) {
    415   AsmExpr a = parse_mul(d);
    416   for (;;) {
    417     AsmTok t = d_peek(d);
    418     if (!tok_is_punct(t, '+') && !tok_is_punct(t, '-')) return a;
    419     u32 op = t.v.punct;
    420     (void)d_next(d);
    421     AsmExpr b = parse_mul(d);
    422     /* `sym - .`: a PC-relative data reference. `.` is the location of the
    423      * field being emitted, so the relocation's P equals its own offset and the
    424      * RELA addend stays `a.value` (typically 0). */
    425     if (op == '-' && b.is_here) {
    426       if (!a.sym) d_panicf(d, "asm: '- .' requires a symbol operand");
    427       a.pcrel = 1;
    428       continue;
    429     }
    430     if (a.is_here || b.is_here)
    431       d_panicf(d, "asm: '.' location counter only valid as `sym - .`");
    432     if (op == '+') {
    433       if (a.sym && b.sym) d_panicf(d, "asm: cannot add two symbols");
    434       if (b.sym) {
    435         /* const + (b.sym [- b.sym2] + k): adopt b's symbol(s). */
    436         a.sym = b.sym;
    437         a.sym2 = b.sym2;
    438         a.value += b.value;
    439       } else
    440         a.value += b.value;
    441     } else {
    442       if (b.sym) {
    443         /* sym1 - sym2: a symbol difference. Resolves to a link-time constant
    444          * (both symbols in one section) or a relocation pair otherwise. */
    445         if (!a.sym) d_panicf(d, "asm: cannot subtract symbol from constant");
    446         if (a.sym2 || b.sym2)
    447           d_panicf(d, "asm: more than one symbol difference in expression");
    448         a.sym2 = b.sym;
    449         a.value -= b.value;
    450       } else
    451         a.value -= b.value;
    452     }
    453   }
    454 }
    455 
    456 static AsmExpr parse_shift(AsmDriver* d) {
    457   AsmExpr a = parse_add(d);
    458   for (;;) {
    459     AsmTok t = d_peek(d);
    460     if (!tok_is_punct(t, ASM_P_SHL) && !tok_is_punct(t, ASM_P_SHR)) return a;
    461     u32 op = t.v.punct;
    462     (void)d_next(d);
    463     AsmExpr b = parse_add(d);
    464     if (a.sym || b.sym) d_panicf(d, "asm: shift on symbolic operand");
    465     if (op == ASM_P_SHL)
    466       a.value = (i64)((u64)a.value << (b.value & 63));
    467     else
    468       a.value = a.value >> (b.value & 63);
    469   }
    470 }
    471 
    472 static AsmExpr parse_band(AsmDriver* d) {
    473   AsmExpr a = parse_shift(d);
    474   for (;;) {
    475     AsmTok t = d_peek(d);
    476     if (!tok_is_punct(t, '&')) return a;
    477     (void)d_next(d);
    478     AsmExpr b = parse_shift(d);
    479     if (a.sym || b.sym) d_panicf(d, "asm: '&' on symbolic operand");
    480     a.value &= b.value;
    481   }
    482 }
    483 
    484 static AsmExpr parse_bxor(AsmDriver* d) {
    485   AsmExpr a = parse_band(d);
    486   for (;;) {
    487     AsmTok t = d_peek(d);
    488     if (!tok_is_punct(t, '^')) return a;
    489     (void)d_next(d);
    490     AsmExpr b = parse_band(d);
    491     if (a.sym || b.sym) d_panicf(d, "asm: '^' on symbolic operand");
    492     a.value ^= b.value;
    493   }
    494 }
    495 
    496 static AsmExpr parse_bor(AsmDriver* d) {
    497   AsmExpr a = parse_bxor(d);
    498   for (;;) {
    499     AsmTok t = d_peek(d);
    500     if (!tok_is_punct(t, '|')) return a;
    501     (void)d_next(d);
    502     AsmExpr b = parse_bxor(d);
    503     if (a.sym || b.sym) d_panicf(d, "asm: '|' on symbolic operand");
    504     a.value |= b.value;
    505   }
    506 }
    507 
    508 static AsmExpr parse_expr(AsmDriver* d) { return parse_bor(d); }
    509 
    510 /* ---- public helpers exposed to per-arch parser ---- */
    511 
    512 AsmTok asm_driver_peek(AsmDriver* d) { return d_peek(d); }
    513 AsmTok asm_driver_next(AsmDriver* d) { return d_next(d); }
    514 int asm_driver_at_eol(AsmDriver* d) { return d_is_eol(d); }
    515 SrcLoc asm_driver_loc(AsmDriver* d) { return d_loc(d); }
    516 MCEmitter* asm_driver_mc(AsmDriver* d) { return d->mc; }
    517 ObjBuilder* asm_driver_ob(AsmDriver* d) { return d->ob; }
    518 Compiler* asm_driver_compiler(AsmDriver* d) { return d->c; }
    519 Pool* asm_driver_pool(AsmDriver* d) { return d->pool; }
    520 
    521 _Noreturn void asm_driver_panic(AsmDriver* d, const char* fmt, ...) {
    522   va_list ap;
    523   va_start(ap, fmt);
    524   compiler_panicv(d->c, d_loc(d), fmt, ap);
    525 }
    526 
    527 ObjSymId asm_driver_intern_sym(AsmDriver* d, Sym name) {
    528   return intern_sym(d, name);
    529 }
    530 
    531 ObjSecId asm_driver_cur_section(AsmDriver* d) {
    532   if (d->cur_sec == OBJ_SEC_NONE) {
    533     if (!d->n_text) d->n_text = pool_intern_slice(d->pool, SLICE_LIT(".text"));
    534     d->cur_sec =
    535         ensure_section(d, d->n_text, SEC_TEXT, (u16)(SF_ALLOC | SF_EXEC), 4);
    536     mc_set_section(d->mc, d->cur_sec);
    537   }
    538   return d->cur_sec;
    539 }
    540 
    541 int asm_driver_eat_comma(AsmDriver* d) {
    542   AsmTok t = d_peek(d);
    543   if (tok_is_punct(t, ',')) {
    544     (void)d_next(d);
    545     return 1;
    546   }
    547   return 0;
    548 }
    549 
    550 int asm_driver_eat_punct(AsmDriver* d, u32 p) {
    551   AsmTok t = d_peek(d);
    552   if (tok_is_punct(t, p)) {
    553     (void)d_next(d);
    554     return 1;
    555   }
    556   /* `#` arrives as ASM_TOK_HASH from the C lexer; accept it as the
    557    * immediate-prefix punctuator here. */
    558   if (p == '#' && t.kind == ASM_TOK_HASH) {
    559     (void)d_next(d);
    560     return 1;
    561   }
    562   return 0;
    563 }
    564 
    565 void asm_driver_expect_punct(AsmDriver* d, u32 p, const char* what) {
    566   if (!asm_driver_eat_punct(d, p)) {
    567     /* Single-char punctuators (incl. '#') carry their ASCII value in `p`;
    568      * print it directly. Multi-char puncts fall back to the description. */
    569     char ch = (char)p;
    570     if (p >= 0x20 && p < 0x7f)
    571       d_panicf(d, "asm: expected '%c' (%.*s)", ch,
    572                SLICE_ARG(slice_from_cstr(what)));
    573     d_panicf(d, "asm: expected punctuator (%.*s)",
    574              SLICE_ARG(slice_from_cstr(what)));
    575   }
    576 }
    577 
    578 i64 asm_driver_parse_const(AsmDriver* d) {
    579   AsmExpr e = parse_expr(d);
    580   if (e.sym) d_panicf(d, "asm: constant expression expected");
    581   return e.value;
    582 }
    583 
    584 void asm_driver_parse_sym_expr(AsmDriver* d, ObjSymId* sym_out, i64* off_out) {
    585   AsmExpr e = parse_expr(d);
    586   *sym_out = e.sym;
    587   *off_out = e.value;
    588 }
    589 
    590 int asm_driver_tok_is_punct(AsmTok t, u32 p) {
    591   if (tok_is_punct(t, p)) return 1;
    592   /* `#` arrives as ASM_TOK_HASH from the C lexer. */
    593   if (p == '#' && t.kind == ASM_TOK_HASH) return 1;
    594   return 0;
    595 }
    596 
    597 /* ---- string-literal decoding ---- */
    598 
    599 /* Decode a string literal's spelling into raw bytes. *out is the heap buffer
    600  * (allocated with size *cap_out, which the caller must pass back to free —
    601  * the decoded length *nout <= *cap_out once escapes collapse). */
    602 static void decode_string(AsmDriver* d, Sym spelling, u8** out, u32* nout,
    603                           size_t* cap_out) {
    604   size_t n = 0;
    605   const char* p = asm_str(d, spelling, &n);
    606   /* Skip any encoding prefix (L/u/u8/U). */
    607   while (n && (*p == 'L' || *p == 'u' || *p == 'U' || *p == '8')) {
    608     ++p;
    609     --n;
    610   }
    611   if (n < 2 || p[0] != '"' || p[n - 1] != '"')
    612     d_panicf(d, "asm: malformed string literal");
    613   size_t cap = n ? n : 1;
    614   u8* buf = (u8*)d->heap->alloc(d->heap, cap, 1);
    615   u32 k = 0;
    616   for (size_t i = 1; i + 1 < n; ++i) {
    617     char c = p[i];
    618     if (c != '\\') {
    619       buf[k++] = (u8)c;
    620       continue;
    621     }
    622     ++i;
    623     if (i + 1 >= n) break;
    624     char e = p[i];
    625     switch (e) {
    626       case 'n':
    627         buf[k++] = '\n';
    628         break;
    629       case 't':
    630         buf[k++] = '\t';
    631         break;
    632       case 'r':
    633         buf[k++] = '\r';
    634         break;
    635       case '\\':
    636         buf[k++] = '\\';
    637         break;
    638       case '"':
    639         buf[k++] = '"';
    640         break;
    641       case '\'':
    642         buf[k++] = '\'';
    643         break;
    644       case '0':
    645         buf[k++] = 0;
    646         break;
    647       case 'b':
    648         buf[k++] = 8;
    649         break;
    650       case 'f':
    651         buf[k++] = 12;
    652         break;
    653       case 'v':
    654         buf[k++] = 11;
    655         break;
    656       case 'a':
    657         buf[k++] = 7;
    658         break;
    659       case 'x': {
    660         u32 v = 0;
    661         int dn = 0;
    662         while (i + 2 < n) {
    663           char h = p[i + 1];
    664           int dv;
    665           if (h >= '0' && h <= '9')
    666             dv = h - '0';
    667           else if (h >= 'a' && h <= 'f')
    668             dv = 10 + (h - 'a');
    669           else if (h >= 'A' && h <= 'F')
    670             dv = 10 + (h - 'A');
    671           else
    672             break;
    673           v = v * 16 + (u32)dv;
    674           ++i;
    675           if (++dn >= 2) break;
    676         }
    677         buf[k++] = (u8)v;
    678         break;
    679       }
    680       default:
    681         if (e >= '0' && e <= '7') {
    682           u32 v = (u32)(e - '0');
    683           int dn = 1;
    684           while (dn < 3 && i + 2 < n) {
    685             char h = p[i + 1];
    686             if (h < '0' || h > '7') break;
    687             v = v * 8 + (u32)(h - '0');
    688             ++i;
    689             ++dn;
    690           }
    691           buf[k++] = (u8)v;
    692         } else {
    693           buf[k++] = (u8)e;
    694         }
    695         break;
    696     }
    697   }
    698   *out = buf;
    699   *nout = k;
    700   *cap_out = cap;
    701 }
    702 
    703 /* ---- directives ---- */
    704 
    705 static Sym expect_ident(AsmDriver* d, const char* what) {
    706   AsmTok t = d_peek(d);
    707   if (t.kind != ASM_TOK_IDENT)
    708     d_panicf(d, "asm: %.*s: expected identifier",
    709              SLICE_ARG(slice_from_cstr(what)));
    710   (void)d_next(d);
    711   return t.v.ident;
    712 }
    713 
    714 static void emit_le(AsmDriver* d, u64 v, u32 width) {
    715   u8 buf[8];
    716   for (u32 i = 0; i < width; ++i) buf[i] = (u8)(v >> (8 * i));
    717   (void)asm_driver_cur_section(d);
    718   mc_emit_bytes(d->mc, buf, width);
    719 }
    720 
    721 /* Record a `sym1 - sym2` data difference for resolution after the parse,
    722  * once every label is defined. The field already holds the net addend. */
    723 static void record_sym_diff(AsmDriver* d, ObjSecId sec, u32 ofs, u32 width,
    724                             ObjSymId minuend, ObjSymId subtrahend, i64 addend) {
    725   if (VEC_GROW(d->heap, d->diffs, d->diffs_cap, d->ndiffs + 1))
    726     d_panicf(d, "asm: out of memory recording symbol difference");
    727   AsmDiff* df = &d->diffs[d->ndiffs++];
    728   df->section = sec;
    729   df->offset = ofs;
    730   df->width = width;
    731   df->minuend = minuend;
    732   df->subtrahend = subtrahend;
    733   df->addend = addend;
    734   df->loc = d_loc(d);
    735 }
    736 
    737 static void emit_int_directive(AsmDriver* d, u32 width) {
    738   for (;;) {
    739     AsmExpr e = parse_expr(d);
    740     if (e.sym && e.sym2) {
    741       /* `sym1 - sym2` difference: emit the net addend as a placeholder and
    742        * defer resolution until all labels are defined (a forward-referenced
    743        * minuend like `_end` is still undefined here). */
    744       (void)asm_driver_cur_section(d);
    745       u32 ofs = mc_pos(d->mc);
    746       emit_le(d, (u64)e.value, width);
    747       record_sym_diff(d, d->cur_sec, ofs, width, e.sym, e.sym2, e.value);
    748     } else if (e.sym) {
    749       RelocKind k;
    750       if (e.pcrel) {
    751         /* `sym - .`: PC-relative data. Only the 32/64-bit widths codegen
    752          * emits via kit_cg_data_pcrel are supported. */
    753         if (width == 4)
    754           k = R_PC32;
    755         else if (width == 8)
    756           k = R_PC64;
    757         else
    758           d_panicf(d, "asm: PC-relative `sym - .` needs .long/.quad");
    759       } else if (width == 4)
    760         k = R_ABS32;
    761       else if (width == 8)
    762         k = R_ABS64;
    763       else
    764         d_panicf(d, "asm: symbolic .byte/.hword not supported");
    765       (void)asm_driver_cur_section(d);
    766       u32 ofs = mc_pos(d->mc);
    767       /* Write the addend into the data field, not zero. Mach-O relocations
    768        * carry the addend implicitly in the relocated field (REL form); writing
    769        * zero loses it (every `.quad sym+N` would resolve to sym+0 — a switch
    770        * jump table dispatching into hyperspace). codegen pre-writes the addend
    771        * the same way. On ELF (RELA) the linker overwrites the field with S+A,
    772        * so the pre-written value is harmless there. */
    773       emit_le(d, (u64)e.value, width);
    774       mc_emit_reloc_at(d->mc, d->cur_sec, ofs, k, e.sym, e.value, 1, 0);
    775     } else {
    776       emit_le(d, (u64)e.value, width);
    777     }
    778     if (!asm_driver_eat_comma(d)) break;
    779   }
    780 }
    781 
    782 static void do_directive(AsmDriver* d, Sym name) {
    783   if (sym_eq(d, name, "text")) {
    784     if (!d->n_text) d->n_text = pool_intern_slice(d->pool, SLICE_LIT(".text"));
    785     set_section(d, d->n_text, SEC_TEXT, (u16)(SF_ALLOC | SF_EXEC), 4);
    786     d_skip_to_eol(d);
    787     return;
    788   }
    789   if (sym_eq(d, name, "data")) {
    790     if (!d->n_data) d->n_data = pool_intern_slice(d->pool, SLICE_LIT(".data"));
    791     set_section(d, d->n_data, SEC_DATA, (u16)(SF_ALLOC | SF_WRITE), 8);
    792     d_skip_to_eol(d);
    793     return;
    794   }
    795   if (sym_eq(d, name, "rodata")) {
    796     if (!d->n_rodata)
    797       d->n_rodata = pool_intern_slice(d->pool, SLICE_LIT(".rodata"));
    798     set_section(d, d->n_rodata, SEC_RODATA, (u16)SF_ALLOC, 8);
    799     d_skip_to_eol(d);
    800     return;
    801   }
    802   if (sym_eq(d, name, "bss")) {
    803     if (!d->n_bss) d->n_bss = pool_intern_slice(d->pool, SLICE_LIT(".bss"));
    804     set_section(d, d->n_bss, SEC_BSS, (u16)(SF_ALLOC | SF_WRITE), 8);
    805     d_skip_to_eol(d);
    806     return;
    807   }
    808   if (sym_eq(d, name, "section")) {
    809     Sym sname = 0;
    810     AsmTok t = d_peek(d);
    811     if (t.kind == ASM_TOK_STR) {
    812       size_t n = 0;
    813       const char* p = asm_str(d, t.spelling, &n);
    814       if (n >= 2 && p[0] == '"')
    815         sname = pool_intern_slice(d->pool, (Slice){.s = p + 1, .len = n - 2});
    816       (void)d_next(d);
    817     } else if (t.kind == ASM_TOK_IDENT || tok_is_punct(t, '.')) {
    818       /* A bare section name. The lexer breaks a dotted name like
    819        * `.rodata.toy.merge` into PUNCT('.')+IDENT segments (the `.`+digit
    820        * identifier rule does not glue `.`+letter), so reassemble the full
    821        * dotted spelling by consuming each adjacent `.segment`. Stops at the
    822        * `, "flags"` operands (the next token is then a comma). */
    823       char buf[128];
    824       size_t bn = 0;
    825       int leading_dot = tok_is_punct(t, '.');
    826       if (leading_dot) {
    827         (void)d_next(d);
    828         buf[bn++] = '.';
    829       }
    830       AsmTok id = d_next(d);
    831       if (id.kind != ASM_TOK_IDENT) d_panicf(d, "asm: .section: bad name");
    832       for (;;) {
    833         size_t ni = 0;
    834         const char* nm = asm_str(d, id.spelling, &ni);
    835         if (bn + ni >= sizeof buf) d_panicf(d, "asm: .section: name too long");
    836         for (size_t i = 0; i < ni; ++i) buf[bn++] = nm[i];
    837         /* Glue a following `.<ident>` (or `.<num>`) segment, no whitespace. */
    838         if (!tok_is_punct(d_peek(d), '.')) break;
    839         (void)d_next(d); /* '.' */
    840         AsmTok seg = d_peek(d);
    841         if (seg.kind != ASM_TOK_IDENT && seg.kind != ASM_TOK_NUM) {
    842           /* A lone trailing '.' is not part of the name; put it back is not
    843            * supported, but section names never end in '.', so this is a
    844            * malformed directive. */
    845           d_panicf(d, "asm: .section: bad name");
    846         }
    847         if (bn + 1 >= sizeof buf) d_panicf(d, "asm: .section: name too long");
    848         buf[bn++] = '.';
    849         id = d_next(d);
    850       }
    851       sname = pool_intern_slice(d->pool, (Slice){.s = buf, .len = bn});
    852     } else {
    853       d_panicf(d, "asm: .section: expected name");
    854     }
    855     SecKind kind = SEC_OTHER;
    856     u16 sem = SSEM_PROGBITS;
    857     u16 flags = 0;
    858     u32 entsize = 0;
    859     int have_flags = 0;
    860     int macho_2pos = 0;
    861 
    862     /* Mach-O `.section segname,sectname[,type[,attrs]]`: the token after the
    863      * first comma is a bare sectname IDENT (vs GNU's "flags" STRING). kit as
    864      * parses the dialect of its target only (no hybrid), so this branch is
    865      * gated on the Mach-O object format. Rebuild the comma-joined "seg,sect"
    866      * name that the Mach-O writer's name_to_seg_sect splits back. */
    867     if (d->c->target.obj == KIT_OBJ_MACHO && tok_is_punct(d_peek(d), ',')) {
    868       (void)d_next(d); /* eat ',' */
    869       AsmTok sect = d_next(d);
    870       size_t sgn = 0, scn = 0;
    871       const char* sgp;
    872       const char* scp;
    873       char buf[128];
    874       if (sect.kind != ASM_TOK_IDENT)
    875         d_panicf(d, "asm: .section: expected Mach-O sectname after ','");
    876       sgp = asm_str(d, sname, &sgn);
    877       scp = asm_str(d, sect.v.ident, &scn);
    878       if (sgn + 1 + scn >= sizeof buf)
    879         d_panicf(d, "asm: .section: name too long");
    880       memcpy(buf, sgp, sgn);
    881       buf[sgn] = ',';
    882       memcpy(buf + sgn + 1, scp, scn);
    883       sname =
    884           pool_intern_slice(d->pool, (Slice){.s = buf, .len = sgn + 1 + scn});
    885       macho_2pos = 1;
    886       /* Optional trailing Mach-O type/attribute fields (regular,
    887        * cstring_literals, …): accept and consume; map the few affecting
    888        * flags. */
    889       while (asm_driver_eat_comma(d)) {
    890         AsmTok ty = d_peek(d);
    891         if (ty.kind == ASM_TOK_IDENT) {
    892           size_t tn = 0;
    893           const char* tp = asm_str(d, ty.v.ident, &tn);
    894           if (tn == 16 && memcmp(tp, "cstring_literals", 16) == 0)
    895             flags |= SF_STRINGS;
    896           (void)d_next(d);
    897         } else if (ty.kind == ASM_TOK_NUM) {
    898           (void)d_next(d);
    899         } else {
    900           break;
    901         }
    902       }
    903     }
    904 
    905     /* Optional GNU-as operands: , "flags" [, @type [, entsize]]. The emitter
    906      * (src/api/asm_emit.c) writes these for SEC_OTHER named sections; parse
    907      * them back so a global's section flags/entsize round-trip faithfully. */
    908     if (!macho_2pos && asm_driver_eat_comma(d)) {
    909       AsmTok ft = d_peek(d);
    910       if (ft.kind == ASM_TOK_STR) {
    911         size_t fn = 0;
    912         const char* fp = asm_str(d, ft.spelling, &fn);
    913         size_t fi;
    914         for (fi = 0; fp && fi < fn; ++fi) {
    915           switch (fp[fi]) {
    916             case 'a':
    917               flags |= SF_ALLOC;
    918               break;
    919             case 'w':
    920               flags |= SF_WRITE;
    921               break;
    922             case 'x':
    923               flags |= SF_EXEC;
    924               break;
    925             case 'M':
    926               flags |= SF_MERGE;
    927               break;
    928             case 'S':
    929               flags |= SF_STRINGS;
    930               break;
    931             case 'T':
    932               flags |= SF_TLS;
    933               break;
    934             case 'R':
    935               flags |= SF_RETAIN;
    936               break;
    937             default:
    938               break; /* surrounding quotes / unknown letters */
    939           }
    940         }
    941         have_flags = 1;
    942         (void)d_next(d);
    943         if (asm_driver_eat_comma(d)) {
    944           AsmTok ty = d_peek(d);
    945           Sym tag = 0;
    946           if (tok_is_punct(ty, '@')) {
    947             (void)d_next(d);
    948             AsmTok ti = d_next(d); /* the @type ident (progbits/nobits/note) */
    949             if (ti.kind == ASM_TOK_IDENT) tag = ti.v.ident;
    950           } else if (ty.kind == ASM_TOK_IDENT) {
    951             tag = d_next(d).v.ident;
    952           }
    953           if (tag) {
    954             if (sym_eq(d, tag, "note")) {
    955               sem = SSEM_NOTE;
    956             } else if (sym_eq(d, tag, "nobits")) {
    957               sem = SSEM_NOBITS;
    958             } else if (sym_eq(d, tag, "init_array")) {
    959               sem = SSEM_INIT_ARRAY;
    960             } else if (sym_eq(d, tag, "fini_array")) {
    961               sem = SSEM_FINI_ARRAY;
    962             } else if (sym_eq(d, tag, "preinit_array")) {
    963               sem = SSEM_PREINIT_ARRAY;
    964             }
    965           }
    966           if (asm_driver_eat_comma(d)) {
    967             AsmTok es = d_peek(d);
    968             if (es.kind == ASM_TOK_NUM) {
    969               entsize = (u32)lit_to_i64(d, es.spelling);
    970               (void)d_next(d);
    971             }
    972           }
    973         }
    974       }
    975     }
    976 
    977     {
    978       size_t nn = 0;
    979       const char* p = asm_str(d, sname, &nn);
    980       if (macho_2pos) {
    981         /* Canonical Apple seg,sect → SecKind via the shared inverse of the
    982          * writer's name_to_seg_sect; unrecognized spellings (e.g.
    983          * __TEXT,__eh_frame) fall back to SEC_OTHER. Flags are derived from the
    984          * resolved kind: every Mach-O section is allocated (so SEC_OTHER keeps
    985          * SF_ALLOC), and __TEXT,__cstring additionally carries SF_STRINGS. The
    986          * comma name is preserved so the writer round-trips seg/sect verbatim.
    987          */
    988         if (!obj_macho_seckind_for_secname(p, nn, &kind)) kind = SEC_OTHER;
    989         switch (kind) {
    990           case SEC_TEXT:
    991             flags |= (u16)(SF_ALLOC | SF_EXEC);
    992             break;
    993           case SEC_RODATA:
    994             flags |= (u16)SF_ALLOC;
    995             if (nn == 16 && memcmp(p, "__TEXT,__cstring", 16) == 0)
    996               flags |= (u16)SF_STRINGS;
    997             break;
    998           case SEC_DATA:
    999           case SEC_BSS:
   1000             flags |= (u16)(SF_ALLOC | SF_WRITE);
   1001             break;
   1002           case SEC_DEBUG:
   1003             break;
   1004           default:
   1005             flags |= (u16)SF_ALLOC;
   1006             break;
   1007         }
   1008       } else if (have_flags) {
   1009         /* Explicit flags: a canonical name keeps its kind; any other name is a
   1010          * SEC_OTHER named section (matching codegen for section(...) globals).
   1011          */
   1012         if (p && nn == 5 && memcmp(p, ".text", 5) == 0)
   1013           kind = SEC_TEXT;
   1014         else if (p && nn == 7 && memcmp(p, ".rodata", 7) == 0)
   1015           kind = SEC_RODATA;
   1016         else if (p && nn == 5 && memcmp(p, ".data", 5) == 0)
   1017           kind = SEC_DATA;
   1018         else if (p && nn == 4 && memcmp(p, ".bss", 4) == 0)
   1019           kind = SEC_BSS;
   1020         else
   1021           kind = SEC_OTHER;
   1022       } else if (p) {
   1023         /* No flag string: infer kind+flags from a canonical name prefix. */
   1024         if (nn >= 5 && memcmp(p, ".text", 5) == 0) {
   1025           kind = SEC_TEXT;
   1026           flags = (u16)(SF_ALLOC | SF_EXEC);
   1027         } else if (nn >= 7 && memcmp(p, ".rodata", 7) == 0) {
   1028           kind = SEC_RODATA;
   1029           flags = (u16)SF_ALLOC;
   1030         } else if (nn >= 5 && memcmp(p, ".data", 5) == 0) {
   1031           kind = SEC_DATA;
   1032           flags = (u16)(SF_ALLOC | SF_WRITE);
   1033         } else if (nn >= 4 && memcmp(p, ".bss", 4) == 0) {
   1034           kind = SEC_BSS;
   1035           flags = (u16)(SF_ALLOC | SF_WRITE);
   1036         }
   1037       }
   1038     }
   1039     if (kind == SEC_BSS) sem = SSEM_NOBITS;
   1040     if (sem == SSEM_NOTE) kind = SEC_OTHER;
   1041 
   1042     /* Consume any remaining operands (e.g. ,unique,N or group fields). */
   1043     d_skip_to_eol(d);
   1044     {
   1045       ObjSecId sid = ensure_section_ex(d, sname, kind, sem, flags, 1);
   1046       if (entsize) obj_section_set_entsize(d->ob, sid, entsize);
   1047       d->cur_sec = sid;
   1048       mc_set_section(d->mc, sid);
   1049     }
   1050     return;
   1051   }
   1052   if (sym_eq(d, name, "globl") || sym_eq(d, name, "global")) {
   1053     Sym n = expect_ident(d, ".globl");
   1054     sym_mut(d, intern_sym(d, n))->bind = (u16)SB_GLOBAL;
   1055     d_skip_to_eol(d);
   1056     return;
   1057   }
   1058   if (sym_eq(d, name, "local")) {
   1059     Sym n = expect_ident(d, ".local");
   1060     sym_mut(d, intern_sym(d, n))->bind = (u16)SB_LOCAL;
   1061     d_skip_to_eol(d);
   1062     return;
   1063   }
   1064   /* `.weak_definition` is the Mach-O spelling for a weak *defined* symbol
   1065    * (clang rejects GNU `.weak` on Mach-O). It pairs with a `.globl`; kit
   1066    * collapses both to SB_WEAK, which the Mach-O emitter turns into
   1067    * N_EXT|N_WEAK_DEF and ELF into STB_WEAK. */
   1068   if (sym_eq(d, name, "weak") || sym_eq(d, name, "weak_definition")) {
   1069     Sym n = expect_ident(d, ".weak");
   1070     sym_mut(d, intern_sym(d, n))->bind = (u16)SB_WEAK;
   1071     d_skip_to_eol(d);
   1072     return;
   1073   }
   1074   if (sym_eq(d, name, "hidden")) {
   1075     Sym n = expect_ident(d, ".hidden");
   1076     sym_mut(d, intern_sym(d, n))->vis = (u8)SV_HIDDEN;
   1077     d_skip_to_eol(d);
   1078     return;
   1079   }
   1080   if (sym_eq(d, name, "protected")) {
   1081     Sym n = expect_ident(d, ".protected");
   1082     sym_mut(d, intern_sym(d, n))->vis = (u8)SV_PROTECTED;
   1083     d_skip_to_eol(d);
   1084     return;
   1085   }
   1086   if (sym_eq(d, name, "internal")) {
   1087     Sym n = expect_ident(d, ".internal");
   1088     sym_mut(d, intern_sym(d, n))->vis = (u8)SV_INTERNAL;
   1089     d_skip_to_eol(d);
   1090     return;
   1091   }
   1092   if (sym_eq(d, name, "type")) {
   1093     Sym n = expect_ident(d, ".type");
   1094     ObjSymId id = intern_sym(d, n);
   1095     if (!asm_driver_eat_comma(d)) d_panicf(d, "asm: .type: expected ','");
   1096     AsmTok t = d_next(d);
   1097     Sym tag = 0;
   1098     if (tok_is_punct(t, '@') || tok_is_punct(t, '%')) {
   1099       AsmTok ti = d_next(d);
   1100       if (ti.kind != ASM_TOK_IDENT) d_panicf(d, "asm: .type: tag");
   1101       tag = ti.v.ident;
   1102     } else if (t.kind == ASM_TOK_IDENT) {
   1103       tag = t.v.ident;
   1104     } else if (t.kind == ASM_TOK_STR) {
   1105       size_t sn = 0;
   1106       const char* sp = asm_str(d, t.spelling, &sn);
   1107       if (sn >= 2 && sp[0] == '"' && sp[sn - 1] == '"')
   1108         tag = pool_intern_slice(d->pool, (Slice){.s = sp + 1, .len = sn - 2});
   1109     } else {
   1110       d_panicf(d, "asm: .type: tag");
   1111     }
   1112     if (tag && sym_eq(d, tag, "function")) {
   1113       sym_mut(d, id)->kind = (u16)SK_FUNC;
   1114       if (asm_sym_is_thumb_func(d, n)) sym_mut(d, id)->value |= 1u;
   1115     } else if (tag && sym_eq(d, tag, "object"))
   1116       sym_mut(d, id)->kind = (u16)SK_OBJ;
   1117     else if (tag && sym_eq(d, tag, "tls_object"))
   1118       sym_mut(d, id)->kind = (u16)SK_TLS;
   1119     else if (tag && sym_eq(d, tag, "gnu_indirect_function"))
   1120       sym_mut(d, id)->kind = (u16)SK_IFUNC;
   1121     d_skip_to_eol(d);
   1122     return;
   1123   }
   1124   if (sym_eq(d, name, "thumb_func")) {
   1125     if (asm_thumb_func_syms(d) && !d_is_eol(d)) {
   1126       mark_thumb_func(d, expect_ident(d, ".thumb_func"));
   1127     } else if (asm_thumb_func_syms(d)) {
   1128       d->pending_thumb_func = 1;
   1129     }
   1130     d_skip_to_eol(d);
   1131     return;
   1132   }
   1133   if (sym_eq(d, name, "size")) {
   1134     Sym n = expect_ident(d, ".size");
   1135     ObjSymId id = intern_sym(d, n);
   1136     if (!asm_driver_eat_comma(d)) d_panicf(d, "asm: .size: expected ','");
   1137     /* Recognize `. - NAME`. */
   1138     AsmTok t = d_peek(d);
   1139     i64 sz = 0;
   1140     if (tok_is_punct(t, '.')) {
   1141       (void)d_next(d);
   1142       if (tok_is_punct(d_peek(d), '-')) {
   1143         (void)d_next(d);
   1144         AsmTok rid = d_peek(d);
   1145         if (rid.kind == ASM_TOK_IDENT && rid.v.ident == n) {
   1146           (void)d_next(d);
   1147           const ObjSym* os = obj_symbol_get(d->ob, id);
   1148           if (os && os->section_id == d->cur_sec) {
   1149             u64 start = os->value;
   1150             if (asm_thumb_func_syms(d) && os->kind == SK_FUNC)
   1151               start &= ~(u64)1;
   1152             sz = (i64)mc_pos(d->mc) - (i64)start;
   1153           }
   1154         }
   1155       }
   1156     } else {
   1157       AsmExpr e = parse_expr(d);
   1158       if (!e.sym) sz = e.value;
   1159     }
   1160     if (sz < 0) sz = 0;
   1161     sym_mut(d, id)->size = (u64)sz;
   1162     d_skip_to_eol(d);
   1163     return;
   1164   }
   1165   if (sym_eq(d, name, "byte")) {
   1166     emit_int_directive(d, 1);
   1167     d_skip_to_eol(d);
   1168     return;
   1169   }
   1170   if (sym_eq(d, name, "hword") || sym_eq(d, name, "short") ||
   1171       sym_eq(d, name, "2byte")) {
   1172     emit_int_directive(d, 2);
   1173     d_skip_to_eol(d);
   1174     return;
   1175   }
   1176   if (sym_eq(d, name, "word") || sym_eq(d, name, "long") ||
   1177       sym_eq(d, name, "int") || sym_eq(d, name, "4byte")) {
   1178     emit_int_directive(d, 4);
   1179     d_skip_to_eol(d);
   1180     return;
   1181   }
   1182   if (sym_eq(d, name, "quad") || sym_eq(d, name, "8byte") ||
   1183       sym_eq(d, name, "dword") || sym_eq(d, name, "xword")) {
   1184     emit_int_directive(d, 8);
   1185     d_skip_to_eol(d);
   1186     return;
   1187   }
   1188   /* .inst WORD[, WORD...] — emit raw 32-bit instruction word(s), little-endian
   1189    * (AArch64/RISC-V are fixed 4-byte). This is how `cc -S` round-trips an
   1190    * instruction the disassembler can't decode yet (`.inst 0x<word>`); silently
   1191    * dropping it — the old behavior — deletes the instruction and miscompiles.
   1192    * Matches GNU as / llvm-mc, which emit the word. */
   1193   if (sym_eq(d, name, "inst")) {
   1194     (void)asm_driver_cur_section(d);
   1195     for (;;) {
   1196       i64 v = asm_driver_parse_const(d);
   1197       emit_le(d, (u64)v, 4);
   1198       if (!asm_driver_eat_comma(d)) break;
   1199     }
   1200     d_skip_to_eol(d);
   1201     return;
   1202   }
   1203   if (sym_eq(d, name, "ascii") || sym_eq(d, name, "asciz") ||
   1204       sym_eq(d, name, "string")) {
   1205     int term = !sym_eq(d, name, "ascii");
   1206     for (;;) {
   1207       AsmTok t = d_peek(d);
   1208       if (t.kind != ASM_TOK_STR)
   1209         d_panicf(d, "asm: .ascii/.string: expected string");
   1210       (void)d_next(d);
   1211       u8* buf = NULL;
   1212       u32 n = 0;
   1213       size_t cap = 0;
   1214       decode_string(d, t.spelling, &buf, &n, &cap);
   1215       (void)asm_driver_cur_section(d);
   1216       mc_emit_bytes(d->mc, buf, n);
   1217       if (term) emit_le(d, 0, 1);
   1218       d->heap->free(d->heap, buf, cap);
   1219       if (!asm_driver_eat_comma(d)) break;
   1220     }
   1221     d_skip_to_eol(d);
   1222     return;
   1223   }
   1224   if (sym_eq(d, name, "zero") || sym_eq(d, name, "skip") ||
   1225       sym_eq(d, name, "space")) {
   1226     i64 n = asm_driver_parse_const(d);
   1227     i64 fill = 0;
   1228     if (asm_driver_eat_comma(d)) fill = asm_driver_parse_const(d);
   1229     if (n > 0) {
   1230       (void)asm_driver_cur_section(d);
   1231       mc_emit_fill(d->mc, (size_t)n, (u8)fill);
   1232     }
   1233     d_skip_to_eol(d);
   1234     return;
   1235   }
   1236   if (sym_eq(d, name, "fill")) {
   1237     i64 n = asm_driver_parse_const(d);
   1238     i64 size = 1, val = 0;
   1239     if (asm_driver_eat_comma(d)) size = asm_driver_parse_const(d);
   1240     if (asm_driver_eat_comma(d)) val = asm_driver_parse_const(d);
   1241     if (size < 1 || size > 8) d_panicf(d, "asm: .fill: size out of range");
   1242     (void)asm_driver_cur_section(d);
   1243     for (i64 i = 0; i < n; ++i) emit_le(d, (u64)val, (u32)size);
   1244     d_skip_to_eol(d);
   1245     return;
   1246   }
   1247   if (sym_eq(d, name, "align") || sym_eq(d, name, "balign")) {
   1248     i64 a = asm_driver_parse_const(d);
   1249     i64 fill = 0;
   1250     if (asm_driver_eat_comma(d)) fill = asm_driver_parse_const(d);
   1251     if (a <= 0 || (a & (a - 1))) d_panicf(d, "asm: .align: not a power of 2");
   1252     (void)asm_driver_cur_section(d);
   1253     mc_emit_align(d->mc, (u32)a, (u8)fill);
   1254     d_skip_to_eol(d);
   1255     return;
   1256   }
   1257   if (sym_eq(d, name, "p2align")) {
   1258     i64 lg = asm_driver_parse_const(d);
   1259     i64 fill = 0;
   1260     if (asm_driver_eat_comma(d)) fill = asm_driver_parse_const(d);
   1261     if (lg < 0 || lg > 16) d_panicf(d, "asm: .p2align: out of range");
   1262     (void)asm_driver_cur_section(d);
   1263     mc_emit_align(d->mc, 1u << (u32)lg, (u8)fill);
   1264     d_skip_to_eol(d);
   1265     return;
   1266   }
   1267   if (sym_eq(d, name, "set") || sym_eq(d, name, "equ")) {
   1268     Sym n = expect_ident(d, ".set");
   1269     if (!asm_driver_eat_comma(d)) d_panicf(d, "asm: .set: expected ','");
   1270     AsmExpr e = parse_expr(d);
   1271     AsmEqu eq;
   1272     eq.value = e.value;
   1273     eq.sym = e.sym;
   1274     eq.has_sym = e.sym ? 1 : 0;
   1275     eq.pad[0] = eq.pad[1] = eq.pad[2] = 0;
   1276     SymEquMap_set(&d->equ_map, n, eq);
   1277     d_skip_to_eol(d);
   1278     return;
   1279   }
   1280 
   1281   /* .comm/.lcomm NAME, SIZE[, ALIGN] — declare a common symbol. Previously
   1282    * skipped, which silently produced no symbol and reserved no space. Model
   1283    * both as SK_COMMON (the linker allocates .bss space); .comm is global,
   1284    * .lcomm local. */
   1285   if (sym_eq(d, name, "comm") || sym_eq(d, name, "lcomm")) {
   1286     int is_local = sym_eq(d, name, "lcomm");
   1287     Sym nm = expect_ident(d, ".comm");
   1288     i64 size = 0, align = 1;
   1289     if (!asm_driver_eat_comma(d)) d_panicf(d, "asm: .comm: expected ','");
   1290     size = asm_driver_parse_const(d);
   1291     if (asm_driver_eat_comma(d)) align = asm_driver_parse_const(d);
   1292     if (size < 0) size = 0;
   1293     if (align < 1) align = 1;
   1294     {
   1295       ObjSym* s = sym_mut(d, intern_sym(d, nm));
   1296       s->kind = (u16)SK_COMMON;
   1297       s->bind = (u16)(is_local ? SB_LOCAL : SB_GLOBAL);
   1298       s->size = (u64)size;
   1299       s->common_align = (u64)align;
   1300     }
   1301     d_skip_to_eol(d);
   1302     return;
   1303   }
   1304   /* .uleb128/.sleb128 VALUE[, VALUE...] — emit LEB128-encoded bytes.
   1305    * Previously skipped, which emitted nothing and corrupted any hand-written
   1306    * DWARF / exception tables that follow. */
   1307   if (sym_eq(d, name, "uleb128") || sym_eq(d, name, "sleb128")) {
   1308     int sgn = sym_eq(d, name, "sleb128");
   1309     (void)asm_driver_cur_section(d);
   1310     for (;;) {
   1311       i64 v = asm_driver_parse_const(d);
   1312       u8 buf[16];
   1313       u32 n = 0;
   1314       if (sgn) {
   1315         int more = 1;
   1316         while (more) {
   1317           u8 b = (u8)((u64)v & 0x7fu);
   1318           v >>= 7; /* arithmetic right shift keeps the sign */
   1319           if ((v == 0 && !(b & 0x40u)) || (v == -1 && (b & 0x40u)))
   1320             more = 0;
   1321           else
   1322             b |= 0x80u;
   1323           buf[n++] = b;
   1324         }
   1325       } else {
   1326         u64 uv = (u64)v;
   1327         do {
   1328           u8 b = (u8)(uv & 0x7fu);
   1329           uv >>= 7;
   1330           if (uv) b |= 0x80u;
   1331           buf[n++] = b;
   1332         } while (uv);
   1333       }
   1334       mc_emit_bytes(d->mc, buf, n);
   1335       if (!asm_driver_eat_comma(d)) break;
   1336     }
   1337     d_skip_to_eol(d);
   1338     return;
   1339   }
   1340 
   1341   /* CFI block + accepted-but-ignored directives.  Keep parser
   1342    * forward-progress without aborting the whole TU. */
   1343   if (starts_with(d, name, "cfi_") || sym_eq(d, name, "file") ||
   1344       sym_eq(d, name, "loc") || sym_eq(d, name, "ident") ||
   1345       sym_eq(d, name, "popsection") || sym_eq(d, name, "pushsection") ||
   1346       sym_eq(d, name, "previous") ||
   1347       sym_eq(d, name, "subsections_via_symbols") || sym_eq(d, name, "macro") ||
   1348       sym_eq(d, name, "endm") || sym_eq(d, name, "if") ||
   1349       sym_eq(d, name, "endif") || sym_eq(d, name, "else") ||
   1350       sym_eq(d, name, "include") ||
   1351       /* RISC-V `.option rvc/norvc/relax/norelax/push/pop/...`: kit's own
   1352        * cc -S emits `.option norvc`/`.option norelax` to pin its fixed
   1353        * instruction layout (see rv64_file_prologue). kit-as never compresses
   1354        * or relaxes, so it already honors these implicitly — accept and ignore
   1355        * rather than treat as an unknown directive. */
   1356       sym_eq(d, name, "option")) {
   1357     d_skip_to_eol(d);
   1358     return;
   1359   }
   1360 
   1361   /* Unknown directive — recover. */
   1362   d_skip_to_eol(d);
   1363 }
   1364 
   1365 /* ---- deferred symbol-difference resolution ----
   1366  *
   1367  * `.quad sym1 - sym2` (and the narrower data widths) is recorded at emit time
   1368  * and lowered here, once the parse has defined every label. Three outcomes,
   1369  * matching GNU as:
   1370  *
   1371  *   (a) Both symbols defined in one section — the difference is a pure
   1372  *       assembly-time constant (the section base cancels). Fold it into the
   1373  *       field; no relocation. Works for any width.
   1374  *   (b) The subtrahend is defined in the fixup's own section (the minuend may
   1375  *       be anywhere, including undefined / linker-script-provided like `_end`).
   1376  *       Reduce to a PC-relative relocation against the minuend: at the fixup
   1377  *       site P, `M + A' - P` with `A' = addend + offset - sym2.value` equals
   1378  *       `M - sym2` because `P - offset` is the section base and
   1379  *       `base + sym2.value == sym2`. This is the arm64/riscv kernel
   1380  *       `image_size = _end - _start` case. Needs .long/.quad.
   1381  *   (c) Otherwise (cross-section, or an undefined subtrahend) — an R_ADD/R_SUB
   1382  *       relocation pair the linker applies additively to the field (which holds
   1383  *       the net addend): `field += minuend; field -= subtrahend`. Emitted
   1384  *       adjacently so the Mach-O writer fuses them into a SUBTRACTOR pair and
   1385  *       RISC-V ELF maps each to the R_RISCV_ADD / R_RISCV_SUB families. */
   1386 static void patch_le(AsmDriver* d, ObjSecId sec, u32 ofs, u64 v, u32 width) {
   1387   u8 buf[8];
   1388   for (u32 i = 0; i < width; ++i) buf[i] = (u8)(v >> (8 * i));
   1389   obj_patch(d->ob, sec, ofs, buf, width);
   1390 }
   1391 
   1392 static void resolve_sym_diffs(AsmDriver* d) {
   1393   for (u32 i = 0; i < d->ndiffs; ++i) {
   1394     AsmDiff* df = &d->diffs[i];
   1395     const ObjSym* m = obj_symbol_get(d->ob, df->minuend);
   1396     const ObjSym* s = obj_symbol_get(d->ob, df->subtrahend);
   1397     int m_def = m && m->section_id != OBJ_SEC_NONE;
   1398     int s_def = s && s->section_id != OBJ_SEC_NONE;
   1399 
   1400     /* (a) constant fold */
   1401     if (m_def && s_def && m->section_id == s->section_id) {
   1402       i64 c = (i64)m->value - (i64)s->value + df->addend;
   1403       patch_le(d, df->section, df->offset, (u64)c, df->width);
   1404       continue;
   1405     }
   1406 
   1407     /* (b) subtrahend in the fixup's section -> PC-relative reloc on minuend */
   1408     if (s_def && s->section_id == df->section) {
   1409       RelocKind k;
   1410       if (df->width == 8)
   1411         k = R_PC64;
   1412       else if (df->width == 4)
   1413         k = R_PC32;
   1414       else
   1415         compiler_panic(d->c, df->loc,
   1416                        "asm: PC-relative symbol difference needs .long/.quad");
   1417       i64 ad = df->addend + (i64)df->offset - (i64)s->value;
   1418       patch_le(d, df->section, df->offset, (u64)ad, df->width);
   1419       mc_emit_reloc_at(d->mc, df->section, df->offset, k, df->minuend, ad, 1, 0);
   1420       continue;
   1421     }
   1422 
   1423     /* (c) general difference -> R_ADD/R_SUB pair */
   1424     {
   1425       RelocKind ka, ks;
   1426       switch (df->width) {
   1427         case 1:
   1428           ka = R_ADD8;
   1429           ks = R_SUB8;
   1430           break;
   1431         case 2:
   1432           ka = R_ADD16;
   1433           ks = R_SUB16;
   1434           break;
   1435         case 4:
   1436           ka = R_ADD32;
   1437           ks = R_SUB32;
   1438           break;
   1439         case 8:
   1440           ka = R_ADD64;
   1441           ks = R_SUB64;
   1442           break;
   1443         default:
   1444           compiler_panic(d->c, df->loc, "asm: bad symbol-difference width %u",
   1445                          (unsigned)df->width);
   1446       }
   1447       mc_emit_reloc_at(d->mc, df->section, df->offset, ka, df->minuend, 0, 1, 0);
   1448       mc_emit_reloc_at(d->mc, df->section, df->offset, ks, df->subtrahend, 0, 1,
   1449                        0);
   1450     }
   1451   }
   1452 }
   1453 
   1454 /* ---- same-section branch relaxation ----
   1455  *
   1456  * The per-arch parser emits a relocation for every symbolic branch target,
   1457  * even one that resolves within the current section (a forward reference like
   1458  * `b .Lfoo` is only known to be local once `.Lfoo:` is seen). GNU as / llvm-mc
   1459  * — and kit's own codegen — instead resolve such intra-section branches at
   1460  * assembly time: compute the displacement, patch the instruction, and emit no
   1461  * relocation. Matching that is what makes `cc -S | as` reproduce `cc -c`'s
   1462  * .text relocation table for control-flow-bearing code (the L1 round-trip
   1463  * lane; see doc/TESTING.md).
   1464  *
   1465  * We relax only PC-relative *branch* relocations (never CALL26 — a call keeps
   1466  * its relocation on both sides) whose target is a symbol defined in the same
   1467  * section, with local binding, and not a function entry. The "local" guard
   1468  * matches GNU as (a global symbol may be interposed, so its branch keeps the
   1469  * relocation); the "not a function" guard matches kit codegen, which keeps
   1470  * the relocation for an intra-file call/tail-call to a function symbol while
   1471  * resolving branches to internal labels.
   1472  *
   1473  * Restricted to the AArch64 branch kinds for now — the round-trip vertical
   1474  * slice is aa64; rv64/x64 keep their current behavior. */
   1475 static int is_relaxable_branch_kind(u16 kind) {
   1476   switch (kind) {
   1477     case R_AARCH64_JUMP26:
   1478     case R_AARCH64_CONDBR19:
   1479     case R_AARCH64_TSTBR14:
   1480     case R_AARCH64_ADR_PREL_LO21: /* adr to a local code label (&&label) */
   1481       return 1;
   1482     default:
   1483       return 0;
   1484   }
   1485 }
   1486 
   1487 static void relax_local_branches(AsmDriver* d) {
   1488   u32 total = obj_reloc_total(d->ob), i;
   1489   for (i = 0; i < total; ++i) {
   1490     const Reloc* r = obj_reloc_at(d->ob, i);
   1491     const ObjSym* tgt;
   1492     const Section* sec;
   1493     u8 insn[4];
   1494     if (!r || r->removed) continue;
   1495     if (!is_relaxable_branch_kind(r->kind)) continue;
   1496     tgt = obj_symbol_get(d->ob, r->sym);
   1497     if (!tgt) continue;
   1498     if (tgt->section_id != r->section_id) continue; /* cross-section / undef */
   1499     if (tgt->bind != SB_LOCAL) continue;            /* preemptible; keep */
   1500     if (tgt->kind == SK_FUNC) continue;             /* call/tail-call; keep */
   1501     /* Read-only access to the in-progress section is fine via the const
   1502      * accessor; the byte write goes back through obj_patch so we don't reach
   1503      * into Section internals. */
   1504     sec = obj_section_get(d->ob, r->section_id);
   1505     if (!sec) continue;
   1506     if ((u64)r->offset + 4 > sec->bytes.total) continue;
   1507     buf_read(&sec->bytes, r->offset, insn, 4);
   1508     /* Section-relative S and P make the base cancel: disp = S + A - P. */
   1509     link_reloc_apply(d->c, (RelocKind)r->kind, insn, tgt->value, r->addend,
   1510                      r->offset);
   1511     obj_patch(d->ob, r->section_id, r->offset, insn, 4);
   1512     /* No public per-reloc tombstone setter exists on ObjBuilder; the const
   1513      * cast mutates the still-private in-progress object we own outright (we
   1514      * built every reloc in this builder). Tolerated until obj exposes an
   1515      * obj_reloc_remove() mutator alongside obj_section/symbol_remove. */
   1516     ((Reloc*)r)->removed = 1;
   1517   }
   1518 }
   1519 
   1520 /* ---- driver loop ---- */
   1521 
   1522 static void process_label(AsmDriver* d, Sym name) {
   1523   ObjSymId id = intern_sym(d, name);
   1524   int is_thumb_func = 0;
   1525   (void)asm_driver_cur_section(d);
   1526   const ObjSym* os = obj_symbol_get(d->ob, id);
   1527   if (os && os->section_id != OBJ_SEC_NONE)
   1528     d_panicf(d, "asm: symbol defined twice");
   1529   if (asm_thumb_func_syms(d)) {
   1530     is_thumb_func = d->pending_thumb_func || asm_sym_is_thumb_func(d, name);
   1531     d->pending_thumb_func = 0;
   1532   }
   1533   obj_symbol_define(d->ob, id, d->cur_sec,
   1534                     (u64)mc_pos(d->mc) | (is_thumb_func ? 1u : 0u), 0);
   1535   /* Promote SK_UNDEF (forward ref via reloc) to SK_NOTYPE so it's a
   1536    * real defined symbol; explicit `.type SYM, @function` will refine. */
   1537   if (is_thumb_func)
   1538     sym_mut(d, id)->kind = (u16)SK_FUNC;
   1539   else if (os && os->kind == SK_UNDEF)
   1540     sym_mut(d, id)->kind = (u16)SK_NOTYPE;
   1541 }
   1542 
   1543 static Sym maybe_compose_mnemonic(AsmDriver* d, Sym head) {
   1544   /* Loops to accept multi-dot mnemonics like RISC-V's `fcvt.w.s` /
   1545    * `amoadd.d` — peel one `.ident` per pass, intern the joined token,
   1546    * and stop when the next token isn't a touching dot. */
   1547   for (;;) {
   1548     AsmTok t = d_peek(d);
   1549     if (!tok_is_punct(t, '.')) return head;
   1550     if (t.flags & ASM_TF_HAS_SPACE) return head;
   1551     (void)d_next(d);
   1552     AsmTok rest = d_next(d);
   1553     if (rest.kind != ASM_TOK_IDENT)
   1554       d_panicf(d, "asm: composite mnemonic: expected ident");
   1555     size_t hn = 0, rn = 0;
   1556     const char* hp = asm_str(d, head, &hn);
   1557     const char* rp = asm_str(d, rest.v.ident, &rn);
   1558     size_t n = hn + 1 + rn;
   1559     if (n >= 64) d_panicf(d, "asm: mnemonic too long");
   1560     char buf[64];
   1561     for (size_t i = 0; i < hn; ++i) buf[i] = hp[i];
   1562     buf[hn] = '.';
   1563     for (size_t i = 0; i < rn; ++i) buf[hn + 1 + i] = rp[i];
   1564     head = pool_intern_slice(d->pool, (Slice){.s = buf, .len = n});
   1565   }
   1566 }
   1567 
   1568 /* ---- inline-asm driver constructor ----
   1569  *
   1570  * Inline-asm template walkers (per-arch) re-lex pre-substituted source
   1571  * text through the same per-mnemonic parsers used by the standalone .s
   1572  * driver.  This constructor builds a minimally-initialized AsmDriver
   1573  * around a caller-supplied memory-backed AsmLexer + MCEmitter.
   1574  *
   1575  * The driver does not own the AsmLexer or MCEmitter, does not allocate a
   1576  * default section (inline asm emits into whatever section the wrapping
   1577  * cg has selected on its MCEmitter), and skips the standalone driver's
   1578  * per-arch handle (`d->arch_asm`) — the caller has already opened its own
   1579  * arch asm handle to thread per-block bound state through. */
   1580 AsmDriver* asm_driver_open_inline(Compiler* c, MCEmitter* mc, AsmLexer* lex) {
   1581   Heap* heap = (Heap*)c->ctx->heap;
   1582   AsmDriver* d = (AsmDriver*)heap->alloc(heap, sizeof *d, _Alignof(AsmDriver));
   1583   memset(d, 0, sizeof *d);
   1584   d->c = c;
   1585   d->lex = lex;
   1586   d->mc = mc;
   1587   d->ob = mc->obj;
   1588   d->pool = c->global;
   1589   d->heap = heap;
   1590   /* The MCEmitter's section is whatever cg has set; do not override it.
   1591    * cur_sec == OBJ_SEC_NONE means "ask the MCEmitter on demand" — we use
   1592    * mc->section_id directly via asm_driver_cur_section's lazy init for
   1593    * standalone, but inline asm should never reach that path because the
   1594    * MCEmitter already has its section. Pre-seed cur_sec from the
   1595    * MCEmitter so emit_reloc_at calls get the right section id. */
   1596   d->cur_sec = mc->section_id;
   1597   SymSecMap_init(&d->sec_map, heap);
   1598   SymSymMap_init(&d->sym_map, heap);
   1599   SymU8Map_init(&d->thumb_func_map, heap);
   1600   SymEquMap_init(&d->equ_map, heap);
   1601   d->arch_asm = NULL; /* caller owns its own arch asm handle */
   1602   return d;
   1603 }
   1604 
   1605 void asm_driver_close_inline(AsmDriver* d) {
   1606   if (!d) return;
   1607   SymSecMap_fini(&d->sec_map);
   1608   SymSymMap_fini(&d->sym_map);
   1609   SymU8Map_fini(&d->thumb_func_map);
   1610   SymEquMap_fini(&d->equ_map);
   1611   if (d->diffs) d->heap->free(d->heap, d->diffs, sizeof(AsmDiff) * d->diffs_cap);
   1612   Heap* heap = d->heap;
   1613   heap->free(heap, d, sizeof *d);
   1614 }
   1615 
   1616 void asm_parse(Compiler* c, AsmLexer* l, MCEmitter* mc) {
   1617   AsmDriver d;
   1618   memset(&d, 0, sizeof d);
   1619   d.c = c;
   1620   d.lex = l;
   1621   d.mc = mc;
   1622   d.ob = mc->obj;
   1623   d.pool = c->global;
   1624   d.heap = (Heap*)c->ctx->heap;
   1625   d.cur_sec = OBJ_SEC_NONE;
   1626   SymSecMap_init(&d.sec_map, d.heap);
   1627   SymSymMap_init(&d.sym_map, d.heap);
   1628   SymU8Map_init(&d.thumb_func_map, d.heap);
   1629   SymEquMap_init(&d.equ_map, d.heap);
   1630   {
   1631     const ArchImpl* arch = arch_for_compiler(c);
   1632     if (!arch || !arch->asm_new) {
   1633       SrcLoc loc = asm_lex_loc(l);
   1634       compiler_panic(c, loc, "asm_parse: unsupported target arch %d",
   1635                      (int)c->target.arch);
   1636     }
   1637     d.arch_asm = arch->asm_new(c);
   1638   }
   1639 
   1640   for (;;) {
   1641     AsmTok t = d_peek(&d);
   1642     if (t.kind == ASM_TOK_EOF) break;
   1643     if (t.kind == ASM_TOK_NEWLINE) {
   1644       (void)d_next(&d);
   1645       continue;
   1646     }
   1647     if (t.kind == ASM_TOK_HASH) {
   1648       /* cpp-style linemarker; skip the whole line. */
   1649       d_skip_to_eol(&d);
   1650       continue;
   1651     }
   1652     if (tok_is_punct(t, '.')) {
   1653       (void)d_next(&d);
   1654       AsmTok id = d_next(&d);
   1655       if (id.kind != ASM_TOK_IDENT)
   1656         d_panicf(&d, "asm: expected directive name after '.'");
   1657       do_directive(&d, id.v.ident);
   1658       d_eat_eol(&d);
   1659       continue;
   1660     }
   1661     if (t.kind == ASM_TOK_IDENT) {
   1662       Sym head = t.v.ident;
   1663       (void)d_next(&d);
   1664       AsmTok nxt = d_peek(&d);
   1665       if (tok_is_punct(nxt, ':')) {
   1666         (void)d_next(&d);
   1667         process_label(&d, head);
   1668         continue;
   1669       }
   1670       Sym mnemonic = maybe_compose_mnemonic(&d, head);
   1671       d.arch_asm->insn(d.arch_asm, &d, mnemonic);
   1672       d_skip_to_eol(&d);
   1673       continue;
   1674     }
   1675     /* Anything else: recover by skipping the line. */
   1676     d_skip_to_eol(&d);
   1677   }
   1678 
   1679   resolve_sym_diffs(&d);
   1680   promote_undef_externs(&d);
   1681   relax_local_branches(&d);
   1682 
   1683   if (d.arch_asm && d.arch_asm->destroy) d.arch_asm->destroy(d.arch_asm);
   1684   SymSecMap_fini(&d.sec_map);
   1685   SymSymMap_fini(&d.sym_map);
   1686   SymU8Map_fini(&d.thumb_func_map);
   1687   SymEquMap_fini(&d.equ_map);
   1688   if (d.diffs) d.heap->free(d.heap, d.diffs, sizeof(AsmDiff) * d.diffs_cap);
   1689 }