kit

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

commit 064a82b6a17001f25f47a4e05ed1df0cf653d60c
parent fe7b3634e9edffdaffc357413b74ccd933045dc8
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 09:28:15 -0700

perf(pp): drop non-directive newline one frame lower — skip pp_next round-trip

Diffstat:
Mlang/cpp/pp/pp.c | 14++++++++++++--
Mlang/cpp/pp/pp_expand.c | 49+++++++++++++++++++++++++++++++++++++++----------
2 files changed, 51 insertions(+), 12 deletions(-)

diff --git a/lang/cpp/pp/pp.c b/lang/cpp/pp/pp.c @@ -179,10 +179,21 @@ void push_buf_uniform(Pp* pp, Tok* toks, u32 n, HidesetId hs_uniform) { * Public streaming entries * ============================================================ */ +/* Defined in pp_expand.c (deliberately not in pp_priv.h): the §C.3a + * newline-filtering reader. Identical to pp_next_raw_into but drops + * non-directive newlines one frame lower, so the ~51% of produced tokens that + * this loop would otherwise read-and-discard never round-trip through the call. */ +void pp_next_into(Pp* pp, Tok* out); + Tok pp_next(Pp* pp) { /* Public: filter newlines so consumers like the C parser don't need * to handle them. pp_emit_text uses pp_next_raw via its own loop. * + * The top-of-loop read uses pp_next_into, which already drops non-directive + * newlines internally; the peek-after-hash (t2) and pragma-swallow reads + * below stay on pp_next_raw_into because they must still see a TOK_NEWLINE + * (as a non-pragma push-back token, and as the swallow terminator). + * * Also drop forwarded `#pragma` lines: do_pragma pushes the directive * back onto the source stack so pp_emit_text can re-emit it verbatim * in cpp mode, but the C parser (cc mode) would see the trailing @@ -190,8 +201,7 @@ Tok pp_next(Pp* pp) { * `pragma`, swallow tokens through the next NEWLINE. */ for (;;) { Tok t; - pp_next_raw_into(pp, &t); - if (t.kind == TOK_NEWLINE) continue; + pp_next_into(pp, &t); if (t.kind == TOK_PP_HASH) { Tok t2; pp_next_raw_into(pp, &t2); diff --git a/lang/cpp/pp/pp_expand.c b/lang/cpp/pp/pp_expand.c @@ -922,17 +922,31 @@ static int try_expand_func_macro(Pp* pp, const Macro* m, const Tok* invoke, * Defined here; also declared in pp_priv.h so pp.c can call it. * ============================================================ */ -/* pp_next_raw_into: out-pointer form. Reads from the top source into *out, - * applies macro expansion when an identifier names a macro that isn't - * blue-painted, and consumes directives in-place. TOK_NEWLINE is preserved - * for pp_emit_text. +/* pp_pull_into: shared core of pp_next_raw_into / pp_next_into. Reads from the + * top source into *out, applies macro expansion when an identifier names a + * macro that isn't blue-painted, and consumes directives in-place. * - * Tok is 24B, so the by-value form (pp_next_raw, below) returns indirectly - * via sret and pays a 24B inter-frame copy at every `return t`. Writing - * through `out` lets the token — and lex_next's own sret — land in the - * caller's slot directly. The hot SRC_BUF case (macro replay) is inlined - * here so the common token never pays a call into src_next_raw_into. */ -void pp_next_raw_into(Pp* pp, Tok* out) { + * `skip_nl` short-circuits the parser-facing newline drop ONE FRAME LOWER: the + * public pp_next (the C-parser feed) discards every non-directive TOK_NEWLINE + * at the top of its loop, and 51% of all produced tokens are exactly those + * newlines. When skip_nl is set, such a newline is consumed here (the loop just + * continues) instead of being returned and round-tripped through pp_next's sret + * — the token never escapes this frame. This is sound because a TOK_NEWLINE can + * never be a directive introducer (TOK_PP_HASH), a macro-name TOK_IDENT, or + * TOK_EOF, so in the un-skipped path it falls straight through to the trailing + * `return`; continuing the loop instead is byte-identical to the caller having + * read and dropped it, modulo the (idempotent) arena-reset re-check at the loop + * head. skip_nl is set ONLY by pp_next, which never runs during #if expansion, + * so the in_if_expansion `defined`-operator state machine below is unaffected. + * pp_emit_text and the pragma-swallow / hash-peek paths pass skip_nl=0 so they + * still observe newlines for -E line reconstruction and directive termination. + * + * Tok is 24B, so the by-value form (pp_next_raw) returns indirectly via sret + * and pays a 24B inter-frame copy at every `return t`. Writing through `out` + * lets the token — and lex_next's own sret — land in the caller's slot + * directly. The hot SRC_BUF case (macro replay) is inlined here so the common + * token never pays a call into src_next_raw_into. */ +static void pp_pull_into(Pp* pp, Tok* out, int skip_nl) { HidesetId hs; u8 src_kind; for (;;) { @@ -967,6 +981,11 @@ void pp_next_raw_into(Pp* pp, Tok* out) { src_next_raw_into(pp, out, &hs, &src_kind); } if (out->kind == TOK_EOF) return; + /* §C.3a: absorb the parser-facing non-directive newline drop here so it + * never round-trips up through pp_next's sret. A newline is whitespace, + * not a directive introducer, so dropping it before the directive / macro + * checks below is the same as pp_next dropping it after the return. */ + if (skip_nl && out->kind == TOK_NEWLINE) continue; if (out->kind == TOK_PP_HASH && (out->flags & TF_AT_BOL) && src_kind == SRC_LEX) { process_directive(pp, out->loc); @@ -1112,6 +1131,16 @@ void pp_next_raw_into(Pp* pp, Tok* out) { } } +/* pp_next_raw_into: out-pointer form, every token surfaced (TOK_NEWLINE + * preserved for pp_emit_text and for directive/pragma line termination). */ +void pp_next_raw_into(Pp* pp, Tok* out) { pp_pull_into(pp, out, /*skip_nl=*/0); } + +/* pp_next_into: out-pointer form for the C-parser feed (pp_next). Identical to + * pp_next_raw_into but drops non-directive newlines internally, so the 51% of + * produced tokens that pp_next would otherwise discard never escape this frame. + * Declared locally (extern) in pp.c — kept out of pp_priv.h on purpose. */ +void pp_next_into(Pp* pp, Tok* out) { pp_pull_into(pp, out, /*skip_nl=*/1); } + /* Thin by-value shim: the mutual-recursion entry (expand_arg_to_eof) and any * external caller use this; the hot -E loops call pp_next_raw_into directly. */ Tok pp_next_raw(Pp* pp) {