kit

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

pp_directive.c (60678B)


      1 /* pp_directive.c — if-stack, PP expression evaluator, #include search/open,
      2  * #line, #pragma, #error, #embed, and directive dispatch. */
      3 
      4 #include "pp/pp_priv.h"
      5 
      6 static void destringize(Pp* pp, const Tok* str_tok, char* out, size_t cap,
      7                         size_t* out_len);
      8 static void pp_warn(Pp* pp, SrcLoc loc, const char* fmt, ...);
      9 
     10 /* ============================================================
     11  * If-stack
     12  * ============================================================ */
     13 
     14 static void if_push(Pp* pp, IfFrame f) {
     15   if (pp->ifstk_n == pp->ifstk_cap) {
     16     u32 nc = pp->ifstk_cap ? pp->ifstk_cap * 2 : 4;
     17     pp->ifstk = pp_xrealloc(pp, pp->ifstk, sizeof(IfFrame) * pp->ifstk_cap,
     18                             sizeof(IfFrame) * nc, _Alignof(IfFrame));
     19     pp->ifstk_cap = nc;
     20   }
     21   pp->ifstk[pp->ifstk_n++] = f;
     22 }
     23 
     24 static IfFrame* if_top(Pp* pp) {
     25   return pp->ifstk_n ? &pp->ifstk[pp->ifstk_n - 1] : NULL;
     26 }
     27 
     28 static void if_pop(Pp* pp) {
     29   if (pp->ifstk_n) --pp->ifstk_n;
     30 }
     31 
     32 /* ============================================================
     33  * Directive line reader
     34  * ============================================================ */
     35 
     36 /* Read tokens up through (and including) the next TOK_NEWLINE / TOK_EOF.
     37  * Drops the newline; collected tokens are arena-allocated and returned via
     38  * *out_toks/out_n. */
     39 void read_directive_line(Pp* pp, Tok** out_toks, u32* out_n) {
     40   Tok* buf = NULL;
     41   u32 cap = 0, n = 0;
     42   Tok t;
     43   u8 saved_rd = pp->reading_directive;
     44   pp->reading_directive = 1;
     45   for (;;) {
     46     t = src_next_raw(pp, NULL);
     47     if (t.kind == TOK_NEWLINE || t.kind == TOK_EOF) break;
     48     if (n == cap) {
     49       u32 nc = cap ? cap * 2 : 8;
     50       Tok* nb = (Tok*)arena_alloc(pp->arena, sizeof(Tok) * nc, _Alignof(Tok));
     51       if (cap) memcpy(nb, buf, sizeof(Tok) * cap);
     52       buf = nb;
     53       cap = nc;
     54     }
     55     buf[n++] = t;
     56   }
     57   pp->reading_directive = saved_rd;
     58   *out_toks = buf;
     59   *out_n = n;
     60 }
     61 
     62 /* ============================================================
     63  * PP expression evaluator (§6.10.1)
     64  * ============================================================ */
     65 
     66 /* Parse a C integer constant from a pp-number's spelling. Suffixes (u, l,
     67  * etc.) are ignored. Recognizes decimal, hex (0x...), and octal (0...). */
     68 static i64 parse_pp_int(const char* s, size_t n) {
     69   int base = 10;
     70   size_t i = 0;
     71   u64 val =
     72       0; /* unsigned: #if arithmetic wraps on overflow, signed would be UB */
     73   if (n >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
     74     base = 16;
     75     i = 2;
     76   } else if (n >= 1 && s[0] == '0') {
     77     base = 8;
     78     i = 1;
     79   }
     80   for (; i < n; ++i) {
     81     char c = s[i];
     82     int d;
     83     if (c >= '0' && c <= '9')
     84       d = c - '0';
     85     else if (base == 16 && c >= 'a' && c <= 'f')
     86       d = c - 'a' + 10;
     87     else if (base == 16 && c >= 'A' && c <= 'F')
     88       d = c - 'A' + 10;
     89     else
     90       break;
     91     if (d >= base) break;
     92     val = val * (u64)base + (u64)d;
     93   }
     94   return (i64)val;
     95 }
     96 
     97 /* Pre-pass: replace `defined X` / `defined ( X )` with a 0/1 pp-number,
     98  * preserving the rest of the token sequence. The operand of `defined` is
     99  * NOT macro-expanded. Output is a fresh TokVec. */
    100 static void prepass_defined(Pp* pp, const Tok* in, u32 nin, TokVec* out) {
    101   u32 i;
    102   for (i = 0; i < nin; ++i) {
    103     if (in[i].kind == TOK_IDENT && tok_ident(&in[i]) == pp->sym_defined) {
    104       int has_paren = 0;
    105       Sym ident = 0;
    106       u32 j = i + 1;
    107       if (j < nin && in[j].kind == TOK_PUNCT && tok_punct(&in[j]) == '(') {
    108         has_paren = 1;
    109         ++j;
    110       }
    111       if (j >= nin || in[j].kind != TOK_IDENT) {
    112         compiler_panic(pp->c, pp_materialize_loc(pp, in[i].loc),
    113                        "operand of 'defined' must be an identifier");
    114       }
    115       ident = tok_ident(&in[j]);
    116       ++j;
    117       if (has_paren) {
    118         if (j >= nin || in[j].kind != TOK_PUNCT || tok_punct(&in[j]) != ')') {
    119           compiler_panic(pp->c, pp_materialize_loc(pp, in[i].loc),
    120                          "expected ')' after 'defined' operand");
    121         }
    122         ++j;
    123       }
    124       {
    125         Tok t;
    126         /* `__has_include` / `__has_include_next` are not macros (their operand
    127          * isn't a normal token sequence) but, like clang/GCC, they answer
    128          * `defined()` with 1 so the guarded idiom
    129          *   #if defined(__has_include) && __has_include(<x>)
    130          * — common in system headers — takes its intended branch. */
    131         int is_defined = mt_get(pp, ident) != NULL ||
    132                          ident == pp->sym_has_include ||
    133                          ident == pp->sym_has_include_next;
    134         memset(&t, 0, sizeof(t));
    135         t.kind = TOK_NUM;
    136         t.flags = in[i].flags & (TF_AT_BOL | TF_HAS_SPACE);
    137         t.loc = in[i].loc;
    138         t.aux = 0;
    139         t.text = text_intern_ref(
    140             pp, is_defined ? kit_slice_cstr("1") : kit_slice_cstr("0"));
    141         tv_push(pp, out, t);
    142       }
    143       i = j - 1;
    144     } else {
    145       tv_push(pp, out, in[i]);
    146     }
    147   }
    148 }
    149 
    150 /* Macro-expand a sequence of pre-#if tokens to completion.
    151  *
    152  * Sets pp->in_if_expansion for the duration so pp_next_raw can keep
    153  * `defined`-operator operands raw even when they ride out of a macro
    154  * body via the ## operator. Without this flag a macro body like
    155  *   #define G(x) (!defined(__G_DEFINED_ ## x))
    156  * would have the pasted operand expanded if it happens to name an
    157  * already-defined macro, leaving the second prepass to choke on
    158  * `defined()`. */
    159 static void expand_for_if(Pp* pp, const Tok* in, u32 nin, TokVec* out) {
    160   Tok* slice;
    161   u8 saved;
    162   if (nin == 0) return;
    163   slice = arena_array(pp->arena, Tok, nin);
    164   memcpy(slice, in, sizeof(Tok) * nin);
    165   saved = pp->in_if_expansion;
    166   pp->in_if_expansion = 1;
    167   pp->defined_skip = 0;
    168   expand_arg_to_eof(pp, slice, nin, out);
    169   pp->in_if_expansion = saved;
    170   pp->defined_skip = 0;
    171 }
    172 
    173 /* Replace remaining identifiers with `0` per §6.10.1 ¶4, after `defined`
    174  * has been handled. */
    175 static void replace_remaining_if_identifiers(Pp* pp, TokVec* toks) {
    176   u32 i;
    177   TextRef zero = text_intern_ref(pp, kit_slice_cstr("0"));
    178   for (i = 0; i < toks->n; ++i) {
    179     if (toks->data[i].kind == TOK_IDENT) {
    180       toks->data[i].kind = TOK_NUM;
    181       toks->data[i].aux = 0;
    182       toks->data[i].text = zero;
    183     }
    184   }
    185 }
    186 
    187 /* Recursive-descent expression evaluator over an expanded token list. */
    188 typedef struct EE {
    189   Pp* pp;
    190   const Tok* toks;
    191   u32 n;
    192   u32 pos;
    193   SrcLoc loc;
    194   SrcLoc op_loc; /* loc of the binary operator being applied (for panics) */
    195 } EE;
    196 
    197 static i64 ee_ternary(EE* e);
    198 
    199 static const Tok* ee_peek(EE* e) {
    200   return e->pos < e->n ? &e->toks[e->pos] : NULL;
    201 }
    202 
    203 static int ee_match_punct(EE* e, u32 p) {
    204   const Tok* t = ee_peek(e);
    205   if (t && t->kind == TOK_PUNCT && tok_punct(t) == p) {
    206     ++e->pos;
    207     return 1;
    208   }
    209   return 0;
    210 }
    211 
    212 static i64 ee_primary(EE* e) {
    213   const Tok* t = ee_peek(e);
    214   if (!t) compiler_panic(e->pp->c, e->loc, "#if: missing operand");
    215   if (t->kind == TOK_NUM) {
    216     KitSlice s = pp_text_slice(e->pp, t);
    217     ++e->pos;
    218     return parse_pp_int(s.s, s.len);
    219   }
    220   if (t->kind == TOK_CHR) {
    221     /* Treat as the codepoint of the first character (post-decoding
    222      * not implemented; cover the common case of a single ASCII
    223      * char). */
    224     KitSlice s = pp_text_slice(e->pp, t);
    225     ++e->pos;
    226     if (s.len >= 3 && s.s[0] == '\'') return (unsigned char)s.s[1];
    227     return 0;
    228   }
    229   if (t->kind == TOK_PUNCT && tok_punct(t) == '(') {
    230     i64 v;
    231     ++e->pos;
    232     v = ee_ternary(e);
    233     if (!ee_match_punct(e, ')')) {
    234       compiler_panic(e->pp->c, pp_materialize_loc(e->pp, t->loc),
    235                      "#if: expected ')'");
    236     }
    237     return v;
    238   }
    239   compiler_panic(e->pp->c, pp_materialize_loc(e->pp, t->loc),
    240                  "#if: unexpected token in expression");
    241   return 0;
    242 }
    243 
    244 static i64 ee_unary(EE* e) {
    245   const Tok* t = ee_peek(e);
    246   if (t && t->kind == TOK_PUNCT) {
    247     u32 p = tok_punct(t);
    248     if (p == '!' || p == '-' || p == '+' || p == '~') {
    249       i64 v;
    250       ++e->pos;
    251       v = ee_unary(e);
    252       switch (p) {
    253         case '!':
    254           return v ? 0 : 1;
    255         case '-':
    256           return -v;
    257         case '+':
    258           return v;
    259         case '~':
    260           return ~v;
    261       }
    262     }
    263   }
    264   return ee_primary(e);
    265 }
    266 
    267 /* One row per binary operator, highest `prec` binds tightest. All listed
    268  * operators are left-associative; ternary (right-assoc) stays special-cased
    269  * in ee_ternary. `apply` folds (lhs OP rhs) and owns the div/mod-by-zero
    270  * panic (it needs the operator loc, threaded via EE::op_loc). */
    271 typedef i64 (*EeApply)(EE* e, i64 a, i64 b);
    272 
    273 static i64 eb_mul(EE* e, i64 a, i64 b) {
    274   (void)e;
    275   return a * b;
    276 }
    277 static i64 eb_div(EE* e, i64 a, i64 b) {
    278   if (b == 0) compiler_panic(e->pp->c, e->op_loc, "#if: division by zero");
    279   return a / b;
    280 }
    281 static i64 eb_mod(EE* e, i64 a, i64 b) {
    282   if (b == 0) compiler_panic(e->pp->c, e->op_loc, "#if: modulo by zero");
    283   return a % b;
    284 }
    285 static i64 eb_add(EE* e, i64 a, i64 b) {
    286   (void)e;
    287   return a + b;
    288 }
    289 static i64 eb_sub(EE* e, i64 a, i64 b) {
    290   (void)e;
    291   return a - b;
    292 }
    293 static i64 eb_shl(EE* e, i64 a, i64 b) {
    294   (void)e;
    295   return a << b;
    296 }
    297 static i64 eb_shr(EE* e, i64 a, i64 b) {
    298   (void)e;
    299   return a >> b;
    300 }
    301 static i64 eb_lt(EE* e, i64 a, i64 b) {
    302   (void)e;
    303   return a < b;
    304 }
    305 static i64 eb_gt(EE* e, i64 a, i64 b) {
    306   (void)e;
    307   return a > b;
    308 }
    309 static i64 eb_le(EE* e, i64 a, i64 b) {
    310   (void)e;
    311   return a <= b;
    312 }
    313 static i64 eb_ge(EE* e, i64 a, i64 b) {
    314   (void)e;
    315   return a >= b;
    316 }
    317 static i64 eb_eq(EE* e, i64 a, i64 b) {
    318   (void)e;
    319   return a == b;
    320 }
    321 static i64 eb_ne(EE* e, i64 a, i64 b) {
    322   (void)e;
    323   return a != b;
    324 }
    325 static i64 eb_band(EE* e, i64 a, i64 b) {
    326   (void)e;
    327   return a & b;
    328 }
    329 static i64 eb_bxor(EE* e, i64 a, i64 b) {
    330   (void)e;
    331   return a ^ b;
    332 }
    333 static i64 eb_bor(EE* e, i64 a, i64 b) {
    334   (void)e;
    335   return a | b;
    336 }
    337 static i64 eb_logand(EE* e, i64 a, i64 b) {
    338   (void)e;
    339   return a && b;
    340 }
    341 static i64 eb_logor(EE* e, i64 a, i64 b) {
    342   (void)e;
    343   return a || b;
    344 }
    345 
    346 typedef struct EeOp {
    347   u32 punct; /* P_* / ASCII codepoint of the operator token */
    348   u8 prec;   /* higher binds tighter */
    349   EeApply apply;
    350 } EeOp;
    351 
    352 static const EeOp ee_ops[] = {
    353     {'*', 10, eb_mul},  {'/', 10, eb_div},     {'%', 10, eb_mod},
    354     {'+', 9, eb_add},   {'-', 9, eb_sub},      {P_SHL, 8, eb_shl},
    355     {P_SHR, 8, eb_shr}, {'<', 7, eb_lt},       {'>', 7, eb_gt},
    356     {P_LE, 7, eb_le},   {P_GE, 7, eb_ge},      {P_EQ, 6, eb_eq},
    357     {P_NE, 6, eb_ne},   {'&', 5, eb_band},     {'^', 4, eb_bxor},
    358     {'|', 3, eb_bor},   {P_AND, 2, eb_logand}, {P_OR, 1, eb_logor},
    359 };
    360 
    361 static const EeOp* ee_lookup_op(const Tok* t) {
    362   size_t i;
    363   if (!t || t->kind != TOK_PUNCT) return NULL;
    364   for (i = 0; i < sizeof(ee_ops) / sizeof(ee_ops[0]); ++i) {
    365     if (ee_ops[i].punct == tok_punct(t)) return &ee_ops[i];
    366   }
    367   return NULL;
    368 }
    369 
    370 /* Precedence-climbing fold of all left-associative binary operators. */
    371 static i64 ee_binary(EE* e, int min_prec) {
    372   i64 v = ee_unary(e);
    373   for (;;) {
    374     const Tok* t = ee_peek(e);
    375     const EeOp* op = ee_lookup_op(t);
    376     SrcLoc op_loc;
    377     i64 rhs;
    378     if (!op || op->prec < min_prec) break;
    379     op_loc = pp_materialize_loc(e->pp, t->loc);
    380     ++e->pos;
    381     /* Left-associative: parse the RHS with strictly higher precedence so
    382      * same-prec operators fold left-to-right. */
    383     rhs = ee_binary(e, op->prec + 1);
    384     e->op_loc = op_loc;
    385     v = op->apply(e, v, rhs);
    386   }
    387   return v;
    388 }
    389 
    390 static i64 ee_ternary(EE* e) {
    391   i64 c = ee_binary(e, 1);
    392   if (ee_match_punct(e, '?')) {
    393     i64 a = ee_ternary(e);
    394     i64 b;
    395     if (!ee_match_punct(e, ':')) {
    396       compiler_panic(e->pp->c, e->loc, "#if: ':' expected in ternary");
    397     }
    398     b = ee_ternary(e);
    399     return c ? a : b;
    400   }
    401   return c;
    402 }
    403 
    404 /* Header-resolution helpers used by the __has_include pre-pass below; the
    405  * definitions live further down with the #include machinery. */
    406 static void parse_include_path(Pp* pp, const Tok* line, u32 n, LocRef loc,
    407                                char* path_out, size_t cap, int* system_out);
    408 static int find_and_open_include(Pp* pp, const char* path, int system,
    409                                  LocRef loc, const u8** data, size_t* size,
    410                                  char* resolved, size_t resolved_cap,
    411                                  int* resolved_system_out, u32* next_start_out);
    412 static int find_and_open_include_next(Pp* pp, const char* path, u32 start,
    413                                       const u8** data, size_t* size,
    414                                       char* resolved, size_t resolved_cap,
    415                                       int* resolved_system_out,
    416                                       u32* next_start_out);
    417 
    418 /* Pre-pass: replace `__has_include(<h>)` / `__has_include("h")` (and the
    419  * `__has_include_next` variant) with the pp-number 1 or 0 per whether the
    420  * header resolves against the include search path — the same search a real
    421  * #include would perform. Runs BEFORE macro expansion so the `<...>` form's
    422  * `<` and `>` are never mistaken for comparison operators, and so the header
    423  * tokens are not subject to the surrounding #if's identifier→0 rewrite. The
    424  * operand itself is still macro-expanded (via parse_include_path), matching
    425  * the `__has_include(HEADER_MACRO)` idiom. Output is a fresh TokVec. */
    426 static void prepass_has_include(Pp* pp, const Tok* in, u32 nin, TokVec* out) {
    427   u32 i;
    428   for (i = 0; i < nin; ++i) {
    429     int is_has = 0, is_next = 0;
    430     u32 j, op_n, depth, s;
    431     const Tok* op_first;
    432     char path[4096];
    433     char resolved[4096];
    434     int system_form = 0, resolved_system = 0, present = 0;
    435     u32 next_start = 0, next_search = 0;
    436     const u8* data;
    437     size_t size;
    438     Tok t;
    439     const char* what;
    440 
    441     if (in[i].kind == TOK_IDENT) {
    442       if (tok_ident(&in[i]) == pp->sym_has_include)
    443         is_has = 1;
    444       else if (tok_ident(&in[i]) == pp->sym_has_include_next)
    445         is_has = is_next = 1;
    446     }
    447     if (!is_has) {
    448       tv_push(pp, out, in[i]);
    449       continue;
    450     }
    451     what = is_next ? "__has_include_next" : "__has_include";
    452 
    453     j = i + 1;
    454     if (j >= nin || in[j].kind != TOK_PUNCT || tok_punct(&in[j]) != '(') {
    455       compiler_panic(pp->c, pp_materialize_loc(pp, in[i].loc),
    456                      "expected '(' after %s", what);
    457     }
    458     ++j; /* past '(' */
    459     op_first = &in[j];
    460     depth = 1;
    461     op_n = 0;
    462     for (; j < nin; ++j, ++op_n) {
    463       if (in[j].kind == TOK_PUNCT && tok_punct(&in[j]) == '(') {
    464         ++depth;
    465       } else if (in[j].kind == TOK_PUNCT && tok_punct(&in[j]) == ')') {
    466         if (--depth == 0) break;
    467       }
    468     }
    469     if (j >= nin)
    470       compiler_panic(pp->c, pp_materialize_loc(pp, in[i].loc),
    471                      "unterminated %s", what);
    472     /* in[j] is the matching ')'. */
    473 
    474     parse_include_path(pp, op_first, op_n, in[i].loc, path, sizeof(path),
    475                        &system_form);
    476     if (is_next) {
    477       for (s = pp->nsources; s > 0; --s) {
    478         TokSrc* tp = &pp->sources[s - 1];
    479         if (tp->kind == SRC_LEX && tp->lex) {
    480           next_search = tp->inc_next_start;
    481           break;
    482         }
    483       }
    484       present = find_and_open_include_next(pp, path, next_search, &data, &size,
    485                                            resolved, sizeof(resolved),
    486                                            &resolved_system, &next_start);
    487     } else {
    488       present = find_and_open_include(pp, path, system_form, in[i].loc, &data,
    489                                       &size, resolved, sizeof(resolved),
    490                                       &resolved_system, &next_start);
    491     }
    492 
    493     memset(&t, 0, sizeof(t));
    494     t.kind = TOK_NUM;
    495     t.flags = in[i].flags & (TF_AT_BOL | TF_HAS_SPACE);
    496     t.loc = in[i].loc;
    497     t.aux = 0;
    498     t.text = text_intern_ref(
    499         pp, present ? kit_slice_cstr("1") : kit_slice_cstr("0"));
    500     tv_push(pp, out, t);
    501     i = j; /* resume past the matching ')' */
    502   }
    503 }
    504 
    505 i64 eval_if_expr(Pp* pp, const Tok* line, u32 n, LocRef loc) {
    506   TokVec defs = {0};
    507   TokVec hasinc = {0};
    508   TokVec exp = {0};
    509   TokVec defs2 = {0};
    510   EE e;
    511   i64 v;
    512 
    513   prepass_defined(pp, line, n, &defs);
    514   prepass_has_include(pp, defs.data, defs.n, &hasinc);
    515   expand_for_if(pp, hasinc.data, hasinc.n, &exp);
    516   prepass_defined(pp, exp.data, exp.n, &defs2);
    517   replace_remaining_if_identifiers(pp, &defs2);
    518 
    519   e.pp = pp;
    520   e.toks = defs2.data;
    521   e.n = defs2.n;
    522   e.pos = 0;
    523   e.loc = pp_materialize_loc(pp, loc);
    524   v = ee_ternary(&e);
    525   if (e.pos != e.n) {
    526     compiler_panic(pp->c, e.loc,
    527                    "#if: unexpected trailing tokens in expression");
    528   }
    529   return v;
    530 }
    531 
    532 /* ============================================================
    533  * Conditional inclusion helpers
    534  * ============================================================ */
    535 
    536 static void consume_to_newline(Pp* pp) {
    537   Tok t;
    538   do {
    539     t = src_next_raw(pp, NULL);
    540   } while (t.kind != TOK_NEWLINE && t.kind != TOK_EOF);
    541 }
    542 
    543 /* Drive the source forward consuming tokens until we either:
    544  *   - reach a balancing #endif (pops the frame, returns), or
    545  *   - reach a #elif / #else that flips the top frame to IF_INCLUDE
    546  *     (returns with that frame active).
    547  * Nested #if directives inside the skipped group are tracked via
    548  * `local_depth`. Unrecognised directives in skipped groups are tolerated
    549  * (§6.10 ¶4, covered by `8c_skipped_relaxed_syntax`). */
    550 static void skip_until_active(Pp* pp) {
    551   int local_depth = 0;
    552   while (pp->ifstk_n > 0) {
    553     IfFrame* top = if_top(pp);
    554     Tok t;
    555     if (top->state == IF_INCLUDE && local_depth == 0) return;
    556     t = src_next_raw(pp, NULL);
    557     if (t.kind == TOK_EOF) {
    558       compiler_panic(pp->c, pp_materialize_loc(pp, top->loc),
    559                      "unterminated #if / #ifdef");
    560     }
    561     if (t.kind != TOK_PP_HASH || (t.flags & TF_AT_BOL) == 0) continue;
    562 
    563     /* Read directive name (or null directive). */
    564     {
    565       Tok nt = src_next_raw(pp, NULL);
    566       Sym name;
    567       if (nt.kind == TOK_NEWLINE || nt.kind == TOK_EOF) continue;
    568       if (nt.kind != TOK_IDENT) {
    569         consume_to_newline(pp);
    570         continue;
    571       }
    572       name = tok_ident(&nt);
    573       if (name == pp->sym_if || name == pp->sym_ifdef ||
    574           name == pp->sym_ifndef) {
    575         ++local_depth;
    576         consume_to_newline(pp);
    577         continue;
    578       }
    579       if (name == pp->sym_endif) {
    580         consume_to_newline(pp);
    581         if (local_depth > 0) {
    582           --local_depth;
    583           continue;
    584         }
    585         if_pop(pp);
    586         return;
    587       }
    588       if (name == pp->sym_else) {
    589         consume_to_newline(pp);
    590         if (local_depth > 0) continue;
    591         if (top->has_else) {
    592           compiler_panic(pp->c, pp_materialize_loc(pp, t.loc),
    593                          "duplicate #else");
    594         }
    595         top->has_else = 1;
    596         if (top->state == IF_SEEK_TRUE) {
    597           top->state = IF_INCLUDE;
    598           return;
    599         }
    600         top->state = IF_DONE;
    601         continue;
    602       }
    603       if (name == pp->sym_elif) {
    604         if (local_depth > 0 || top->has_else || top->state == IF_DONE) {
    605           consume_to_newline(pp);
    606           continue;
    607         }
    608         if (top->state == IF_SEEK_TRUE) {
    609           Tok* line;
    610           u32 ln;
    611           i64 v;
    612           read_directive_line(pp, &line, &ln);
    613           v = eval_if_expr(pp, line, ln, t.loc);
    614           if (v != 0) {
    615             top->state = IF_INCLUDE;
    616             return;
    617           }
    618           continue;
    619         }
    620         /* Was IF_INCLUDE; #elif means we're done. (Should already
    621          * have been transitioned to DONE before entering this
    622          * skip — defensive.) */
    623         top->state = IF_DONE;
    624         consume_to_newline(pp);
    625         continue;
    626       }
    627       /* Other directive — relaxed: skip silently. */
    628       consume_to_newline(pp);
    629       continue;
    630     }
    631   }
    632 }
    633 
    634 /* ============================================================
    635  * Predefined macro name guard
    636  * ============================================================ */
    637 
    638 static int is_predefined_macro_name(Pp* pp, Sym name) {
    639   return name == pp->sym_va_args || name == pp->sym_line__ ||
    640          name == pp->sym_file__ || name == pp->sym_date__ ||
    641          name == pp->sym_time__;
    642   /* __STDC__/__STDC_HOSTED__/__STDC_VERSION__ are registered as real
    643    * macros, so the macro-table lookup catches them. */
    644 }
    645 
    646 /* ============================================================
    647  * #ifdef / #if / #elif / #else / #endif
    648  * ============================================================ */
    649 
    650 static void do_ifdef(Pp* pp, const Tok* line, u32 n, int negate, LocRef loc) {
    651   int defined;
    652   IfFrame f;
    653   if (n < 1 || line[0].kind != TOK_IDENT) {
    654     compiler_panic(pp->c, pp_materialize_loc(pp, loc),
    655                    negate ? "#ifndef: expected identifier"
    656                           : "#ifdef: expected identifier");
    657   }
    658   defined = (mt_get(pp, tok_ident(&line[0])) != NULL) ||
    659             is_predefined_macro_name(pp, tok_ident(&line[0]));
    660   if (negate) defined = !defined;
    661   memset(&f, 0, sizeof(f));
    662   f.state = defined ? IF_INCLUDE : IF_SEEK_TRUE;
    663   f.loc = loc;
    664   if_push(pp, f);
    665   if (!defined) skip_until_active(pp);
    666 }
    667 
    668 static void do_if_directive(Pp* pp, const Tok* line, u32 n, LocRef loc) {
    669   i64 v = eval_if_expr(pp, line, n, loc);
    670   IfFrame f;
    671   memset(&f, 0, sizeof(f));
    672   f.state = v ? IF_INCLUDE : IF_SEEK_TRUE;
    673   f.loc = loc;
    674   if_push(pp, f);
    675   if (!v) skip_until_active(pp);
    676 }
    677 
    678 static void do_elif(Pp* pp, LocRef loc) {
    679   /* We only reach do_elif from the active branch — meaning the
    680    * preceding group emitted code. So we must skip the rest. */
    681   IfFrame* top = if_top(pp);
    682   if (!top) compiler_panic(pp->c, pp_materialize_loc(pp, loc), "stray #elif");
    683   if (top->has_else)
    684     compiler_panic(pp->c, pp_materialize_loc(pp, loc), "#elif after #else");
    685   top->state = IF_DONE;
    686   skip_until_active(pp);
    687 }
    688 
    689 static void do_else(Pp* pp, LocRef loc) {
    690   IfFrame* top = if_top(pp);
    691   if (!top) compiler_panic(pp->c, pp_materialize_loc(pp, loc), "stray #else");
    692   if (top->has_else)
    693     compiler_panic(pp->c, pp_materialize_loc(pp, loc), "duplicate #else");
    694   top->has_else = 1;
    695   top->state = IF_DONE;
    696   skip_until_active(pp);
    697 }
    698 
    699 static void do_endif(Pp* pp, LocRef loc) {
    700   if (!if_top(pp))
    701     compiler_panic(pp->c, pp_materialize_loc(pp, loc), "stray #endif");
    702   if_pop(pp);
    703 }
    704 
    705 /* ============================================================
    706  * #include (§6.10.2)
    707  * ============================================================ */
    708 
    709 /* Arena-heap adapter backing the IncCache slot table: alloc/realloc bump
    710  * from pp->arena (immortal until pp_free), free is a no-op. A resize
    711  * orphans the small old slot table in the arena — bounded by the final
    712  * table size and negligible. Mirrors the parser's c_arena_heap. */
    713 static void* inc_cache_alloc(KitHeap* h, size_t n, size_t align) {
    714   return kit_arena_alloc((KitArena*)h->user, n, align);
    715 }
    716 static void* inc_cache_realloc(KitHeap* h, void* old, size_t old_n,
    717                                size_t new_n, size_t align) {
    718   void* q = kit_arena_alloc((KitArena*)h->user, new_n, align);
    719   if (q && old && old_n) memcpy(q, old, old_n < new_n ? old_n : new_n);
    720   return q;
    721 }
    722 static void inc_cache_free(KitHeap* h, void* p, size_t n) {
    723   (void)h;
    724   (void)p;
    725   (void)n;
    726 }
    727 
    728 /* Lazily set up the header-content cache, resolution memo, and their
    729  * shared arena-backed heap. */
    730 static void inc_cache_ensure(Pp* pp) {
    731   if (pp->inc_cache_ready) return;
    732   pp->inc_cache_heap.alloc = inc_cache_alloc;
    733   pp->inc_cache_heap.realloc = inc_cache_realloc;
    734   pp->inc_cache_heap.free = inc_cache_free;
    735   pp->inc_cache_heap.user = pp->arena;
    736   IncCache_init(&pp->inc_cache, &pp->inc_cache_heap);
    737   IncResolveMap_init(&pp->inc_resolve, &pp->inc_cache_heap);
    738   pp->inc_cache_ready = 1;
    739 }
    740 
    741 /* Intern a candidate/resolved path string into a Sym usable as a cache
    742  * key. Returns 0 (an invalid key) only for the empty string, which never
    743  * names a real header. */
    744 static Sym inc_cache_key(Pp* pp, const char* path) {
    745   return kit_sym_intern(pp->pool->c, kit_slice_cstr(path));
    746 }
    747 
    748 /* Read `path` via the host's file_io and copy its bytes into the pp
    749  * arena so they outlive io->release. Returns 1 on success. */
    750 static int try_open_include(Pp* pp, const char* path, const u8** data_out,
    751                             size_t* size_out) {
    752   KitFileData fd;
    753   const KitFileIO* io;
    754   u8* buf;
    755 
    756   memset(&fd, 0, sizeof(fd));
    757   io = kit_compiler_context(pp->c)->file_io;
    758   if (!io || !io->read_all) {
    759     compiler_panic(pp->c, (SrcLoc){0, 0, 0},
    760                    "#include: env.file_io is not configured");
    761   }
    762   if (io->read_all(io->user, path, &fd) != KIT_OK) return 0;
    763   {
    764     size_t sz = fd.size;
    765     buf = (u8*)arena_alloc(pp->arena, sz ? sz : 1, 1);
    766     if (sz && fd.data) memcpy(buf, fd.data, sz);
    767     if (io->release) io->release(io->user, &fd); /* zeros fd */
    768     *data_out = buf;
    769     *size_out = sz;
    770   }
    771   return 1;
    772 }
    773 
    774 /* Open `path` for #include, serving from the header-content cache when the
    775  * same resolved path was already read this TU. A hit returns the cached
    776  * arena bytes with NO syscalls; a miss reads via try_open_include and
    777  * caches the result keyed on the path string. The returned (data,size) is
    778  * byte-identical to a fresh read, so this never affects which file wins a
    779  * search nor the bytes that feed the lexer. */
    780 static int open_include_cached(Pp* pp, const char* path, const u8** data_out,
    781                                size_t* size_out) {
    782   Sym key;
    783   IncEntry* hit;
    784   inc_cache_ensure(pp);
    785   key = inc_cache_key(pp, path);
    786   if (key) {
    787     hit = IncCache_get(&pp->inc_cache, key);
    788     if (hit) {
    789       *data_out = hit->data;
    790       *size_out = hit->size;
    791       return 1;
    792     }
    793   }
    794   if (!try_open_include(pp, path, data_out, size_out)) return 0;
    795   if (key) {
    796     IncEntry e = {0};
    797     e.data = *data_out;
    798     e.size = *size_out;
    799     IncCache_set(&pp->inc_cache, key, e);
    800   }
    801   return 1;
    802 }
    803 
    804 /* Return the includer's directory for resolving a quoted include, or "."
    805  * for in-memory/builtin sources (where CWD is the natural fallback, like
    806  * gcc treats stdin). `dir_out` must point to a buffer of size >= cap. */
    807 static int includer_dir(Pp* pp, LocRef loc, char* dir_out, size_t cap) {
    808   KitSourceFile sf;
    809   const char* p = NULL;
    810   size_t plen = 0;
    811   const char* slash;
    812   size_t dlen;
    813   memset(&sf, 0, sizeof(sf));
    814   if (kit_source_file(pp->c, loc.file_id, &sf) == 0 && sf.name) {
    815     KitSlice s = kit_sym_str(pp->pool->c, sf.name);
    816     p = s.s;
    817     plen = s.len;
    818   }
    819   if (!p || plen == 0 || p[0] == '<') {
    820     if (cap < 2) return 0;
    821     dir_out[0] = '.';
    822     dir_out[1] = 0;
    823     return 1;
    824   }
    825   slash = NULL;
    826   {
    827     size_t i;
    828     for (i = plen; i > 0; --i) {
    829       if (p[i - 1] == '/') {
    830         slash = p + i - 1;
    831         break;
    832       }
    833     }
    834   }
    835   if (!slash) {
    836     if (cap < 2) return 0;
    837     dir_out[0] = '.';
    838     dir_out[1] = 0;
    839     return 1;
    840   }
    841   dlen = (size_t)(slash - p);
    842   if (dlen == 0) dlen = 1; /* path was "/x" — dir is "/" */
    843   if (dlen + 1 > cap) return 0;
    844   memcpy(dir_out, p, dlen);
    845   dir_out[dlen] = 0;
    846   return 1;
    847 }
    848 
    849 /* Build the resolution-memo key for a (spelling, system, includer-dir)
    850  * request and intern it. The key folds the search-affecting inputs into
    851  * one string: a leading '<' (system) or '"' (quoted) tag, and — for the
    852  * quoted form, whose includer-relative step is includer-specific — the
    853  * includer directory followed by a '\n' separator (which can appear in
    854  * neither a path nor a header spelling) before the spelling. System form
    855  * skips the includer step, so its key omits the dir. Returns 0 only if
    856  * the key would overflow the scratch buffer (memo simply not used). */
    857 static Sym inc_resolve_key(Pp* pp, const char* path, size_t plen, int system,
    858                            LocRef loc) {
    859   char key[4096 + 4096 + 8];
    860   size_t pos = 0;
    861   key[pos++] = system ? '<' : '"';
    862   if (!system) {
    863     char dir[4096];
    864     if (!includer_dir(pp, loc, dir, sizeof(dir))) return 0;
    865     {
    866       size_t dlen = kit_slice_cstr(dir).len;
    867       if (pos + dlen + 1 + plen >= sizeof(key)) return 0;
    868       memcpy(key + pos, dir, dlen);
    869       pos += dlen;
    870       key[pos++] = '\n';
    871     }
    872   } else if (pos + plen >= sizeof(key)) {
    873     return 0;
    874   }
    875   memcpy(key + pos, path, plen);
    876   pos += plen;
    877   return kit_sym_intern(pp->pool->c, (KitSlice){.s = key, .len = pos});
    878 }
    879 
    880 /* Record a successful dir-search resolution under its spelling key, so the
    881  * next request for the same spelling skips the search. No-op when the key
    882  * was unavailable (rkey == 0). `resolved` is a NUL-terminated path.
    883  * `next_start` is the inc_dirs index a #include_next from the resolved file
    884  * resumes at (0 for the includer-relative / absolute wins). */
    885 static void inc_resolve_record(Pp* pp, Sym rkey, const char* resolved,
    886                                int resolved_system, u32 next_start) {
    887   IncResolved e;
    888   if (!rkey) return;
    889   e.path = kit_sym_intern(pp->pool->c, kit_slice_cstr(resolved));
    890   e.system = (u8)(resolved_system ? 1 : 0);
    891   e.next_start = next_start;
    892   IncResolveMap_set(&pp->inc_resolve, rkey, e);
    893 }
    894 
    895 /* Search for a header. Absolute paths are opened verbatim. Quoted form
    896  * ("...") additionally searches the directory of the file containing the
    897  * #include first (per C §6.10.2); bracket form (<...>) skips that step.
    898  * Both forms then walk the configured -I / -isystem dirs in order.
    899  *
    900  * A resolution memo short-circuits the repeat case: when the same spelling
    901  * (with the same system flag and, for quoted form, the same includer dir)
    902  * was resolved before, we go straight to the winning path — skipping the
    903  * dir-by-dir ENOENT storm — and the content cache serves its bytes. */
    904 /* Walk pp->inc_dirs[start ..] looking for `path`, opening the first hit.
    905  * On success fills data/size, writes the resolved path and its -isystem
    906  * flag, and sets *next_start to one past the winning dir (the resume point
    907  * for a subsequent #include_next from the resolved file). Returns 0 if no
    908  * configured dir holds the header. */
    909 static int search_inc_dirs(Pp* pp, const char* path, u32 start, const u8** data,
    910                            size_t* size, char* resolved, size_t resolved_cap,
    911                            int* resolved_system_out, u32* next_start) {
    912   char buf[4096];
    913   size_t plen = kit_slice_cstr(path).len;
    914   u32 i;
    915   for (i = start; i < pp->ninc_dirs; ++i) {
    916     const char* d = pp->inc_dirs[i].path;
    917     size_t dlen = kit_slice_cstr(d).len;
    918     if (dlen + 1 + plen + 1 > sizeof(buf)) continue;
    919     memcpy(buf, d, dlen);
    920     buf[dlen] = '/';
    921     memcpy(buf + dlen + 1, path, plen);
    922     buf[dlen + 1 + plen] = 0;
    923     if (open_include_cached(pp, buf, data, size)) {
    924       if (dlen + 1 + plen + 1 > resolved_cap) return 0;
    925       memcpy(resolved, buf, dlen + 1 + plen + 1);
    926       *resolved_system_out = pp->inc_dirs[i].system ? 1 : 0;
    927       *next_start = i + 1;
    928       return 1;
    929     }
    930   }
    931   return 0;
    932 }
    933 
    934 static int find_and_open_include(Pp* pp, const char* path, int system,
    935                                  LocRef loc, const u8** data, size_t* size,
    936                                  char* resolved, size_t resolved_cap,
    937                                  int* resolved_system_out,
    938                                  u32* next_start_out) {
    939   char buf[4096];
    940   size_t plen = kit_slice_cstr(path).len;
    941   Sym rkey = 0;
    942 
    943   /* Absolute paths and the includer-relative ("...") step are not system
    944    * search dirs; only a configured -isystem dir flips this to 1 below. A
    945    * #include_next from such a file resumes at the head of the search path. */
    946   *resolved_system_out = 0;
    947   *next_start_out = 0;
    948 
    949   if (plen > 0 && path[0] == '/') {
    950     if (open_include_cached(pp, path, data, size)) {
    951       if (plen + 1 > resolved_cap) return 0;
    952       memcpy(resolved, path, plen + 1);
    953       return 1;
    954     }
    955     return 0;
    956   }
    957 
    958   /* Probe the resolution memo (covers only the dir-search winners below;
    959    * absolute paths return above). On a hit, reopen the recorded resolved
    960    * path — which the content cache already holds, so this is syscall-free
    961    * — and reproduce the byte-identical resolved string + system flag. */
    962   inc_cache_ensure(pp);
    963   rkey = inc_resolve_key(pp, path, plen, system, loc);
    964   if (rkey) {
    965     IncResolved* hit = IncResolveMap_get(&pp->inc_resolve, rkey);
    966     if (hit) {
    967       KitSlice rp = kit_sym_str(pp->pool->c, hit->path);
    968       if (rp.len + 1 > resolved_cap) return 0;
    969       if (!open_include_cached(pp, rp.s, data, size)) return 0;
    970       memcpy(resolved, rp.s, rp.len + 1);
    971       *resolved_system_out = hit->system ? 1 : 0;
    972       *next_start_out = hit->next_start;
    973       return 1;
    974     }
    975   }
    976 
    977   if (!system) {
    978     char dir[4096];
    979     if (includer_dir(pp, loc, dir, sizeof(dir))) {
    980       size_t dlen = kit_slice_cstr(dir).len;
    981       if (dlen + 1 + plen + 1 <= sizeof(buf)) {
    982         memcpy(buf, dir, dlen);
    983         buf[dlen] = '/';
    984         memcpy(buf + dlen + 1, path, plen);
    985         buf[dlen + 1 + plen] = 0;
    986         if (open_include_cached(pp, buf, data, size)) {
    987           if (dlen + 1 + plen + 1 > resolved_cap) return 0;
    988           memcpy(resolved, buf, dlen + 1 + plen + 1);
    989           inc_resolve_record(pp, rkey, resolved, 0, 0);
    990           return 1;
    991         }
    992       }
    993     }
    994   }
    995   if (search_inc_dirs(pp, path, 0, data, size, resolved, resolved_cap,
    996                       resolved_system_out, next_start_out)) {
    997     inc_resolve_record(pp, rkey, resolved, *resolved_system_out,
    998                        *next_start_out);
    999     return 1;
   1000   }
   1001   return 0;
   1002 }
   1003 
   1004 /* #include_next: like #include, but the search resumes at `start` (one past
   1005  * the dir the including file was found in) and skips both the absolute-path
   1006  * fast path and the includer-relative step. Bypasses the resolution memo —
   1007  * the memo key does not encode the resume point, and #include_next is rare. */
   1008 static int find_and_open_include_next(Pp* pp, const char* path, u32 start,
   1009                                       const u8** data, size_t* size,
   1010                                       char* resolved, size_t resolved_cap,
   1011                                       int* resolved_system_out,
   1012                                       u32* next_start_out) {
   1013   *resolved_system_out = 0;
   1014   *next_start_out = 0;
   1015   inc_cache_ensure(pp);
   1016   return search_inc_dirs(pp, path, start, data, size, resolved, resolved_cap,
   1017                          resolved_system_out, next_start_out);
   1018 }
   1019 
   1020 /* Decode a directly-lexed TOK_HEADER name into a NUL-terminated path.
   1021  * Classifies <...> (system) vs "..." (local), enforces the destination
   1022  * capacity, and writes the unwrapped contents to out. `what` is the
   1023  * directive name used in panic messages (e.g. "#include", "#embed"). */
   1024 static void header_name_to_path(Pp* pp, KitSlice slc, char* out, size_t cap,
   1025                                 int* system_out, LocRef loc, const char* what) {
   1026   const char* s = slc.s;
   1027   size_t slen = slc.len;
   1028   if (slen < 2)
   1029     compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1030                    "%s: malformed header name", what);
   1031   if (s[0] == '<' && s[slen - 1] == '>')
   1032     *system_out = 1;
   1033   else if (s[0] == '"' && s[slen - 1] == '"')
   1034     *system_out = 0;
   1035   else
   1036     compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1037                    "%s: malformed header name", what);
   1038   if (slen - 2 + 1 > cap)
   1039     compiler_panic(pp->c, pp_materialize_loc(pp, loc), "%s: path too long",
   1040                    what);
   1041   memcpy(out, s + 1, slen - 2);
   1042   out[slen - 2] = 0;
   1043 }
   1044 
   1045 /* Parse the directive arguments into (path, system_flag). Handles:
   1046  *   - directly-lexed TOK_HEADER: < ... > or " ... "
   1047  *   - macro-replaced form: line is macro-expanded, then expected to
   1048  *     produce either a TOK_STR ("...") or a < ... > sequence. */
   1049 static void parse_include_path(Pp* pp, const Tok* line, u32 n, LocRef loc,
   1050                                char* path_out, size_t cap, int* system_out) {
   1051   if (n == 0)
   1052     compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1053                    "#include: missing path");
   1054 
   1055   if (line[0].kind == TOK_HEADER) {
   1056     KitSlice sl = pp_text_slice(pp, &line[0]);
   1057     header_name_to_path(pp, sl, path_out, cap, system_out, loc, "#include");
   1058     return;
   1059   }
   1060 
   1061   /* Macro-replaced form. */
   1062   {
   1063     TokVec exp = {0};
   1064     Tok* slice = arena_array(pp->arena, Tok, n);
   1065     memcpy(slice, line, sizeof(Tok) * n);
   1066     expand_arg_to_eof(pp, slice, n, &exp);
   1067 
   1068     if (exp.n == 0) {
   1069       compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1070                      "#include: empty after macro replacement");
   1071     }
   1072     if (exp.data[0].kind == TOK_STR) {
   1073       KitSlice sl = pp_text_slice(pp, &exp.data[0]);
   1074       const char* s = sl.s;
   1075       size_t slen = sl.len;
   1076       if (slen < 2 || s[0] != '"' || s[slen - 1] != '"') {
   1077         compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1078                        "#include: malformed string");
   1079       }
   1080       if (slen - 2 + 1 > cap) {
   1081         compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1082                        "#include: path too long");
   1083       }
   1084       memcpy(path_out, s + 1, slen - 2);
   1085       path_out[slen - 2] = 0;
   1086       *system_out = 0;
   1087       return;
   1088     }
   1089     if (exp.data[0].kind == TOK_PUNCT && tok_punct(&exp.data[0]) == '<') {
   1090       size_t pos = 0;
   1091       u32 i;
   1092       for (i = 1; i < exp.n; ++i) {
   1093         size_t slen = 0;
   1094         const char* s = NULL;
   1095         if (exp.data[i].kind == TOK_PUNCT && tok_punct(&exp.data[i]) == '>') {
   1096           break;
   1097         }
   1098         {
   1099           KitSlice sl = pp_text_slice(pp, &exp.data[i]);
   1100           if (sl.len) {
   1101             s = sl.s;
   1102             slen = sl.len;
   1103           }
   1104         }
   1105         if (s && pos + slen + 1 <= cap) {
   1106           memcpy(path_out + pos, s, slen);
   1107           pos += slen;
   1108         }
   1109       }
   1110       path_out[pos] = 0;
   1111       *system_out = 1;
   1112       return;
   1113     }
   1114     compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1115                    "#include: expected \"...\" or <...> after expansion");
   1116   }
   1117 }
   1118 
   1119 /* Shared core for #include and #include_next (is_next == 1). */
   1120 static void do_include(Pp* pp, const Tok* line, u32 n, LocRef loc,
   1121                        int is_next) {
   1122   char path[4096];
   1123   char resolved[4096];
   1124   int system_form = 0;
   1125   int resolved_system = 0;
   1126   u32 next_start = 0;
   1127   const u8* data;
   1128   size_t size;
   1129   Lexer* lex;
   1130   u32 includer_id = 0;
   1131   u32 next_search = 0;
   1132   u32 included_id;
   1133   u32 i;
   1134   int found;
   1135   TokSrc s;
   1136   Sym pkey;
   1137 
   1138   parse_include_path(pp, line, n, loc, path, sizeof(path), &system_form);
   1139 
   1140   /* Locate the current includer (topmost SRC_LEX source): its file_id seeds
   1141    * the include graph, and — for #include_next — its inc_next_start is the
   1142    * point in the search path from which we resume. */
   1143   for (i = pp->nsources; i > 0; --i) {
   1144     TokSrc* tp = &pp->sources[i - 1];
   1145     if (tp->kind == SRC_LEX && tp->lex) {
   1146       includer_id = lex_file_id(tp->lex);
   1147       next_search = tp->inc_next_start;
   1148       break;
   1149     }
   1150   }
   1151 
   1152   if (is_next)
   1153     found = find_and_open_include_next(pp, path, next_search, &data, &size,
   1154                                        resolved, sizeof(resolved),
   1155                                        &resolved_system, &next_start);
   1156   else
   1157     found = find_and_open_include(pp, path, system_form, loc, &data, &size,
   1158                                   resolved, sizeof(resolved), &resolved_system,
   1159                                   &next_start);
   1160   if (!found) {
   1161     compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1162                    "%s: file not found: %.*s",
   1163                    is_next ? "#include_next" : "#include",
   1164                    KIT_SLICE_ARG(kit_slice_cstr(path)));
   1165   }
   1166 
   1167   /* Multiple-include optimization: if this resolved file was already lexed and
   1168    * is either #pragma once or still wrapped by a defined #ifndef guard, skip
   1169    * re-lexing it entirely — it would emit no tokens anyway. This is the classic
   1170    * controlling-macro optimization (tcc/gcc/clang have it); kit's IncCache
   1171    * already serves the bytes syscall-free, but re-lexing them through
   1172    * skip_until_active still costs the full lex+intern of every token. */
   1173   pkey = inc_cache_key(pp, resolved);
   1174   if (pkey) {
   1175     for (i = pp->nsources; i > 0; --i) {
   1176       TokSrc* tp = &pp->sources[i - 1];
   1177       if (tp->kind == SRC_LEX && tp->once && tp->path_key == pkey) return;
   1178     }
   1179   }
   1180   if (pkey && pp->inc_cache_ready) {
   1181     IncEntry* e = IncCache_get(&pp->inc_cache, pkey);
   1182     if (e && (e->once || (e->guard && mt_get(pp, e->guard) != NULL))) return;
   1183   }
   1184 
   1185   /* Inherit the parser-feed newline policy from the primary source: in cc mode
   1186    * an #include'd file's non-directive newlines are dropped at the lexer too
   1187    * (the parser never sees them); -E / cpp leaves them on. */
   1188   {
   1189     SourceSpec spec;
   1190     memset(&spec, 0, sizeof spec);
   1191     spec.name = kit_slice_cstr(resolved);
   1192     spec.bytes = (const char*)data;
   1193     spec.len = (u32)size;
   1194     spec.flags = (pp->parser_feed ? SRC_PARSER_FEED : 0u) |
   1195                  (resolved_system ? SRC_SYSTEM : 0u);
   1196     lex = lex_open(pp->c, &spec);
   1197   }
   1198   included_id = lex_file_id(lex);
   1199   /* Adopt the source buffer + splice table for lazy loc/text materialization
   1200    * (must happen before the source is pushed). */
   1201   pp_register_srcinfo(pp, lex);
   1202 
   1203   memset(&s, 0, sizeof(s));
   1204   s.kind = SRC_LEX;
   1205   s.lex = lex;
   1206   s.inc_next_start = next_start;
   1207   s.path_key = pkey;
   1208   s.guard_if_base = pp->ifstk_n;
   1209   src_push(pp, s);
   1210 
   1211   kit_source_add_include(pp->c, includer_id, included_id,
   1212                          pp_materialize_loc(pp, loc), system_form,
   1213                          resolved_system);
   1214 }
   1215 
   1216 /* ============================================================
   1217  * #line (§6.10.4)
   1218  * ============================================================ */
   1219 
   1220 /* Find the topmost SRC_LEX source on the stack — that's the "current
   1221  * file" whose line/file should track #line directives. */
   1222 TokSrc* current_lex_src(Pp* pp) {
   1223   u32 i;
   1224   for (i = pp->nsources; i > 0; --i) {
   1225     TokSrc* s = &pp->sources[i - 1];
   1226     if (s->kind == SRC_LEX) return s;
   1227   }
   1228   return NULL;
   1229 }
   1230 
   1231 static void do_line(Pp* pp, const Tok* line, u32 n, LocRef loc) {
   1232   /* Macro-replace arguments first (a2). */
   1233   TokVec exp = {0};
   1234   Tok* slice;
   1235   TokSrc* lex_src;
   1236   i64 target_line;
   1237   Sym target_file = 0;
   1238 
   1239   if (n == 0)
   1240     compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1241                    "#line: missing arguments");
   1242   slice = arena_array(pp->arena, Tok, n);
   1243   memcpy(slice, line, sizeof(Tok) * n);
   1244   expand_arg_to_eof(pp, slice, n, &exp);
   1245 
   1246   if (exp.n == 0 || exp.data[0].kind != TOK_NUM) {
   1247     compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1248                    "#line: expected line number");
   1249   }
   1250   {
   1251     KitSlice s = pp_text_slice(pp, &exp.data[0]);
   1252     target_line = parse_pp_int(s.s, s.len);
   1253   }
   1254   if (exp.n >= 2) {
   1255     if (exp.data[1].kind != TOK_STR) {
   1256       compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1257                      "#line: file argument must be a string");
   1258     }
   1259     {
   1260       KitSlice s = pp_text_slice(pp, &exp.data[1]);
   1261       if (s.len >= 2 && s.s[0] == '"' && s.s[s.len - 1] == '"') {
   1262         /* Destringize to logical bytes (undo \" and \\): the overlay file is
   1263          * stored unescaped, like a real source path, and __FILE__ re-escapes
   1264          * it uniformly when expanded. */
   1265         char* fbuf = (char*)arena_alloc(pp->arena, s.len, 1);
   1266         size_t flen = 0;
   1267         destringize(pp, &exp.data[1], fbuf, s.len, &flen);
   1268         target_file =
   1269             kit_sym_intern(pp->pool->c, (KitSlice){.s = fbuf, .len = flen});
   1270       }
   1271     }
   1272   }
   1273 
   1274   lex_src = current_lex_src(pp);
   1275   if (!lex_src)
   1276     compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1277                    "#line outside any file");
   1278   {
   1279     /* Record a positional #line overlay segment: from the cursor offset where
   1280      * the directive takes effect onward, the reported line is the physical line
   1281      * plus `delta`, and __FILE__ reports `target_file` (0 = none given). */
   1282     LocRef effect = lex_here(lex_src->lex);
   1283     u32 phys = pp_phys_line(pp, effect);
   1284     i32 delta = (i32)target_line - (i32)phys;
   1285     pp_add_line_seg(pp, lex_file_id(lex_src->lex), effect.off, delta,
   1286                     target_file);
   1287   }
   1288 }
   1289 
   1290 /* ============================================================
   1291  * #pragma + _Pragma (§6.10.6, §6.10.9)
   1292  * ============================================================ */
   1293 
   1294 /* Push the unmodified directive line back onto the source stack as a
   1295  * buffer, so pp_emit_text writes it as-is. SRC_BUF gates directive
   1296  * recognition off, so this won't recurse. */
   1297 void emit_pragma_line(Pp* pp, const Tok* line, u32 n, LocRef loc) {
   1298   TokVec out = {0};
   1299   u32 i;
   1300   Tok hash, ident, nl;
   1301 
   1302   memset(&hash, 0, sizeof(hash));
   1303   hash.kind = TOK_PP_HASH;
   1304   hash.flags = TF_AT_BOL;
   1305   hash.loc = loc;
   1306   hash.aux = '#';
   1307   hash.text = text_none_ref();
   1308   tv_push(pp, &out, hash);
   1309 
   1310   memset(&ident, 0, sizeof(ident));
   1311   ident.kind = TOK_IDENT;
   1312   ident.flags = 0;
   1313   ident.loc = loc;
   1314   ident.aux = pp->sym_pragma_kw;
   1315   ident.text = text_sym_ref(pp->sym_pragma_kw);
   1316   tv_push(pp, &out, ident);
   1317 
   1318   for (i = 0; i < n; ++i) {
   1319     Tok t = line[i];
   1320     /* Force a leading space between tokens. */
   1321     t.flags |= TF_HAS_SPACE;
   1322     if (i == 0) {
   1323       /* Space between "pragma" and the first arg. */
   1324     }
   1325     tv_push(pp, &out, t);
   1326   }
   1327 
   1328   memset(&nl, 0, sizeof(nl));
   1329   nl.kind = TOK_NEWLINE;
   1330   nl.loc = loc;
   1331   tv_push(pp, &out, nl);
   1332 
   1333   push_buf(pp, out.data, out.n);
   1334 }
   1335 
   1336 static int pragma_num_u32(Pp* pp, const Tok* t, u32* out) {
   1337   const char* s;
   1338   size_t len;
   1339   u32 v = 0;
   1340   KitSlice sl;
   1341   if (!t || t->kind != TOK_NUM || !out) return 0;
   1342   sl = pp_text_slice(pp, t);
   1343   s = sl.s;
   1344   len = sl.len;
   1345   if (!s || len == 0) return 0;
   1346   for (size_t i = 0; i < len; ++i) {
   1347     if (s[i] < '0' || s[i] > '9') break;
   1348     v = v * 10u + (u32)(s[i] - '0');
   1349   }
   1350   *out = v;
   1351   return 1;
   1352 }
   1353 
   1354 static void handle_pragma_pack(Pp* pp, const Tok* line, u32 n, LocRef loc) {
   1355   u32 i = 0;
   1356   if (n < 3 || line[0].kind != TOK_IDENT) return;
   1357   if (!pp_text_eq_cstr(pp, &line[0], "pack")) return;
   1358   if (line[1].kind != TOK_PUNCT || tok_punct(&line[1]) != '(') return;
   1359   i = 2;
   1360   if (i < n && line[i].kind == TOK_PUNCT && tok_punct(&line[i]) == ')') {
   1361     pp->pack_align = 0;
   1362     return;
   1363   }
   1364   if (i < n && line[i].kind == TOK_IDENT) {
   1365     if (pp_text_eq_cstr(pp, &line[i], "push")) {
   1366       if (pp->pack_stack_n <
   1367           (u32)(sizeof pp->pack_stack / sizeof pp->pack_stack[0])) {
   1368         pp->pack_stack[pp->pack_stack_n++] = pp->pack_align;
   1369       } else {
   1370         pp_warn(
   1371             pp, pp_materialize_loc(pp, loc),
   1372             "#pragma pack(push): pack stack overflow (max %u); push dropped",
   1373             (u32)(sizeof pp->pack_stack / sizeof pp->pack_stack[0]));
   1374       }
   1375       ++i;
   1376       if (i < n && line[i].kind == TOK_PUNCT && tok_punct(&line[i]) == ',') {
   1377         u32 v = 0;
   1378         ++i;
   1379         if (i < n && pragma_num_u32(pp, &line[i], &v)) pp->pack_align = v;
   1380       }
   1381       return;
   1382     }
   1383     if (pp_text_eq_cstr(pp, &line[i], "pop")) {
   1384       if (pp->pack_stack_n) pp->pack_align = pp->pack_stack[--pp->pack_stack_n];
   1385       return;
   1386     }
   1387   }
   1388   {
   1389     u32 v = 0;
   1390     if (pragma_num_u32(pp, &line[i], &v)) pp->pack_align = v;
   1391   }
   1392 }
   1393 
   1394 static void do_pragma(Pp* pp, const Tok* line, u32 n, LocRef loc) {
   1395   /* #pragma once: mark the current file include-once for the multiple-include
   1396    * optimization (see do_include). Still forwarded to output like any pragma.
   1397    */
   1398   if (n >= 1 && line[0].kind == TOK_IDENT &&
   1399       tok_ident(&line[0]) == pp->sym_once) {
   1400     TokSrc* gls = current_lex_src(pp);
   1401     if (gls) gls->once = 1;
   1402   }
   1403   /* Forward unrecognised pragmas to the output. STDC pragmas pass
   1404    * through too; we don't act on them yet. */
   1405   handle_pragma_pack(pp, line, n, loc);
   1406   emit_pragma_line(pp, line, n, loc);
   1407 }
   1408 
   1409 /* Destringize a string literal token's content: strip surrounding quotes
   1410  * and undo the `\"` and `\\` escapes. Other escape sequences pass
   1411  * through verbatim — the result is fed back through the lexer, which
   1412  * does its own escape handling for any string literals nested inside. */
   1413 static void destringize(Pp* pp, const Tok* str_tok, char* out, size_t cap,
   1414                         size_t* out_len) {
   1415   KitSlice sl = pp_text_slice(pp, str_tok);
   1416   const char* s = sl.s;
   1417   size_t slen = sl.len;
   1418   size_t i, w = 0;
   1419   if (slen < 2 || s[0] != '"' || s[slen - 1] != '"') {
   1420     compiler_panic(pp->c, pp_materialize_loc(pp, str_tok->loc),
   1421                    "_Pragma: argument must be a string literal");
   1422   }
   1423   for (i = 1; i + 1 < slen; ++i) {
   1424     char c = s[i];
   1425     if (c == '\\' && i + 2 < slen && (s[i + 1] == '\\' || s[i + 1] == '"')) {
   1426       ++i;
   1427       c = s[i];
   1428     }
   1429     if (w + 1 >= cap)
   1430       compiler_panic(pp->c, pp_materialize_loc(pp, str_tok->loc),
   1431                      "_Pragma: payload too long");
   1432     out[w++] = c;
   1433   }
   1434   out[w] = 0;
   1435   *out_len = w;
   1436 }
   1437 
   1438 /* Handle a `_Pragma("...")` invocation. Caller has consumed the
   1439  * `_Pragma` identifier. Reads `(` STR `)`, destringizes, re-lexes the
   1440  * payload, and emits a #pragma directive line. */
   1441 int try_expand_pragma_op(Pp* pp, const Tok* invoke) {
   1442   Tok lp, str, rp;
   1443   char buf[1024];
   1444   size_t buf_n = 0;
   1445   Lexer* lex;
   1446   TokVec args = {0};
   1447 
   1448   /* Peek '(' (skipping NL). Use peek_for_invoke_paren for consistency,
   1449    * but we need the saved-back behavior for a non-match. */
   1450   {
   1451     int saw_ws;
   1452     if (!peek_for_invoke_paren(pp, &saw_ws)) {
   1453       return 0; /* not an invocation; emit _Pragma as ident */
   1454     }
   1455     (void)saw_ws;
   1456   }
   1457   /* Read the string literal arg. */
   1458   str = src_next_raw(pp, NULL);
   1459   if (str.kind != TOK_STR) {
   1460     compiler_panic(pp->c, pp_materialize_loc(pp, invoke->loc),
   1461                    "_Pragma: expected string literal");
   1462   }
   1463   rp = src_next_raw(pp, NULL);
   1464   if (rp.kind != TOK_PUNCT || tok_punct(&rp) != ')') {
   1465     compiler_panic(pp->c, pp_materialize_loc(pp, invoke->loc),
   1466                    "_Pragma: expected ')'");
   1467   }
   1468   (void)lp;
   1469 
   1470   destringize(pp, &str, buf, sizeof(buf) - 2, &buf_n);
   1471   /* Append a NL so the lexer terminates cleanly. */
   1472   buf[buf_n++] = '\n';
   1473   buf[buf_n] = 0;
   1474 
   1475   /* Re-lex into args. Bytes need to live until lex_close; copy into
   1476    * arena. */
   1477   {
   1478     SourceSpec spec;
   1479     char* arena_buf = (char*)arena_alloc(pp->arena, buf_n + 1, 1);
   1480     memcpy(arena_buf, buf, buf_n + 1);
   1481     memset(&spec, 0, sizeof spec);
   1482     spec.name = kit_slice_cstr("<_Pragma>");
   1483     spec.bytes = arena_buf;
   1484     spec.len = (u32)buf_n;
   1485     lex = lex_open(pp->c, &spec);
   1486   }
   1487   for (;;) {
   1488     Tok t;
   1489     lex_next(lex, &t);
   1490     if (t.kind == TOK_EOF || t.kind == TOK_NEWLINE) break;
   1491     /* These tokens outlive `lex`; a TEXT_SRC spelling points into the lexer's
   1492      * own buffer, so intern it now while the lexer is still open. */
   1493     if (t.text.kind == TEXT_SRC)
   1494       t.text = text_intern_ref(pp, lex_text_slice(lex, t.text));
   1495     tv_push(pp, &args, t);
   1496   }
   1497   lex_close(lex);
   1498 
   1499   emit_pragma_line(pp, args.data, args.n, invoke->loc);
   1500   return 1;
   1501 }
   1502 
   1503 /* ============================================================
   1504  * #error / #warning
   1505  * ============================================================ */
   1506 
   1507 static void directive_message(Pp* pp, const Tok* line, u32 n, CharBuf* cb) {
   1508   u32 i;
   1509   for (i = 0; i < n; ++i) {
   1510     KitSlice slc = pp_text_slice(pp, &line[i]);
   1511     const char* s = slc.s;
   1512     size_t sl = slc.len;
   1513     if (i > 0) cb_putc(pp, cb, ' ');
   1514     if (s && sl) cb_append(pp, cb, s, (u32)sl);
   1515   }
   1516   cb_putc(pp, cb, 0);
   1517 }
   1518 
   1519 static void pp_warn(Pp* pp, SrcLoc loc, const char* fmt, ...) {
   1520   KitDiagSink* sink = kit_compiler_context(pp->c)->diag;
   1521   va_list ap;
   1522   if (sink && sink->emit) {
   1523     va_start(ap, fmt);
   1524     sink->emit(sink, KIT_DIAG_WARN, loc, fmt, ap);
   1525     va_end(ap);
   1526   }
   1527   if (sink) sink->warnings++;
   1528 }
   1529 
   1530 static void do_error(Pp* pp, const Tok* line, u32 n, LocRef loc) {
   1531   CharBuf cb = {0};
   1532   directive_message(pp, line, n, &cb);
   1533   compiler_panic(pp->c, pp_materialize_loc(pp, loc), "#error: %.*s",
   1534                  KIT_SLICE_ARG(kit_slice_cstr(cb.data ? cb.data : "")));
   1535 }
   1536 
   1537 static void do_warning(Pp* pp, const Tok* line, u32 n, LocRef loc) {
   1538   CharBuf cb = {0};
   1539   directive_message(pp, line, n, &cb);
   1540   pp_warn(pp, pp_materialize_loc(pp, loc), "#warning: %.*s",
   1541           KIT_SLICE_ARG(kit_slice_cstr(cb.data ? cb.data : "")));
   1542 }
   1543 
   1544 /* ============================================================
   1545  * #embed (C23, §6.10.* per N3033)
   1546  * ============================================================ */
   1547 
   1548 static void do_embed(Pp* pp, const Tok* line, u32 n, LocRef loc) {
   1549   char path[4096];
   1550   char resolved[4096];
   1551   int system_form = 0;
   1552   const u8* data;
   1553   size_t size;
   1554   u32 j;
   1555   /* Optional embed parameters parsed below. */
   1556   i64 limit_n = -1;
   1557   Tok* if_empty_toks = NULL;
   1558   u32 if_empty_n = 0;
   1559   /* Header-name path: first token. */
   1560   u32 arg_start = 0;
   1561 
   1562   if (n == 0)
   1563     compiler_panic(pp->c, pp_materialize_loc(pp, loc), "#embed: missing path");
   1564 
   1565   if (line[0].kind == TOK_HEADER) {
   1566     KitSlice slc = pp_text_slice(pp, &line[0]);
   1567     header_name_to_path(pp, slc, path, sizeof(path), &system_form, loc,
   1568                         "#embed");
   1569     arg_start = 1;
   1570   } else {
   1571     compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1572                    "#embed: header-name argument required");
   1573   }
   1574 
   1575   /* Parse trailing parameters: limit(N), if_empty(...). */
   1576   j = arg_start;
   1577   while (j < n) {
   1578     if (line[j].kind == TOK_IDENT) {
   1579       KitSlice slc = pp_text_slice(pp, &line[j]);
   1580       const char* s = slc.s;
   1581       size_t sl = slc.len;
   1582       if (sl == 5 && memcmp(s, "limit", 5) == 0) {
   1583         if (j + 1 >= n || line[j + 1].kind != TOK_PUNCT ||
   1584             tok_punct(&line[j + 1]) != '(') {
   1585           compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1586                          "#embed: expected '(' after limit");
   1587         }
   1588         j += 2;
   1589         if (j >= n || line[j].kind != TOK_NUM) {
   1590           compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1591                          "#embed: limit() expects an integer");
   1592         }
   1593         {
   1594           KitSlice s2 = pp_text_slice(pp, &line[j]);
   1595           limit_n = parse_pp_int(s2.s, s2.len);
   1596         }
   1597         ++j;
   1598         if (j >= n || line[j].kind != TOK_PUNCT || tok_punct(&line[j]) != ')') {
   1599           compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1600                          "#embed: expected ')' to close limit");
   1601         }
   1602         ++j;
   1603         continue;
   1604       }
   1605       if (sl == 8 && memcmp(s, "if_empty", 8) == 0) {
   1606         u32 depth = 0;
   1607         u32 start;
   1608         if (j + 1 >= n || line[j + 1].kind != TOK_PUNCT ||
   1609             tok_punct(&line[j + 1]) != '(') {
   1610           compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1611                          "#embed: expected '(' after if_empty");
   1612         }
   1613         j += 2;
   1614         start = j;
   1615         while (j < n) {
   1616           if (line[j].kind == TOK_PUNCT) {
   1617             if (tok_punct(&line[j]) == '(')
   1618               ++depth;
   1619             else if (tok_punct(&line[j]) == ')') {
   1620               if (depth == 0) break;
   1621               --depth;
   1622             }
   1623           }
   1624           ++j;
   1625         }
   1626         if (j >= n) {
   1627           compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1628                          "#embed: unterminated if_empty");
   1629         }
   1630         if_empty_toks = arena_array(pp->arena, Tok, j - start ? j - start : 1);
   1631         if_empty_n = j - start;
   1632         memcpy(if_empty_toks, line + start, sizeof(Tok) * if_empty_n);
   1633         ++j; /* skip ')' */
   1634         continue;
   1635       }
   1636     }
   1637     compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1638                    "#embed: unexpected token in parameter list");
   1639   }
   1640 
   1641   {
   1642     /* #embed does not record a dependency edge, so the resolved-dir system
   1643      * flag and #include_next resume point are not consumed here. */
   1644     int embed_resolved_system = 0;
   1645     u32 embed_next_start = 0;
   1646     if (!find_and_open_include(pp, path, system_form, loc, &data, &size,
   1647                                resolved, sizeof(resolved),
   1648                                &embed_resolved_system, &embed_next_start)) {
   1649       compiler_panic(pp->c, pp_materialize_loc(pp, loc),
   1650                      "#embed: file not found: %.*s",
   1651                      KIT_SLICE_ARG(kit_slice_cstr(path)));
   1652     }
   1653   }
   1654 
   1655   /* Apply limit(). */
   1656   {
   1657     size_t emit_n = size;
   1658     if (limit_n >= 0 && (u64)limit_n < emit_n) emit_n = (size_t)limit_n;
   1659     if (emit_n == 0) {
   1660       /* Empty: emit if_empty payload (or nothing). */
   1661       if (if_empty_toks && if_empty_n) {
   1662         push_buf(pp, if_empty_toks, if_empty_n);
   1663       }
   1664       return;
   1665     }
   1666     /* Build a buffer of pp-numbers separated by ',' punctuators. */
   1667     {
   1668       TokVec out = {0};
   1669       size_t i;
   1670       for (i = 0; i < emit_n; ++i) {
   1671         char numbuf[8];
   1672         int nl = 0;
   1673         u8 v = data[i];
   1674         /* "u8 -> decimal" without sprintf. */
   1675         if (v == 0) {
   1676           numbuf[nl++] = '0';
   1677         } else {
   1678           char tmp[4];
   1679           int k = 0;
   1680           while (v) {
   1681             tmp[k++] = (char)('0' + (v % 10));
   1682             v /= 10;
   1683           }
   1684           while (k > 0) numbuf[nl++] = tmp[--k];
   1685         }
   1686         {
   1687           Tok t;
   1688           memset(&t, 0, sizeof(t));
   1689           t.kind = TOK_NUM;
   1690           t.loc = loc;
   1691           t.aux = 0;
   1692           t.text =
   1693               text_intern_ref(pp, (KitSlice){.s = numbuf, .len = (size_t)nl});
   1694           if (i == 0) t.flags = TF_AT_BOL;
   1695           /* Bytes after a comma get a leading space to match
   1696            * clang's `, ` separator format. */
   1697           else
   1698             t.flags = TF_HAS_SPACE;
   1699           tv_push(pp, &out, t);
   1700         }
   1701         if (i + 1 < emit_n) {
   1702           Tok comma;
   1703           memset(&comma, 0, sizeof(comma));
   1704           comma.kind = TOK_PUNCT;
   1705           comma.aux = ',';
   1706           comma.loc = loc;
   1707           comma.text = text_none_ref();
   1708           tv_push(pp, &out, comma);
   1709         }
   1710       }
   1711       push_buf(pp, out.data, out.n);
   1712     }
   1713   }
   1714 }
   1715 
   1716 /* ============================================================
   1717  * Directive dispatch
   1718  * ============================================================ */
   1719 
   1720 void process_directive(Pp* pp, LocRef hash_loc) {
   1721   Tok* line;
   1722   u32 n;
   1723   Sym name;
   1724 
   1725   read_directive_line(pp, &line, &n);
   1726   if (n == 0) {
   1727     /* Null directive: '#' newline. Nothing to do. */
   1728     return;
   1729   }
   1730   if (line[0].kind != TOK_IDENT) {
   1731     compiler_panic(pp->c, pp_materialize_loc(pp, line[0].loc),
   1732                    "expected directive name after '#'");
   1733   }
   1734   name = tok_ident(&line[0]);
   1735 
   1736   /* Multiple-include-guard state machine, run on the current file source
   1737    * before dispatch. The only directive that may open a whole-file guard is
   1738    * `#ifndef MACRO` as the very first directive (nothing but whitespace before
   1739    * it, at the file's base if-depth); any other directive seen at START, or any
   1740    * directive at all after the controlling #endif (AFTER), disqualifies it.
   1741    * #pragma is exempt — #pragma once is its own (orthogonal) mechanism. */
   1742   {
   1743     TokSrc* gls = current_lex_src(pp);
   1744     if (gls && name != pp->sym_pragma) {
   1745       if (gls->guard_state == GUARD_START) {
   1746         if (name == pp->sym_ifndef && n >= 2 && line[1].kind == TOK_IDENT &&
   1747             pp->ifstk_n == gls->guard_if_base) {
   1748           gls->guard_macro = tok_ident(&line[1]);
   1749           gls->guard_state = GUARD_IN;
   1750         } else {
   1751           gls->guard_state = GUARD_FAILED;
   1752         }
   1753       } else if (gls->guard_state == GUARD_AFTER) {
   1754         gls->guard_state = GUARD_FAILED;
   1755       }
   1756     }
   1757   }
   1758 
   1759   if (name == pp->sym_define)
   1760     do_define(pp, line + 1, n - 1);
   1761   else if (name == pp->sym_undef)
   1762     do_undef(pp, line + 1, n - 1);
   1763   else if (name == pp->sym_if)
   1764     do_if_directive(pp, line + 1, n - 1, hash_loc);
   1765   else if (name == pp->sym_ifdef)
   1766     do_ifdef(pp, line + 1, n - 1, 0, hash_loc);
   1767   else if (name == pp->sym_ifndef)
   1768     do_ifdef(pp, line + 1, n - 1, 1, hash_loc);
   1769   else if (name == pp->sym_elif)
   1770     do_elif(pp, hash_loc);
   1771   else if (name == pp->sym_else)
   1772     do_else(pp, hash_loc);
   1773   else if (name == pp->sym_endif)
   1774     do_endif(pp, hash_loc);
   1775   else if (name == pp->sym_include)
   1776     do_include(pp, line + 1, n - 1, hash_loc, 0);
   1777   else if (name == pp->sym_include_next)
   1778     do_include(pp, line + 1, n - 1, hash_loc, 1);
   1779   else if (name == pp->sym_line)
   1780     do_line(pp, line + 1, n - 1, hash_loc);
   1781   else if (name == pp->sym_pragma)
   1782     do_pragma(pp, line + 1, n - 1, hash_loc);
   1783   else if (name == pp->sym_error)
   1784     do_error(pp, line + 1, n - 1, hash_loc);
   1785   else if (name == pp->sym_warning)
   1786     do_warning(pp, line + 1, n - 1, hash_loc);
   1787   else if (name == pp->sym_embed)
   1788     do_embed(pp, line + 1, n - 1, hash_loc);
   1789   else {
   1790     compiler_panic(pp->c, pp_materialize_loc(pp, line[0].loc),
   1791                    "unsupported directive");
   1792   }
   1793 
   1794   /* The controlling #endif (the one returning the if-stack to the file's base
   1795    * depth) closes a candidate guard. After this only whitespace/newlines may
   1796    * follow for the guard to hold; any further token/directive flips it to
   1797    * FAILED (see the START/AFTER handling above and the content check in
   1798    * src_next_raw_into). do_include/do_undef etc. do not change current_lex_src
   1799    * out from under us here because this only fires for #endif. */
   1800   if (name == pp->sym_endif) {
   1801     TokSrc* gls = current_lex_src(pp);
   1802     if (gls && gls->guard_state == GUARD_IN &&
   1803         pp->ifstk_n == gls->guard_if_base) {
   1804       gls->guard_state = GUARD_AFTER;
   1805     }
   1806   }
   1807 }