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:
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