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 }