kit

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

commit e665229aefa465755b17d605f20610d6cf4bd52e
parent 8aa842d5dfa4e091671abf3ac05d9f90e3974271
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 10 Jun 2026 22:50:57 -0700

perf(lex): redesign scanner on a splice-folded buffer

The lexer wove translation phase 2 (backslash-newline line splicing) into every
byte access: peek(off) re-walked off+1 bytes from the cursor re-testing for a
splice each step, bump() ran skip_splices, and identifier/number/string scans
plus intern all paid the per-byte splice test — though splices are vanishingly
rare in real source. lex_next/peek were among the hottest functions on every
compile.

Restructure phase 2 into a single up-front pass (lex_fold_splices): fold every
\<newline> out of the input once, so the scanner walks clean logical bytes with
plain direct indexing. Splice-free input (the overwhelming common case) is
borrowed verbatim, zero-copy; otherwise a folded heap copy plus a small sorted
array of fold offsets keeps line/col exact (lex_sync_splices advances the line
as the cursor passes each removed newline). peek() becomes a bounds-checked
index, bump() drops skip_splices, and intern/number scanning read straight from
the folded buffer — the per-token splice pre-scan and reusable splice scratch
are gone entirely.

vs the pre-lexer state: body-size -7%, type-decl -9%, pp-macro -E -6%, fn-count
-4.5%. Object output, -E output, -g line tables, and diagnostics all
byte-identical — including a splice battery (mid-token / mid-string / leading /
trailing / consecutive line continuations) where an error across a splice still
reports the correct physical line. Verified: test-pp/parse/toy/debug/dwarf/
smoke-x64/smoke-rv64 + 21 rt/ sources byte-identical.

Diffstat:
Mlang/cpp/lex/lex.c | 207++++++++++++++++++++++++++++++++++++-------------------------------------------
1 file changed, 95 insertions(+), 112 deletions(-)

