kit

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

asm_lex.c (20359B)


      1 /* Assembler lexer. Streams tokens out of a borrowed source buffer.
      2  *
      3  * It intentionally keeps C-like number/string spelling rules because .S
      4  * sources arrive after C preprocessing and GNU as accepts those spellings
      5  * in directives and expressions. It does not own macro expansion or C
      6  * keyword classification.
      7  *
      8  * Comments are consumed as whitespace; physical newlines surface as
      9  * ASM_TOK_NEWLINE so the asm driver can keep line-oriented directive and
     10  * instruction parsing. */
     11 
     12 #include "asm/asm_lex.h"
     13 
     14 #include <string.h>
     15 
     16 #include "core/heap.h"
     17 #include "core/pool.h"
     18 #include "core/slice.h"
     19 
     20 struct AsmLexer {
     21   Compiler* c;
     22   Pool* pool;
     23   Heap* heap;
     24   const char* src;
     25   size_t len;
     26   size_t pos;
     27   u32 file_id;
     28   u32 line;
     29   u32 col;
     30   u8 at_bol;
     31   u8 had_space;
     32 };
     33 
     34 /* §5.1.1.2 translation phase 2: splice physical lines joined by
     35  * backslash-newline. Advance past any splice sequence at l->pos so the
     36  * cursor never rests on the leading backslash of a splice. */
     37 static void skip_splices(AsmLexer* l) {
     38   while (l->pos + 1 < l->len && l->src[l->pos] == '\\' &&
     39          l->src[l->pos + 1] == '\n') {
     40     l->pos += 2;
     41     l->line++;
     42     l->col = 1;
     43   }
     44 }
     45 
     46 /* Logical peek: returns the off-th post-splice byte starting at l->pos,
     47  * or -1 at end of input. Does not mutate l->pos. */
     48 static int peek(const AsmLexer* l, size_t off) {
     49   size_t pos = l->pos;
     50   size_t k = 0;
     51   while (pos < l->len) {
     52     if (pos + 1 < l->len && l->src[pos] == '\\' && l->src[pos + 1] == '\n') {
     53       pos += 2;
     54       continue;
     55     }
     56     if (k == off) return (unsigned char)l->src[pos];
     57     ++pos;
     58     ++k;
     59   }
     60   return -1;
     61 }
     62 
     63 static int bump(AsmLexer* l) {
     64   int ch;
     65   skip_splices(l);
     66   if (l->pos >= l->len) return -1;
     67   ch = (unsigned char)l->src[l->pos++];
     68   if (ch == '\n') {
     69     l->line++;
     70     l->col = 1;
     71   } else {
     72     l->col++;
     73   }
     74   return ch;
     75 }
     76 
     77 static int is_digit(int c) { return c >= '0' && c <= '9'; }
     78 static int is_hex_digit(int c) {
     79   return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
     80          (c >= 'A' && c <= 'F');
     81 }
     82 /* Identifier-start byte (§6.4.2.1). Letters and underscore are ASCII; bytes
     83  * ≥ 0x80 are accepted as the implementation-defined "other characters"
     84  * permitted in identifiers — in practice UTF-8 lead/continuation bytes for
     85  * extended source characters. UCNs are matched separately via ucn_len since
     86  * they span multiple source bytes. */
     87 static int is_alpha(int c) {
     88   return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' ||
     89          c >= 0x80;
     90 }
     91 static int is_alnum(int c) { return is_alpha(c) || is_digit(c); }
     92 
     93 /* Match a UCN at offset `off` from the current position. Returns the total
     94  * length (6 for \uXXXX, 10 for \UXXXXXXXX), or 0 if no UCN matches. The
     95  * range constraints from §6.4.3 (no UCN < 00A0 except $/@/`, and none in
     96  * D800–DFFF) are not enforced here — the lexical form is matched and any
     97  * downstream phase that cares can diagnose. */
     98 static int ucn_len(const AsmLexer* l, size_t off) {
     99   int n, i;
    100   if (peek(l, off) != '\\') return 0;
    101   if (peek(l, off + 1) == 'u')
    102     n = 4;
    103   else if (peek(l, off + 1) == 'U')
    104     n = 8;
    105   else
    106     return 0;
    107   for (i = 0; i < n; ++i) {
    108     if (!is_hex_digit(peek(l, off + 2 + i))) return 0;
    109   }
    110   return 2 + n;
    111 }
    112 
    113 static SrcLoc asm_lex_here(const AsmLexer* l) {
    114   SrcLoc loc;
    115   loc.file_id = l->file_id;
    116   loc.line = l->line;
    117   loc.col = l->col;
    118   return loc;
    119 }
    120 
    121 AsmLexer* asm_lex_open_mem(Compiler* c, const char* name, const char* src,
    122                            size_t len) {
    123   Heap* h = (Heap*)c->ctx->heap;
    124   AsmLexer* l = (AsmLexer*)h->alloc(h, sizeof(*l), _Alignof(AsmLexer));
    125   if (!l) return NULL;
    126   memset(l, 0, sizeof(*l));
    127   l->c = c;
    128   l->pool = c->global;
    129   l->heap = h;
    130   l->src = src ? src : "";
    131   l->len = src ? len : 0;
    132   l->pos = 0;
    133   if (source_add_memory(c->sources, slice_from_cstr(name), &l->file_id) !=
    134       KIT_OK) {
    135     h->free(h, l, sizeof(*l));
    136     return NULL;
    137   }
    138   l->line = 1;
    139   l->col = 1;
    140   l->at_bol = 1;
    141   l->had_space = 0;
    142   return l;
    143 }
    144 
    145 void asm_lex_close(AsmLexer* l) {
    146   if (!l) return;
    147   l->heap->free(l->heap, l, sizeof(*l));
    148 }
    149 
    150 SrcLoc asm_lex_loc(const AsmLexer* l) { return asm_lex_here(l); }
    151 u32 asm_lex_file_id(const AsmLexer* l) { return l->file_id; }
    152 
    153 /* Intern bytes [start, end) with line splices (\<newline>) removed, so token
    154  * spellings reflect post-phase-2 logical text. */
    155 static Sym intern_spliced(AsmLexer* l, size_t start, size_t end) {
    156   size_t i;
    157   int has_splice = 0;
    158   char* buf;
    159   size_t k;
    160   Sym sym;
    161 
    162   for (i = start; i + 1 < end; ++i) {
    163     if (l->src[i] == '\\' && l->src[i + 1] == '\n') {
    164       has_splice = 1;
    165       break;
    166     }
    167   }
    168   if (!has_splice)
    169     return pool_intern_slice(l->pool,
    170                              (Slice){.s = l->src + start, .len = end - start});
    171 
    172   buf = (char*)l->heap->alloc(l->heap, end - start, 1);
    173   k = 0;
    174   for (i = start; i < end;) {
    175     if (i + 1 < end && l->src[i] == '\\' && l->src[i + 1] == '\n') {
    176       i += 2;
    177       continue;
    178     }
    179     buf[k++] = l->src[i++];
    180   }
    181   sym = pool_intern_slice(l->pool, (Slice){.s = buf, .len = k});
    182   l->heap->free(l->heap, buf, end - start);
    183   return sym;
    184 }
    185 
    186 /* Skip whitespace and comments. Returns 1 if a newline boundary was crossed
    187  * via comment consumption (caller still emits the explicit newline token on
    188  * an in-source '\n'). */
    189 static void skip_ws_and_comments(AsmLexer* l) {
    190   for (;;) {
    191     int ch = peek(l, 0);
    192     if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\v' || ch == '\f') {
    193       bump(l);
    194       l->had_space = 1;
    195       continue;
    196     }
    197     if (ch == '/' && peek(l, 1) == '/') {
    198       bump(l);
    199       bump(l);
    200       while (peek(l, 0) >= 0 && peek(l, 0) != '\n') bump(l);
    201       l->had_space = 1;
    202       continue;
    203     }
    204     if (ch == '/' && peek(l, 1) == '*') {
    205       bump(l);
    206       bump(l);
    207       while (peek(l, 0) >= 0) {
    208         if (peek(l, 0) == '*' && peek(l, 1) == '/') {
    209           bump(l);
    210           bump(l);
    211           break;
    212         }
    213         bump(l);
    214       }
    215       l->had_space = 1;
    216       continue;
    217     }
    218     break;
    219   }
    220 }
    221 
    222 /* Consume a pp-number per §6.4.8. The cursor is positioned at the leading
    223  * digit (or `.` followed by a digit) on entry. */
    224 static void scan_pp_number(AsmLexer* l) {
    225   if (peek(l, 0) == '.') bump(l);
    226   bump(l); /* first digit */
    227   while (l->pos < l->len) {
    228     int c = peek(l, 0);
    229     int n = peek(l, 1);
    230     if ((c == 'e' || c == 'E' || c == 'p' || c == 'P') &&
    231         (n == '+' || n == '-')) {
    232       bump(l);
    233       bump(l);
    234     } else if (is_alnum(c) || c == '.') {
    235       bump(l);
    236     } else {
    237       break;
    238     }
    239   }
    240 }
    241 
    242 /* 1 if the pp-number text is a floating constant (§6.4.4.2): contains a
    243  * radix `.`, a hex `p`/`P` exponent, or a decimal `e`/`E` exponent. */
    244 static int pp_number_is_float(const char* s, size_t n) {
    245   int is_hex = 0;
    246   size_t i = 0;
    247   if (n >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
    248     is_hex = 1;
    249     i = 2;
    250   }
    251   for (; i < n; ++i) {
    252     char c = s[i];
    253     if (c == '.') return 1;
    254     if (is_hex && (c == 'p' || c == 'P')) return 1;
    255     if (!is_hex && (c == 'e' || c == 'E')) {
    256       if (i + 1 < n) {
    257         char nx = s[i + 1];
    258         if (nx == '+' || nx == '-' || (nx >= '0' && nx <= '9')) return 1;
    259       }
    260     }
    261   }
    262   return 0;
    263 }
    264 
    265 /* Consume a quoted body — string ('"') or character ('\''). The cursor is
    266  * positioned at the opening quote on entry. Returns 1 on an unterminated or
    267  * newline-broken literal, 0 on a clean close. */
    268 static int scan_quoted(AsmLexer* l, int quote) {
    269   bump(l); /* opening quote */
    270   for (;;) {
    271     int ch = peek(l, 0);
    272     if (ch < 0) return 1;
    273     if (ch == quote) {
    274       bump(l);
    275       return 0;
    276     }
    277     if (ch == '\n') return 1;
    278     if (ch == '\\') {
    279       bump(l); /* backslash */
    280       if (peek(l, 0) < 0) return 1;
    281       bump(l); /* the escaped char */
    282       continue;
    283     }
    284     bump(l);
    285   }
    286 }
    287 
    288 AsmTok asm_lex_next(AsmLexer* l) {
    289   AsmTok t;
    290   SrcLoc tloc;
    291   size_t start;
    292   int ch;
    293 
    294   memset(&t, 0, sizeof(t));
    295 
    296   /* Skip whitespace and comments. A newline token is emitted before any
    297    * subsequent content tokens for the line that follows. */
    298   for (;;) {
    299     skip_ws_and_comments(l);
    300     skip_splices(l);
    301     if (l->pos >= l->len) {
    302       t.kind = ASM_TOK_EOF;
    303       t.loc = asm_lex_here(l);
    304       return t;
    305     }
    306     if (peek(l, 0) == '\n') {
    307       tloc = asm_lex_here(l);
    308       bump(l);
    309       t.kind = ASM_TOK_NEWLINE;
    310       t.loc = tloc;
    311       l->at_bol = 1;
    312       l->had_space = 0;
    313       return t;
    314     }
    315     /* `;` is the GNU-as statement separator (the line comment on these targets
    316      * is `@`/`//`/`#`, never `;`). A macro that packs several directives/labels/
    317      * instructions onto one logical line via a `;` SEPARATOR (e.g. compiler-rt's
    318      * DEFINE_AEABI_*) depends on this — surface it as a newline boundary so each
    319      * statement is parsed on its own. */
    320     if (peek(l, 0) == ';') {
    321       tloc = asm_lex_here(l);
    322       bump(l);
    323       t.kind = ASM_TOK_NEWLINE;
    324       t.loc = tloc;
    325       l->at_bol = 1;
    326       l->had_space = 0;
    327       return t;
    328     }
    329     break;
    330   }
    331 
    332   tloc = asm_lex_here(l);
    333   start = l->pos;
    334   ch = peek(l, 0);
    335 
    336   if (l->at_bol) t.flags |= ASM_TF_AT_BOL;
    337   if (l->had_space) t.flags |= ASM_TF_HAS_SPACE;
    338   l->at_bol = 0;
    339   l->had_space = 0;
    340   t.loc = tloc;
    341 
    342   /* String / character literal, with optional encoding prefix. The prefix
    343    * length and encoding flag are decoded together so the spelling we
    344    * intern includes the prefix bytes. */
    345   {
    346     int sp_len = -1;
    347     int is_char = 0;
    348     u32 encf = 0;
    349 
    350     if (ch == '"') {
    351       sp_len = 0;
    352       is_char = 0;
    353     } else if (ch == '\'') {
    354       sp_len = 0;
    355       is_char = 1;
    356     } else if (ch == 'L' && peek(l, 1) == '"') {
    357       sp_len = 1;
    358       is_char = 0;
    359       encf = ASM_TF_STR_WIDE;
    360     } else if (ch == 'L' && peek(l, 1) == '\'') {
    361       sp_len = 1;
    362       is_char = 1;
    363       encf = ASM_TF_STR_WIDE;
    364     } else if (ch == 'u' && peek(l, 1) == '8' && peek(l, 2) == '"') {
    365       sp_len = 2;
    366       is_char = 0;
    367       encf = ASM_TF_STR_U8;
    368     } else if (ch == 'u' && peek(l, 1) == '"') {
    369       sp_len = 1;
    370       is_char = 0;
    371       encf = ASM_TF_STR_U16;
    372     } else if (ch == 'u' && peek(l, 1) == '\'') {
    373       sp_len = 1;
    374       is_char = 1;
    375       encf = ASM_TF_STR_U16;
    376     } else if (ch == 'U' && peek(l, 1) == '"') {
    377       sp_len = 1;
    378       is_char = 0;
    379       encf = ASM_TF_STR_U32;
    380     } else if (ch == 'U' && peek(l, 1) == '\'') {
    381       sp_len = 1;
    382       is_char = 1;
    383       encf = ASM_TF_STR_U32;
    384     }
    385 
    386     if (sp_len >= 0) {
    387       int i;
    388       for (i = 0; i < sp_len; ++i) bump(l);
    389       if (scan_quoted(l, is_char ? '\'' : '"')) t.flags |= ASM_TF_LITERAL_BAD;
    390       t.kind = (u16)(is_char ? ASM_TOK_CHR : ASM_TOK_STR);
    391       t.flags |= encf;
    392       t.spelling = intern_spliced(l, start, l->pos);
    393       t.v.str = t.spelling;
    394       return t;
    395     }
    396   }
    397 
    398   /* Local-label identifier: a `.L`-prefixed symbol name (the universal GNU
    399    * convention for assembler-local labels, e.g. `.Lkit_ro.0`, `.L.str`,
    400    * `.LBB0_1`). Lexed as a single ASM_TOK_IDENT — including the leading dot
    401    * and any embedded dots — so it flows through the same operand / label /
    402    * `.type` paths as an ordinary identifier. This is unambiguous against
    403    * directives: no assembler directive begins with `.L`, so `.text`,
    404    * `.section`, `.quad` etc. still tokenize as PUNCT('.') + IDENT and reach
    405    * the directive dispatcher. Embedded `.` is consumed only when followed by
    406    * another symbol char, so `.Lfoo, x` and `.Lfoo+4` stop at the delimiter. */
    407   if (ch == '.' && peek(l, 1) == 'L') {
    408     bump(l); /* '.' */
    409     bump(l); /* 'L' */
    410     for (;;) {
    411       int c = peek(l, 0);
    412       if (is_alnum(c) || c == '$') {
    413         bump(l);
    414       } else if (c == '.' && (is_alnum(peek(l, 1)) || peek(l, 1) == '$' ||
    415                               peek(l, 1) == '_')) {
    416         bump(l);
    417       } else {
    418         break;
    419       }
    420     }
    421     t.kind = ASM_TOK_IDENT;
    422     t.spelling = intern_spliced(l, start, l->pos);
    423     t.v.ident = t.spelling;
    424     return t;
    425   }
    426 
    427   /* Identifier (§6.4.2). Encoding-prefix candidates above are matched
    428    * before this since L/u/U followed by a quote is a literal, not an
    429    * identifier. The grammar's identifier-nondigit covers letters, _,
    430    * extended source chars (impl-defined; bytes ≥ 0x80 here), and UCNs
    431    * (§6.4.3) — the latter span multiple source bytes so they're matched
    432    * via ucn_len rather than the per-byte is_alpha predicate. */
    433   {
    434     int u = ucn_len(l, 0);
    435     if (is_alpha(ch) || u) {
    436       if (u) {
    437         int i;
    438         for (i = 0; i < u; ++i) bump(l);
    439       } else
    440         bump(l);
    441       for (;;) {
    442         int c = peek(l, 0);
    443         if (is_alnum(c)) {
    444           bump(l);
    445         } else if (c == '.' && is_digit(peek(l, 1))) {
    446           /* Discriminator-mangled symbol: `name.N` (static locals, lambda /
    447            * block-scope renaming, e.g. `acc.1`). A `.` followed by a digit
    448            * continues the identifier. Restricted to `.`+digit so it never
    449            * swallows a `.`-led mnemonic suffix (`b.eq`, `fcvt.w.s`) or the
    450            * `.size foo, .-foo` location-counter dot. */
    451           bump(l);
    452         } else if ((u = ucn_len(l, 0))) {
    453           int i;
    454           for (i = 0; i < u; ++i) bump(l);
    455         } else {
    456           break;
    457         }
    458       }
    459       t.kind = ASM_TOK_IDENT;
    460       t.spelling = intern_spliced(l, start, l->pos);
    461       t.v.ident = t.spelling;
    462       return t;
    463     }
    464   }
    465 
    466   /* Preprocessor-number shaped token, classified to ASM_TOK_NUM /
    467    * ASM_TOK_FLT for expression diagnostics and future directive support. */
    468   if (is_digit(ch) || (ch == '.' && is_digit(peek(l, 1)))) {
    469     size_t plen;
    470     char* pbuf;
    471     size_t i, k;
    472     scan_pp_number(l);
    473     /* Classify on the post-splice text (the spelling we'll intern). */
    474     plen = l->pos - start;
    475     pbuf = (char*)l->heap->alloc(l->heap, plen ? plen : 1, 1);
    476     k = 0;
    477     for (i = start; i < l->pos;) {
    478       if (i + 1 < l->pos && l->src[i] == '\\' && l->src[i + 1] == '\n') {
    479         i += 2;
    480         continue;
    481       }
    482       pbuf[k++] = l->src[i++];
    483     }
    484     t.kind = (u16)(pp_number_is_float(pbuf, k) ? ASM_TOK_FLT : ASM_TOK_NUM);
    485     /* Preserve common C-style integer/float suffixes in token flags. The
    486      * current assembler expression evaluator ignores them, but keeping the
    487      * spelling metadata makes the lexer useful for future directive work. */
    488     if (t.kind == ASM_TOK_FLT) {
    489       size_t j = k;
    490       while (j > 0) {
    491         char c = pbuf[j - 1];
    492         if (c == 'f' || c == 'F') {
    493           t.flags |= ASM_TF_FLT_F;
    494           --j;
    495           continue;
    496         }
    497         if (c == 'l' || c == 'L') {
    498           t.flags |= ASM_TF_FLT_L;
    499           --j;
    500           continue;
    501         }
    502         break;
    503       }
    504     } else {
    505       size_t j = k;
    506       while (j > 0) {
    507         char c = pbuf[j - 1];
    508         if (c == 'u' || c == 'U') {
    509           t.flags |= ASM_TF_INT_U;
    510           --j;
    511           continue;
    512         }
    513         if (c == 'l' || c == 'L') {
    514           if (j >= 2 && (pbuf[j - 2] == 'l' || pbuf[j - 2] == 'L')) {
    515             t.flags |= ASM_TF_INT_LL;
    516             j -= 2;
    517           } else {
    518             t.flags |= ASM_TF_INT_L;
    519             --j;
    520           }
    521           continue;
    522         }
    523         break;
    524       }
    525     }
    526     t.spelling = pool_intern_slice(l->pool, (Slice){.s = pbuf, .len = k});
    527     l->heap->free(l->heap, pbuf, plen ? plen : 1);
    528     return t;
    529   }
    530 
    531   /* Punctuator, longest match. `#` is a distinct token because it is both
    532    * an asm immediate marker and, at BOL in preprocessed assembler, a line
    533    * marker introducer. */
    534   {
    535     int n0 = peek(l, 0);
    536     int n1 = peek(l, 1);
    537     int n2 = peek(l, 2);
    538     int n3 = peek(l, 3);
    539     int adv = 1;
    540     u32 punct = ASM_P_NONE;
    541     u16 kind = ASM_TOK_PUNCT;
    542     int i;
    543 
    544     switch (n0) {
    545       case '#':
    546         if (n1 == '#') {
    547           adv = 2;
    548           kind = ASM_TOK_HASH_HASH;
    549           punct = ASM_P_HASH_HASH;
    550         } else {
    551           adv = 1;
    552           kind = ASM_TOK_HASH;
    553           punct = '#';
    554         }
    555         break;
    556       case '.':
    557         if (n1 == '.' && n2 == '.') {
    558           adv = 3;
    559           punct = ASM_P_ELLIPSIS;
    560         } else {
    561           adv = 1;
    562           punct = '.';
    563         }
    564         break;
    565       case '-':
    566         if (n1 == '>') {
    567           adv = 2;
    568           punct = ASM_P_ARROW;
    569         } else if (n1 == '-') {
    570           adv = 2;
    571           punct = ASM_P_DEC;
    572         } else if (n1 == '=') {
    573           adv = 2;
    574           punct = ASM_P_SUB_ASSIGN;
    575         } else {
    576           adv = 1;
    577           punct = '-';
    578         }
    579         break;
    580       case '+':
    581         if (n1 == '+') {
    582           adv = 2;
    583           punct = ASM_P_INC;
    584         } else if (n1 == '=') {
    585           adv = 2;
    586           punct = ASM_P_ADD_ASSIGN;
    587         } else {
    588           adv = 1;
    589           punct = '+';
    590         }
    591         break;
    592       case '<':
    593         if (n1 == '<' && n2 == '=') {
    594           adv = 3;
    595           punct = ASM_P_SHL_ASSIGN;
    596         } else if (n1 == '<') {
    597           adv = 2;
    598           punct = ASM_P_SHL;
    599         } else if (n1 == '=') {
    600           adv = 2;
    601           punct = ASM_P_LE;
    602         } else if (n1 == ':') {
    603           adv = 2;
    604           punct = '[';
    605         } /* digraph */
    606         else if (n1 == '%') {
    607           adv = 2;
    608           punct = '{';
    609         } /* digraph */
    610         else {
    611           adv = 1;
    612           punct = '<';
    613         }
    614         break;
    615       case '>':
    616         if (n1 == '>' && n2 == '=') {
    617           adv = 3;
    618           punct = ASM_P_SHR_ASSIGN;
    619         } else if (n1 == '>') {
    620           adv = 2;
    621           punct = ASM_P_SHR;
    622         } else if (n1 == '=') {
    623           adv = 2;
    624           punct = ASM_P_GE;
    625         } else {
    626           adv = 1;
    627           punct = '>';
    628         }
    629         break;
    630       case '=':
    631         if (n1 == '=') {
    632           adv = 2;
    633           punct = ASM_P_EQ;
    634         } else {
    635           adv = 1;
    636           punct = '=';
    637         }
    638         break;
    639       case '!':
    640         if (n1 == '=') {
    641           adv = 2;
    642           punct = ASM_P_NE;
    643         } else {
    644           adv = 1;
    645           punct = '!';
    646         }
    647         break;
    648       case '&':
    649         if (n1 == '&') {
    650           adv = 2;
    651           punct = ASM_P_AND;
    652         } else if (n1 == '=') {
    653           adv = 2;
    654           punct = ASM_P_AND_ASSIGN;
    655         } else {
    656           adv = 1;
    657           punct = '&';
    658         }
    659         break;
    660       case '|':
    661         if (n1 == '|') {
    662           adv = 2;
    663           punct = ASM_P_OR;
    664         } else if (n1 == '=') {
    665           adv = 2;
    666           punct = ASM_P_OR_ASSIGN;
    667         } else {
    668           adv = 1;
    669           punct = '|';
    670         }
    671         break;
    672       case '^':
    673         if (n1 == '=') {
    674           adv = 2;
    675           punct = ASM_P_XOR_ASSIGN;
    676         } else {
    677           adv = 1;
    678           punct = '^';
    679         }
    680         break;
    681       case '*':
    682         if (n1 == '=') {
    683           adv = 2;
    684           punct = ASM_P_MUL_ASSIGN;
    685         } else {
    686           adv = 1;
    687           punct = '*';
    688         }
    689         break;
    690       case '/':
    691         if (n1 == '=') {
    692           adv = 2;
    693           punct = ASM_P_DIV_ASSIGN;
    694         } else {
    695           adv = 1;
    696           punct = '/';
    697         }
    698         break;
    699       case '%':
    700         if (n1 == ':' && n2 == '%' && n3 == ':') {
    701           adv = 4;
    702           kind = ASM_TOK_HASH_HASH;
    703           punct = ASM_P_HASH_HASH;
    704         } else if (n1 == ':') {
    705           adv = 2;
    706           kind = ASM_TOK_HASH;
    707           punct = '#';
    708         } else if (n1 == '=') {
    709           adv = 2;
    710           punct = ASM_P_MOD_ASSIGN;
    711         } else if (n1 == '>') {
    712           adv = 2;
    713           punct = '}';
    714         } /* digraph */
    715         else {
    716           adv = 1;
    717           punct = '%';
    718         }
    719         break;
    720       case ':':
    721         if (n1 == '>') {
    722           adv = 2;
    723           punct = ']';
    724         } /* digraph */
    725         else {
    726           adv = 1;
    727           punct = ':';
    728         }
    729         break;
    730       case '(':
    731       case ')':
    732       case '{':
    733       case '}':
    734       case '[':
    735       case ']':
    736       case ',':
    737       case ';':
    738       case '?':
    739       case '~':
    740         adv = 1;
    741         punct = (u32)n0;
    742         break;
    743       default:
    744         /* Unknown byte. Surface as a single-char punct so the token
    745          * stream still progresses; PP/parse may diagnose. */
    746         adv = 1;
    747         punct = (u32)n0;
    748         break;
    749     }
    750 
    751     for (i = 0; i < adv; ++i) bump(l);
    752     t.kind = kind;
    753     t.v.punct = punct;
    754     t.spelling = intern_spliced(l, start, l->pos);
    755     return t;
    756   }
    757 }