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