diff --git a/lang/cpp/lex/lex.c b/lang/cpp/lex/lex.c @@ -20,6 +20,10 @@ 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. */ const char* src; size_t len; size_t pos; @@ -32,62 +36,92 @@ struct Lexer { * 0 = none, 1 = saw pp-hash, 2 = saw `#include`/etc and the next * token may be a header-name. */ u8 dstate; - /* Grow-once scratch reused for per-token splice removal: a (rare) token - * containing a `\<newline>` packs its bytes here instead of a fresh heap - * block, so capacity sticks at the high-water mark and steady-state lexing - * never allocates per token. Contents are transient -- valid only until the - * next token. */ - char* scratch; - size_t scratch_cap; + 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. */ + u32* splices; + u32 nsplices; + u32 next_splice; }; -/* At least `need` bytes of reusable scratch (see Lexer.scratch), growing - * geometrically and retaining capacity. NULL only on OOM. */ -static char* lex_scratch(Lexer* l, size_t need) { - if (need > l->scratch_cap) { - size_t cap = l->scratch_cap ? l->scratch_cap : 64; - char* p; - while (cap < need) cap *= 2; - p = (char*)l->heap->realloc(l->heap, l->scratch, l->scratch_cap, cap, 1); - if (!p) return NULL; - l->scratch = p; - l->scratch_cap = cap; +/* §5.1.1.2 translation phase 2: fold every `\<newline>` line splice out of the + * input once, up front, so the scanner never tests for splices per byte. The + * common (splice-free) input is borrowed verbatim; otherwise a heap copy holds + * the folded text and `splices[]` records each fold point (a logical offset) so + * line numbering still advances across the removed physical newline. */ +static void lex_fold_splices(Lexer* l, const char* src, size_t len) { + size_t i; + size_t nspl = 0; + char* buf; + u32* spl; + size_t w, s, wcap; + l->next_splice = 0; + for (i = 0; i + 1 < len; ++i) + if (src[i] == '\\' && src[i + 1] == '\n') ++nspl; + if (nspl == 0) { + l->src = src; + l->len = len; + l->owns_src = 0; + l->splices = NULL; + l->nsplices = 0; + return; + } + /* Each folded splice removes exactly 2 bytes, so the folded length is known + * up front; allocate it exactly so the close-time free size matches. */ + wcap = len - 2u * nspl; + spl = (u32*)l->heap->alloc(l->heap, nspl * sizeof(u32), _Alignof(u32)); + buf = wcap ? (char*)l->heap->alloc(l->heap, wcap, 1) : NULL; + if (!spl || (wcap && !buf)) { + /* OOM: fall back to the raw input. The (rare) splice then survives into a + * token spelling, but lexing stays memory-safe. */ + if (buf) l->heap->free(l->heap, buf, wcap); + if (spl) l->heap->free(l->heap, spl, nspl * sizeof(u32)); + l->src = src; + l->len = len; + l->owns_src = 0; + l->splices = NULL; + l->nsplices = 0; + return; + } + w = 0; + s = 0; + for (i = 0; i < len;) { + if (i + 1 < len && src[i] == '\\' && src[i + 1] == '\n') { + spl[s++] = (u32)w; /* fold point = logical offset of the next byte */ + i += 2; + continue; + } + buf[w++] = src[i++]; } - return l->scratch; + l->src = wcap ? buf : ""; + l->len = w; /* == wcap */ + l->owns_src = wcap ? 1u : 0u; + l->splices = spl; + l->nsplices = (u32)s; } -/* §5.1.1.2 translation phase 2: splice physical lines joined by - * backslash-newline. Advance past any splice sequence at l->pos so the - * cursor never rests on the leading backslash of a splice. */ -static void skip_splices(Lexer* l) { - while (l->pos + 1 < l->len && l->src[l->pos] == '\\' && - l->src[l->pos + 1] == '\n') { - l->pos += 2; +/* 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) { l->line++; l->col = 1; + l->next_splice++; } } -/* Logical peek: returns the off-th post-splice byte starting at l->pos, - * or -1 at end of input. Does not mutate l->pos. */ +/* 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. */ static int peek(const Lexer* l, size_t off) { - size_t pos = l->pos; - size_t k = 0; - while (pos < l->len) { - if (pos + 1 < l->len && l->src[pos] == '\\' && l->src[pos + 1] == '\n') { - pos += 2; - continue; - } - if (k == off) return (unsigned char)l->src[pos]; - ++pos; - ++k; - } - return -1; + size_t p = l->pos + off; + return p < l->len ? (unsigned char)l->src[p] : -1; } static int bump(Lexer* l) { int ch; - skip_splices(l); if (l->pos >= l->len) return -1; ch = (unsigned char)l->src[l->pos++]; if (ch == '\n') { @@ -96,6 +130,7 @@ static int bump(Lexer* l) { } else { l->col++; } + lex_sync_splices(l); return ch; } @@ -156,13 +191,13 @@ Lexer* lex_open_mem(Compiler* c, const char* name, const char* src, return NULL; } l->heap = h; - l->src = src ? src : ""; - l->len = src ? len : 0; + lex_fold_splices(l, src ? src : "", src ? len : 0); l->pos = 0; 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 */ l->at_bol = 1; l->had_space = 0; return l; @@ -170,7 +205,9 @@ Lexer* lex_open_mem(Compiler* c, const char* name, const char* src, void lex_close(Lexer* l) { if (!l) return; - if (l->scratch) l->heap->free(l->heap, l->scratch, l->scratch_cap); + if (l->owns_src) l->heap->free(l->heap, (char*)l->src, l->len); + if (l->splices) + l->heap->free(l->heap, l->splices, l->nsplices * sizeof(u32)); c_pool_free(l->pool); l->heap->free(l->heap, l, sizeof(*l)); } @@ -193,35 +230,11 @@ void lex_skip_shebang(Lexer* l) { SrcLoc lex_loc(const Lexer* l) { return lex_here(l); } u32 lex_file_id(const Lexer* l) { return l->file_id; } -/* Intern bytes [start, end) with line splices (\<newline>) removed, so token - * spellings reflect post-phase-2 logical text. */ -static Sym intern_spliced(Lexer* l, size_t start, size_t end) { - size_t i; - int has_splice = 0; - char* buf; - size_t k; - - for (i = start; i + 1 < end; ++i) { - if (l->src[i] == '\\' && l->src[i + 1] == '\n') { - has_splice = 1; - break; - } - } - if (!has_splice) - return kit_sym_intern(l->pool->c, - (KitSlice){.s = l->src + start, .len = end - start}); - - buf = lex_scratch(l, end - start); - if (!buf) return 0; - k = 0; - for (i = start; i < end;) { - if (i + 1 < end && l->src[i] == '\\' && l->src[i + 1] == '\n') { - i += 2; - continue; - } - buf[k++] = l->src[i++]; - } - return kit_sym_intern(l->pool->c, (KitSlice){.s = buf, .len = k}); +/* Intern a token's spelling [start, end) 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) { + return kit_sym_intern(l->pool->c, + (KitSlice){.s = l->src + start, .len = end - start}); } /* §6.4.7 header-name lookahead: in include-directive context, a `<` or `"` @@ -350,7 +363,6 @@ Tok lex_next(Lexer* l) { * subsequent content tokens for the line that follows. */ for (;;) { skip_ws_and_comments(l); - skip_splices(l); if (l->pos >= l->len) { t.kind = TOK_EOF; t.loc = lex_here(l); @@ -396,7 +408,7 @@ Tok lex_next(Lexer* l) { bump(l); } t.kind = TOK_HEADER; - t.spelling = intern_spliced(l, start, l->pos); + t.spelling = lex_intern(l, start, l->pos); t.v.str = t.spelling; l->dstate = 0; return t; @@ -452,7 +464,7 @@ Tok lex_next(Lexer* 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 = intern_spliced(l, start, l->pos); + t.spelling = lex_intern(l, start, l->pos); t.v.str = t.spelling; l->dstate = 0; return t; @@ -485,7 +497,7 @@ Tok lex_next(Lexer* l) { } } t.kind = TOK_IDENT; - t.spelling = intern_spliced(l, start, l->pos); + 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); @@ -499,42 +511,13 @@ Tok lex_next(Lexer* l) { /* pp-number (§6.4.8), then classified to TOK_NUM / TOK_FLT. */ if (is_digit(ch) || (ch == '.' && is_digit(peek(l, 1)))) { - size_t plen; const char* text; - size_t i, k; - int has_splice = 0; + size_t k; scan_pp_number(l); - plen = l->pos - start; - /* The interned spelling is the post-splice text. The common case has no - * `\<newline>` inside the number, so its bytes are already contiguous in - * the source -- classify and intern straight from there with no copy. A - * splice packs into the lexer's reused scratch, never a per-token alloc. */ - for (i = start; i + 1 < l->pos; ++i) { - if (l->src[i] == '\\' && l->src[i + 1] == '\n') { - has_splice = 1; - break; - } - } - if (!has_splice) { - text = l->src + start; - k = plen; - } else { - char* sb = lex_scratch(l, plen ? plen : 1); - if (sb) { - k = 0; - for (i = start; i < l->pos;) { - if (i + 1 < l->pos && l->src[i] == '\\' && l->src[i + 1] == '\n') { - i += 2; - continue; - } - sb[k++] = l->src[i++]; - } - text = sb; - } else { - text = l->src + start; /* OOM: fall back to the raw source bytes */ - k = plen; - } - } + /* 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; 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, @@ -806,7 +789,7 @@ Tok lex_next(Lexer* l) { for (i = 0; i < adv; ++i) bump(l); t.kind = kind; t.v.punct = punct; - t.spelling = intern_spliced(l, start, l->pos); + t.spelling = lex_intern(l, start, l->pos); if (kind == TOK_PP_HASH) l->dstate = 1; else