kit

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

commit 0e5337e33c91ef60bf270d726103e970d665e1d5
parent 7387376e6968085c62386dbfb8a2c26ed6fa54d2
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon, 15 Jun 2026 17:09:58 -0700

Merge Phase 1: macro-availability rewrite (hideset → disabled_depth + TF_NO_EXPAND)

Diffstat:
Mlang/cpp/pp/pp.c | 68+++++++++++++++++++++++++++++---------------------------------------
Mlang/cpp/pp/pp_directive.c | 43++++++++++++-------------------------------
Mlang/cpp/pp/pp_expand.c | 223++++++++++++++++++-------------------------------------------------------------
Mlang/cpp/pp/pp_priv.h | 99+++++++++++++++++++++++++++-----------------------------------------------------
Mtest/pp/CORPUS.md | 3+++
Atest/pp/cases/66_rescan_self_apply_paren.c | 2++
Atest/pp/cases/66_rescan_self_apply_paren.expected | 1+
Atest/pp/cases/67_rescan_arg_self_apply.c | 3+++
Atest/pp/cases/67_rescan_arg_self_apply.expected | 1+
Atest/pp/cases/68_rescan_func_name_no_paren_then_paren.c | 4++++
Atest/pp/cases/68_rescan_func_name_no_paren_then_paren.expected | 1+
11 files changed, 139 insertions(+), 309 deletions(-)

