kit

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

pp_priv.h (22228B)


      1 /* pp_priv.h — shared types, helpers, and cross-module forward declarations
      2  * for the preprocessor split (pp.c / pp_expand.c / pp_directive.c).
      3  * NOT part of the public API; included only within lang/cpp/pp/. */
      4 
      5 #ifndef KIT_PP_PRIV_H
      6 #define KIT_PP_PRIV_H
      7 
      8 #include <kit/support/symtab.h>
      9 #include <stdlib.h>
     10 #include <string.h>
     11 
     12 #include "cpp_support.h"
     13 #include "pp/pp.h"
     14 
     15 /* ============================================================
     16  * Internal token kinds
     17  * ============================================================ */
     18 
     19 /* Outside the range used by the lexer (TOK_KW_LAST = 0x1000). */
     20 #define TOK_PP_PARAM ((u16)0x1100)
     21 #define TOK_PP_PLACEMARKER ((u16)0x1101) /* empty-arg substitution marker */
     22 
     23 /* ============================================================
     24  * Types
     25  * ============================================================ */
     26 
     27 typedef struct Macro {
     28   Sym name;
     29   LocRef def_loc;
     30   u8 is_func;
     31   u8 is_variadic;
     32   /* Pure cache of the body (NOT part of macro identity): 1 iff the body
     33    * contains at least one TOK_PP_PASTE (`##`). Lets a no-`##` body be
     34    * replayed by pointer without the per-invocation copy + paste pass. */
     35   u8 has_paste;
     36   u8 pad[1];
     37   /* cpplib-style macro-disabled count (replaces the Prosser hideset): the
     38    * number of this macro's replacement-list frames currently live on the
     39    * source stack. Incremented when a replacement frame for this macro is
     40    * pushed, decremented when it pops (see push_buf_uniform/replay + src_pop).
     41    * While > 0 the macro is unavailable for expansion, so a recursive
     42    * occurrence in its own (or a nested) replacement is returned with the
     43    * permanent TF_NO_EXPAND bit set instead of re-expanding. Auto-zeros: do_define
     44    * mints Macro via arena_znew. */
     45   u16 disabled_depth;
     46   u32 n_params;
     47   Sym* params; /* parameter names                              */
     48   Tok* body;   /* body tokens; TOK_PP_PARAM kind + aux=param idx */
     49   u32 body_len;
     50 } Macro;
     51 
     52 /* MacroTab = Sym -> Macro* (NULL = not a macro). Sym-indexed dense table:
     53  * the macro binding is read with one in-range load on every identifier in
     54  * the expander's hot loop, replacing a per-identifier hashmap probe. */
     55 KIT_SYMTAB_DEFINE(MacroTab, Macro*);
     56 
     57 typedef enum { SRC_LEX = 1, SRC_BUF = 2 } SrcKind;
     58 
     59 /* Multiple-include-guard detection state for a SRC_LEX file (the classic
     60  * "controlling macro" optimization). A file whose entire body is wrapped in
     61  * `#ifndef GUARD ... #endif` (with nothing but whitespace/comments outside)
     62  * can be skipped on a later #include while GUARD is defined — it would emit no
     63  * tokens anyway. Detected live as the file is first lexed (no second scan):
     64  * START -> IN on the opening `#ifndef GUARD`; IN -> AFTER on the matching
     65  * `#endif`; any code before the opening directive or after the controlling
     66  * `#endif` -> FAILED. Committed to the IncCache at src_pop (see pp.c). */
     67 typedef enum GuardState {
     68   GUARD_START = 0, /* only whitespace/newlines seen so far              */
     69   GUARD_IN = 1,    /* inside the controlling #ifndef ... #endif         */
     70   GUARD_AFTER = 2, /* controlling #endif closed; only ws may follow     */
     71   GUARD_FAILED = 3 /* not a whole-file guard                            */
     72 } GuardState;
     73 
     74 typedef struct TokSrc {
     75   u8 kind;
     76   /* When set on a SRC_BUF: src_next_raw returns TOK_EOF when this is
     77    * the top source and it's exhausted, instead of popping. The caller
     78    * (e.g. argument pre-expansion) explicitly pops the scope when done.
     79    * This bounds expansion to a single argument's token stream. */
     80   u8 scope_top;
     81   /* Read-time loc/flags override for a pointer-replayed macro body (SRC_BUF).
     82    * When has_loc_override, every token read from this buffer gets
     83    * out->loc = loc_override, and the FIRST token (s->i == 0 at read) also has
     84    * its BOL/SPACE bits replaced by first_flags_or. This reproduces the
     85    * mutations expand_object_macro used to bake into a fresh body copy, so the
     86    * immutable definition-time m->body can be replayed in place. 0 = no
     87    * override (every push_buf/push_buf_uniform memsets the whole TokSrc). */
     88   u8 has_loc_override;
     89   u8 pad[1];
     90   u16 first_flags_or;
     91   /* SRC_LEX */
     92   Lexer* lex;
     93   /* SRC_BUF */
     94   Tok* toks;
     95   /* cpplib macro-disabled frame owner. Non-NULL only on a macro
     96    * replacement-list frame (object-like replay, function-like substituted body,
     97    * paste result); pushing such a frame increments owner->disabled_depth and
     98    * src_pop decrements it, so the macro stays unavailable for the whole rescan
     99    * of its replacement (and any nested replacement above it). NULL on every
    100    * non-macro buffer (pushed-back peek tokens, #pragma/#embed payloads,
    101    * arg-prescan scopes); every push_buf* memsets the whole TokSrc, so NULL is
    102    * the default. */
    103   Macro* disabled_owner;
    104   /* Per-token loc applied when has_loc_override (see scope_top block). */
    105   LocRef loc_override;
    106   u32 i;
    107   u32 n;
    108   /* #line state is recorded positionally in the source's SrcInfo overlay
    109    * segments (see SrcInfo / pp_materialize_loc), not on the frame: a lean
    110    * token carries only a byte offset, and line/file are resolved on demand. */
    111   /* SRC_LEX only: the index into pp->inc_dirs from which a `#include_next`
    112    * (or `__has_include_next`) appearing in this file begins its search —
    113    * i.e. one past the search dir this file was itself found in. 0 for the
    114    * top-level translation unit and for files found via the includer-relative
    115    * ("...") step or an absolute path, so an `#include_next` there scans the
    116    * whole search path (matching GCC). */
    117   u32 inc_next_start;
    118   /* Multiple-include-guard detection (SRC_LEX only). guard_if_base is the
    119    * if-stack depth at file entry, used to recognize the controlling #endif;
    120    * path_key is the IncCache key (interned resolved path, 0 for the top-level
    121    * TU and other non-cached sources) used to commit the memo at src_pop. */
    122   u8 guard_state; /* GuardState */
    123   u8 once;        /* #pragma once seen in this file */
    124   Sym guard_macro;
    125   u32 guard_if_base;
    126   Sym path_key;
    127 } TokSrc;
    128 
    129 typedef enum IfState {
    130   IF_INCLUDE = 1,   /* group active, emit code                       */
    131   IF_SEEK_TRUE = 2, /* skip, looking for the first true elif/else    */
    132   IF_DONE = 3,      /* skip, already had a true branch               */
    133 } IfState;
    134 
    135 typedef struct IfFrame {
    136   u8 state;
    137   u8 has_else;
    138   u8 pad[2];
    139   LocRef loc;
    140 } IfFrame;
    141 
    142 /* ============================================================
    143  * Source-info registry (lazy loc + retained text)
    144  * ============================================================ */
    145 
    146 /* A #line overlay segment: from byte offset `off` onward (until the next
    147  * segment), the reported line number is the physical line plus `delta`, and
    148  * __FILE__ reports `file` (0 = the source's registry name). Recorded by do_line
    149  * in offset order; consulted by pp_materialize_loc / pp_materialize_file. */
    150 typedef struct LineSeg {
    151   u32 off;
    152   i32 delta;
    153   Sym file;
    154 } LineSeg;
    155 
    156 /* Per-source retained metadata, keyed by file_id. The folded logical buffer and
    157  * splice table are retained until pp_free so that (a) TEXT_SRC spellings stay
    158  * resolvable after the lexer is popped (macro bodies, parser lookahead/replay),
    159  * and (b) line/col can be reconstructed lazily from a byte offset. The line
    160  * index is built on first materialization by scanning the buffer for '\n' and
    161  * merging the splice fold points. */
    162 typedef struct SrcInfo {
    163   const char* buf; /* folded logical buffer (borrowed/arena/owned) */
    164   u32 len;
    165   u8 owns_buf;   /* PP frees buf at pp_free (a folded heap copy) */
    166   u8 line_built; /* line_off has been computed */
    167   u32 shebang_off;
    168   u32* splices; /* fold offsets; PP frees at pp_free */
    169   u32 nsplices;
    170   u32* line_off; /* line_off[k] = start offset of physical line k+1 */
    171   u32 nlines;
    172   u32 line_cursor; /* last line index returned — loc materialization is
    173                     * near-monotonic, so this makes the common lookup O(1) */
    174   LineSeg* segs;   /* #line overlay, offset-ordered */
    175   u32 nsegs;
    176   u32 segs_cap;
    177 } SrcInfo;
    178 
    179 /* Sym-keyed hashmaps below (include caches). See core/hashmap.h. */
    180 #include <kit/support/hashmap.h>
    181 
    182 /* IncCache = Sym(resolved-path) -> cached header bytes. The same guarded
    183  * header included many times across a TU is read from disk once; later
    184  * inclusions reuse the arena-resident bytes (lex_open only borrows
    185  * them and the pp arena keeps them alive until pp_free), saving the
    186  * open+fstat+read+close + malloc/memcpy/free per repeat. Keyed strictly
    187  * on the resolved-path STRING so distinct spellings/symlinks never
    188  * over-merge; the cached bytes are byte-identical to a fresh read. */
    189 typedef struct IncEntry {
    190   const u8* data;
    191   size_t size;
    192   /* Multiple-include optimization (committed at src_pop, consulted at the next
    193    * #include of the same resolved path). guard = the file's controlling
    194    * #ifndef macro (0 = none); once = the file carried #pragma once. */
    195   Sym guard;
    196   u8 once;
    197 } IncEntry;
    198 static inline u32 inc_hash_(Sym s) { return kit_hash_u32((u32)s); }
    199 KIT_HASHMAP_DEFINE(IncCache, Sym, IncEntry, inc_hash_);
    200 
    201 /* IncResolveMap = Sym(spelling key) -> resolved (path Sym, system flag).
    202  * Memoizes the winner of the -I/-isystem dir search so a repeated request
    203  * for the same header spelling skips the ENOENT storm of failed opens on
    204  * the earlier dirs. The key folds the system flag (and, for quoted form,
    205  * the includer directory) into the spelling so the resolved result is the
    206  * exact one a fresh search would produce — byte-identical resolved path
    207  * and resolved_system flag feeding source_add_include / DWARF / -M. */
    208 typedef struct IncResolved {
    209   Sym path;       /* interned resolved path string                  */
    210   u8 system;      /* the resolved_system flag (winning -isystem)   */
    211   u32 next_start; /* inc_dirs index a #include_next from the   */
    212                   /* resolved file starts at (see TokSrc)      */
    213 } IncResolved;
    214 static inline u32 incres_hash_(Sym s) { return kit_hash_u32((u32)s); }
    215 KIT_HASHMAP_DEFINE(IncResolveMap, Sym, IncResolved, incres_hash_);
    216 
    217 /* ============================================================
    218  * Pp struct (definition shared across all three TUs)
    219  * ============================================================ */
    220 
    221 struct Pp {
    222   Compiler* c;
    223   Pool* pool;
    224 
    225   /* Source stack — top of stack is sources[nsources-1]. */
    226   TokSrc* sources;
    227   u32 nsources;
    228   u32 sources_cap;
    229 
    230   /* Per-file retained metadata, indexed by compiler file_id (dense from 1).
    231    * Holds the retained source buffer + splice table + lazy line index + #line
    232    * overlay so loc/text materialize after the lexer is popped. */
    233   SrcInfo* srcinfo;
    234   u32 srcinfo_cap;
    235 
    236   /* Macro table (Sym-indexed dense; value = Macro*, NULL = not a macro). */
    237   MacroTab macros;
    238 
    239   /* Header-content cache (key = interned resolved-path Sym, value =
    240    * arena-resident bytes). Collapses N re-includes of the same header to
    241    * one read + N-1 cache hits. Lazily initialized on first #include; its
    242    * slot table is backed by inc_cache_heap (an arena adapter over
    243    * pp->arena), so nothing needs freeing at pp_free — the arena is. */
    244   IncCache inc_cache;
    245   KitHeap inc_cache_heap;
    246   u8 inc_cache_ready;
    247 
    248   /* Include-resolution memo (spelling -> winning resolved path + system
    249    * flag). Shares inc_cache_heap (gated by inc_cache_ready). */
    250   IncResolveMap inc_resolve;
    251 
    252   /* Conditional inclusion stack (#if / #ifdef / #ifndef → #endif). */
    253   IfFrame* ifstk;
    254   u32 ifstk_n;
    255   u32 ifstk_cap;
    256 
    257   /* Include directories (stage 9). */
    258   struct {
    259     const char* path;
    260     u8 system;
    261   }* inc_dirs;
    262   u32 ninc_dirs;
    263   u32 inc_dirs_cap;
    264 
    265   /* Current #pragma pack maximum field alignment. 0 means natural. */
    266   u32 pack_align;
    267   u32 pack_stack[16];
    268   u32 pack_stack_n;
    269 
    270   /* Permanent arena: macro bodies, params, #include file data.
    271    * Lives until pp_free. */
    272   KitArena* arena;
    273 
    274   /* Transient expansion scratch: the macro-substitution token buffers
    275    * (tv_grow), arg slices, paste/stringize buffers -- everything that
    276    * backs a SRC_BUF or is consumed within one expansion. Reset to its
    277    * high-water mark by pp_next_raw whenever the source stack drains back to a
    278    * lexer (no SRC_BUF live), so memory is O(expansion depth), not
    279    * O(expansions). Nothing the caller keeps points in here: tokens are
    280    * returned by value and spellings are interned in the pool. */
    281   KitArena* xarena;
    282 
    283   /* Cached interned identifiers used for directive recognition. */
    284   Sym sym_define;
    285   Sym sym_undef;
    286   Sym sym_include;
    287   Sym sym_include_next;     /* GCC/clang #include_next extension       */
    288   Sym sym_assembler;        /* __ASSEMBLER__ — asm-with-cpp `#` leniency */
    289   Sym sym_has_include;      /* __has_include() #if operator            */
    290   Sym sym_has_include_next; /* __has_include_next() #if operator       */
    291   Sym sym_if;
    292   Sym sym_ifdef;
    293   Sym sym_ifndef;
    294   Sym sym_elif;
    295   Sym sym_else;
    296   Sym sym_endif;
    297   Sym sym_line;
    298   Sym sym_pragma;
    299   Sym sym_once; /* "once" — #pragma once (multiple-include opt) */
    300   Sym sym_error;
    301   Sym sym_warning;
    302   Sym sym_embed;
    303   Sym sym_defined;
    304   Sym sym_va_args;
    305   Sym sym_line__; /* __LINE__   */
    306   Sym sym_file__; /* __FILE__   */
    307   Sym sym_date__; /* __DATE__   */
    308   Sym sym_time__; /* __TIME__   */
    309   Sym sym_stdc__; /* __STDC__   */
    310   Sym sym_stdc_hosted__;
    311   Sym sym_stdc_version__;
    312   Sym sym__pragma;   /* _Pragma operator */
    313   Sym sym_pragma_kw; /* "pragma" — for synthesized #pragma */
    314 
    315   /* Pre-formatted "Mmm dd yyyy" / "hh:mm:ss" string spellings for
    316    * __DATE__ and __TIME__, derived from SOURCE_DATE_EPOCH (or
    317    * time(NULL) if unset). */
    318   Sym val_date_str;
    319   Sym val_time_str;
    320 
    321   /* Token-paste (`##`) scratch. paste_name_sym is the interned "<paste>" name,
    322    * cached so each paste open doesn't re-intern the literal. paste_lex is a
    323    * single reused memory lexer (re-pointed via lex_reset_mem per paste) so the
    324    * thousands of tiny paste buffers don't each alloc/free a 288B Lexer. Both
    325    * are lazily initialized on the first paste and torn down in pp_free. */
    326   Sym paste_name_sym;
    327   Lexer* paste_lex;
    328 
    329   /* Defined-operator handling during #if expansion.
    330    *
    331    * The first prepass in eval_if_expr replaces `defined X` / `defined
    332    * (X)` literally found in the directive line, but `defined()` can
    333    * also come from macro bodies (mingw's intrin-impl.h uses
    334    * `defined(__INTRINSIC_DEFINED_ ## name)` inside a #define).  When
    335    * the expander processes such a body, the identifier inside
    336    * `defined(...)` must NOT be macro-expanded — otherwise an empty
    337    * macro X would turn `defined(X)` into `defined()` and the
    338    * post-expansion prepass would reject it.
    339    *
    340    * This pair of fields tracks the state across `pp_next_raw` calls
    341    * within `expand_for_if`:
    342    *   in_if_expansion: 1 inside an #if's expand_arg_to_eof call
    343    *   defined_skip:    0 normally; 2 after emitting `defined` (a bare
    344    *                    `defined IDENT` marks the IDENT and resets, while
    345    *                    a `(` advances to 3); 3 after `defined (` (waiting
    346    *                    for the operand IDENT, advancing to 4); 4 after
    347    *                    `defined ( IDENT` (waiting for the closing `)`).
    348    * The expander uses these to mark the operand IDENT TF_NO_EXPAND
    349    * before the macro-expansion check at the head of pp_next_raw. */
    350   u8 in_if_expansion;
    351   u8 defined_skip;
    352   /* Set while read_directive_line is pulling a directive's own tokens from the
    353    * lexer, so the multiple-include-guard content check in src_next_raw_into
    354    * doesn't count directive-internal tokens (e.g. the `ifndef` / macro name of
    355    * the guard itself) as file content. */
    356   u8 reading_directive;
    357   /* cc parser-feed mode: set when the primary source is pushed with
    358    * SRC_PARSER_FEED, so every SRC_LEX lexer (primary + each #include) is opened
    359    * with newline suppression (the parser drops non-directive newlines). The
    360    * -E / cpp path leaves this clear so newlines surface for text
    361    * reconstruction. */
    362   u8 parser_feed;
    363 };
    364 
    365 /* ============================================================
    366  * Allocation helpers (defined in pp.c, used everywhere)
    367  * ============================================================ */
    368 
    369 static inline Heap* pp_heap(Pp* pp) {
    370   return kit_compiler_context(pp->c)->heap;
    371 }
    372 
    373 static inline void* pp_xrealloc(Pp* pp, void* p, size_t old_n, size_t new_n,
    374                                 size_t align) {
    375   Heap* h = pp_heap(pp);
    376   void* q = h->realloc(h, p, old_n, new_n, align);
    377   if (!q) compiler_panic(pp->c, (SrcLoc){0, 0, 0}, "pp: out of memory");
    378   return q;
    379 }
    380 
    381 static inline void pp_xfree(Pp* pp, void* p, size_t n) {
    382   if (p) pp_heap(pp)->free(pp_heap(pp), p, n);
    383 }
    384 
    385 /* ============================================================
    386  * Lean-token constructors (synthetic tokens)
    387  * ============================================================ */
    388 
    389 /* text_none_ref / text_sym_ref / locref_none live in lex.h (shared with the
    390  * parser). text_intern_ref additionally interns, so it needs the Pp pool. */
    391 static inline TextRef text_intern_ref(Pp* pp, KitSlice sl) {
    392   return text_sym_ref(kit_sym_intern(pp->pool->c, sl));
    393 }
    394 
    395 /* ============================================================
    396  * Token-vector helpers
    397  * ============================================================ */
    398 
    399 typedef struct TokVec {
    400   Tok* data;
    401   u32 n;
    402   u32 cap;
    403 } TokVec;
    404 
    405 static inline void tv_grow(Pp* pp, TokVec* v, u32 want) {
    406   u32 nc;
    407   if (v->cap >= want) return;
    408   nc = v->cap ? v->cap * 2 : 8;
    409   while (nc < want) nc *= 2;
    410   {
    411     Tok* nb = arena_array(pp->xarena, Tok, nc);
    412     if (v->n) memcpy(nb, v->data, sizeof(Tok) * v->n);
    413     v->data = nb;
    414     v->cap = nc;
    415   }
    416 }
    417 
    418 static inline void tv_push(Pp* pp, TokVec* v, Tok t) {
    419   tv_grow(pp, v, v->n + 1);
    420   v->data[v->n++] = t;
    421 }
    422 
    423 /* Growable char buffer (arena-backed). */
    424 typedef struct CharBuf {
    425   char* data;
    426   u32 len;
    427   u32 cap;
    428 } CharBuf;
    429 
    430 static inline void cb_append(Pp* pp, CharBuf* b, const char* s, u32 n) {
    431   if (b->len + n > b->cap) {
    432     u32 nc = b->cap ? b->cap * 2 : 64;
    433     while (nc < b->len + n) nc *= 2;
    434     {
    435       char* nb = (char*)arena_alloc(pp->xarena, nc, 1);
    436       if (b->len) memcpy(nb, b->data, b->len);
    437       b->data = nb;
    438       b->cap = nc;
    439     }
    440   }
    441   if (n) memcpy(b->data + b->len, s, n);
    442   b->len += n;
    443 }
    444 
    445 static inline void cb_putc(Pp* pp, CharBuf* b, char c) {
    446   cb_append(pp, b, &c, 1);
    447 }
    448 
    449 /* ============================================================
    450  * Cross-module forward declarations
    451  * ============================================================ */
    452 
    453 /* --- pp.c (source stack) → pp_expand.c, pp_directive.c --- */
    454 /* Out-pointer form (hot path); src_next_raw is the by-value shim over it for
    455  * the cold/general callers. */
    456 void src_next_raw_into(Pp* pp, Tok* out, u8* src_kind_out);
    457 Tok src_next_raw(Pp* pp, u8* src_kind_out);
    458 void src_push(Pp* pp, TokSrc s);
    459 void src_pop(Pp* pp);
    460 void push_buf(Pp* pp, Tok* toks, u32 n);
    461 /* Push a macro replacement-list buffer. `owner` is the macro whose replacement
    462  * this is (NULL for a non-macro payload): a non-NULL owner increments
    463  * owner->disabled_depth at push and src_pop decrements it, so the macro stays
    464  * unavailable for the whole rescan of its replacement (cpplib disabled-frame
    465  * model). */
    466 void push_buf_uniform(Pp* pp, Tok* toks, u32 n, Macro* owner);
    467 /* Push an immutable, read-only token buffer (e.g. a no-`##` macro body) for
    468  * pointer-replay. The buffer is never written through s->toks; per-read
    469  * loc/flags overrides reproduce the mutations the old body-copy path baked in.
    470  * `owner` follows the same disabled-frame rule as push_buf_uniform. */
    471 void push_buf_replay(Pp* pp, const Tok* toks, u32 n, Macro* owner,
    472                      LocRef loc_override, u16 first_flags_or);
    473 
    474 /* pp_next_raw (public, in pp.h) is the mutual-recursion entry:
    475  * expand_arg_to_eof and the -E loop call it; it drives directives and
    476  * expansion. pp_next_into is the internal newline-dropping pull (skip_nl=1)
    477  * used by pp_next_parse. */
    478 void pp_next_into(Pp* pp, Tok* out);
    479 
    480 /* --- SrcInfo registry (pp.c) → pp_directive.c --- */
    481 /* Get (growing as needed) the SrcInfo for a file_id. */
    482 SrcInfo* pp_srcinfo(Pp* pp, u32 file_id);
    483 /* Register a freshly-opened, about-to-be-pushed lexer: adopt its folded buffer
    484  * + splice table + shebang offset into the SrcInfo registry (retained to
    485  * pp_free) so loc/text stay materializable after the lexer is popped. */
    486 void pp_register_srcinfo(Pp* pp, Lexer* lex);
    487 /* Record a #line overlay segment (effect offset, line delta, file name sym). */
    488 void pp_add_line_seg(Pp* pp, u32 file_id, u32 off, i32 delta, Sym file);
    489 /* Physical line at a location, ignoring #line overlay (used by do_line). */
    490 u32 pp_phys_line(Pp* pp, LocRef loc);
    491 /* __FILE__ name sym at a location (overlay file if set, else registry name). */
    492 Sym pp_materialize_file(Pp* pp, LocRef loc);
    493 
    494 /* --- pp_expand.c → pp.c, pp_directive.c --- */
    495 void expand_arg_to_eof(Pp* pp, Tok* in, u32 nin, TokVec* out);
    496 
    497 /* Macro binding lookup/define/undef. Inlined Sym-indexed loads (no hashmap
    498  * probe): mt_get runs on every identifier the expander sees. NULL/absent =
    499  * not a macro; #undef stores NULL. */
    500 static inline Macro* mt_get(Pp* pp, Sym name) {
    501   return MacroTab_get(&pp->macros, name);
    502 }
    503 static inline void mt_put(Pp* pp, Sym name, Macro* m) {
    504   MacroTab_set(&pp->macros, name, m);
    505 }
    506 static inline void mt_del(Pp* pp, Sym name) {
    507   MacroTab_set(&pp->macros, name, NULL);
    508 }
    509 
    510 /* --- pp_directive.c → pp_expand.c --- */
    511 i64 eval_if_expr(Pp* pp, const Tok* line, u32 n, LocRef loc);
    512 void process_directive(Pp* pp, LocRef hash_loc);
    513 
    514 /* --- pp_directive.c internal helpers called from pp_expand.c --- */
    515 void emit_pragma_line(Pp* pp, const Tok* line, u32 n, LocRef loc);
    516 int peek_for_invoke_paren(Pp* pp, int* ws_has_space_out);
    517 int try_expand_pragma_op(Pp* pp, const Tok* invoke);
    518 
    519 /* --- pp_directive.c: read_directive_line (used by pp.c/pp_define) --- */
    520 void read_directive_line(Pp* pp, Tok** out_toks, u32* out_n);
    521 
    522 /* --- pp_expand.c: do_define / do_undef (used by pp.c/pp_define) --- */
    523 void do_define(Pp* pp, const Tok* line, u32 n);
    524 void do_undef(Pp* pp, const Tok* line, u32 n);
    525 
    526 /* --- pp_directive.c helpers needed by pp_expand.c (_Pragma) --- */
    527 TokSrc* current_lex_src(Pp* pp);
    528 
    529 #endif /* KIT_PP_PRIV_H */