kit

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

commit 93bc5574ad37395fd7281f02b8b8a921b7eebcbe
parent fc5dd99294051d4ccc87d9e0e0b43fbf33ab5f6f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 12 Jun 2026 15:19:36 -0700

perf(lex): raw-cursor scanner + 256-entry char-class table + deferred column

Rewrite lex_next from a per-byte pos/accessor walk into a raw-cursor scan, the
single biggest -O0 frontend lever (lex_next was ~58% of -c self-time).

Four compounding changes:
- Raw cursor: hold cur/end pointers across the hot scan instead of round-tripping
  pos/len/src/col through the Lexer struct on every byte.
- Char-class table: one cclass[256] load (CC_SPACE/CC_DIGIT/CC_IDST/CC_IDCONT)
  replaces the branchy is_space/is_alpha/is_alnum/is_digit cascades and drives the
  first-char dispatch.
- Fused whitespace+comment skip into the table walk, so a token with no leading
  space pays only a failed CC_SPACE test.
- Deferred column: drop per-byte col tracking; track line + a line_start pointer
  and compute col = cur - line_start + 1 once per built SrcLoc.

Line/column + splices stay bit-exact. The splice-free common path pays nothing
for splices. The splice-present path reconciles fold points via
lex_catchup_splices (token bodies contain no '\n', so an end-of-body catch-up is
exact) and keeps a per-byte slow path for the one spot where '\n' and folds
interleave (block comments).

Measured (RELEASE, best-of-7 instructions, sqlite3.c amalgamation):
  -E             1.060 B -> 0.975 B  (-84.3 M, -8.0%)
  -fsyntax-only  1.932 B -> 1.848 B  (-84.2 M, -4.4%)
  -c             2.303 B -> 2.219 B  (-83.9 M, -3.6%)
A flat ~84 M off every phase (the shared scanner). vs tcc -c (0.663 B): 3.47x ->
3.35x.

Byte-identical: perf_identity_gate.sh green (60/60, incl. the line-splice
battery) + an extra block-comment x splice diff; sqlite compiles/links/runs
(84|2). test-pp/parse/asm/toy all 0 fail.

Diffstat:
Mdoc/plan/PERF.md | 42++++++++++++++++++++++++------------------
Mlang/cpp/lex/lex.c | 637++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
2 files changed, 425 insertions(+), 254 deletions(-)