diff --git a/lang/cpp/pp/pp.c b/lang/cpp/pp/pp.c @@ -5,9 +5,11 @@ * line structure of the source. * * The token-source stack carries either a Lexer (file or #include'd file) or - * a pre-built Tok[] buffer (macro expansion). Each buffer token carries a - * hideset (Prosser, the standard's "nested-replacement" rule) recording - * which macro names it must not be re-expanded by during rescan. + * a pre-built Tok[] buffer (macro expansion). A macro replacement-list buffer + * is a "disabled frame" (cpplib model): while it is live on the source stack + * its owning macro's disabled_depth is non-zero, so a recursive occurrence of + * that macro during rescan is returned permanently un-expanded (TF_NO_EXPAND) + * rather than re-replaced. This replaces the per-token Prosser hideset. * * Residual module: source stack, pp_next / pp_next_raw (public streaming), * pp_new/free, predefined macros, lifecycle, keyword interning. */ @@ -40,6 +42,10 @@ void src_pop(Pp* pp) { TokSrc* t; if (!pp->nsources) return; t = &pp->sources[pp->nsources - 1]; + /* Re-enable the macro whose replacement-list frame this is (cpplib + * disabled-frame model): popping the frame ends the rescan during which the + * macro was unavailable. Balances the increment in push_buf_uniform/replay. */ + if (t->disabled_owner) --t->disabled_owner->disabled_depth; if (t->kind == SRC_LEX && t->lex) { /* Commit the multiple-include memo for this file before closing it: a * fully #ifndef-wrapped file records its controlling macro; a #pragma once @@ -74,13 +80,12 @@ static void tok_eof(Tok* out) { * source the token came from (SRC_LEX vs SRC_BUF). Used by pp_next_raw to * gate directive recognition to lex-sourced tokens only — a `#` produced by * macro expansion never starts a directive (§6.10.3.4 ¶3). */ -void src_next_raw_into(Pp* pp, Tok* out, HidesetId* hs_out, u8* src_kind_out) { +void src_next_raw_into(Pp* pp, Tok* out, u8* src_kind_out) { TokSrc* s; while ((s = src_top(pp)) != NULL) { if (s->kind == SRC_BUF) { if (s->i < s->n) { *out = s->toks[s->i]; - if (hs_out) *hs_out = s->hs ? s->hs[s->i] : s->hs_uniform; if (src_kind_out) *src_kind_out = SRC_BUF; /* Pointer-replayed body: apply the per-invocation loc and (first token * only) BOL/SPACE flags the old fresh-copy path baked in. */ @@ -96,7 +101,6 @@ void src_next_raw_into(Pp* pp, Tok* out, HidesetId* hs_out, u8* src_kind_out) { } if (s->scope_top) { tok_eof(out); - if (hs_out) *hs_out = HS_EMPTY; if (src_kind_out) *src_kind_out = SRC_BUF; return; } @@ -110,7 +114,6 @@ void src_next_raw_into(Pp* pp, Tok* out, HidesetId* hs_out, u8* src_kind_out) { src_pop(pp); continue; } - if (hs_out) *hs_out = HS_EMPTY; if (src_kind_out) *src_kind_out = SRC_LEX; return; } @@ -127,20 +130,18 @@ void src_next_raw_into(Pp* pp, Tok* out, HidesetId* hs_out, u8* src_kind_out) { /* #line numbering is no longer applied here: a lean token carries only a * byte offset, and the #line delta is recorded as a positional overlay * segment (see pp_add_line_seg) applied lazily by pp_materialize_loc. */ - if (hs_out) *hs_out = HS_EMPTY; if (src_kind_out) *src_kind_out = SRC_LEX; return; } tok_eof(out); - if (hs_out) *hs_out = HS_EMPTY; if (src_kind_out) *src_kind_out = SRC_LEX; } /* Thin by-value shim for the cold/general callers (arg collection, paren * peek) that pass a NULL src_kind and don't sit in the hot -E loop. */ -Tok src_next_raw(Pp* pp, HidesetId* hs_out, u8* src_kind_out) { +Tok src_next_raw(Pp* pp, u8* src_kind_out) { Tok t; - src_next_raw_into(pp, &t, hs_out, src_kind_out); + src_next_raw_into(pp, &t, src_kind_out); return t; } @@ -148,29 +149,34 @@ Tok src_next_raw(Pp* pp, HidesetId* hs_out, u8* src_kind_out) { * Buffer source push helpers * ============================================================ */ -void push_buf(Pp* pp, Tok* toks, HidesetId* hs, u32 n) { +/* Push a non-macro buffer source (pushed-back peek tokens, a #pragma / #embed + * payload, an arg-prescan scope). No disabled frame: these payloads do not + * disable any macro during their rescan. */ +void push_buf(Pp* pp, Tok* toks, u32 n) { TokSrc s; memset(&s, 0, sizeof(s)); s.kind = SRC_BUF; s.toks = toks; - s.hs = hs; s.i = 0; s.n = n; src_push(pp, s); } -/* Push a buffer whose tokens all share one hideset (the common macro-body - * case): no per-token array, just the scalar in hs_uniform (read when - * TokSrc.hs is NULL). */ -void push_buf_uniform(Pp* pp, Tok* toks, u32 n, HidesetId hs_uniform) { +/* Push a macro replacement-list buffer (function-like substituted body, paste + * result). `owner` is the macro being expanded: marking the frame with it and + * incrementing owner->disabled_depth makes the macro unavailable for the whole + * rescan of this replacement (cpplib disabled-frame model); src_pop decrements + * it. Set the owner on `s` BEFORE src_push (which copies by value); the + * increment lands on the macro itself, which is fine. */ +void push_buf_uniform(Pp* pp, Tok* toks, u32 n, Macro* owner) { TokSrc s; memset(&s, 0, sizeof(s)); s.kind = SRC_BUF; s.toks = toks; - s.hs = NULL; - s.hs_uniform = hs_uniform; s.i = 0; s.n = n; + s.disabled_owner = owner; + if (owner) ++owner->disabled_depth; src_push(pp, s); } @@ -180,19 +186,19 @@ void push_buf_uniform(Pp* pp, Tok* toks, u32 n, HidesetId hs_uniform) { * value and never write back through s->toks. The per-read loc/flags override * (has_loc_override) reproduces the mutations the old fresh-copy path baked in, * so the shared body is surfaced byte-for-byte without copying it. */ -void push_buf_replay(Pp* pp, const Tok* toks, u32 n, HidesetId hs_uniform, +void push_buf_replay(Pp* pp, const Tok* toks, u32 n, Macro* owner, LocRef loc_override, u16 first_flags_or) { TokSrc s; memset(&s, 0, sizeof(s)); s.kind = SRC_BUF; s.toks = (Tok*)toks; - s.hs = NULL; - s.hs_uniform = hs_uniform; s.has_loc_override = 1; s.loc_override = loc_override; s.first_flags_or = first_flags_or; s.i = 0; s.n = n; + s.disabled_owner = owner; + if (owner) ++owner->disabled_depth; src_push(pp, s); } @@ -231,10 +237,8 @@ void pp_next_parse(Pp* pp, Tok* out) { /* Not a pragma — push the peeked token back as a 1-element buffer * so the next pp_next_raw returns it, and surface the hash now. */ Tok* keep = arena_array(pp->xarena, Tok, 1); - HidesetId* hs = arena_array(pp->xarena, HidesetId, 1); keep[0] = t2; - hs[0] = HS_EMPTY; - push_buf(pp, keep, hs, 1); + push_buf(pp, keep, 1); *out = t; return; } @@ -1125,18 +1129,6 @@ Pp* pp_new(Compiler* c) { h->free(h, pp, sizeof(*pp)); return NULL; } - /* Reserve hideset slot 0 for HS_EMPTY. The slot is unused but the - * indexing convention costs only a pointer. */ - pp->hsets_cap = 8; - pp->hsets = (Hideset**)pp_xrealloc( - pp, NULL, 0, sizeof(Hideset*) * pp->hsets_cap, _Alignof(Hideset*)); - pp->hsets[0] = NULL; - pp->hsets_n = 1; - pp->hs_index_cap = 64; - pp->hs_index_used = 0; - pp->hs_index = (HidesetId*)pp_xrealloc( - pp, NULL, 0, sizeof(HidesetId) * pp->hs_index_cap, _Alignof(HidesetId)); - memset(pp->hs_index, 0, sizeof(HidesetId) * pp->hs_index_cap); MacroTab_init(&pp->macros, h); pp_intern_keywords(pp); compute_date_time(pp); @@ -1169,8 +1161,6 @@ void pp_free(Pp* pp) { } pp_xfree(pp, pp->sources, sizeof(TokSrc) * pp->sources_cap); MacroTab_fini(&pp->macros); - pp_xfree(pp, pp->hsets, sizeof(Hideset*) * pp->hsets_cap); - pp_xfree(pp, pp->hs_index, sizeof(HidesetId) * pp->hs_index_cap); pp_xfree(pp, pp->ifstk, sizeof(IfFrame) * pp->ifstk_cap); pp_xfree(pp, pp->inc_dirs, sizeof(*pp->inc_dirs) * pp->inc_dirs_cap); c_pool_free(pp->pool); diff --git a/lang/cpp/pp/pp_directive.c b/lang/cpp/pp/pp_directive.c @@ -40,11 +40,10 @@ void read_directive_line(Pp* pp, Tok** out_toks, u32* out_n) { Tok* buf = NULL; u32 cap = 0, n = 0; Tok t; - HidesetId hs; u8 saved_rd = pp->reading_directive; pp->reading_directive = 1; for (;;) { - t = src_next_raw(pp, &hs, NULL); + t = src_next_raw(pp, NULL); if (t.kind == TOK_NEWLINE || t.kind == TOK_EOF) break; if (n == cap) { u32 nc = cap ? cap * 2 : 8; @@ -166,7 +165,7 @@ static void expand_for_if(Pp* pp, const Tok* in, u32 nin, TokVec* out) { saved = pp->in_if_expansion; pp->in_if_expansion = 1; pp->defined_skip = 0; - expand_arg_to_eof(pp, slice, NULL, nin, out); + expand_arg_to_eof(pp, slice, nin, out); pp->in_if_expansion = saved; pp->defined_skip = 0; } @@ -537,7 +536,7 @@ i64 eval_if_expr(Pp* pp, const Tok* line, u32 n, LocRef loc) { static void consume_to_newline(Pp* pp) { Tok t; do { - t = src_next_raw(pp, NULL, NULL); + t = src_next_raw(pp, NULL); } while (t.kind != TOK_NEWLINE && t.kind != TOK_EOF); } @@ -554,7 +553,7 @@ static void skip_until_active(Pp* pp) { IfFrame* top = if_top(pp); Tok t; if (top->state == IF_INCLUDE && local_depth == 0) return; - t = src_next_raw(pp, NULL, NULL); + t = src_next_raw(pp, NULL); if (t.kind == TOK_EOF) { compiler_panic(pp->c, pp_materialize_loc(pp, top->loc), "unterminated #if / #ifdef"); @@ -563,7 +562,7 @@ static void skip_until_active(Pp* pp) { /* Read directive name (or null directive). */ { - Tok nt = src_next_raw(pp, NULL, NULL); + Tok nt = src_next_raw(pp, NULL); Sym name; if (nt.kind == TOK_NEWLINE || nt.kind == TOK_EOF) continue; if (nt.kind != TOK_IDENT) { @@ -1064,7 +1063,7 @@ static void parse_include_path(Pp* pp, const Tok* line, u32 n, LocRef loc, TokVec exp = {0}; Tok* slice = arena_array(pp->arena, Tok, n); memcpy(slice, line, sizeof(Tok) * n); - expand_arg_to_eof(pp, slice, NULL, n, &exp); + expand_arg_to_eof(pp, slice, n, &exp); if (exp.n == 0) { compiler_panic(pp->c, pp_materialize_loc(pp, loc), @@ -1236,7 +1235,7 @@ static void do_line(Pp* pp, const Tok* line, u32 n, LocRef loc) { "#line: missing arguments"); slice = arena_array(pp->arena, Tok, n); memcpy(slice, line, sizeof(Tok) * n); - expand_arg_to_eof(pp, slice, NULL, n, &exp); + expand_arg_to_eof(pp, slice, n, &exp); if (exp.n == 0 || exp.data[0].kind != TOK_NUM) { compiler_panic(pp->c, pp_materialize_loc(pp, loc), @@ -1291,7 +1290,6 @@ static void do_line(Pp* pp, const Tok* line, u32 n, LocRef loc) { * recognition off, so this won't recurse. */ void emit_pragma_line(Pp* pp, const Tok* line, u32 n, LocRef loc) { TokVec out = {0}; - HidesetId* hids; u32 i; Tok hash, ident, nl; @@ -1326,9 +1324,7 @@ void emit_pragma_line(Pp* pp, const Tok* line, u32 n, LocRef loc) { nl.loc = loc; tv_push(pp, &out, nl); - hids = arena_array(pp->arena, HidesetId, out.n ? out.n : 1); - for (i = 0; i < out.n; ++i) hids[i] = HS_EMPTY; - push_buf(pp, out.data, hids, out.n); + push_buf(pp, out.data, out.n); } static int pragma_num_u32(Pp* pp, const Tok* t, u32* out) { @@ -1453,18 +1449,12 @@ int try_expand_pragma_op(Pp* pp, const Tok* invoke) { (void)saw_ws; } /* Read the string literal arg. */ - { - HidesetId hs; - str = src_next_raw(pp, &hs, NULL); - } + str = src_next_raw(pp, NULL); if (str.kind != TOK_STR) { compiler_panic(pp->c, pp_materialize_loc(pp, invoke->loc), "_Pragma: expected string literal"); } - { - HidesetId hs; - rp = src_next_raw(pp, &hs, NULL); - } + rp = src_next_raw(pp, NULL); if (rp.kind != TOK_PUNCT || tok_punct(&rp) != ')') { compiler_panic(pp->c, pp_materialize_loc(pp, invoke->loc), "_Pragma: expected ')'"); @@ -1663,17 +1653,13 @@ static void do_embed(Pp* pp, const Tok* line, u32 n, LocRef loc) { if (emit_n == 0) { /* Empty: emit if_empty payload (or nothing). */ if (if_empty_toks && if_empty_n) { - HidesetId* hids = arena_array(pp->arena, HidesetId, if_empty_n); - u32 i; - for (i = 0; i < if_empty_n; ++i) hids[i] = HS_EMPTY; - push_buf(pp, if_empty_toks, hids, if_empty_n); + push_buf(pp, if_empty_toks, if_empty_n); } return; } /* Build a buffer of pp-numbers separated by ',' punctuators. */ { TokVec out = {0}; - HidesetId* hids; size_t i; for (i = 0; i < emit_n; ++i) { char numbuf[8]; @@ -1716,12 +1702,7 @@ static void do_embed(Pp* pp, const Tok* line, u32 n, LocRef loc) { tv_push(pp, &out, comma); } } - hids = arena_array(pp->arena, HidesetId, out.n ? out.n : 1); - { - u32 k; - for (k = 0; k < out.n; ++k) hids[k] = HS_EMPTY; - } - push_buf(pp, out.data, hids, out.n); + push_buf(pp, out.data, out.n); } } } diff --git a/lang/cpp/pp/pp_expand.c b/lang/cpp/pp/pp_expand.c @@ -1,5 +1,8 @@ -/* pp_expand.c — hideset table, macro hashmap, #define/#undef, substitution, - * paste, stringize, argument prescan, func/object macro expansion. */ +/* pp_expand.c — macro table, #define/#undef, substitution, paste, stringize, + * argument prescan, func/object macro expansion. Macro-expansion availability + * uses the cpplib disabled-frame model (Macro.disabled_depth + the per-token + * TF_NO_EXPAND bit), not a per-token Prosser hideset; see pp.c / the identifier + * test in pp_pull_into below. */ #include "pp/pp_priv.h" @@ -8,127 +11,6 @@ static int body_tokens_equal(Pp* pp, const Tok* a, u32 na, const Tok* b, static int macros_equal(Pp* pp, const Macro* a, const Macro* b); /* ============================================================ - * Hideset table - * ============================================================ */ - -static int sym_in_array(const Sym* a, u32 n, Sym s) { - u32 i; - for (i = 0; i < n; ++i) - if (a[i] == s) return 1; - return 0; -} - -/* FNV-1a over the (sorted) Sym array — the dedup index key. */ -static u32 hs_hash(const Sym* names, u32 n) { - u32 h = 0x811C9DC5u, i; - for (i = 0; i < n; ++i) { - h ^= names[i]; - h *= 0x01000193u; - } - return h; -} - -/* Re-place every live hideset (ids 1..hsets_n-1) into a freshly zeroed index - * of capacity nc (a power of two). Called when the load factor crosses 3/4. */ -static void hs_index_rebuild(Pp* pp, u32 nc) { - u32 mask, k; - pp->hs_index = (HidesetId*)pp_xrealloc( - pp, pp->hs_index, sizeof(HidesetId) * pp->hs_index_cap, - sizeof(HidesetId) * nc, _Alignof(HidesetId)); - for (k = 0; k < nc; ++k) pp->hs_index[k] = 0; - pp->hs_index_cap = nc; - mask = nc - 1; - for (k = 1; k < pp->hsets_n; ++k) { - u32 i = pp->hsets[k]->hash & mask; - while (pp->hs_index[i] != 0) i = (i + 1) & mask; - pp->hs_index[i] = (HidesetId)k; - } -} - -/* Intern a hideset: return the canonical id for `names` (content-deduplicated), - * allocating a new one on first sight. Ids are assigned in first-appearance - * order (identical to the historical linear scan — the id is internal-only, but - * the order match keeps the dedup behavior bit-for-bit). */ -static HidesetId hs_register(Pp* pp, const Sym* names, u32 n) { - Hideset* h; - u32 hash, mask, i, j; - HidesetId id; - if (n == 0) return HS_EMPTY; - - hash = hs_hash(names, n); - mask = pp->hs_index_cap - 1; - i = hash & mask; - /* Probe for an existing identical hideset; the loop also lands `i` on the - * first empty slot for this hash, which is where a miss inserts. */ - while ((id = pp->hs_index[i]) != 0) { - Hideset* e = pp->hsets[id]; - if (e->hash == hash && e->n == n) { - for (j = 0; j < n; ++j) - if (e->names[j] != names[j]) break; - if (j == n) return id; - } - i = (i + 1) & mask; - } - - if (pp->hsets_n == pp->hsets_cap) { - u32 nc = pp->hsets_cap ? pp->hsets_cap * 2 : 8; - pp->hsets = - (Hideset**)pp_xrealloc(pp, pp->hsets, sizeof(Hideset*) * pp->hsets_cap, - sizeof(Hideset*) * nc, _Alignof(Hideset*)); - pp->hsets_cap = nc; - } - h = (Hideset*)arena_alloc(pp->arena, - sizeof(Hideset) + sizeof(Sym) * (n ? n - 1 : 0), - _Alignof(Hideset)); - h->n = n; - h->hash = hash; - for (j = 0; j < n; ++j) h->names[j] = names[j]; - id = (HidesetId)pp->hsets_n; - pp->hsets[pp->hsets_n++] = h; - /* Insert at the empty slot the probe found, then grow+rehash if loaded. */ - pp->hs_index[i] = id; - if (++pp->hs_index_used * 4u >= pp->hs_index_cap * 3u) - hs_index_rebuild(pp, pp->hs_index_cap * 2u); - return id; -} - -int hs_contains(Pp* pp, HidesetId id, Sym s) { - Hideset* h; - if (id == HS_EMPTY || s == 0) return 0; - h = pp->hsets[id]; - return sym_in_array(h->names, h->n, s); -} - -HidesetId hs_add(Pp* pp, HidesetId id, Sym s) { - Sym buf[64]; - Hideset* h; - u32 n; - u32 i; - - if (s == 0) return id; - if (hs_contains(pp, id, s)) return id; - - n = (id == HS_EMPTY) ? 0 : pp->hsets[id]->n; - if (n + 1 > sizeof(buf) / sizeof(buf[0])) { - compiler_panic(pp->c, (SrcLoc){0, 0, 0}, "pp: hideset overflow"); - } - if (id != HS_EMPTY) { - h = pp->hsets[id]; - for (i = 0; i < h->n; ++i) buf[i] = h->names[i]; - } - /* Keep sorted (numerically) for canonical hideset identity. */ - { - u32 pos = n; - while (pos > 0 && buf[pos - 1] > s) { - buf[pos] = buf[pos - 1]; - --pos; - } - buf[pos] = s; - } - return hs_register(pp, buf, n + 1); -} - -/* ============================================================ * #define / #undef * ============================================================ */ /* mt_get / mt_put / mt_del are inlined Sym-indexed loads in pp_priv.h @@ -379,11 +261,9 @@ static void subst_phase2(Pp* pp, const Tok* in, u32 nin, const Tok* invoke, /* Build a buffer of the macro's body (with hidesets) and push it. The * first expanded token inherits the invocation token's TF_AT_BOL / * TF_HAS_SPACE so output formatting matches the invocation site. */ -static void expand_object_macro(Pp* pp, const Macro* m, const Tok* invoke, - HidesetId invoke_hs) { +static void expand_object_macro(Pp* pp, Macro* m, const Tok* invoke) { TokVec body = {0}; Tok* tmp; - HidesetId hs; u32 i; if (m->body_len == 0) { @@ -394,12 +274,12 @@ static void expand_object_macro(Pp* pp, const Macro* m, const Tok* invoke, * definition-time m->body by pointer, applying the per-invocation loc and * first-token flag transfer at read time (push_buf_replay). Byte-identical * to the slow path (same tokens, same loc, same first-token flags), with - * zero per-invocation token copies or body allocs. */ + * zero per-invocation token copies or body allocs. The frame disables `m` + * for the whole rescan of its replacement (cpplib model). */ if (!m->has_paste) { u16 ff = (u16)((m->body[0].flags & ~(TF_AT_BOL | TF_HAS_SPACE)) | (invoke->flags & (TF_AT_BOL | TF_HAS_SPACE))); - hs = hs_add(pp, invoke_hs, m->name); - push_buf_replay(pp, m->body, m->body_len, hs, invoke->loc, ff); + push_buf_replay(pp, m->body, m->body_len, m, invoke->loc, ff); return; } @@ -417,8 +297,7 @@ static void expand_object_macro(Pp* pp, const Macro* m, const Tok* invoke, (invoke->flags & (TF_AT_BOL | TF_HAS_SPACE))); for (i = 0; i < body.n; ++i) body.data[i].loc = invoke->loc; - hs = hs_add(pp, invoke_hs, m->name); - push_buf_uniform(pp, body.data, body.n, hs); + push_buf_uniform(pp, body.data, body.n, m); } /* ============================================================ @@ -433,22 +312,19 @@ static void expand_object_macro(Pp* pp, const Macro* m, const Tok* invoke, * are restored as a buffer source so subsequent reads still see them. */ int peek_for_invoke_paren(Pp* pp, int* ws_has_space_out) { TokVec saved = {0}; - HsVec saved_hs = {0}; int saw_ws = 0; Tok t; - HidesetId hs; for (;;) { - t = src_next_raw(pp, &hs, NULL); + t = src_next_raw(pp, NULL); if (t.kind == TOK_NEWLINE) { saw_ws = 1; tv_push(pp, &saved, t); - hsv_push(pp, &saved_hs, hs); continue; } if (t.kind == TOK_EOF) { /* No '(' — push back saved tokens, leave EOF for next read. */ - if (saved.n) push_buf(pp, saved.data, saved_hs.data, saved.n); + if (saved.n) push_buf(pp, saved.data, saved.n); *ws_has_space_out = saw_ws; return 0; } @@ -461,8 +337,7 @@ int peek_for_invoke_paren(Pp* pp, int* ws_has_space_out) { } /* Save this non-`(` token too and push back. */ tv_push(pp, &saved, t); - hsv_push(pp, &saved_hs, hs); - push_buf(pp, saved.data, saved_hs.data, saved.n); + push_buf(pp, saved.data, saved.n); *ws_has_space_out = saw_ws; return 0; } @@ -471,7 +346,7 @@ int peek_for_invoke_paren(Pp* pp, int* ws_has_space_out) { /* Run macro expansion on a fixed token sequence to completion, yielding the * fully-expanded token sequence. Used to pre-expand each function-macro * argument before substitution (§6.10.3.1 ¶1). */ -void expand_arg_to_eof(Pp* pp, Tok* in, HidesetId* hs, u32 nin, TokVec* out) { +void expand_arg_to_eof(Pp* pp, Tok* in, u32 nin, TokVec* out) { TokSrc src; Tok t; @@ -479,7 +354,6 @@ void expand_arg_to_eof(Pp* pp, Tok* in, HidesetId* hs, u32 nin, TokVec* out) { src.kind = SRC_BUF; src.scope_top = 1; src.toks = in; - src.hs = hs; src.n = nin; src_push(pp, src); @@ -493,7 +367,11 @@ void expand_arg_to_eof(Pp* pp, Tok* in, HidesetId* hs, u32 nin, TokVec* out) { } tv_push(pp, out, t); } - /* Pop our scope source. */ + /* Pop our scope source directly (not src_pop). This is the one place the + * disabled-frame decrement in src_pop is intentionally bypassed: the + * arg-prescan scope frame is pushed above with src.disabled_owner == NULL (it + * disables no macro — arguments are pre-expanded with the invoking macro NOT + * yet disabled), so there is no disabled_depth to decrement here. */ --pp->nsources; } @@ -503,7 +381,6 @@ void expand_arg_to_eof(Pp* pp, Tok* in, HidesetId* hs, u32 nin, TokVec* out) { typedef struct ArgList { /* Unexpanded arg tokens (raw as collected from invocation). */ Tok* raw; - HidesetId* raw_hs; u32 raw_n; u32* raw_start; /* size n_args + 1 (sentinel = raw_n) */ /* Pre-expanded tokens. */ @@ -518,14 +395,12 @@ typedef struct ArgList { static Tok read_invocation_args(Pp* pp, const Macro* m, LocRef invoke_loc, ArgList* out) { TokVec raw = {0}; - HsVec raw_hs = {0}; u32* starts; u32 starts_cap = 0; u32 n_args = 0; u32 cur_start = 0; int depth = 0; Tok t; - HidesetId hs; int first_token_of_arg = 1; Tok close_tok; @@ -535,7 +410,7 @@ static Tok read_invocation_args(Pp* pp, const Macro* m, LocRef invoke_loc, starts[0] = 0; for (;;) { - t = src_next_raw(pp, &hs, NULL); + t = src_next_raw(pp, NULL); if (t.kind == TOK_EOF) { compiler_panic(pp->c, pp_materialize_loc(pp, invoke_loc), "unterminated function-like macro invocation"); @@ -583,7 +458,6 @@ static Tok read_invocation_args(Pp* pp, const Macro* m, LocRef invoke_loc, if (m->is_variadic && n_args + 1 >= m->n_params) { /* This comma is part of __VA_ARGS__. Push it. */ tv_push(pp, &raw, t); - hsv_push(pp, &raw_hs, hs); first_token_of_arg = 0; continue; } @@ -603,7 +477,6 @@ static Tok read_invocation_args(Pp* pp, const Macro* m, LocRef invoke_loc, } } tv_push(pp, &raw, t); - hsv_push(pp, &raw_hs, hs); first_token_of_arg = 0; } done: @@ -643,7 +516,6 @@ done: } } out->raw = raw.data; - out->raw_hs = raw_hs.data; out->raw_n = raw.n; out->raw_start = starts; out->n_args = n_args; @@ -665,8 +537,7 @@ static void preexpand_args(Pp* pp, ArgList* a) { * own it without aliasing. */ Tok* slice = arena_array(pp->xarena, Tok, hi - lo); memcpy(slice, &a->raw[lo], sizeof(Tok) * (hi - lo)); - expand_arg_to_eof(pp, slice, a->raw_hs ? &a->raw_hs[lo] : NULL, hi - lo, - &exp); + expand_arg_to_eof(pp, slice, hi - lo, &exp); } exp_start[i + 1] = exp.n; } @@ -1005,12 +876,10 @@ static void substitute_body(Pp* pp, const Macro* m, ArgList* a, * pre-expand them, substitute the body, push the result. Returns 1 if * the invocation was performed, 0 if there was no `(` (the caller should * emit the identifier as-is). */ -static int try_expand_func_macro(Pp* pp, const Macro* m, const Tok* invoke, - HidesetId invoke_hs) { +static int try_expand_func_macro(Pp* pp, Macro* m, const Tok* invoke) { int saw_ws; ArgList args; TokVec body = {0}; - HidesetId result_hs; Tok close_tok; if (!peek_for_invoke_paren(pp, &saw_ws)) { @@ -1023,16 +892,13 @@ static int try_expand_func_macro(Pp* pp, const Macro* m, const Tok* invoke, (void)close_tok; preexpand_args(pp, &args); - /* Hideset of result = invocation hideset ∪ {macro_name}. The standard - * intersects with the closing `)`'s hideset for blue-paint purity, but - * for the freshly-collected `)` from the lex source that's the empty - * set, so the union form suffices here. */ - result_hs = hs_add(pp, invoke_hs, m->name); substitute_body(pp, m, &args, invoke, &body); - /* result_hs is uniform across the entire substituted body, so push it as a - * single-hideset buffer — no per-token parallel array. */ - push_buf_uniform(pp, body.data, body.n, result_hs); + /* Push the substituted body as a disabled frame owned by `m`: the macro is + * unavailable for the whole rescan of this replacement (cpplib model), so a + * recursive occurrence of `m` is returned permanently un-expanded. The + * arguments were already pre-expanded above with `m` NOT disabled. */ + push_buf_uniform(pp, body.data, body.n, m); return 1; } @@ -1066,7 +932,6 @@ static int try_expand_func_macro(Pp* pp, const Macro* m, const Tok* invoke, * 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 (;;) { TokSrc* s; @@ -1088,14 +953,13 @@ static void pp_pull_into(Pp* pp, Tok* out, int skip_nl) { if (!kit_arena_is_empty(pp->xarena)) kit_arena_reset(pp->xarena); } /* Fast path: top source is a non-exhausted SRC_BUF (the dominant - * macro-replay case). Pull the token, hideset, and kind inline so the - * common token avoids the call + sret setup of src_next_raw_into. The - * scope_top-EOF and #line-delta cases never apply to a plain in-bounds - * SRC_BUF read, so they stay on the general (cold) path. */ + * macro-replay case). Pull the token and kind inline so the common token + * avoids the call + sret setup of src_next_raw_into. The scope_top-EOF and + * #line-delta cases never apply to a plain in-bounds SRC_BUF read, so they + * stay on the general (cold) path. */ if (pp->nsources != 0 && (s = &pp->sources[pp->nsources - 1])->kind == SRC_BUF && s->i < s->n) { *out = s->toks[s->i]; - hs = s->hs ? s->hs[s->i] : s->hs_uniform; src_kind = SRC_BUF; /* Pointer-replayed body: apply the per-invocation loc and (first token * only) BOL/SPACE flags the old fresh-copy path baked in. Identical to @@ -1109,7 +973,7 @@ static void pp_pull_into(Pp* pp, Tok* out, int skip_nl) { } ++s->i; } else { - src_next_raw_into(pp, out, &hs, &src_kind); + src_next_raw_into(pp, out, &src_kind); } if (out->kind == TOK_EOF) return; /* §C.3a/§C.3b: absorb the parser-facing non-directive newline drop here so @@ -1131,7 +995,7 @@ static void pp_pull_into(Pp* pp, Tok* out, int skip_nl) { * newline is handled exactly as before. */ if (skip_nl && out->kind == TOK_NEWLINE) { do { - src_next_raw_into(pp, out, &hs, &src_kind); + src_next_raw_into(pp, out, &src_kind); } while (out->kind == TOK_NEWLINE); if (out->kind == TOK_EOF) return; } @@ -1255,15 +1119,28 @@ static void pp_pull_into(Pp* pp, Tok* out, int skip_nl) { { Macro* m = mt_get(pp, id); - if (m && !hs_contains(pp, hs, m->name)) { + if (m && m->disabled_depth != 0) { + /* cpplib: the macro is disabled because one of its own + * replacement-list frames is still being rescanned. Mark this + * occurrence permanently un-expandable, so the bit survives if this + * token is later captured as an argument, substituted into another + * macro, or replayed (this is what keeps `foo(foo)(1)` and + * `id(foo(foo)(1))` as `foo(1)`). */ + out->flags |= TF_NO_EXPAND; + return; + } + if (m) { if (!m->is_func) { - expand_object_macro(pp, m, out, hs); + expand_object_macro(pp, m, out); continue; } - if (try_expand_func_macro(pp, m, out, hs)) { + if (try_expand_func_macro(pp, m, out)) { continue; } - /* No '(' followed; emit as plain identifier. */ + /* Function-like macro name NOT followed by '(': emit as a plain + * identifier WITHOUT setting TF_NO_EXPAND, so a later rescan that + * sees a '(' supplied by another macro can still expand it (keeps + * `id(f L 1))` with `#define L (` able to become `1`). */ } } } diff --git a/lang/cpp/pp/pp_priv.h b/lang/cpp/pp/pp_priv.h @@ -34,6 +34,15 @@ typedef struct Macro { * replayed by pointer without the per-invocation copy + paste pass. */ u8 has_paste; u8 pad[1]; + /* cpplib-style macro-disabled count (replaces the Prosser hideset): the + * number of this macro's replacement-list frames currently live on the + * source stack. Incremented when a replacement frame for this macro is + * pushed, decremented when it pops (see push_buf_uniform/replay + src_pop). + * While > 0 the macro is unavailable for expansion, so a recursive + * occurrence in its own (or a nested) replacement is returned with the + * permanent TF_NO_EXPAND bit set instead of re-expanding. Auto-zeros: do_define + * mints Macro via arena_znew. */ + u16 disabled_depth; u32 n_params; Sym* params; /* parameter names */ Tok* body; /* body tokens; TOK_PP_PARAM kind + aux=param idx */ @@ -45,16 +54,6 @@ typedef struct Macro { * the expander's hot loop, replacing a per-identifier hashmap probe. */ KIT_SYMTAB_DEFINE(MacroTab, Macro*); -typedef u32 HidesetId; -#define HS_EMPTY 0u - -typedef struct Hideset { - u32 n; - u32 hash; /* content hash of names[0..n) — dedup index key (see hs_register) - */ - Sym names[1]; /* flexible; allocated with extra trailing slots */ -} Hideset; - typedef enum { SRC_LEX = 1, SRC_BUF = 2 } SrcKind; /* Multiple-include-guard detection state for a SRC_LEX file (the classic @@ -93,14 +92,15 @@ typedef struct TokSrc { Lexer* lex; /* SRC_BUF */ Tok* toks; - /* Per-token hidesets. When `hs` is non-NULL it is a parallel array (one id - * per token) — used only by the argument-prescan path, where tokens can - * carry differing hidesets. When `hs` is NULL the whole buffer shares the - * single `hs_uniform` id: macro-body expansions (object- and function-like) - * are uniform, so they take this path and skip the per-token array entirely - * (no alloc, no fill, one scalar instead of n copies). */ - HidesetId* hs; - HidesetId hs_uniform; + /* cpplib macro-disabled frame owner. Non-NULL only on a macro + * replacement-list frame (object-like replay, function-like substituted body, + * paste result); pushing such a frame increments owner->disabled_depth and + * src_pop decrements it, so the macro stays unavailable for the whole rescan + * of its replacement (and any nested replacement above it). NULL on every + * non-macro buffer (pushed-back peek tokens, #pragma/#embed payloads, + * arg-prescan scopes); every push_buf* memsets the whole TokSrc, so NULL is + * the default. */ + Macro* disabled_owner; /* Per-token loc applied when has_loc_override (see scope_top block). */ LocRef loc_override; u32 i; @@ -254,18 +254,6 @@ struct Pp { u32 ifstk_n; u32 ifstk_cap; - /* Hideset table. Element 0 reserved as HS_EMPTY. */ - Hideset** hsets; - u32 hsets_n; - u32 hsets_cap; - /* Content-addressed dedup index over hsets: open-addressed, power-of-two, - * slot holds a HidesetId (0 = empty). Replaces the O(n) linear scan in - * hs_register with an O(1) probe — the per-macro-invocation dedup was the - * superlinear cost on macro-heavy input. */ - HidesetId* hs_index; - u32 hs_index_cap; - u32 hs_index_used; - /* Include directories (stage 9). */ struct { const char* path; @@ -279,12 +267,12 @@ struct Pp { u32 pack_stack[16]; u32 pack_stack_n; - /* Permanent arena: macro bodies, params, hidesets, #include file data. + /* Permanent arena: macro bodies, params, #include file data. * Lives until pp_free. */ KitArena* arena; - /* Transient expansion scratch: the macro-substitution token/hideset buffers - * (tv_grow/hsv_grow), arg slices, paste/stringize buffers -- everything that + /* Transient expansion scratch: the macro-substitution token buffers + * (tv_grow), arg slices, paste/stringize buffers -- everything that * backs a SRC_BUF or is consumed within one expansion. Reset to its * high-water mark by pp_next_raw whenever the source stack drains back to a * lexer (no SRC_BUF live), so memory is O(expansion depth), not @@ -413,12 +401,6 @@ typedef struct TokVec { u32 cap; } TokVec; -typedef struct HsVec { - HidesetId* data; - u32 n; - u32 cap; -} HsVec; - static inline void tv_grow(Pp* pp, TokVec* v, u32 want) { u32 nc; if (v->cap >= want) return; @@ -437,24 +419,6 @@ static inline void tv_push(Pp* pp, TokVec* v, Tok t) { v->data[v->n++] = t; } -static inline void hsv_grow(Pp* pp, HsVec* v, u32 want) { - u32 nc; - if (v->cap >= want) return; - nc = v->cap ? v->cap * 2 : 8; - while (nc < want) nc *= 2; - { - HidesetId* nb = arena_array(pp->xarena, HidesetId, nc); - if (v->n) memcpy(nb, v->data, sizeof(HidesetId) * v->n); - v->data = nb; - v->cap = nc; - } -} - -static inline void hsv_push(Pp* pp, HsVec* v, HidesetId hs) { - hsv_grow(pp, v, v->n + 1); - v->data[v->n++] = hs; -} - /* Growable char buffer (arena-backed). */ typedef struct CharBuf { char* data; @@ -488,17 +452,22 @@ static inline void cb_putc(Pp* pp, CharBuf* b, char c) { /* --- pp.c (source stack) → pp_expand.c, pp_directive.c --- */ /* Out-pointer form (hot path); src_next_raw is the by-value shim over it for * the cold/general callers. */ -void src_next_raw_into(Pp* pp, Tok* out, HidesetId* hs_out, u8* src_kind_out); -Tok src_next_raw(Pp* pp, HidesetId* hs_out, u8* src_kind_out); +void src_next_raw_into(Pp* pp, Tok* out, u8* src_kind_out); +Tok src_next_raw(Pp* pp, u8* src_kind_out); void src_push(Pp* pp, TokSrc s); void src_pop(Pp* pp); -void push_buf(Pp* pp, Tok* toks, HidesetId* hs, u32 n); -void push_buf_uniform(Pp* pp, Tok* toks, u32 n, HidesetId hs_uniform); +void push_buf(Pp* pp, Tok* toks, u32 n); +/* Push a macro replacement-list buffer. `owner` is the macro whose replacement + * this is (NULL for a non-macro payload): a non-NULL owner increments + * owner->disabled_depth at push and src_pop decrements it, so the macro stays + * unavailable for the whole rescan of its replacement (cpplib disabled-frame + * model). */ +void push_buf_uniform(Pp* pp, Tok* toks, u32 n, Macro* owner); /* Push an immutable, read-only token buffer (e.g. a no-`##` macro body) for * pointer-replay. The buffer is never written through s->toks; per-read * loc/flags overrides reproduce the mutations the old body-copy path baked in. - */ -void push_buf_replay(Pp* pp, const Tok* toks, u32 n, HidesetId hs_uniform, + * `owner` follows the same disabled-frame rule as push_buf_uniform. */ +void push_buf_replay(Pp* pp, const Tok* toks, u32 n, Macro* owner, LocRef loc_override, u16 first_flags_or); /* pp_next_raw (public, in pp.h) is the mutual-recursion entry: @@ -522,9 +491,7 @@ u32 pp_phys_line(Pp* pp, LocRef loc); Sym pp_materialize_file(Pp* pp, LocRef loc); /* --- pp_expand.c → pp.c, pp_directive.c --- */ -HidesetId hs_add(Pp* pp, HidesetId id, Sym s); -int hs_contains(Pp* pp, HidesetId id, Sym s); -void expand_arg_to_eof(Pp* pp, Tok* in, HidesetId* hs, u32 nin, TokVec* out); +void expand_arg_to_eof(Pp* pp, Tok* in, u32 nin, TokVec* out); /* Macro binding lookup/define/undef. Inlined Sym-indexed loads (no hashmap * probe): mt_get runs on every identifier the expander sees. NULL/absent = diff --git a/test/pp/CORPUS.md b/test/pp/CORPUS.md @@ -104,6 +104,9 @@ Both runners pin the values of the otherwise-environmental predefined macros: | 63_rescan_not_directive | 6.10.3.4 ¶3 | tokens produced by expansion are not a directive | | 64_rescan_self_in_func | 6.10.3.4 ¶2 | function-like macro does not re-invoke itself in rescan | | 65_rescan_arg_hideset_prescan | 6.10.3.4 ¶2 | argument prescan keeps no-reexpand state from caller | +| 66_rescan_self_apply_paren | 6.10.3.4 ¶2 | disabled macro name kept unavailable when re-applied | +| 67_rescan_arg_self_apply | 6.10.3.4 ¶2 | unavailable bit survives argument capture into id() | +| 68_rescan_func_name_no_paren_then_paren | 6.10.3.4 ¶1 | func-like name with no `(` may expand when one is supplied later | ### 70–79 Variadic macros (§6.10.3 ¶12, §6.10.3.1 ¶2) diff --git a/test/pp/cases/66_rescan_self_apply_paren.c b/test/pp/cases/66_rescan_self_apply_paren.c @@ -0,0 +1,2 @@ +#define foo(x) foo +foo(foo)(1) diff --git a/test/pp/cases/66_rescan_self_apply_paren.expected b/test/pp/cases/66_rescan_self_apply_paren.expected @@ -0,0 +1 @@ +foo(1) diff --git a/test/pp/cases/67_rescan_arg_self_apply.c b/test/pp/cases/67_rescan_arg_self_apply.c @@ -0,0 +1,3 @@ +#define id(x) x +#define foo(x) foo +id(foo(foo)(1)) diff --git a/test/pp/cases/67_rescan_arg_self_apply.expected b/test/pp/cases/67_rescan_arg_self_apply.expected @@ -0,0 +1 @@ +foo(1) diff --git a/test/pp/cases/68_rescan_func_name_no_paren_then_paren.c b/test/pp/cases/68_rescan_func_name_no_paren_then_paren.c @@ -0,0 +1,4 @@ +#define L ( +#define f(a) a +#define id(x) x +id(f L 1)) diff --git a/test/pp/cases/68_rescan_func_name_no_paren_then_paren.expected b/test/pp/cases/68_rescan_func_name_no_paren_then_paren.expected @@ -0,0 +1 @@ +1