kit

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

pp_expand.c (45435B)


      1 /* pp_expand.c — macro table, #define/#undef, substitution, paste, stringize,
      2  * argument prescan, func/object macro expansion. Macro-expansion availability
      3  * uses the cpplib disabled-frame model (Macro.disabled_depth + the per-token
      4  * TF_NO_EXPAND bit), not a per-token Prosser hideset; see pp.c / the identifier
      5  * test in pp_pull_into below. */
      6 
      7 #include "pp/pp_priv.h"
      8 
      9 static int body_tokens_equal(Pp* pp, const Tok* a, u32 na, const Tok* b,
     10                              u32 nb);
     11 static int macros_equal(Pp* pp, const Macro* a, const Macro* b);
     12 
     13 /* ============================================================
     14  * #define / #undef
     15  * ============================================================ */
     16 /* mt_get / mt_put / mt_del are inlined Sym-indexed loads in pp_priv.h
     17  * (MacroTab, a SYMTAB mapping Sym -> Macro*). */
     18 
     19 void do_define(Pp* pp, const Tok* line, u32 n) {
     20   Macro* m;
     21   u32 i = 0;
     22   Sym name;
     23   LocRef def_loc;
     24   Macro* existing;
     25 
     26   if (i >= n || line[i].kind != TOK_IDENT) {
     27     compiler_panic(pp->c,
     28                    n ? pp_materialize_loc(pp, line[0].loc) : (SrcLoc){0, 0, 0},
     29                    "#define: expected macro name");
     30   }
     31   name = tok_ident(&line[i]);
     32   def_loc = line[i].loc;
     33   ++i;
     34 
     35   m = arena_znew(pp->arena, Macro);
     36   m->name = name;
     37   m->def_loc = def_loc;
     38 
     39   /* Function-like vs object-like: '(' immediately after the name with no
     40    * intervening whitespace. */
     41   if (i < n && line[i].kind == TOK_PUNCT && tok_punct(&line[i]) == '(' &&
     42       (line[i].flags & TF_HAS_SPACE) == 0) {
     43     Sym* params = NULL;
     44     u32 pcap = 0, pn = 0;
     45     ++i;
     46     m->is_func = 1;
     47     if (i < n && line[i].kind == TOK_PUNCT && tok_punct(&line[i]) == ')') {
     48       ++i;
     49     } else {
     50       for (;;) {
     51         if (i >= n) {
     52           compiler_panic(pp->c, pp_materialize_loc(pp, def_loc),
     53                          "#define: unterminated parameter list");
     54         }
     55         if (line[i].kind == TOK_PUNCT && tok_punct(&line[i]) == P_ELLIPSIS) {
     56           /* Append a synthetic __VA_ARGS__ param so body-rewrite
     57            * matches the standard identifier directly. */
     58           if (pn == pcap) {
     59             u32 nc = pcap ? pcap * 2 : 4;
     60             Sym* nb = arena_array(pp->arena, Sym, nc);
     61             if (pcap) memcpy(nb, params, sizeof(Sym) * pcap);
     62             params = nb;
     63             pcap = nc;
     64           }
     65           params[pn++] = pp->sym_va_args;
     66           m->is_variadic = 1;
     67           ++i;
     68         } else if (line[i].kind == TOK_IDENT) {
     69           if (pn == pcap) {
     70             u32 nc = pcap ? pcap * 2 : 4;
     71             Sym* nb = arena_array(pp->arena, Sym, nc);
     72             if (pcap) memcpy(nb, params, sizeof(Sym) * pcap);
     73             params = nb;
     74             pcap = nc;
     75           }
     76           params[pn++] = tok_ident(&line[i]);
     77           ++i;
     78           /* GNU named variadic: `args...` — the named parameter itself collects
     79            * the trailing arguments (the body refers to it by name rather than
     80            * __VA_ARGS__). The variadic arg-collection below is positional on
     81            * the last param, so we just mark the macro variadic and eat the
     82            * ellipsis; the "'...' must be last" check still fires if a comma
     83            * follows. Linux UAPI headers use this (e.g. <linux/stddef.h>'s
     84            * __struct_group). */
     85           if (i < n && line[i].kind == TOK_PUNCT &&
     86               tok_punct(&line[i]) == P_ELLIPSIS) {
     87             m->is_variadic = 1;
     88             ++i;
     89           }
     90         } else {
     91           compiler_panic(pp->c, pp_materialize_loc(pp, line[i].loc),
     92                          "#define: bad parameter list");
     93         }
     94         if (i >= n) {
     95           compiler_panic(pp->c, pp_materialize_loc(pp, def_loc),
     96                          "#define: unterminated parameter list");
     97         }
     98         if (line[i].kind == TOK_PUNCT && tok_punct(&line[i]) == ')') {
     99           ++i;
    100           break;
    101         }
    102         if (m->is_variadic) {
    103           compiler_panic(pp->c, pp_materialize_loc(pp, line[i].loc),
    104                          "#define: '...' must be last parameter");
    105         }
    106         if (line[i].kind == TOK_PUNCT && tok_punct(&line[i]) == ',') {
    107           ++i;
    108           continue;
    109         }
    110         compiler_panic(pp->c, pp_materialize_loc(pp, line[i].loc),
    111                        "#define: expected ',' or ')'");
    112       }
    113     }
    114     m->params = params;
    115     m->n_params = pn;
    116   }
    117 
    118   /* Refuse define/undef of a few names the spec reserves: `defined`
    119    * and a small set of mandatory predefined macros. */
    120   if (name == pp->sym_defined || name == pp->sym_line__ ||
    121       name == pp->sym_file__ || name == pp->sym_date__ ||
    122       name == pp->sym_time__) {
    123     compiler_panic(pp->c, pp_materialize_loc(pp, def_loc),
    124                    "#define of a reserved / predefined name is not allowed");
    125   }
    126   /* Static predefineds are already in the macro table; redefining
    127    * with a different body is caught by the existing macros_equal
    128    * check below, but #define of __STDC__ et al. with the SAME body
    129    * should also be rejected. */
    130   if (name == pp->sym_stdc__ || name == pp->sym_stdc_hosted__ ||
    131       name == pp->sym_stdc_version__) {
    132     /* Allow re-registration of the predefined value at pp_new time
    133      * but reject user-level redefinition. We detect "user-level"
    134      * by checking whether it's already in the table — at pp_new the
    135      * first call goes through cleanly. */
    136     if (mt_get(pp, name)) {
    137       compiler_panic(pp->c, pp_materialize_loc(pp, def_loc),
    138                      "#define of a mandatory predefined macro is not allowed");
    139     }
    140   }
    141 
    142   /* Body: rewrite parameter occurrences to TOK_PP_PARAM. */
    143   {
    144     u32 body_n = n - i;
    145     u32 j;
    146     m->body = body_n ? arena_array(pp->arena, Tok, body_n) : NULL;
    147     m->body_len = body_n;
    148     for (j = 0; j < body_n; ++j) {
    149       Tok t = line[i + j];
    150       if (t.kind == TOK_PP_PASTE) m->has_paste = 1;
    151       if (m->is_func && t.kind == TOK_IDENT) {
    152         u32 p;
    153         for (p = 0; p < m->n_params; ++p) {
    154           if (m->params[p] == tok_ident(&t)) {
    155             /* Rewrite in place to TOK_PP_PARAM; leave `text` as the original
    156              * TEXT_SRC span (the param-name spelling, needed by the
    157              * redefinition compare). The parameter index lives in `aux`. */
    158             t.kind = TOK_PP_PARAM;
    159             t.aux = p;
    160             break;
    161           }
    162         }
    163       }
    164       /* §6.10.3 ¶5: __VA_ARGS__ outside a variadic macro is
    165        * undefined behavior; we diagnose. */
    166       if (!m->is_variadic && t.kind == TOK_IDENT &&
    167           tok_ident(&t) == pp->sym_va_args) {
    168         compiler_panic(pp->c, pp_materialize_loc(pp, t.loc),
    169                        "__VA_ARGS__ may only appear in a variadic macro body");
    170       }
    171       m->body[j] = t;
    172     }
    173     /* Drop the leading-space bit on the first body token: it reflects
    174      * the whitespace between the macro name (or close-paren) and the
    175      * body, which is irrelevant to expansion output. */
    176     if (m->body_len) m->body[0].flags &= (u16)~TF_HAS_SPACE;
    177   }
    178 
    179   existing = mt_get(pp, name);
    180   if (existing) {
    181     if (!macros_equal(pp, existing, m)) {
    182       compiler_panic(pp->c, pp_materialize_loc(pp, def_loc),
    183                      "macro redefined with different replacement");
    184     }
    185     return;
    186   }
    187   mt_put(pp, name, m);
    188 }
    189 
    190 void do_undef(Pp* pp, const Tok* line, u32 n) {
    191   Sym name;
    192   if (!n || line[0].kind != TOK_IDENT) {
    193     compiler_panic(pp->c,
    194                    n ? pp_materialize_loc(pp, line[0].loc) : (SrcLoc){0, 0, 0},
    195                    "#undef: expected identifier");
    196   }
    197   name = tok_ident(&line[0]);
    198   if (name == pp->sym_defined || name == pp->sym_line__ ||
    199       name == pp->sym_file__ || name == pp->sym_date__ ||
    200       name == pp->sym_time__ || name == pp->sym_stdc__ ||
    201       name == pp->sym_stdc_hosted__ || name == pp->sym_stdc_version__) {
    202     compiler_panic(pp->c, pp_materialize_loc(pp, line[0].loc),
    203                    "#undef of a mandatory predefined name is not allowed");
    204   }
    205   mt_del(pp, name);
    206 }
    207 
    208 /* ============================================================
    209  * Body comparison helpers
    210  * ============================================================ */
    211 
    212 static int body_tokens_equal(Pp* pp, const Tok* a, u32 na, const Tok* b,
    213                              u32 nb) {
    214   u32 i;
    215   if (na != nb) return 0;
    216   for (i = 0; i < na; ++i) {
    217     KitSlice as, bs;
    218     if (a[i].kind != b[i].kind) return 0;
    219     /* Two body tokens are equal iff their exact spellings match (§6.10.3 ¶1).
    220      * Compare the resolved bytes directly: a TOK_PP_PARAM keeps its original
    221      * param-name span, so identical parameters compare equal byte-for-byte,
    222      * and interned/source spellings compare the same way the old Sym-equality
    223      * did (identical interned spelling == identical bytes). */
    224     as = pp_text_slice(pp, &a[i]);
    225     bs = pp_text_slice(pp, &b[i]);
    226     if (as.len != bs.len || (as.len && memcmp(as.s, bs.s, as.len) != 0)) {
    227       return 0;
    228     }
    229     /* Whitespace separation must match (§6.10.3 ¶2). The first body
    230      * token's leading-space bit is meaningless (it's whatever was
    231      * between macro name and body); skip i==0 for that bit. */
    232     if (i > 0) {
    233       if ((a[i].flags & TF_HAS_SPACE) != (b[i].flags & TF_HAS_SPACE)) {
    234         return 0;
    235       }
    236     }
    237   }
    238   return 1;
    239 }
    240 
    241 static int macros_equal(Pp* pp, const Macro* a, const Macro* b) {
    242   if (a->is_func != b->is_func) return 0;
    243   if (a->is_variadic != b->is_variadic) return 0;
    244   if (a->n_params != b->n_params) return 0;
    245   {
    246     u32 i;
    247     for (i = 0; i < a->n_params; ++i) {
    248       if (a->params[i] != b->params[i]) return 0;
    249     }
    250   }
    251   return body_tokens_equal(pp, a->body, a->body_len, b->body, b->body_len);
    252 }
    253 
    254 /* ============================================================
    255  * Object-macro expansion
    256  * ============================================================ */
    257 
    258 static void subst_phase2(Pp* pp, const Tok* in, u32 nin, const Tok* invoke,
    259                          TokVec* out);
    260 
    261 /* Build a buffer of the macro's body (with hidesets) and push it. The
    262  * first expanded token inherits the invocation token's TF_AT_BOL /
    263  * TF_HAS_SPACE so output formatting matches the invocation site. */
    264 static void expand_object_macro(Pp* pp, Macro* m, const Tok* invoke) {
    265   TokVec body = {0};
    266   Tok* tmp;
    267   u32 i;
    268 
    269   if (m->body_len == 0) {
    270     return; /* placemarker: nothing to push */
    271   }
    272   /* No `##`: an object body has no params and no placemarkers, so the paste
    273    * phase is a pure identity copy. Skip both copies — replay the immutable
    274    * definition-time m->body by pointer, applying the per-invocation loc and
    275    * first-token flag transfer at read time (push_buf_replay). Byte-identical
    276    * to the slow path (same tokens, same loc, same first-token flags), with
    277    * zero per-invocation token copies or body allocs. The frame disables `m`
    278    * for the whole rescan of its replacement (cpplib model). */
    279   if (!m->has_paste) {
    280     u16 ff = (u16)((m->body[0].flags & ~(TF_AT_BOL | TF_HAS_SPACE)) |
    281                    (invoke->flags & (TF_AT_BOL | TF_HAS_SPACE)));
    282     push_buf_replay(pp, m->body, m->body_len, m, invoke->loc, ff);
    283     return;
    284   }
    285 
    286   /* Run the body through the paste phase: object-like macros may use
    287    * `##`. There are no parameters, so phase 1 reduces to a copy. */
    288   tmp = arena_array(pp->xarena, Tok, m->body_len);
    289   for (i = 0; i < m->body_len; ++i) tmp[i] = m->body[i];
    290   subst_phase2(pp, tmp, m->body_len, invoke, &body);
    291 
    292   if (body.n == 0) return;
    293 
    294   /* Transfer invocation flags onto the first emitted token. */
    295   body.data[0].flags =
    296       (u16)((body.data[0].flags & ~(TF_AT_BOL | TF_HAS_SPACE)) |
    297             (invoke->flags & (TF_AT_BOL | TF_HAS_SPACE)));
    298   for (i = 0; i < body.n; ++i) body.data[i].loc = invoke->loc;
    299 
    300   push_buf_uniform(pp, body.data, body.n, m);
    301 }
    302 
    303 /* ============================================================
    304  * Function-like macro expansion
    305  * ============================================================ */
    306 
    307 /* Peek for an open paren after the just-consumed identifier (which named
    308  * a function-like macro). Newlines are whitespace inside an invocation.
    309  * Returns 1 with `*ws_has_space_out` indicating whether any whitespace
    310  * (newlines or HAS_SPACE) sat between the ident and the `(`. Returns 0 if
    311  * no `(` follows; pushed-back tokens (NLs + the non-`(` token, if any)
    312  * are restored as a buffer source so subsequent reads still see them. */
    313 int peek_for_invoke_paren(Pp* pp, int* ws_has_space_out) {
    314   TokVec saved = {0};
    315   int saw_ws = 0;
    316   Tok t;
    317 
    318   for (;;) {
    319     t = src_next_raw(pp, NULL);
    320     if (t.kind == TOK_NEWLINE) {
    321       saw_ws = 1;
    322       tv_push(pp, &saved, t);
    323       continue;
    324     }
    325     if (t.kind == TOK_EOF) {
    326       /* No '(' — push back saved tokens, leave EOF for next read. */
    327       if (saved.n) push_buf(pp, saved.data, saved.n);
    328       *ws_has_space_out = saw_ws;
    329       return 0;
    330     }
    331     if (t.flags & TF_HAS_SPACE) saw_ws = 1;
    332     if (t.kind == TOK_PUNCT && tok_punct(&t) == '(') {
    333       /* Consumed. The newlines we walked past are whitespace and
    334        * dropped (per spec); they don't go back on the stack. */
    335       *ws_has_space_out = saw_ws;
    336       return 1;
    337     }
    338     /* Save this non-`(` token too and push back. */
    339     tv_push(pp, &saved, t);
    340     push_buf(pp, saved.data, saved.n);
    341     *ws_has_space_out = saw_ws;
    342     return 0;
    343   }
    344 }
    345 
    346 /* Run macro expansion on a fixed token sequence to completion, yielding the
    347  * fully-expanded token sequence. Used to pre-expand each function-macro
    348  * argument before substitution (§6.10.3.1 ¶1). */
    349 void expand_arg_to_eof(Pp* pp, Tok* in, u32 nin, TokVec* out) {
    350   TokSrc src;
    351   Tok t;
    352 
    353   memset(&src, 0, sizeof(src));
    354   src.kind = SRC_BUF;
    355   src.scope_top = 1;
    356   src.toks = in;
    357   src.n = nin;
    358   src_push(pp, src);
    359 
    360   for (;;) {
    361     pp_next_raw(pp, &t); /* drives macro expansion within this scope */
    362     if (t.kind == TOK_EOF) break;
    363     if (t.kind == TOK_NEWLINE) {
    364       /* Newlines inside an arg act as whitespace; convert to
    365        * "next-token has TF_HAS_SPACE". Drop the NL token itself. */
    366       continue;
    367     }
    368     tv_push(pp, out, t);
    369   }
    370   /* Pop our scope source directly (not src_pop). This is the one place the
    371    * disabled-frame decrement in src_pop is intentionally bypassed: the
    372    * arg-prescan scope frame is pushed above with src.disabled_owner == NULL (it
    373    * disables no macro — arguments are pre-expanded with the invoking macro NOT
    374    * yet disabled), so there is no disabled_depth to decrement here. */
    375   --pp->nsources;
    376 }
    377 
    378 /* Argument list for a function-like invocation. Stored as parallel
    379  * (start, end) ranges into a flat unexpanded token vector and a flat
    380  * expanded token vector. */
    381 typedef struct ArgList {
    382   /* Unexpanded arg tokens (raw as collected from invocation). */
    383   Tok* raw;
    384   u32 raw_n;
    385   u32* raw_start; /* size n_args + 1 (sentinel = raw_n) */
    386   /* Pre-expanded tokens. */
    387   Tok* exp;
    388   u32 exp_n;
    389   u32* exp_start; /* size n_args + 1 (sentinel = exp_n) */
    390   u32 n_args;
    391 } ArgList;
    392 
    393 /* Collect arguments. Caller has just consumed the opening `(`. Returns the
    394  * close-paren's token (used as the invocation's last source location). */
    395 static Tok read_invocation_args(Pp* pp, const Macro* m, LocRef invoke_loc,
    396                                 ArgList* out) {
    397   TokVec raw = {0};
    398   u32* starts;
    399   u32 starts_cap = 0;
    400   u32 n_args = 0;
    401   u32 cur_start = 0;
    402   int depth = 0;
    403   Tok t;
    404   int first_token_of_arg = 1;
    405   Tok close_tok;
    406 
    407   memset(out, 0, sizeof(*out));
    408   starts = arena_array(pp->xarena, u32, 8);
    409   starts_cap = 8;
    410   starts[0] = 0;
    411 
    412   for (;;) {
    413     t = src_next_raw(pp, NULL);
    414     if (t.kind == TOK_EOF) {
    415       compiler_panic(pp->c, pp_materialize_loc(pp, invoke_loc),
    416                      "unterminated function-like macro invocation");
    417     }
    418     if (t.kind == TOK_NEWLINE) {
    419       /* A newline inside an invocation is just whitespace; drop it.
    420        * The lexer already records the surrounding-whitespace bits on
    421        * the next token, so nothing needs to be carried across here. */
    422       continue;
    423     }
    424 
    425     if (t.kind == TOK_PUNCT) {
    426       u32 p = tok_punct(&t);
    427       if (p == '(') {
    428         ++depth;
    429       } else if (p == ')') {
    430         if (depth == 0) {
    431           /* End of invocation. Close the current argument. The
    432            * empty-args case (no commas seen, no tokens
    433            * collected) emits a slot only when the macro expects
    434            * at least one argument; arity-0 macros take none. */
    435           close_tok = t;
    436           {
    437             int empty_call =
    438                 (n_args == 0 && raw.n == cur_start && first_token_of_arg);
    439             int want_slot = !empty_call || (m->n_params > 0) || m->is_variadic;
    440             if (want_slot) {
    441               if (n_args + 1 >= starts_cap) {
    442                 u32 nc = starts_cap * 2;
    443                 u32* nb = arena_array(pp->xarena, u32, nc);
    444                 memcpy(nb, starts, sizeof(u32) * starts_cap);
    445                 starts = nb;
    446                 starts_cap = nc;
    447               }
    448               ++n_args;
    449               starts[n_args] = raw.n;
    450             }
    451           }
    452           goto done;
    453         }
    454         --depth;
    455       } else if (p == ',' && depth == 0) {
    456         /* Variadic: once we've filled all named params, the rest
    457          * (commas included) collect into __VA_ARGS__. */
    458         if (m->is_variadic && n_args + 1 >= m->n_params) {
    459           /* This comma is part of __VA_ARGS__. Push it. */
    460           tv_push(pp, &raw, t);
    461           first_token_of_arg = 0;
    462           continue;
    463         }
    464         /* Close current arg, start next. */
    465         if (n_args + 1 >= starts_cap) {
    466           u32 nc = starts_cap * 2;
    467           u32* nb = arena_array(pp->xarena, u32, nc);
    468           memcpy(nb, starts, sizeof(u32) * starts_cap);
    469           starts = nb;
    470           starts_cap = nc;
    471         }
    472         ++n_args;
    473         starts[n_args] = raw.n;
    474         cur_start = raw.n;
    475         first_token_of_arg = 1;
    476         continue;
    477       }
    478     }
    479     tv_push(pp, &raw, t);
    480     first_token_of_arg = 0;
    481   }
    482 done:
    483   /* Validate arity. */
    484   {
    485     u32 expected = m->n_params;
    486     if (m->is_variadic) {
    487       if (n_args < (expected ? expected - 1 : 0)) {
    488         /* Allow exactly expected-1 (empty __VA_ARGS__) by
    489          * synthesizing an empty trailing arg. */
    490         if (n_args + 1 == (expected ? expected - 1 : 0)) {
    491           /* off by one — fall through to error */
    492         }
    493         compiler_panic(pp->c, pp_materialize_loc(pp, invoke_loc),
    494                        "too few arguments to variadic macro invocation");
    495       }
    496       /* Synthesize an empty __VA_ARGS__ if caller passed exactly
    497        * the named-parameter count. */
    498       if (n_args + 1 == expected) {
    499         if (n_args + 1 >= starts_cap) {
    500           u32 nc = starts_cap * 2;
    501           u32* nb = arena_array(pp->xarena, u32, nc);
    502           memcpy(nb, starts, sizeof(u32) * starts_cap);
    503           starts = nb;
    504           starts_cap = nc;
    505         }
    506         ++n_args;
    507         starts[n_args] = raw.n;
    508       }
    509     } else {
    510       if (n_args != expected) {
    511         /* Spec: arity-0 macro `M()` invoked as `M()` is allowed and
    512          * has 0 args. Above logic produces 0 in that case. */
    513         compiler_panic(pp->c, pp_materialize_loc(pp, invoke_loc),
    514                        "wrong number of arguments to function-like macro");
    515       }
    516     }
    517   }
    518   out->raw = raw.data;
    519   out->raw_n = raw.n;
    520   out->raw_start = starts;
    521   out->n_args = n_args;
    522   return close_tok;
    523 }
    524 
    525 /* Build pre-expanded args. */
    526 static void preexpand_args(Pp* pp, ArgList* a) {
    527   TokVec exp = {0};
    528   u32* exp_start;
    529   u32 i;
    530   exp_start = arena_array(pp->xarena, u32, a->n_args + 1);
    531   exp_start[0] = 0;
    532   for (i = 0; i < a->n_args; ++i) {
    533     u32 lo = a->raw_start[i];
    534     u32 hi = a->raw_start[i + 1];
    535     if (hi > lo) {
    536       /* Copy the slice into a fresh buffer so expand_arg_to_eof can
    537        * own it without aliasing. */
    538       Tok* slice = arena_array(pp->xarena, Tok, hi - lo);
    539       memcpy(slice, &a->raw[lo], sizeof(Tok) * (hi - lo));
    540       expand_arg_to_eof(pp, slice, hi - lo, &exp);
    541     }
    542     exp_start[i + 1] = exp.n;
    543   }
    544   a->exp = exp.data;
    545   a->exp_n = exp.n;
    546   a->exp_start = exp_start;
    547 }
    548 
    549 /* Build a stringized TOK_STR from the unexpanded argument tokens
    550  * `arg[lo..hi)`. The first token's leading-space flag is ignored (leading
    551  * whitespace stripped). Inside string/char-literal spellings, '"' and '\'
    552  * are escaped. */
    553 static Tok make_stringize(Pp* pp, const Tok* arg, u32 lo, u32 hi, LocRef loc) {
    554   CharBuf b = {0};
    555   u32 i;
    556   Tok t;
    557 
    558   cb_putc(pp, &b, '"');
    559   for (i = lo; i < hi; ++i) {
    560     const Tok* at = &arg[i];
    561     KitSlice sl = pp_text_slice(pp, at);
    562     const char* s = sl.s;
    563     size_t slen = sl.len;
    564     if (i > lo && (at->flags & TF_HAS_SPACE)) cb_putc(pp, &b, ' ');
    565     if (s && slen) {
    566       int esc = (at->kind == TOK_STR || at->kind == TOK_CHR);
    567       size_t k;
    568       for (k = 0; k < slen; ++k) {
    569         char c = s[k];
    570         if (esc && (c == '\\' || c == '"')) cb_putc(pp, &b, '\\');
    571         cb_putc(pp, &b, c);
    572       }
    573     }
    574   }
    575   cb_putc(pp, &b, '"');
    576 
    577   memset(&t, 0, sizeof(t));
    578   t.kind = TOK_STR;
    579   t.aux = 0;
    580   t.loc = loc;
    581   t.text = text_intern_ref(pp, (KitSlice){.s = b.data, .len = b.len});
    582   return t;
    583 }
    584 
    585 /* Concatenate two token spellings and re-lex into a single token. Empty
    586  * (placemarker) sides collapse to the other side per §6.10.3.3 ¶2. */
    587 static Tok paste_tokens(Pp* pp, Tok lhs, Tok rhs, LocRef loc) {
    588   char buf[1024];
    589   size_t alen = 0, blen = 0;
    590   const char* a;
    591   const char* b;
    592   Lexer* lex;
    593   Tok t1, t2;
    594 
    595   if (lhs.kind == TOK_PP_PLACEMARKER) return rhs;
    596   if (rhs.kind == TOK_PP_PLACEMARKER) return lhs;
    597 
    598   {
    599     KitSlice s = pp_text_slice(pp, &lhs);
    600     if (s.len) {
    601       a = s.s;
    602       alen = s.len;
    603     } else {
    604       a = "";
    605     }
    606   }
    607   {
    608     KitSlice s = pp_text_slice(pp, &rhs);
    609     if (s.len) {
    610       b = s.s;
    611       blen = s.len;
    612     } else {
    613       b = "";
    614     }
    615   }
    616   if (alen + blen + 2 > sizeof(buf)) {
    617     compiler_panic(pp->c, pp_materialize_loc(pp, loc),
    618                    "token paste: spelling too long");
    619   }
    620   if (alen) memcpy(buf, a, alen);
    621   if (blen) memcpy(buf + alen, b, blen);
    622   buf[alen + blen] = '\n';
    623   buf[alen + blen + 1] = 0;
    624   /* INVARIANT for the re-lex's no-splice fast path: this buffer is two
    625    * already-folded token spellings (a, b) followed by exactly one '\n'. A
    626    * token spelling never ends in a lone '\\' immediately before that '\n' (a
    627    * backslash-newline would have been folded out at the original lex), so the
    628    * buffer is free of `\<newline>` line-splices and the paste lexer is opened
    629    * with SRC_NO_SPLICES to skip the splice scan. */
    630 
    631   /* Intern the "<paste>" name once and reuse a single lexer across pastes: the
    632    * lexer is re-pointed (not reallocated) per paste, but still draws a fresh
    633    * sequential file_id so the file_id order matches a per-paste open exactly.
    634    * t1.loc is overwritten below, so the paste file_id is dead for output. */
    635   if (!pp->paste_name_sym) {
    636     pp->paste_name_sym = kit_sym_intern(pp->c, kit_slice_cstr("<paste>"));
    637   }
    638   {
    639     SourceSpec spec;
    640     memset(&spec, 0, sizeof spec);
    641     spec.name_sym = pp->paste_name_sym;
    642     spec.bytes = buf;
    643     spec.len = (u32)(alen + blen + 1);
    644     spec.flags = SRC_NO_SPLICES;
    645     if (pp->paste_lex) {
    646       lex_reset(pp->paste_lex, &spec);
    647     } else {
    648       pp->paste_lex = lex_open(pp->c, &spec);
    649     }
    650   }
    651   lex = pp->paste_lex;
    652   lex_next(lex, &t1);
    653   lex_next(lex, &t2);
    654   if (t1.kind == TOK_EOF) {
    655     /* Both empty (shouldn't reach here since we handled placemarkers). */
    656     return lhs;
    657   }
    658   if (t2.kind != TOK_NEWLINE && t2.kind != TOK_EOF) {
    659     compiler_panic(pp->c, pp_materialize_loc(pp, loc),
    660                    "token pasting yields multiple tokens, invalid");
    661   }
    662 
    663   /* The paste buffer is transient (a stack array re-pointed per paste), so a
    664    * source-derived spelling must be interned before the buffer is reused. */
    665   if (t1.text.kind == TEXT_SRC) {
    666     t1.text = text_intern_ref(pp, lex_text_slice(lex, t1.text));
    667   }
    668 
    669   /* Inherit positional flags from LHS (it sat in the same slot). */
    670   t1.flags = (u16)((t1.flags & ~(TF_AT_BOL | TF_HAS_SPACE)) |
    671                    (lhs.flags & (TF_AT_BOL | TF_HAS_SPACE)));
    672   t1.loc = loc;
    673   return t1;
    674 }
    675 
    676 /* Phase 1 (param substitution). For each parameter occurrence in the
    677  * body: if adjacent to ## or # (handled separately), substitute the raw
    678  * argument tokens; otherwise substitute the pre-expanded form. Empty raw
    679  * args become a TOK_PP_PLACEMARKER which phase 2 collapses.
    680  *
    681  * `no_paste` is the no-`##` fast path (set only when m->has_paste == 0, so the
    682  * body provably has no TOK_PP_PASTE and adj_paste is always false): instead of
    683  * emitting a placemarker for an empty arg, it accumulates the empty arg's
    684  * BOL/SPACE flags into a `carry` and ORs them onto the NEXT emitted token —
    685  * exactly what phase 2's placemarker-strip would do. The output is then already
    686  * placemarker-free and identity under phase 2, so substitute_body can skip the
    687  * second pass entirely. A trailing empty arg's carry has no surviving token to
    688  * land on and is dropped, matching phase 2 (which leaves a trailing carry
    689  * unused). */
    690 static void subst_phase1_impl(Pp* pp, const Macro* m, ArgList* a,
    691                               const Tok* invoke, TokVec* out, int no_paste) {
    692   u32 j;
    693   u16 carry = 0; /* no_paste only: pending BOL/SPACE from stripped empty args */
    694   /* Reserve a cheap upper bound up front so the per-expansion output vector
    695    * doesn't re-grow from cap 0 (0->8->16->32 doubling memcpys); arg expansion
    696    * may exceed it, which just falls back to the doubling. */
    697   tv_grow(pp, out, m->body_len + 4u);
    698   for (j = 0; j < m->body_len; ++j) {
    699     const Tok* bt = &m->body[j];
    700     if (bt->kind == TOK_PP_HASH) {
    701       /* §6.10.3.2: # must be followed by a parameter. EXCEPT under
    702        * assembler-with-cpp (__ASSEMBLER__ defined), where a `#` not before a
    703        * parameter is the assembler's immediate prefix (e.g. `cmp r0, #0` inside
    704        * a function-like macro) — pass it through literally, matching gcc/clang.
    705        * It is emitted mid-line, so it never starts a directive downstream. */
    706       if (j + 1 >= m->body_len || m->body[j + 1].kind != TOK_PP_PARAM) {
    707         if (mt_get(pp, pp->sym_assembler)) {
    708           Tok lit = *bt;
    709           if (carry) {
    710             lit.flags |= carry;
    711             carry = 0;
    712           }
    713           tv_push(pp, out, lit);
    714           continue;
    715         }
    716         compiler_panic(pp->c, pp_materialize_loc(pp, bt->loc),
    717                        "'#' is not followed by a macro parameter");
    718       }
    719       {
    720         u32 p = m->body[j + 1].aux;
    721         u32 lo = a->raw_start[p];
    722         u32 hi = a->raw_start[p + 1];
    723         Tok s = make_stringize(pp, a->raw, lo, hi, invoke->loc);
    724         s.flags = (u16)((s.flags & ~(TF_AT_BOL | TF_HAS_SPACE)) |
    725                         (bt->flags & (TF_AT_BOL | TF_HAS_SPACE)));
    726         if (carry) {
    727           s.flags |= carry;
    728           carry = 0;
    729         }
    730         tv_push(pp, out, s);
    731         ++j;
    732         continue;
    733       }
    734     }
    735     if (bt->kind == TOK_PP_PARAM) {
    736       u32 p = bt->aux;
    737       int adj_paste =
    738           (j > 0 && m->body[j - 1].kind == TOK_PP_PASTE) ||
    739           (j + 1 < m->body_len && m->body[j + 1].kind == TOK_PP_PASTE);
    740 
    741       u32 lo, hi;
    742       if (adj_paste) {
    743         lo = a->raw_start[p];
    744         hi = a->raw_start[p + 1];
    745       } else {
    746         lo = a->exp_start[p];
    747         hi = a->exp_start[p + 1];
    748       }
    749 
    750       if (lo == hi) {
    751         if (no_paste) {
    752           /* No-`##` fast path: a placemarker only ever matters as a phase-2
    753            * strip carrier, so skip it and accumulate its BOL/SPACE for the
    754            * next emitted token (identical to phase 2's strip carry). */
    755           carry |= bt->flags & (TF_AT_BOL | TF_HAS_SPACE);
    756         } else {
    757           /* Empty argument → placemarker. */
    758           Tok pm;
    759           memset(&pm, 0, sizeof(pm));
    760           pm.kind = TOK_PP_PLACEMARKER;
    761           pm.flags = bt->flags & (TF_AT_BOL | TF_HAS_SPACE);
    762           pm.loc = invoke->loc;
    763           tv_push(pp, out, pm);
    764         }
    765       } else {
    766         u32 k;
    767         int first = 1;
    768         Tok* src = adj_paste ? a->raw : a->exp;
    769         for (k = lo; k < hi; ++k) {
    770           Tok t = src[k];
    771           if (first) {
    772             t.flags = (u16)((t.flags & ~(TF_AT_BOL | TF_HAS_SPACE)) |
    773                             (bt->flags & (TF_AT_BOL | TF_HAS_SPACE)));
    774             first = 0;
    775           }
    776           if (carry) {
    777             t.flags |= carry;
    778             carry = 0;
    779           }
    780           tv_push(pp, out, t);
    781         }
    782       }
    783       continue;
    784     }
    785     if (carry) {
    786       Tok t = *bt;
    787       t.flags |= carry;
    788       carry = 0;
    789       tv_push(pp, out, t);
    790     } else {
    791       tv_push(pp, out, *bt);
    792     }
    793   }
    794   /* A trailing carry (empty arg(s) at the very end with no following token)
    795    * is dropped, matching phase 2: its strip pass leaves a trailing carry with
    796    * no surviving token to OR onto. */
    797   (void)carry;
    798 }
    799 
    800 /* Phase 1 in the general (placemarker-emitting) form; phase 2 follows. */
    801 static void subst_phase1(Pp* pp, const Macro* m, ArgList* a, const Tok* invoke,
    802                          TokVec* out) {
    803   subst_phase1_impl(pp, m, a, invoke, out, /*no_paste=*/0);
    804 }
    805 
    806 /* Phase 2 (paste). Walk the post-substitute buffer; for each TOK_PP_PASTE,
    807  * splice the previous output token with the next input token. Then strip
    808  * remaining placemarkers. */
    809 static void subst_phase2(Pp* pp, const Tok* in, u32 nin, const Tok* invoke,
    810                          TokVec* out) {
    811   u32 i;
    812   /* Track whether any placemarker actually lands in `out`. The strip pass
    813    * below is a full O(out->n) re-walk + element-wise compaction; the common
    814    * macro body has no empty-arg/paste placemarkers, so when none was emitted
    815    * we skip the second walk entirely. */
    816   int had_pm = 0;
    817   /* Phase-2 output is at most the input length (paste/placemarker only
    818    * shrink); reserve it to avoid re-growing from cap 0. */
    819   tv_grow(pp, out, nin);
    820   for (i = 0; i < nin; ++i) {
    821     Tok t = in[i];
    822     if (t.kind == TOK_PP_PASTE) {
    823       Tok lhs, rhs, pasted;
    824       if (out->n == 0 || i + 1 >= nin) {
    825         compiler_panic(pp->c, pp_materialize_loc(pp, invoke->loc),
    826                        "'##' at start or end of replacement list");
    827       }
    828       lhs = out->data[--out->n];
    829       rhs = in[++i];
    830       /* paste_tokens collapses a placemarker operand to the other side, so
    831        * the result is a placemarker only when both operands were. */
    832       pasted = paste_tokens(pp, lhs, rhs, invoke->loc);
    833       if (pasted.kind == TOK_PP_PLACEMARKER) had_pm = 1;
    834       tv_push(pp, out, pasted);
    835       continue;
    836     }
    837     if (t.kind == TOK_PP_PLACEMARKER) had_pm = 1;
    838     tv_push(pp, out, t);
    839   }
    840   if (!had_pm) return; /* no placemarker to strip — skip the compaction walk */
    841   /* Strip placemarkers, preserving leading-space flag on the next token. */
    842   {
    843     u32 r = 0, w = 0;
    844     u16 carry = 0;
    845     for (r = 0; r < out->n; ++r) {
    846       if (out->data[r].kind == TOK_PP_PLACEMARKER) {
    847         carry |= out->data[r].flags & (TF_AT_BOL | TF_HAS_SPACE);
    848         continue;
    849       }
    850       if (carry) {
    851         out->data[r].flags |= carry;
    852         carry = 0;
    853       }
    854       if (w != r) out->data[w] = out->data[r];
    855       ++w;
    856     }
    857     out->n = w;
    858   }
    859 }
    860 
    861 /* Wrapper: phases 1 and 2 in sequence, plus invocation-loc / flag transfer. */
    862 static void substitute_body(Pp* pp, const Macro* m, ArgList* a,
    863                             const Tok* invoke, TokVec* out) {
    864   TokVec phase1 = {0};
    865   u32 i;
    866   if (!m->has_paste) {
    867     /* No `##` in the body: phase 2 (paste + placemarker strip) is the identity
    868      * on phase-1 output once empty-arg placemarkers are elided. Run phase 1 in
    869      * its no_paste mode straight into `out`, skipping the second full-body copy
    870      * + re-walk. Byte-identical: no_paste carries empty-arg BOL/SPACE forward
    871      * exactly as the strip pass would, and with no TOK_PP_PASTE there is no
    872      * paste work to do. */
    873     subst_phase1_impl(pp, m, a, invoke, out, /*no_paste=*/1);
    874   } else {
    875     subst_phase1(pp, m, a, invoke, &phase1);
    876     subst_phase2(pp, phase1.data, phase1.n, invoke, out);
    877   }
    878   /* Invocation flags onto first emitted token. */
    879   if (out->n) {
    880     out->data[0].flags =
    881         (u16)((out->data[0].flags & ~(TF_AT_BOL | TF_HAS_SPACE)) |
    882               (invoke->flags & (TF_AT_BOL | TF_HAS_SPACE)));
    883   }
    884   /* Locations to invocation site. */
    885   for (i = 0; i < out->n; ++i) out->data[i].loc = invoke->loc;
    886 }
    887 
    888 /* Expand a function-like macro invocation: peek for `(`, collect args,
    889  * pre-expand them, substitute the body, push the result. Returns 1 if
    890  * the invocation was performed, 0 if there was no `(` (the caller should
    891  * emit the identifier as-is). */
    892 static int try_expand_func_macro(Pp* pp, Macro* m, const Tok* invoke) {
    893   int saw_ws;
    894   ArgList args;
    895   TokVec body = {0};
    896   Tok close_tok;
    897 
    898   if (!peek_for_invoke_paren(pp, &saw_ws)) {
    899     return 0;
    900   }
    901   (void)saw_ws;
    902   read_invocation_args(pp, m, invoke->loc, &args);
    903   /* Note: assigned to silence unused-result; we don't use the close tok yet. */
    904   close_tok.kind = 0;
    905   (void)close_tok;
    906   preexpand_args(pp, &args);
    907 
    908   substitute_body(pp, m, &args, invoke, &body);
    909 
    910   /* Push the substituted body as a disabled frame owned by `m`: the macro is
    911    * unavailable for the whole rescan of this replacement (cpplib model), so a
    912    * recursive occurrence of `m` is returned permanently un-expanded. The
    913    * arguments were already pre-expanded above with `m` NOT disabled. */
    914   push_buf_uniform(pp, body.data, body.n, m);
    915   return 1;
    916 }
    917 
    918 /* ============================================================
    919  * pp_next_raw — mutual recursion entry (called from expand_arg_to_eof)
    920  * Defined here; also declared in pp_priv.h so pp.c can call it.
    921  * ============================================================ */
    922 
    923 /* pp_pull_into: shared core of pp_next_raw / pp_next_into. Reads from the
    924  * top source into *out, applies macro expansion when an identifier names a
    925  * macro that isn't blue-painted, and consumes directives in-place.
    926  *
    927  * `skip_nl` short-circuits the parser-facing newline drop ONE FRAME LOWER: the
    928  * public pp_next (the C-parser feed) discards every non-directive TOK_NEWLINE
    929  * at the top of its loop, and 51% of all produced tokens are exactly those
    930  * newlines. When skip_nl is set, such a newline is consumed here (the loop just
    931  * continues) instead of being returned and round-tripped through pp_next's sret
    932  * — the token never escapes this frame. This is sound because a TOK_NEWLINE can
    933  * never be a directive introducer (TOK_PP_HASH), a macro-name TOK_IDENT, or
    934  * TOK_EOF, so in the un-skipped path it falls straight through to the trailing
    935  * `return`; continuing the loop instead is byte-identical to the caller having
    936  * read and dropped it, modulo the (idempotent) arena-reset re-check at the loop
    937  * head. skip_nl is set ONLY by pp_next, which never runs during #if expansion,
    938  * so the in_if_expansion `defined`-operator state machine below is unaffected.
    939  * pp_emit_text and the pragma-swallow / hash-peek paths pass skip_nl=0 so they
    940  * still observe newlines for -E line reconstruction and directive termination.
    941  *
    942  * Tok is 24B, so the by-value form (pp_next_raw) returns indirectly via sret
    943  * and pays a 24B inter-frame copy at every `return t`. Writing through `out`
    944  * lets the token — and lex_next's own sret — land in the caller's slot
    945  * directly. The hot SRC_BUF case (macro replay) is inlined here so the common
    946  * token never pays a call into src_next_raw_into. */
    947 static void pp_pull_into(Pp* pp, Tok* out, int skip_nl) {
    948   u8 src_kind;
    949   for (;;) {
    950     TokSrc* s;
    951     /* Reclaim the transient expansion scratch whenever the source stack has
    952      * drained back to a lexer. A SRC_BUF always sits above every SRC_LEX (a
    953      * macro can't #include), so a lexer on top means no expansion buffer is
    954      * live -- nothing in xarena is referenced, and the previously returned
    955      * token was a value copy with a pool-interned spelling. This is the only
    956      * reset site; it never fires mid-expansion (a SRC_BUF, incl. the
    957      * scope_top arg-prescan buffer, is on top then) nor during raw argument
    958      * collection (which uses src_next_raw, not this function). Skipped inside
    959      * #if expansion, whose bounded condition keeps its scratch live. The reset
    960      * is further skipped when the xarena is already at its baseline (a plain
    961      * lexer run that allocated no scratch): kit_arena_is_empty is the pristine
    962      * guard, so the dead reset on plain-token runs is elided -- byte-identical
    963      * since resetting an already-pristine arena does nothing. */
    964     if (!pp->in_if_expansion &&
    965         (pp->nsources == 0 || pp->sources[pp->nsources - 1].kind == SRC_LEX)) {
    966       if (!kit_arena_is_empty(pp->xarena)) kit_arena_reset(pp->xarena);
    967     }
    968     /* Fast path: top source is a non-exhausted SRC_BUF (the dominant
    969      * macro-replay case). Pull the token and kind inline so the common token
    970      * avoids the call + sret setup of src_next_raw_into. The scope_top-EOF and
    971      * #line-delta cases never apply to a plain in-bounds SRC_BUF read, so they
    972      * stay on the general (cold) path. */
    973     if (pp->nsources != 0 &&
    974         (s = &pp->sources[pp->nsources - 1])->kind == SRC_BUF && s->i < s->n) {
    975       *out = s->toks[s->i];
    976       src_kind = SRC_BUF;
    977       /* Pointer-replayed body: apply the per-invocation loc and (first token
    978        * only) BOL/SPACE flags the old fresh-copy path baked in. Identical to
    979        * the slow read site in src_next_raw_into so both paths agree. */
    980       if (s->has_loc_override) {
    981         if (s->i == 0) {
    982           out->flags = (u16)((out->flags & ~(TF_AT_BOL | TF_HAS_SPACE)) |
    983                              s->first_flags_or);
    984         }
    985         out->loc = s->loc_override;
    986       }
    987       ++s->i;
    988     } else {
    989       src_next_raw_into(pp, out, &src_kind);
    990     }
    991     if (out->kind == TOK_EOF) return;
    992     /* §C.3a/§C.3b: absorb the parser-facing non-directive newline drop here so
    993      * it never round-trips up through pp_next's sret. A newline is whitespace,
    994      * not a directive introducer, so dropping it before the directive / macro
    995      * checks below is the same as pp_next dropping it after the return.
    996      *
    997      * §C.3b drains a run of newlines in place via src_next_raw_into rather than
    998      * `continue`-ing the outer loop per newline. The outer loop's only
    999      * per-iteration work before this point is the xarena reclaim
   1000      * (kit_arena_reset, a real call) and the SRC_BUF fast-path probe; for a
   1001      * newline both are redundant — the source stack is unchanged between a
   1002      * newline and the token that follows it (a newline never pushes a source,
   1003      * and a SRC_BUF can only sit *above* the SRC_LEX a file newline came from,
   1004      * so it is not on top here). Skipping them is byte-identical to the §C.3a
   1005      * `continue` while removing one kit_arena_reset call + probe per dropped
   1006      * newline (51% of produced tokens). The drained token re-enters the normal
   1007      * directive / macro flow below, so a `#` or content token after the
   1008      * newline is handled exactly as before. */
   1009     if (skip_nl && out->kind == TOK_NEWLINE) {
   1010       do {
   1011         src_next_raw_into(pp, out, &src_kind);
   1012       } while (out->kind == TOK_NEWLINE);
   1013       if (out->kind == TOK_EOF) return;
   1014     }
   1015     if (out->kind == TOK_PP_HASH && (out->flags & TF_AT_BOL) &&
   1016         src_kind == SRC_LEX) {
   1017       process_directive(pp, out->loc);
   1018       /* No synthesized newline: the comparator collapses
   1019        * whitespace, so blank-line replacement of consumed
   1020        * directives isn't observable here. Directives that produce
   1021        * content (e.g. #include, #embed, #pragma) push their own
   1022        * tokens onto the source stack, which the next loop
   1023        * iteration picks up. */
   1024       continue;
   1025     }
   1026     /* While expanding an #if condition, suppress macro expansion of
   1027      * `defined`-operator operands so a `defined(X)` produced by a
   1028      * macro body whose argument was pasted via ## doesn't accidentally
   1029      * expand an already-defined X to its body (typically empty). See
   1030      * the `defined_skip` field comment in pp_priv.h. */
   1031     if (pp->in_if_expansion) {
   1032       if (pp->defined_skip == 2) {
   1033         if (out->kind == TOK_PUNCT && tok_punct(out) == '(') {
   1034           pp->defined_skip = 3;
   1035         } else if (out->kind == TOK_IDENT) {
   1036           /* `defined IDENT` (no parens): mark the operand and reset. */
   1037           out->flags |= TF_NO_EXPAND;
   1038           pp->defined_skip = 0;
   1039         } else {
   1040           pp->defined_skip = 0;
   1041         }
   1042       } else if (pp->defined_skip == 3) {
   1043         if (out->kind == TOK_IDENT) {
   1044           out->flags |= TF_NO_EXPAND;
   1045           pp->defined_skip = 4;
   1046         } else if (out->kind == TOK_PUNCT && tok_punct(out) == ')') {
   1047           pp->defined_skip = 0;
   1048         }
   1049       } else if (pp->defined_skip == 4) {
   1050         if (out->kind == TOK_PUNCT && tok_punct(out) == ')') {
   1051           pp->defined_skip = 0;
   1052         }
   1053       } else if (out->kind == TOK_IDENT && tok_ident(out) == pp->sym_defined) {
   1054         pp->defined_skip = 2;
   1055       }
   1056     }
   1057     if (out->kind == TOK_IDENT && (out->flags & TF_NO_EXPAND) == 0) {
   1058       Sym id = tok_ident(out);
   1059 
   1060       /* Dynamic predefined macros: __LINE__ / __FILE__ /
   1061        * __DATE__ / __TIME__. Always expand, ignoring the macro
   1062        * table. */
   1063       if (id == pp->sym_line__) {
   1064         char tmp[16], buf[16];
   1065         int k = 0, j = 0;
   1066         u32 ln = pp_materialize_loc(pp, out->loc).line;
   1067         if (ln == 0)
   1068           buf[k++] = '0';
   1069         else {
   1070           while (ln) {
   1071             tmp[j++] = (char)('0' + ln % 10);
   1072             ln /= 10;
   1073           }
   1074           while (j > 0) buf[k++] = tmp[--j];
   1075         }
   1076         out->kind = TOK_NUM;
   1077         out->aux = 0;
   1078         out->text = text_intern_ref(pp, (KitSlice){.s = buf, .len = (size_t)k});
   1079         return;
   1080       }
   1081       if (id == pp->sym_file__) {
   1082         /* pp_materialize_file resolves the __FILE__ name at this location,
   1083          * applying any active #line overlay; no current-lexer probe needed. */
   1084         Sym name = pp_materialize_file(pp, out->loc);
   1085         size_t nlen = 0;
   1086         const char* nstr = NULL;
   1087         char* buf;
   1088         if (name) {
   1089           KitSlice s = kit_sym_str(pp->pool->c, name);
   1090           nstr = s.s;
   1091           nlen = s.len;
   1092         }
   1093         /* The source name is the raw filesystem path (or a #line override,
   1094          * destringized to logical bytes by do_line). Re-stringize it as a
   1095          * valid C string literal: escape '\\' and '"'. On POSIX paths use
   1096          * '/' so this was a no-op; on Windows the path holds backslashes
   1097          * (e.g. C:\\Users\\...), and emitting them raw turns '\\U'/'\\u'/'\\x'
   1098          * into bogus escape sequences (the "malformed UCN" on '\\Users'). */
   1099         {
   1100           size_t bn = 0;
   1101           size_t i;
   1102           buf = (char*)arena_alloc(pp->xarena, nlen * 2 + 2, 1);
   1103           buf[bn++] = '"';
   1104           for (i = 0; i < nlen; ++i) {
   1105             char ch = nstr[i];
   1106             if (ch == '\\' || ch == '"') buf[bn++] = '\\';
   1107             buf[bn++] = ch;
   1108           }
   1109           buf[bn++] = '"';
   1110           out->kind = TOK_STR;
   1111           out->aux = 0;
   1112           out->text = text_intern_ref(pp, (KitSlice){.s = buf, .len = bn});
   1113         }
   1114         return;
   1115       }
   1116       if (id == pp->sym_date__) {
   1117         out->kind = TOK_STR;
   1118         out->aux = 0;
   1119         out->text = text_sym_ref(pp->val_date_str);
   1120         return;
   1121       }
   1122       if (id == pp->sym_time__) {
   1123         out->kind = TOK_STR;
   1124         out->aux = 0;
   1125         out->text = text_sym_ref(pp->val_time_str);
   1126         return;
   1127       }
   1128       if (id == pp->sym__pragma) {
   1129         if (try_expand_pragma_op(pp, out)) continue;
   1130         /* No '(' — fall through and emit as plain ident. */
   1131       }
   1132 
   1133       {
   1134         Macro* m = mt_get(pp, id);
   1135         if (m && m->disabled_depth != 0) {
   1136           /* cpplib: the macro is disabled because one of its own
   1137            * replacement-list frames is still being rescanned. Mark this
   1138            * occurrence permanently un-expandable, so the bit survives if this
   1139            * token is later captured as an argument, substituted into another
   1140            * macro, or replayed (this is what keeps `foo(foo)(1)` and
   1141            * `id(foo(foo)(1))` as `foo(1)`). */
   1142           out->flags |= TF_NO_EXPAND;
   1143           return;
   1144         }
   1145         if (m) {
   1146           if (!m->is_func) {
   1147             expand_object_macro(pp, m, out);
   1148             continue;
   1149           }
   1150           if (try_expand_func_macro(pp, m, out)) {
   1151             continue;
   1152           }
   1153           /* Function-like macro name NOT followed by '(': emit as a plain
   1154            * identifier WITHOUT setting TF_NO_EXPAND, so a later rescan that
   1155            * sees a '(' supplied by another macro can still expand it (keeps
   1156            * `id(f L 1))` with `#define L (` able to become `1`). */
   1157         }
   1158       }
   1159     }
   1160     return;
   1161   }
   1162 }
   1163 
   1164 /* pp_next_raw: public out-pointer form (pp.h), every token surfaced
   1165  * (TOK_NEWLINE preserved for pp_emit_text and for directive/pragma line
   1166  * termination). The mutual-recursion entry: expand_arg_to_eof and the -E loop
   1167  * call it; it drives directives and expansion. */
   1168 void pp_next_raw(Pp* pp, Tok* out) { pp_pull_into(pp, out, /*skip_nl=*/0); }
   1169 
   1170 /* pp_next_into: out-pointer form for the C-parser feed (pp_next_parse).
   1171  * Identical to pp_next_raw but drops non-directive newlines internally, so the
   1172  * 51% of produced tokens that pp_next_parse would otherwise discard never
   1173  * escape this frame. Declared in pp_priv.h. */
   1174 void pp_next_into(Pp* pp, Tok* out) { pp_pull_into(pp, out, /*skip_nl=*/1); }