diff --git a/doc/plan/PERF.md b/doc/plan/PERF.md @@ -35,7 +35,7 @@ recent low-load reading. | compiler | instructions | cycles † | wall † | object | |---|--:|--:|--:|--:| | **tcc 0.9.28** | 0.66 B | 0.22 B | 0.06 s | 2.11 MB | -| **kit (current)** | 2.43 B | — | — | 4.26 MB | +| **kit (current)** | 2.22 B | — | — | 4.26 MB | | clang 22 | 8.73 B | 2.57 B | 0.81 s | 1.50 MB | † low-load reading; re-confirm on a quiet machine (`instructions` is @@ -43,7 +43,7 @@ load-independent and the figure to trust; cycles/wall are pending a quiet-machin re-read). kit beats clang on compile speed and is the fastest *general* backend here, but -**tcc is the bar**: ~3.7× instructions ahead. Closing that is the whole game. +**tcc is the bar**: ~3.35× instructions ahead. Closing that is the whole game. (kit's object is larger because `-O0` codegen is deliberately unoptimized — irrelevant to this goal.) @@ -98,11 +98,12 @@ m build/release/kit cc -c -o /tmp/k.o sqlite3.c --sysroot "$SDK" # + codegen `-fsyntax-only` still drives the full CG value-stack and type lowering (routed to the no-op check backend), so `(-c) − (-fsyntax-only)` isolates **native emit + -object write** only. Current binary: `-E` 1.06 B, `-fsyntax-only` 1.98 B, `-c` -2.43 B → that delta is ~0.44 B / ~18 % — i.e. **codegen+emit is a small slice; -the frontend is the rest.** Within that frontend, `-E` alone (1.06 B) is just +object write** only. Current binary: `-E` 0.98 B, `-fsyntax-only` 1.85 B, `-c` +2.22 B → that delta is ~0.37 B / ~17 % — i.e. **codegen+emit is a small slice; +the frontend is the rest.** Within that frontend, `-E` alone (0.98 B) is just under half, and the scanner (`lex_next`) is the bulk of it — see *Where the time -goes*. +goes*. (The scanner rewrite below cut a flat ~84 M instructions off every phase: +`-E` 1.06 → 0.98 B / −8.0 %, `-c` 2.30 → 2.22 B / −3.6 %.) **4. Hotspot profile** (self-time per function). One run is far too fast for `sample` (a `-c` is ~0.2 s now), so merge the `Sort by top of stack` sections @@ -177,22 +178,27 @@ optimization). - **Preprocessor hideset** — O(1) content-addressed dedup; per-buffer scalar id (no per-token `HidesetId` array on the uniform macro-body path). - **Sym-centric macro + keyword tables** — flat `Sym`-indexed array loads, no hash. +- **Raw-cursor scanner** — `lex_next` walks `cur`/`end` pointers (no per-byte + `pos`/`len`/`col` struct round-trip), classifies via one `cclass[256]` load + (ident-start/cont, digit, space), fuses the whitespace+comment skip into the + table walk (a token with no leading space pays only a failed `CC_SPACE` test), + and **defers the column** to `col = cur - line_start + 1`, computed once per + built `SrcLoc` instead of per byte. Splices stay bit-exact: the splice-free + common path pays nothing (no per-byte splice test); the splice-present path + reconciles fold points with a `lex_catchup_splices` after each token body and a + per-byte slow path for the one place `\n` and folds interleave (block + comments). Flat ~84 M off every phase (`-E` −8.0 %), byte-identical (60/60 gate + + block-comment×splice). The dispatch/classification is now table-driven; the + residual `lex_next` self-time is the raw byte loads themselves + the interning + handoff, so the remaining lever is (2). ### Next levers (ranked) -1. **The scanner loop** (`lex_next`, 58 % — THE lever by a wide margin). The - per-token cost is `skip_ws_and_comments` + the first-char `switch` dispatch + - `scan_ident_run`; `lex_here`/`scan_ident_run` are already tight splice-free - loops, so the win is in the *dispatch and classification*. Replace the branchy - `is_space`/`is_alnum`/punct cascade with a single 256-entry **char-class table** - (one load → ident-start / ident-cont / digit / space / punct), tcc-style; fuse - the whitespace skip into the same table walk so a token with no leading space - pays nothing; defer the per-byte column to a `line_start` pointer. Gate - byte-identical (`-E` + objects + the splice battery). -2. **Identifier interning** (`pool_intern_slice`, ~15 %). Mostly fundamental +1. **Identifier interning** (`pool_intern_slice`, ~15 %). Mostly fundamental (identifiers must be interned); word-at-a-time hashing was a measured dead end - for short ids (see below). Low ceiling — attack only after (1). -3. **`memset`/`memmove` in parse + codegen** (~10 %, callers `cg_adapter` / + for short ids (see below). Now the top frontend lever after the scanner + rewrite, but a low ceiling. +2. **`memset`/`memmove` in parse + codegen** (~10 %, callers `cg_adapter` / `native` / `m_emit_bytes` / `parse_*`, **not** the lexer). Right-size the struct zeroing and buffer copies on the per-expression / per-emit path. diff --git a/lang/cpp/lex/lex.c b/lang/cpp/lex/lex.c @@ -10,26 +10,110 @@ * preprocessor can recognize directives and the paste operator * * Comments (§6.4.9) are consumed as whitespace; physical newlines surface - * as TOK_NEWLINE so PP can implement directive-line semantics. */ + * as TOK_NEWLINE so PP can implement directive-line semantics. + * + * The scanner is a raw-cursor walk: the hot path holds `cur`/`end` as + * pointers into the (line-splice-folded) buffer and classifies each byte with + * one load from `cclass[256]`. There is no per-byte struct round-trip, no + * per-byte column arithmetic, and — on the overwhelmingly common splice-free + * input — no per-byte splice test. The physical column is *deferred*: it is + * not tracked per byte but computed once per token from a `line_start` pointer + * (col = cur - line_start + 1) when a SrcLoc is built. */ #include "lex/lex.h" #include <string.h> +/* Per-byte character classes (§6.4). One table load replaces the branchy + * is_space/is_alpha/is_alnum/is_digit cascades and drives the first-char + * dispatch. Bytes ≥ 0x80 are accepted as identifier characters (the + * implementation-defined "other" source characters — UTF-8 lead/continuation + * bytes in practice). '\n' is deliberately NOT in CC_SPACE: it surfaces as + * TOK_NEWLINE, never skipped here. */ +#define CC_SPACE 0x01u /* ' ' '\t' '\r' '\v' '\f' (NOT '\n') */ +#define CC_DIGIT 0x02u /* '0'-'9' */ +#define CC_IDST 0x04u /* identifier-start: A-Z a-z _ and >= 0x80 */ +#define CC_IDCONT 0x08u /* identifier-continue: CC_IDST plus 0-9 */ + +/* clang-format off */ +static const u8 cclass[256] = { +/*0x00*/ 0,0,0,0,0,0,0,0,0,CC_SPACE,0,CC_SPACE,CC_SPACE,CC_SPACE,0,0, +/*0x10*/ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +/*0x20*/ CC_SPACE,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +/*0x30*/ CC_DIGIT|CC_IDCONT,CC_DIGIT|CC_IDCONT,CC_DIGIT|CC_IDCONT,CC_DIGIT|CC_IDCONT, + CC_DIGIT|CC_IDCONT,CC_DIGIT|CC_IDCONT,CC_DIGIT|CC_IDCONT,CC_DIGIT|CC_IDCONT, + CC_DIGIT|CC_IDCONT,CC_DIGIT|CC_IDCONT,0,0,0,0,0,0, +/*0x40*/ 0,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, +/*0x50*/ CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,0,0,0,0, + CC_IDST|CC_IDCONT, +/*0x60*/ 0,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, +/*0x70*/ CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,0,0,0,0,0, +/*0x80*/ CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, +/*0x90*/ CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, +/*0xA0*/ CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, +/*0xB0*/ CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, +/*0xC0*/ CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, +/*0xD0*/ CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, +/*0xE0*/ CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, +/*0xF0*/ CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, + CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT,CC_IDST|CC_IDCONT, +}; +/* clang-format on */ + struct Lexer { Compiler* c; Pool* pool; Heap* heap; /* `src`/`len` are the post-phase-2 (line-splice-folded) logical bytes the - * scanner walks with plain direct indexing — no per-byte splice test. When - * the input contains no `\<newline>` (the common case) this borrows the input - * verbatim (owns_src = 0, zero-copy); otherwise it is a folded heap copy. */ + * scanner walks. When the input contains no `\<newline>` (the common case) + * this borrows the input verbatim (owns_src = 0, zero-copy); otherwise it is + * a folded heap copy. `cur` is the live cursor and `end` == src + len; both + * stay pointers so the hot scan never reloads pos/len from the struct. */ const char* src; + const char* end; + const char* cur; size_t len; - size_t pos; + /* Deferred column: the column of a byte at `p` is `(p - line_start) + 1`. + * `line_start` points just past the last physical line break the cursor has + * crossed (the byte after a '\n', or the fold point of a `\<newline>`), so + * no per-byte column arithmetic is needed — only a subtraction per built + * SrcLoc. */ + const char* line_start; u32 file_id; u32 line; - u32 col; u8 at_bol; u8 had_space; /* §5.1.1.2 phase 4 directive context for header-name lexing. @@ -38,8 +122,9 @@ struct Lexer { u8 dstate; u8 owns_src; /* src is a heap-allocated folded copy to free at close */ /* Sorted logical offsets at which a `\<newline>` splice was folded out, so - * line/col tracking can still advance the physical line as the cursor passes - * each. Empty (and NULL) for splice-free input. */ + * line tracking can still advance the physical line as the cursor passes + * each. Empty (and NULL) for splice-free input — the common case pays + * nothing for splices. */ u32* splices; u32 nsplices; u32 next_splice; @@ -112,37 +197,37 @@ static void lex_fold_splices(Lexer* l, const char* src, size_t len) { l->nsplices = (u32)s; } -/* Advance the line counter for any fold points at the current logical position - * (a `\<newline>` removed here was a physical line break). Called after every - * cursor advance and once at open for a leading splice. */ -static inline void lex_sync_splices(Lexer* l) { - while (l->next_splice < l->nsplices && l->splices[l->next_splice] == l->pos) { +/* Catch line tracking up to the cursor: process every splice fold point at or + * before `cur` (a `\<newline>` removed there was a physical line break, so it + * bumps the line and starts a fresh column origin at the fold). Called after a + * fast scan that may have jumped over fold points; cheap no-op when no splices + * remain. Only ever called when l->splices != NULL. + * + * Bit-exactness: a token body (identifier/number/string/punct/header) contains + * no '\n', so the only line-break event within it is a fold; processing them + * in order leaves `line` advanced by one per fold and `line_start` at the last + * fold — identical to the old per-byte advance, which is observable only at the + * next SrcLoc (no column is read mid-body). The inclusive `<= cur` bound makes + * a fold sitting exactly at the cursor (a token that starts on a freshly + * spliced line) fire here, matching the old "sync when pos == fold". */ +static void lex_catchup_splices(Lexer* l) { + const char* base = l->src; + u32 ns = l->next_splice; + u32 n = l->nsplices; + while (ns < n && base + l->splices[ns] <= l->cur) { l->line++; - l->col = 1; - l->next_splice++; + l->line_start = base + l->splices[ns]; + ns++; } + l->next_splice = ns; } -/* The off-th logical byte at the cursor, or -1 at end of input. Splices were - * folded out at open, so this is a plain bounds-checked index — no per-byte - * splice walk. */ +/* The off-th logical byte at the cursor, or -1 at end of input. Used only on + * cold lookahead paths (encoding-prefix detection, punctuator longest-match, + * UCNs); the hot scan walks `cur`/`end` directly. */ static int peek(const Lexer* l, size_t off) { - size_t p = l->pos + off; - return p < l->len ? (unsigned char)l->src[p] : -1; -} - -static int bump(Lexer* l) { - int ch; - if (l->pos >= l->len) return -1; - ch = (unsigned char)l->src[l->pos++]; - if (ch == '\n') { - l->line++; - l->col = 1; - } else { - l->col++; - } - lex_sync_splices(l); - return ch; + const char* p = l->cur + off; + return p < l->end ? (unsigned char)*p : -1; } static int is_digit(int c) { return c >= '0' && c <= '9'; } @@ -150,43 +235,19 @@ static int is_hex_digit(int c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } -/* Identifier-start byte (§6.4.2.1). Letters and underscore are ASCII; bytes - * ≥ 0x80 are accepted as the implementation-defined "other characters" - * permitted in identifiers — in practice UTF-8 lead/continuation bytes for - * extended source characters. UCNs are matched separately via ucn_len since - * they span multiple source bytes. */ -static int is_alpha(int c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || - c >= 0x80; -} -static int is_alnum(int c) { return is_alpha(c) || is_digit(c); } - -/* Consume a maximal run of identifier-continuation bytes (is_alnum) from the - * cursor, advancing line/col exactly as a per-byte bump() would. No is_alnum - * byte is '\n', so within a run line changes only when a folded splice point is - * crossed. The common splice-free input (l->splices == NULL) collapses to a - * single pos/col advance with src/len held in registers — no per-byte struct - * reload and no per-byte splice load+branch in the hottest scan loop. Stops at - * the first non-alnum byte (a '\\' UCN lead, a punctuator, or end), which the - * caller re-examines (e.g. for a UCN continuation). */ + +/* Consume a maximal run of identifier-continuation bytes (CC_IDCONT) from the + * cursor in a tight raw-pointer loop: one table load per byte, no struct + * reload, no per-byte column or splice work. No CC_IDCONT byte is '\n', so the + * line is untouched here; any fold points crossed are reconciled once at the + * end. Stops at the first non-CC_IDCONT byte (a '\\' UCN lead, a punctuator, or + * end), which the caller re-examines (e.g. for a UCN continuation). */ static void scan_ident_run(Lexer* l) { - const char* s = l->src; - size_t n = l->len; - if (l->splices == NULL) { - size_t p = l->pos; - while (p < n && is_alnum((unsigned char)s[p])) ++p; - l->col += (u32)(p - l->pos); - l->pos = p; - return; - } - /* Splice-present slow path: keep the per-byte cursor advance and the splice - * line/col sync bit-for-bit identical to bump() (is_alnum bytes are never - * '\n', so only lex_sync_splices can move the line). */ - while (l->pos < n && is_alnum((unsigned char)s[l->pos])) { - l->pos++; - l->col++; - lex_sync_splices(l); - } + const char* p = l->cur; + const char* end = l->end; + while (p < end && (cclass[(unsigned char)*p] & CC_IDCONT)) p++; + l->cur = p; + if (l->splices) lex_catchup_splices(l); } /* Match a UCN at offset `off` from the current position. Returns the total @@ -213,7 +274,7 @@ static SrcLoc lex_here(const Lexer* l) { SrcLoc loc; loc.file_id = l->file_id; loc.line = l->line; - loc.col = l->col; + loc.col = (u32)(l->cur - l->line_start) + 1u; return loc; } @@ -231,12 +292,13 @@ Lexer* lex_open_mem(Compiler* c, const char* name, const char* src, } l->heap = h; lex_fold_splices(l, src ? src : "", src ? len : 0); - l->pos = 0; + l->cur = l->src; + l->end = l->src + l->len; + l->line = 1; + l->line_start = l->src; l->file_id = 0; (void)kit_source_add_memory(c, kit_slice_cstr(name), &l->file_id); - l->line = 1; - l->col = 1; - lex_sync_splices(l); /* a splice folded at offset 0 advances to line 2 */ + if (l->splices) lex_catchup_splices(l); /* a splice folded at offset 0 -> line 2 */ l->at_bol = 1; l->had_space = 0; return l; @@ -261,34 +323,37 @@ void lex_close(Lexer* l) { * remains line 1). No-op unless the buffer begins with the two bytes `#!`. * Apply only to a primary source file, never to includes/paste buffers. */ void lex_skip_shebang(Lexer* l) { - if (!l || l->pos != 0) return; + if (!l || l->cur != l->src) return; if (l->len < 2 || l->src[0] != '#' || l->src[1] != '!') return; - while (l->pos < l->len && l->src[l->pos] != '\n') l->pos++; + while (l->cur < l->end && *l->cur != '\n') l->cur++; + /* The shebang stays line 1; rebase line_start so the trailing newline still + * reports column 1 (matching the old, column-untracked skip). */ + l->line_start = l->cur; } SrcLoc lex_loc(const Lexer* l) { return lex_here(l); } u32 lex_file_id(const Lexer* l) { return l->file_id; } -/* Intern a token's spelling [start, end) straight from the folded buffer (it is +/* Intern a token's spelling [a, b) straight from the folded buffer (it is * already post-phase-2 logical text — splices were removed at open). */ -static Sym lex_intern(Lexer* l, size_t start, size_t end) { +static Sym lex_intern(Lexer* l, const char* a, const char* b) { return kit_sym_intern(l->pool->c, - (KitSlice){.s = l->src + start, .len = end - start}); + (KitSlice){.s = a, .len = (size_t)(b - a)}); } /* Spelling Sym for a non-digraph punctuator, interned once per lexer and then * reused for every later occurrence of the same punctuator. The Punct value * uniquely identifies the source spelling for every non-digraph form (all '+' * tokens are "+", all P_ARROW are "->", ...), so caching by it is exact. The - * first occurrence interns the exact source bytes [start,end), making the Sym + * first occurrence interns the exact source bytes [a,b), making the Sym * bit-identical to what an unconditional lex_intern would have produced. Pool * entry 0 is the reserved "none" sym, so a cached spelling is always nonzero * and 0 is an unambiguous not-yet-interned sentinel. Digraphs are excluded by * the caller: they share a Punct value with their canonical form but spell * differently, so they must intern their source bytes verbatim. */ -static Sym punct_spelling(Lexer* l, u32 punct, size_t start, size_t end) { +static Sym punct_spelling(Lexer* l, u32 punct, const char* a, const char* b) { Sym s = l->punct_sym[punct]; - if (!s) s = l->punct_sym[punct] = lex_intern(l, start, end); + if (!s) s = l->punct_sym[punct] = lex_intern(l, a, b); return s; } @@ -304,34 +369,99 @@ static int matches_include_kw(const char* s, size_t n) { return 0; } -/* Skip whitespace and comments. Returns 1 if a newline boundary was crossed - * via comment consumption (caller still emits the explicit newline token on - * an in-source '\n'). */ -static void skip_ws_and_comments(Lexer* l) { +/* Splice-present per-byte advance: walk one byte, advancing line/line_start + * across a '\n' and across any fold point now at the cursor exactly as the old + * bump()+sync did. Used only by the splice-present whitespace/comment skip, + * where '\n' bytes and fold points interleave and must be processed in order. + * Cursor must be < end. */ +static void lex_bump_sp(Lexer* l) { + char ch = *l->cur++; + if (ch == '\n') { + l->line++; + l->line_start = l->cur; + } + { + const char* base = l->src; + u32 ns = l->next_splice; + u32 n = l->nsplices; + while (ns < n && base + l->splices[ns] == l->cur) { + l->line++; + l->line_start = l->cur; + ns++; + } + l->next_splice = ns; + } +} + +/* Fast whitespace + comment skip (splice-free input): a raw-pointer table walk. + * A token with no leading whitespace pays only the failed CC_SPACE test. '\n' + * is never CC_SPACE (it surfaces as TOK_NEWLINE), so only a block comment can + * advance the physical line here. Sets had_space when anything is consumed. */ +static void skip_ws_fast(Lexer* l) { + const char* p = l->cur; + const char* end = l->end; + int adv = 0; + for (;;) { + const char* q = p; + while (p < end && (cclass[(unsigned char)*p] & CC_SPACE)) p++; + if (p != q) adv = 1; + if (p >= end) break; + if (*p == '/' && p + 1 < end && p[1] == '/') { + p += 2; + while (p < end && *p != '\n') p++; + adv = 1; + continue; + } + if (*p == '/' && p + 1 < end && p[1] == '*') { + p += 2; + while (p < end) { + if (*p == '*' && p + 1 < end && p[1] == '/') { + p += 2; + break; + } + if (*p == '\n') { + l->line++; + l->line_start = p + 1; + } + p++; + } + adv = 1; + continue; + } + break; + } + l->cur = p; + if (adv) l->had_space = 1; +} + +/* Whitespace + comment skip, splice-present path. Mirrors the old per-byte + * loop (peek/bump) exactly so line/line_start stay bit-identical when a fold + * point lands inside a comment between physical newlines. The rare path. */ +static void skip_ws_spliced(Lexer* l) { for (;;) { - int ch = peek(l, 0); + int ch = (l->cur < l->end) ? (unsigned char)*l->cur : -1; if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\v' || ch == '\f') { - bump(l); + lex_bump_sp(l); l->had_space = 1; continue; } - if (ch == '/' && peek(l, 1) == '/') { - bump(l); - bump(l); - while (peek(l, 0) >= 0 && peek(l, 0) != '\n') bump(l); + if (ch == '/' && l->cur + 1 < l->end && l->cur[1] == '/') { + lex_bump_sp(l); + lex_bump_sp(l); + while (l->cur < l->end && *l->cur != '\n') lex_bump_sp(l); l->had_space = 1; continue; } - if (ch == '/' && peek(l, 1) == '*') { - bump(l); - bump(l); - while (peek(l, 0) >= 0) { - if (peek(l, 0) == '*' && peek(l, 1) == '/') { - bump(l); - bump(l); + if (ch == '/' && l->cur + 1 < l->end && l->cur[1] == '*') { + lex_bump_sp(l); + lex_bump_sp(l); + while (l->cur < l->end) { + if (*l->cur == '*' && l->cur + 1 < l->end && l->cur[1] == '/') { + lex_bump_sp(l); + lex_bump_sp(l); break; } - bump(l); + lex_bump_sp(l); } l->had_space = 1; continue; @@ -340,24 +470,34 @@ static void skip_ws_and_comments(Lexer* l) { } } +static void skip_ws_and_comments(Lexer* l) { + if (l->splices) + skip_ws_spliced(l); + else + skip_ws_fast(l); +} + /* Consume a pp-number per §6.4.8. The cursor is positioned at the leading - * digit (or `.` followed by a digit) on entry. */ + * digit (or `.` followed by a digit) on entry. No '\n' is consumable here, so + * line tracking only reconciles fold points crossed (splice-present path). */ static void scan_pp_number(Lexer* l) { - if (peek(l, 0) == '.') bump(l); - bump(l); /* first digit */ - while (l->pos < l->len) { - int c = peek(l, 0); - int n = peek(l, 1); - if ((c == 'e' || c == 'E' || c == 'p' || c == 'P') && - (n == '+' || n == '-')) { - bump(l); - bump(l); - } else if (is_alnum(c) || c == '.') { - bump(l); + const char* p = l->cur; + const char* end = l->end; + if (p < end && *p == '.') p++; + if (p < end) p++; /* first digit (dispatch guarantees it is present) */ + while (p < end) { + unsigned char c = (unsigned char)*p; + if ((c == 'e' || c == 'E' || c == 'p' || c == 'P') && p + 1 < end && + (p[1] == '+' || p[1] == '-')) { + p += 2; + } else if ((cclass[c] & CC_IDCONT) || c == '.') { + p++; } else { break; } } + l->cur = p; + if (l->splices) lex_catchup_splices(l); } /* 1 if the pp-number text is a floating constant (§6.4.4.2): contains a @@ -385,31 +525,88 @@ static int pp_number_is_float(const char* s, size_t n) { /* Consume a quoted body — string ('"') or character ('\''). The cursor is * positioned at the opening quote on entry. Returns 1 on an unterminated or - * newline-broken literal, 0 on a clean close. */ + * newline-broken literal, 0 on a clean close. A '\n' terminates the literal + * without being consumed (it surfaces as the next TOK_NEWLINE), so line + * tracking only reconciles fold points crossed. */ static int scan_quoted(Lexer* l, int quote) { - bump(l); /* opening quote */ + const char* p = l->cur; + const char* end = l->end; + int bad = 0; + p++; /* opening quote */ for (;;) { - int ch = peek(l, 0); - if (ch < 0) return 1; - if (ch == quote) { - bump(l); - return 0; + if (p >= end) { + bad = 1; + break; } - if (ch == '\n') return 1; - if (ch == '\\') { - bump(l); /* backslash */ - if (peek(l, 0) < 0) return 1; - bump(l); /* the escaped char */ + if (*p == quote) { + p++; + break; + } + if (*p == '\n') { + bad = 1; + break; + } + if (*p == '\\') { + p++; + if (p >= end) { + bad = 1; + break; + } + p++; /* the escaped char */ continue; } - bump(l); + p++; + } + l->cur = p; + if (l->splices) lex_catchup_splices(l); + return bad; +} + +/* Finish a string/character literal token: skip `sp_len` prefix bytes, consume + * the quoted body, classify, and intern the full spelling [tok_start, cur). */ +static void finish_str_lit(Lexer* l, Tok* t, const char* tok_start, int sp_len, + int is_char, u32 encf) { + l->cur += sp_len; + if (scan_quoted(l, is_char ? '\'' : '"')) t->flags |= TF_LITERAL_BAD; + t->kind = (u16)(is_char ? TOK_CHR : TOK_STR); + t->flags |= encf; + t->spelling = lex_intern(l, tok_start, l->cur); + t->v.str = t->spelling; + l->dstate = 0; +} + +/* Finish an identifier token (§6.4.2). On entry the cursor is at the first + * identifier byte (an ident-start byte or a UCN lead). Consume the first + * element, then alternate maximal CC_IDCONT runs with UCN continuations. */ +static void finish_ident(Lexer* l, Tok* t, const char* tok_start) { + int u = ucn_len(l, 0); + if (u) + l->cur += u; + else + l->cur += 1; + for (;;) { + scan_ident_run(l); /* consume the maximal CC_IDCONT run in one go */ + u = ucn_len(l, 0); + if (!u) break; + l->cur += u; + } + t->kind = TOK_IDENT; + t->spelling = lex_intern(l, tok_start, l->cur); + t->v.ident = t->spelling; + if (l->dstate == 1) { + KitSlice s = kit_sym_str(l->pool->c, t->spelling); + l->dstate = (s.s && matches_include_kw(s.s, s.len)) ? 2 : 0; + } else { + l->dstate = 0; } } Tok lex_next(Lexer* l) { Tok t; SrcLoc tloc; - size_t start; + const char* tok_start; + const char* end = l->end; + unsigned cc; int ch; /* No per-token memset: every content-token path assigns kind/loc/spelling/v, @@ -425,15 +622,19 @@ Tok lex_next(Lexer* l) { * subsequent content tokens for the line that follows. */ for (;;) { skip_ws_and_comments(l); - if (l->pos >= l->len) { + if (l->cur >= end) { t.kind = TOK_EOF; t.loc = lex_here(l); t.spelling = 0; return t; } - if (peek(l, 0) == '\n') { + if (*l->cur == '\n') { tloc = lex_here(l); - bump(l); + /* consume the newline */ + l->cur++; + l->line++; + l->line_start = l->cur; + if (l->splices) lex_catchup_splices(l); t.kind = TOK_NEWLINE; t.loc = tloc; t.spelling = 0; @@ -446,8 +647,9 @@ Tok lex_next(Lexer* l) { } tloc = lex_here(l); - start = l->pos; - ch = peek(l, 0); + tok_start = l->cur; + ch = (unsigned char)*l->cur; + cc = cclass[(unsigned char)ch]; if (l->at_bol) t.flags |= TF_AT_BOL; if (l->had_space) t.flags |= TF_HAS_SPACE; @@ -455,131 +657,87 @@ Tok lex_next(Lexer* l) { l->had_space = 0; t.loc = tloc; - /* §6.4.7 header-name: only valid in #include / #embed argument context. */ + /* §6.4.7 header-name: only valid in #include / #embed argument context. + * Takes precedence over the string-literal reading of a leading `"`. */ if (l->dstate == 2 && (ch == '<' || ch == '"')) { int closer = (ch == '<') ? '>' : '"'; - bump(l); + const char* p = l->cur + 1; for (;;) { - int c = peek(l, 0); - if (c < 0 || c == '\n') { + if (p >= end || *p == '\n') { t.flags |= TF_LITERAL_BAD; break; } - if (c == closer) { - bump(l); + if (*p == closer) { + p++; break; } - bump(l); + p++; } + l->cur = p; + if (l->splices) lex_catchup_splices(l); t.kind = TOK_HEADER; - t.spelling = lex_intern(l, start, l->pos); + t.spelling = lex_intern(l, tok_start, l->cur); t.v.str = t.spelling; l->dstate = 0; return t; } - /* String / character literal, with optional encoding prefix. The prefix - * length and encoding flag are decoded together so the spelling we - * intern includes the prefix bytes. */ - { - int sp_len = -1; - int is_char = 0; - u32 encf = 0; - - if (ch == '"') { - sp_len = 0; - is_char = 0; - } else if (ch == '\'') { - sp_len = 0; - is_char = 1; - } else if (ch == 'L' && peek(l, 1) == '"') { - sp_len = 1; - is_char = 0; - encf = TF_STR_WIDE; - } else if (ch == 'L' && peek(l, 1) == '\'') { - sp_len = 1; - is_char = 1; - encf = TF_STR_WIDE; - } else if (ch == 'u' && peek(l, 1) == '8' && peek(l, 2) == '"') { - sp_len = 2; - is_char = 0; - encf = TF_STR_U8; - } else if (ch == 'u' && peek(l, 1) == '"') { - sp_len = 1; - is_char = 0; - encf = TF_STR_U16; - } else if (ch == 'u' && peek(l, 1) == '\'') { - sp_len = 1; - is_char = 1; - encf = TF_STR_U16; - } else if (ch == 'U' && peek(l, 1) == '"') { - sp_len = 1; - is_char = 0; - encf = TF_STR_U32; - } else if (ch == 'U' && peek(l, 1) == '\'') { - sp_len = 1; - is_char = 1; - encf = TF_STR_U32; - } - - if (sp_len >= 0) { - int i; - for (i = 0; i < sp_len; ++i) bump(l); - if (scan_quoted(l, is_char ? '\'' : '"')) t.flags |= TF_LITERAL_BAD; - t.kind = (u16)(is_char ? TOK_CHR : TOK_STR); - t.flags |= encf; - t.spelling = lex_intern(l, start, l->pos); - t.v.str = t.spelling; - l->dstate = 0; - return t; - } - } - - /* Identifier (§6.4.2). Encoding-prefix candidates above are matched - * before this since L/u/U followed by a quote is a literal, not an - * identifier. The grammar's identifier-nondigit covers letters, _, - * extended source chars (impl-defined; bytes ≥ 0x80 here), and UCNs - * (§6.4.3) — the latter span multiple source bytes so they're matched - * via ucn_len rather than the per-byte is_alpha predicate. */ - { - int u = ucn_len(l, 0); - if (is_alpha(ch) || u) { - if (u) { - int i; - for (i = 0; i < u; ++i) bump(l); - } else - bump(l); - for (;;) { - scan_ident_run(l); /* consume the maximal is_alnum run in one go */ - if ((u = ucn_len(l, 0))) { - int i; - for (i = 0; i < u; ++i) bump(l); - } else { - break; - } + /* Identifier (§6.4.2), or an encoding-prefixed string/char literal whose + * prefix (L/u/u8/U) is itself an identifier-start byte. The prefix+quote + * forms are matched first since `L"`/`u'`/... is a literal, not the + * identifier `L`/`u`. */ + if (cc & CC_IDST) { + if (ch == 'L') { + if (peek(l, 1) == '"') { + finish_str_lit(l, &t, tok_start, 1, 0, TF_STR_WIDE); + return t; } - t.kind = TOK_IDENT; - t.spelling = lex_intern(l, start, l->pos); - t.v.ident = t.spelling; - if (l->dstate == 1) { - KitSlice s = kit_sym_str(l->pool->c, t.spelling); - l->dstate = (s.s && matches_include_kw(s.s, s.len)) ? 2 : 0; - } else { - l->dstate = 0; + if (peek(l, 1) == '\'') { + finish_str_lit(l, &t, tok_start, 1, 1, TF_STR_WIDE); + return t; + } + } else if (ch == 'u') { + if (peek(l, 1) == '8' && peek(l, 2) == '"') { + finish_str_lit(l, &t, tok_start, 2, 0, TF_STR_U8); + return t; + } + if (peek(l, 1) == '"') { + finish_str_lit(l, &t, tok_start, 1, 0, TF_STR_U16); + return t; + } + if (peek(l, 1) == '\'') { + finish_str_lit(l, &t, tok_start, 1, 1, TF_STR_U16); + return t; + } + } else if (ch == 'U') { + if (peek(l, 1) == '"') { + finish_str_lit(l, &t, tok_start, 1, 0, TF_STR_U32); + return t; + } + if (peek(l, 1) == '\'') { + finish_str_lit(l, &t, tok_start, 1, 1, TF_STR_U32); + return t; } - return t; } + finish_ident(l, &t, tok_start); + return t; + } + + /* Bare string / character literal (no encoding prefix). */ + if (ch == '"' || ch == '\'') { + finish_str_lit(l, &t, tok_start, 0, ch == '\'', 0); + return t; } /* pp-number (§6.4.8), then classified to TOK_NUM / TOK_FLT. */ - if (is_digit(ch) || (ch == '.' && is_digit(peek(l, 1)))) { + if ((cc & CC_DIGIT) || (ch == '.' && is_digit(peek(l, 1)))) { const char* text; size_t k; scan_pp_number(l); /* The spelling is contiguous logical text in the folded buffer (splices * were removed at open), so classify and intern straight from there. */ - text = l->src + start; - k = l->pos - start; + text = tok_start; + k = (size_t)(l->cur - tok_start); t.kind = (u16)(pp_number_is_float(text, k) ? TOK_FLT : TOK_NUM); /* Suffix flags for §6.4.4.1 / §6.4.4.2. The parser dispatches on * TF_INT_U/L/LL and TF_FLT_F/L to pick a TY_* tag for the literal, @@ -628,18 +786,24 @@ Tok lex_next(Lexer* l) { return t; } + /* Identifier introduced by a UCN (§6.4.3): `\uXXXX`/`\UXXXXXXXX` as the + * first element. The '\\' lead is not CC_IDST, so it is matched here. */ + if (ch == '\\' && ucn_len(l, 0)) { + finish_ident(l, &t, tok_start); + return t; + } + /* Punctuator (§6.4.6) — longest match. `#` and `##` (and their digraph * forms `%:` and `%:%:`) become TOK_PP_HASH / TOK_PP_PASTE so PP can * recognize directives and the paste operator. */ { - int n0 = peek(l, 0); + int n0 = ch; int n1 = peek(l, 1); int n2 = peek(l, 2); int n3 = peek(l, 3); int adv = 1; u32 punct = P_NONE; u16 kind = TOK_PUNCT; - int i; /* Set in the six digraph branches (<: <% %> :> %: %:%:): their source * spelling differs from the canonical punct, so they bypass the punct_sym * cache and intern verbatim rather than aliasing the canonical spelling. */ @@ -858,11 +1022,12 @@ Tok lex_next(Lexer* l) { break; } - for (i = 0; i < adv; ++i) bump(l); + l->cur += adv; + if (l->splices) lex_catchup_splices(l); t.kind = kind; t.v.punct = punct; - t.spelling = digraph ? lex_intern(l, start, l->pos) - : punct_spelling(l, punct, start, l->pos); + t.spelling = digraph ? lex_intern(l, tok_start, l->cur) + : punct_spelling(l, punct, tok_start, l->cur); if (kind == TOK_PP_HASH) l->dstate = 1; else