kit

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

lex_runtime.c (43857B)


      1 /* gramlex.c - shared borrowed-input DFA driver for gramgen-generated lexers. */
      2 #include <string.h>
      3 
      4 #include <kit/support/gram_lex_tables.h>
      5 #include <kit/gram_unicode.h>
      6 
      7 static const uint64_t KW_FNV_OFFSET = 0xcbf29ce484222325ull;
      8 static const uint64_t KW_FNV_PRIME = 0x100000001b3ull;
      9 static const uint64_t KW_MIX1 = 0x9e3779b97f4a7c15ull;
     10 static const uint64_t KW_MIX2 = 0xff51afd7ed558ccdull;
     11 static const uint64_t KW_MIX3 = 0xc4ceb9fe1a85ec53ull;
     12 
     13 typedef struct {
     14   KitGramLexInputSpan* span;
     15   size_t off;
     16 } span_pos;
     17 
     18 typedef struct {
     19   uint32_t line, col;
     20   KitGramUnicodePos upos;
     21 } lex_pos;
     22 
     23 typedef struct {
     24   const KitGramLexGrammar* g;
     25 } kit_gram_matcher_impl;
     26 _Static_assert(sizeof(kit_gram_matcher_impl) <= KIT_GRAM_MATCHER_SIZE,
     27                "KitGramMatcher too small");
     28 static kit_gram_matcher_impl* matcher_impl(KitGramMatcher* m) {
     29   return (kit_gram_matcher_impl*)m;
     30 }
     31 
     32 typedef struct {
     33   const KitGramLexGrammar* g;
     34   KitGramLexInput* in;
     35   size_t from;
     36   bool multiline;
     37 } kit_gram_match_iter_impl;
     38 _Static_assert(sizeof(kit_gram_match_iter_impl) <= KIT_GRAM_MATCH_ITER_SIZE,
     39                "KitGramMatchIter too small");
     40 static kit_gram_match_iter_impl* iter_impl(KitGramMatchIter* it) {
     41   return (kit_gram_match_iter_impl*)it;
     42 }
     43 
     44 typedef struct kit_gram_lex_input_impl {
     45   KitGramLexInputConfig cfg;
     46   KitGramLexInputSpan* head;
     47   KitGramLexInputSpan* tail;
     48   size_t off;
     49 
     50   bool finished;
     51   bool failed;
     52   bool mode_set;
     53   KitGramLexInputMode mode;
     54 
     55   uint32_t line, col;
     56   KitGramUnicodePos upos;
     57   KitGramLexError error;
     58 
     59   size_t pending_len;
     60   lex_pos pending_pos;
     61   bool token_valid;
     62   span_pos token_start;
     63   size_t token_len;
     64   const unsigned char* token_lexeme;
     65 } kit_gram_lex_input_impl;
     66 
     67 typedef struct kit_gram_lexer_impl {
     68   const KitGramLexGrammar* g;
     69   KitGramLexInput* input_mem;
     70   kit_gram_lex_input_impl* in;
     71   KitGramLexConfig cfg;
     72 } kit_gram_lexer_impl;
     73 
     74 typedef struct kit_gram_lex_lexeme_impl {
     75   kit_gram_lex_input_impl* in;
     76   KitGramLexInputSpan* span;
     77   size_t off;
     78   size_t remaining;
     79   const unsigned char* single;
     80   size_t single_len;
     81   bool yielded_single;
     82 } kit_gram_lex_lexeme_impl;
     83 
     84 _Static_assert(sizeof(kit_gram_lex_input_impl) <= sizeof(KitGramLexInput),
     85                "KitGramLexInput storage too small - raise KIT_GRAM_LEX_INPUT_SIZE "
     86                "in gramlex.h");
     87 _Static_assert(_Alignof(kit_gram_lex_input_impl) <= _Alignof(KitGramLexInput),
     88                "KitGramLexInput storage under-aligned");
     89 _Static_assert(
     90     sizeof(kit_gram_lexer_impl) <= sizeof(KitGramLexer),
     91     "KitGramLexer storage too small - raise KIT_GRAM_LEXER_SIZE in gramlex.h");
     92 _Static_assert(_Alignof(kit_gram_lexer_impl) <= _Alignof(KitGramLexer),
     93                "KitGramLexer storage under-aligned");
     94 _Static_assert(sizeof(kit_gram_lex_lexeme_impl) <= sizeof(KitGramLexLexeme),
     95                "KitGramLexLexeme storage too small - raise KIT_GRAM_LEX_LEXEME_SIZE "
     96                "in gramlex.h");
     97 _Static_assert(_Alignof(kit_gram_lex_lexeme_impl) <= _Alignof(KitGramLexLexeme),
     98                "KitGramLexLexeme storage under-aligned");
     99 
    100 static kit_gram_lex_input_impl* in_impl(KitGramLexInput* in) {
    101   return (kit_gram_lex_input_impl*)in;
    102 }
    103 static const kit_gram_lex_input_impl* cin_impl(const KitGramLexInput* in) {
    104   return (const kit_gram_lex_input_impl*)in;
    105 }
    106 static kit_gram_lexer_impl* lx_impl(KitGramLexer* lx) { return (kit_gram_lexer_impl*)lx; }
    107 static kit_gram_lex_lexeme_impl* it_impl(KitGramLexLexeme* it) {
    108   return (kit_gram_lex_lexeme_impl*)it;
    109 }
    110 
    111 static uint32_t add_u32_wrap(uint32_t a, size_t b) { return a + (uint32_t)b; }
    112 
    113 static uint32_t cur_line(const kit_gram_lex_input_impl* in) {
    114   return in->mode_set && in->mode == KIT_GRAM_LEX_INPUT_UTF8 ? in->upos.line
    115                                                          : in->line;
    116 }
    117 
    118 static uint32_t cur_col(const kit_gram_lex_input_impl* in) {
    119   return in->mode_set && in->mode == KIT_GRAM_LEX_INPUT_UTF8 ? in->upos.col
    120                                                          : in->col;
    121 }
    122 
    123 static unsigned char current_byte(const kit_gram_lex_input_impl* in) {
    124   return in->head && in->off < in->head->len ? in->head->bytes[in->off] : 0;
    125 }
    126 
    127 static lex_pos current_pos(const kit_gram_lex_input_impl* in) {
    128   return (lex_pos){.line = in->line, .col = in->col, .upos = in->upos};
    129 }
    130 
    131 static void set_pos(kit_gram_lex_input_impl* in, const lex_pos* pos) {
    132   if (in->mode_set && in->mode == KIT_GRAM_LEX_INPUT_UTF8) {
    133     in->upos = pos->upos;
    134   } else {
    135     in->line = pos->line;
    136     in->col = pos->col;
    137   }
    138 }
    139 
    140 typedef struct {
    141   lex_pos pos;
    142   uint32_t cp;
    143   uint8_t need;
    144 } scan_pos;
    145 
    146 static void scan_pos_init(scan_pos* sp, const kit_gram_lex_input_impl* in) {
    147   *sp = (scan_pos){.pos = current_pos(in)};
    148 }
    149 
    150 static void scan_pos_advance(scan_pos* sp, KitGramLexInputMode mode,
    151                              unsigned char b) {
    152   if (mode != KIT_GRAM_LEX_INPUT_UTF8) {
    153     if (b == '\n') {
    154       sp->pos.line++;
    155       sp->pos.col = 1;
    156     } else {
    157       sp->pos.col = add_u32_wrap(sp->pos.col, 1);
    158     }
    159     return;
    160   }
    161 
    162   if (!sp->need) {
    163     if (b < 0x80u) {
    164       kit_gram_unicode_pos_advance(&sp->pos.upos, b);
    165     } else if (b >= 0xC2u && b <= 0xDFu) {
    166       sp->cp = (uint32_t)(b & 0x1Fu);
    167       sp->need = 1;
    168     } else if (b >= 0xE0u && b <= 0xEFu) {
    169       sp->cp = (uint32_t)(b & 0x0Fu);
    170       sp->need = 2;
    171     } else if (b >= 0xF0u && b <= 0xF4u) {
    172       sp->cp = (uint32_t)(b & 0x07u);
    173       sp->need = 3;
    174     }
    175     return;
    176   }
    177 
    178   if (!kit_gram_utf8_is_cont(b)) {
    179     sp->need = 0;
    180     return;
    181   }
    182   sp->cp = (sp->cp << 6) | (uint32_t)(b & 0x3Fu);
    183   if (--sp->need == 0) kit_gram_unicode_pos_advance(&sp->pos.upos, sp->cp);
    184 }
    185 
    186 static void release_span(KitGramLexInputSpan* span) {
    187   if (span->on_consumed) span->on_consumed(span->ud, span->bytes, span->len);
    188   span->next = NULL;
    189 }
    190 
    191 static void release_consumed_front(kit_gram_lex_input_impl* in) {
    192   while (in->head && in->off >= in->head->len) {
    193     KitGramLexInputSpan* span = in->head;
    194     in->head = span->next;
    195     if (in->tail == span) in->tail = NULL;
    196     in->off = 0;
    197     release_span(span);
    198   }
    199 }
    200 
    201 static void release_all(kit_gram_lex_input_impl* in) {
    202   KitGramLexInputSpan* span = in->head;
    203   while (span) {
    204     KitGramLexInputSpan* next = span->next;
    205     release_span(span);
    206     span = next;
    207   }
    208   in->head = NULL;
    209   in->tail = NULL;
    210   in->off = 0;
    211 }
    212 
    213 static void set_error(kit_gram_lex_input_impl* in, uint32_t line, uint32_t col,
    214                       unsigned char byte, const char* message) {
    215   if (!in || in->failed) return;
    216   in->failed = true;
    217   in->pending_len = 0;
    218   in->token_valid = false;
    219   in->error = (KitGramLexError){
    220       .line = line, .col = col, .byte = byte, .message = message};
    221   release_all(in);
    222 }
    223 
    224 static void set_current_error(kit_gram_lex_input_impl* in, unsigned char byte,
    225                               const char* message) {
    226   set_error(in, cur_line(in), cur_col(in), byte, message);
    227 }
    228 
    229 static void set_hook_error(kit_gram_lex_input_impl* in, const KitGramToken* tok,
    230                            const KitGramLexError* err, const char* fallback) {
    231   uint32_t line = err && err->line ? err->line : tok->line;
    232   uint32_t col = err && err->col ? err->col : tok->col;
    233   unsigned char byte = err ? err->byte : 0;
    234   const char* message = err && err->message ? err->message : fallback;
    235   set_error(in, line, col, byte, message);
    236 }
    237 
    238 static size_t gather_from(span_pos pos, unsigned char* out, size_t cap) {
    239   size_t n = 0;
    240   KitGramLexInputSpan* span = pos.span;
    241   size_t off = pos.off;
    242   while (span && n < cap) {
    243     while (off < span->len && n < cap) out[n++] = span->bytes[off++];
    244     span = span->next;
    245     off = 0;
    246   }
    247   return n;
    248 }
    249 
    250 static bool diagnose_utf8_at_current(const kit_gram_lex_input_impl* in,
    251                                      const char** message) {
    252   if (!in->mode_set || in->mode != KIT_GRAM_LEX_INPUT_UTF8 || !in->head ||
    253       in->off >= in->head->len)
    254     return false;
    255   unsigned char tmp[4];
    256   uint32_t cp = 0;
    257   size_t nbytes = 0;
    258   size_t n = gather_from((span_pos){in->head, in->off}, tmp, sizeof tmp);
    259   KitGramUtf8Status st = kit_gram_utf8_decode_one(tmp, n, &cp, &nbytes);
    260   (void)cp;
    261   (void)nbytes;
    262   if (st == KIT_GRAM_UTF8_INVALID) {
    263     *message = "invalid UTF-8";
    264     return true;
    265   }
    266   if (st == KIT_GRAM_UTF8_NEED_MORE && in->finished) {
    267     *message = "incomplete UTF-8";
    268     return true;
    269   }
    270   return false;
    271 }
    272 
    273 static bool advance_cursor_raw(kit_gram_lex_input_impl* in, size_t n) {
    274   if (!n) return true;
    275   if (in->head && in->off + n <= in->head->len) {
    276     in->off += n;
    277     release_consumed_front(in);
    278     return true;
    279   }
    280   size_t remaining = n;
    281   while (remaining && in->head) {
    282     size_t avail = in->head->len - in->off;
    283     size_t take = remaining < avail ? remaining : avail;
    284     in->off += take;
    285     remaining -= take;
    286     release_consumed_front(in);
    287   }
    288   if (remaining) {
    289     set_current_error(in, 0, "input cursor out of range");
    290     return false;
    291   }
    292   return true;
    293 }
    294 
    295 static bool consume_committed(kit_gram_lex_input_impl* in, size_t n,
    296                               const lex_pos* pos) {
    297   if (!advance_cursor_raw(in, n)) return false;
    298   set_pos(in, pos);
    299   return true;
    300 }
    301 
    302 static void commit_pending(kit_gram_lex_input_impl* in) {
    303   if (!in || !in->pending_len || in->failed) return;
    304   size_t n = in->pending_len;
    305   in->pending_len = 0;
    306   in->token_valid = false;
    307   (void)consume_committed(in, n, &in->pending_pos);
    308 }
    309 
    310 /* ---- Tier-1 edge anchors (zero-width DFA transitions) --------------------
    311  * Edge anchors are baked into the DFA: start anchors (\A/^) select an alternate
    312  * start state, and end anchors (\z/$) surface as the per-state end-context
    313  * accept tables. The driver picks the start state from the start-of-match
    314  * context and, at each scanned length, the highest-priority accept whose end
    315  * context holds. Both the tokenizer and the match API share these two helpers.
    316  */
    317 
    318 /* Pick the start DFA state for a scan beginning at a position that is (or is
    319  * not) the start of text / a line. start_text/start_line are 0 when the grammar
    320  * has no start anchors, so state 0 serves every context. */
    321 static uint16_t lex_start_state(const KitGramLexGrammar* g, bool multiline,
    322                                 bool text_start, bool line_start) {
    323   if (text_start) return g->start_text;
    324   if (multiline && line_start) return g->start_line;
    325   return 0;
    326 }
    327 
    328 /* Priority-winning accept at `state` for a match whose end is at_text_end
    329  * and/or before a line break. Plain accepts are always eligible; \z (text)
    330  * accepts at text end; $ (line) accepts at text end or, under multiline, before
    331  * a break. Lower accept id = higher source priority; KIT_GRAM_LEX_ACCEPT_NONE =
    332  * none.
    333  */
    334 static uint16_t lex_pick_accept(const KitGramLexGrammar* g, uint16_t state,
    335                                 bool at_text_end, bool before_break,
    336                                 bool multiline) {
    337   uint16_t best = g->accept[state];
    338   if (at_text_end && g->accept_text) {
    339     uint16_t a = g->accept_text[state];
    340     if (a < best) best = a;
    341   }
    342   if (g->accept_line && (at_text_end || (multiline && before_break))) {
    343     uint16_t a = g->accept_line[state];
    344     if (a < best) best = a;
    345   }
    346   return best;
    347 }
    348 
    349 /* End-of-match context at the lookahead position (span,off): whether it is the
    350  * end of text, and whether a line break begins there. `determined` is false
    351  * when the deciding bytes are not yet available and the input is not finished
    352  * (the tokenizer then reports NEED_MORE). `want_break` is false when no line
    353  * ($) end anchor is in play, so the break scalar need not be decoded. */
    354 typedef struct {
    355   bool determined;
    356   bool at_text_end;
    357   bool before_break;
    358 } scan_end_ctx;
    359 
    360 static scan_end_ctx scan_end_context(const kit_gram_lex_input_impl* in,
    361                                      KitGramLexInputSpan* span, size_t off,
    362                                      bool want_break) {
    363   while (span && off >= span->len) {
    364     off -= span->len;
    365     span = span->next;
    366   }
    367   if (!span) {
    368     if (in->finished) return (scan_end_ctx){true, true, false};
    369     return (scan_end_ctx){false, false, false};
    370   }
    371   if (!want_break) return (scan_end_ctx){true, false, false};
    372   bool brk;
    373   if (in->mode == KIT_GRAM_LEX_INPUT_UTF8) {
    374     unsigned char tmp[4];
    375     size_t n = gather_from((span_pos){span, off}, tmp, sizeof tmp);
    376     uint32_t cp = 0;
    377     size_t nbytes = 0;
    378     KitGramUtf8Status st = kit_gram_utf8_decode_one(tmp, n, &cp, &nbytes);
    379     if (st == KIT_GRAM_UTF8_NEED_MORE && !in->finished)
    380       return (scan_end_ctx){false, false, false};
    381     brk = st == KIT_GRAM_UTF8_OK && kit_gram_unicode_is_line_break(cp);
    382   } else {
    383     brk = span->bytes[off] == '\n';
    384   }
    385   return (scan_end_ctx){true, false, brk};
    386 }
    387 
    388 static bool state_has_outgoing(const KitGramLexGrammar* g, uint16_t state) {
    389   if (!g || state >= g->nstates) return false;
    390   uint16_t stride = g->class_stride ? g->class_stride : g->nclasses;
    391   const uint16_t* row = &g->trans[(size_t)state * stride];
    392   for (uint16_t i = 0; i < g->nclasses; i++)
    393     if (row[i] != KIT_GRAM_LEX_DEAD) return true;
    394   return false;
    395 }
    396 
    397 typedef enum {
    398   SCAN_ACCEPT,
    399   SCAN_NEED_MORE,
    400   SCAN_ERROR,
    401 } scan_status;
    402 
    403 static scan_status scan_one(kit_gram_lexer_impl* lx, uint16_t* accept_out,
    404                             size_t* len_out, lex_pos* pos_out) {
    405   kit_gram_lex_input_impl* in = lx->in;
    406   const KitGramLexGrammar* g = lx->g;
    407   const uint8_t* class_of = g->class_of;
    408   const uint16_t* trans = g->trans;
    409   const uint16_t stride = g->class_stride ? g->class_stride : g->nclasses;
    410   const bool ml = g->multiline;
    411   const bool has_end = g->accept_text || g->accept_line;
    412   const KitGramLexInputMode mode = g->input;
    413 
    414   uint16_t state = 0;
    415   if (g->start_text || g->start_line) {
    416     bool ts = cur_line(in) == 1 && cur_col(in) == 1;
    417     bool ls = cur_col(in) == 1; /* text start or just after a line break */
    418     state = lex_start_state(g, ml, ts, ls);
    419   }
    420   uint16_t best_accept = KIT_GRAM_LEX_ACCEPT_NONE;
    421   size_t best_len = 0;
    422   lex_pos best_pos = current_pos(in);
    423   /* scan_pos drives UTF-8 scalar position folding; the byte fast path tracks
    424    * position arithmetically and never touches it, so initialize it lazily only
    425    * in the branches that use it. */
    426   scan_pos sp;
    427   size_t seen = 0;
    428   KitGramLexInputSpan* span = in->head;
    429   size_t off = in->off;
    430 
    431   if (!has_end) {
    432     /* Fast path: no end anchors, so accepts need no end-context lookahead.
    433      * The per-byte loop is specialized on input mode (loop unswitching). In
    434      * byte mode the column advances exactly one per byte and resets only
    435      * across the rare embedded newline, so the winning end position is
    436      * arithmetic — avoiding both the per-accept 20-byte position-struct copy
    437      * (whose bulk, upos, is unused in byte mode) and the per-byte runtime
    438      * mode branch inside scan_pos_advance. UTF-8 mode keeps the folded
    439      * scan_pos scalar decode. */
    440     const uint16_t* accept_tbl = g->accept;
    441     bool dead = false;
    442     if (mode != KIT_GRAM_LEX_INPUT_UTF8) {
    443       const uint32_t start_col = in->col;
    444       uint32_t line = in->line;
    445       size_t last_nl = 0; /* seen index just past the last '\n'; 0 = none */
    446       while (span) {
    447         const unsigned char* buf = span->bytes;
    448         size_t len = span->len;
    449         while (off < len) {
    450           unsigned char b = buf[off];
    451           uint16_t next = trans[(size_t)state * stride + class_of[b]];
    452           if (next == KIT_GRAM_LEX_DEAD) {
    453             dead = true;
    454             goto fast_done;
    455           }
    456           state = next;
    457           off++;
    458           seen++;
    459           if (b == '\n') {
    460             line++;
    461             last_nl = seen;
    462           }
    463           uint16_t accept = accept_tbl[state];
    464           if (accept != KIT_GRAM_LEX_ACCEPT_NONE) {
    465             best_accept = accept;
    466             best_len = seen;
    467             best_pos.line = line;
    468             best_pos.col = last_nl ? (uint32_t)(1u + (seen - last_nl))
    469                                    : add_u32_wrap(start_col, seen);
    470           }
    471         }
    472         span = span->next;
    473         off = 0;
    474       }
    475     } else {
    476       scan_pos_init(&sp, in);
    477       while (span) {
    478         const unsigned char* buf = span->bytes;
    479         size_t len = span->len;
    480         while (off < len) {
    481           unsigned char b = buf[off];
    482           uint16_t next = trans[(size_t)state * stride + class_of[b]];
    483           if (next == KIT_GRAM_LEX_DEAD) {
    484             dead = true;
    485             goto fast_done;
    486           }
    487           state = next;
    488           off++;
    489           seen++;
    490           scan_pos_advance(&sp, KIT_GRAM_LEX_INPUT_UTF8, b);
    491           uint16_t accept = accept_tbl[state];
    492           if (accept != KIT_GRAM_LEX_ACCEPT_NONE) {
    493             best_accept = accept;
    494             best_len = seen;
    495             best_pos = sp.pos;
    496           }
    497         }
    498         span = span->next;
    499         off = 0;
    500       }
    501     }
    502   fast_done:
    503     if (dead) {
    504       if (best_accept != KIT_GRAM_LEX_ACCEPT_NONE) {
    505         *accept_out = best_accept;
    506         *len_out = best_len;
    507         *pos_out = best_pos;
    508         return SCAN_ACCEPT;
    509       }
    510       const char* message = "invalid token";
    511       diagnose_utf8_at_current(in, &message);
    512       set_current_error(in, current_byte(in), message);
    513       return SCAN_ERROR;
    514     }
    515 
    516     if (best_accept != KIT_GRAM_LEX_ACCEPT_NONE) {
    517       if (in->finished || !state_has_outgoing(g, state)) {
    518         *accept_out = best_accept;
    519         *len_out = best_len;
    520         *pos_out = best_pos;
    521         return SCAN_ACCEPT;
    522       }
    523     } else if (in->finished) {
    524       const char* message = "incomplete token";
    525       diagnose_utf8_at_current(in, &message);
    526       set_current_error(in, current_byte(in), message);
    527       return SCAN_ERROR;
    528     } else if (!state_has_outgoing(g, state)) {
    529       const char* message = "invalid token";
    530       diagnose_utf8_at_current(in, &message);
    531       set_current_error(in, current_byte(in), message);
    532       return SCAN_ERROR;
    533     }
    534     return SCAN_NEED_MORE;
    535   }
    536 
    537   /* End-anchored grammar: evaluate the full candidate (plain plus the eligible
    538    * end-context accepts) at each scanned length. This needs one position of
    539    * lookahead for the end context, so it can report NEED_MORE mid-token. */
    540   const bool want_break = g->accept_line && ml;
    541   bool dead = false;
    542   scan_pos_init(&sp, in);
    543   for (;;) {
    544     while (span && off >= span->len) {
    545       span = span->next;
    546       off = 0;
    547     }
    548     scan_end_ctx ec = scan_end_context(in, span, off, want_break);
    549     if (!ec.determined) {
    550       /* End context unresolved (more bytes may arrive). Record the
    551        * context-free plain accept; wait unless nothing more can change the
    552        * outcome (no extension and no pending end accept at this state). */
    553       uint16_t plain = g->accept[state];
    554       if (plain != KIT_GRAM_LEX_ACCEPT_NONE) {
    555         best_accept = plain;
    556         best_len = seen;
    557         best_pos = sp.pos;
    558       }
    559       bool end_pending =
    560           (g->accept_text && g->accept_text[state] != KIT_GRAM_LEX_ACCEPT_NONE) ||
    561           (g->accept_line && g->accept_line[state] != KIT_GRAM_LEX_ACCEPT_NONE);
    562       if (state_has_outgoing(g, state) || end_pending) return SCAN_NEED_MORE;
    563       break;
    564     }
    565     uint16_t cand =
    566         lex_pick_accept(g, state, ec.at_text_end, ec.before_break, ml);
    567     if (cand != KIT_GRAM_LEX_ACCEPT_NONE) {
    568       best_accept = cand;
    569       best_len = seen;
    570       best_pos = sp.pos;
    571     }
    572     if (!span) break; /* end of text (input finished) */
    573     unsigned char b = span->bytes[off];
    574     uint16_t next = trans[(size_t)state * stride + class_of[b]];
    575     if (next == KIT_GRAM_LEX_DEAD) {
    576       dead = true;
    577       break;
    578     }
    579     state = next;
    580     off++;
    581     seen++;
    582     scan_pos_advance(&sp, mode, b);
    583   }
    584 
    585   if (best_accept != KIT_GRAM_LEX_ACCEPT_NONE) {
    586     *accept_out = best_accept;
    587     *len_out = best_len;
    588     *pos_out = best_pos;
    589     return SCAN_ACCEPT;
    590   }
    591   const char* message = dead ? "invalid token" : "incomplete token";
    592   diagnose_utf8_at_current(in, &message);
    593   set_current_error(in, current_byte(in), message);
    594   return SCAN_ERROR;
    595 }
    596 
    597 static const unsigned char* prepare_lexeme(kit_gram_lex_input_impl* in,
    598                                            size_t len) {
    599   in->token_start = (span_pos){in->head, in->off};
    600   in->token_len = len;
    601   in->token_valid = true;
    602 
    603   if (!len) {
    604     in->token_lexeme = (const unsigned char*)"";
    605     return in->token_lexeme;
    606   }
    607   if (in->head && in->off + len <= in->head->len) {
    608     in->token_lexeme = in->head->bytes + in->off;
    609     return in->token_lexeme;
    610   }
    611   if (in->cfg.carry && len <= in->cfg.carry_cap) {
    612     size_t copied = 0;
    613     KitGramLexInputSpan* span = in->head;
    614     size_t off = in->off;
    615     while (span && copied < len) {
    616       size_t avail = span->len - off;
    617       size_t take = len - copied < avail ? len - copied : avail;
    618       memcpy(in->cfg.carry + copied, span->bytes + off, take);
    619       copied += take;
    620       span = span->next;
    621       off = 0;
    622     }
    623     in->token_lexeme = in->cfg.carry;
    624     return in->token_lexeme;
    625   }
    626   in->token_lexeme = NULL;
    627   return NULL;
    628 }
    629 
    630 /* ---- Extracted-keyword lookup (shared) -----------------------------------
    631  * A literal token shadowed by a more general host recognizer is moved out of
    632  * the DFA into a sorted (lexeme -> kind) table keyed by the host kind. After
    633  * the DFA picks a token whose kind is a host, the runtime binary-searches the
    634  * matched lexeme and rewrites the kind on an exact hit. The two callers (the
    635  * tokenizer and the match API) read the matched bytes differently, so they pass
    636  * a byte accessor; the table is sorted in the same memcmp order the comparator
    637  * uses. */
    638 static const KitGramLexKeywordTable* keyword_table_for(
    639     const KitGramLexGrammar* g, KitGramTokenKind host) {
    640   for (uint16_t i = 0; i < g->nkeyword_tables; i++)
    641     if (g->keyword_tables[i].host == host) return &g->keyword_tables[i];
    642   return NULL;
    643 }
    644 
    645 typedef unsigned char (*kw_byte_fn)(void* ud, size_t i);
    646 
    647 static uint64_t kw_fnv_bytes(const unsigned char* s, size_t len) {
    648   uint64_t h = KW_FNV_OFFSET;
    649   for (size_t i = 0; i < len; i++) h = (h ^ s[i]) * KW_FNV_PRIME;
    650   return h;
    651 }
    652 
    653 static uint64_t kw_fnv_indirect(kw_byte_fn at, void* ud, size_t len) {
    654   uint64_t h = KW_FNV_OFFSET;
    655   for (size_t i = 0; i < len; i++) h = (h ^ at(ud, i)) * KW_FNV_PRIME;
    656   return h;
    657 }
    658 
    659 /* Finish a seed-independent FNV byte hash the same way kit_gram_lex_kw_hash64 does
    660  * after its byte loop. This lets lookup hash the lexeme bytes once, then derive
    661  * both the CHD bucket and displaced slot from the same accumulator. */
    662 static uint64_t kw_hash_finish(uint64_t h, uint32_t seed) {
    663   h ^= (uint64_t)seed * KW_MIX1;
    664   h ^= h >> 33;
    665   h *= KW_MIX2;
    666   h ^= h >> 33;
    667   h *= KW_MIX3;
    668   h ^= h >> 33;
    669   return h;
    670 }
    671 
    672 static bool kw_eq_indirect(kw_byte_fn at, void* ud, size_t len,
    673                            const KitGramLexKeyword* kw) {
    674   if (len != kw->len) return false;
    675   for (size_t i = 0; i < len; i++)
    676     if (at(ud, i) != (unsigned char)kw->lexeme[i]) return false;
    677   return true;
    678 }
    679 
    680 static bool kw_eq_bytes(const unsigned char* s, size_t len,
    681                         const KitGramLexKeyword* kw) {
    682   return len == kw->len && memcmp(s, kw->lexeme, len) == 0;
    683 }
    684 
    685 /* Minimal perfect hash (CHD): bucket via seed 0, displace via the bucket's
    686  * seed, then one byte compare confirms the lexeme. nseeds and nkeywords are
    687  * powers of two (the generator rounds up, padding the slot array with empty
    688  * entries), so both indices are a mask rather than a per-token modulo. */
    689 static bool kw_lookup_hashed(const KitGramLexKeywordTable* t,
    690                              uint64_t byte_hash, const unsigned char* bytes,
    691                              kw_byte_fn at, void* ud, size_t len,
    692                              KitGramTokenKind* out) {
    693   if (!t->nseeds || !t->nkeywords) return false;
    694   if ((t->min_len || t->max_len) && (len < t->min_len || len > t->max_len))
    695     return false;
    696   uint32_t bucket =
    697       (uint32_t)(kw_hash_finish(byte_hash, 0) & (uint64_t)(t->nseeds - 1u));
    698   uint32_t slot = (uint32_t)(kw_hash_finish(byte_hash, t->seeds[bucket]) &
    699                              (uint64_t)(t->nkeywords - 1u));
    700   const KitGramLexKeyword* kw = &t->keywords[slot];
    701   if (!kw->lexeme) return false;
    702   if (bytes) {
    703     if (!kw_eq_bytes(bytes, len, kw)) return false;
    704   } else if (!kw_eq_indirect(at, ud, len, kw)) {
    705     return false;
    706   }
    707   *out = kw->kind;
    708   return true;
    709 }
    710 
    711 static bool kw_lookup_bytes(const KitGramLexKeywordTable* t,
    712                             const unsigned char* bytes, size_t len,
    713                             KitGramTokenKind* out) {
    714   if (!bytes) return false;
    715   if ((t->min_len || t->max_len) && (len < t->min_len || len > t->max_len))
    716     return false;
    717   return kw_lookup_hashed(t, kw_fnv_bytes(bytes, len), bytes, NULL, NULL, len,
    718                           out);
    719 }
    720 
    721 static bool kw_lookup_indirect(const KitGramLexKeywordTable* t, kw_byte_fn at,
    722                                void* ud, size_t len, KitGramTokenKind* out) {
    723   if ((t->min_len || t->max_len) && (len < t->min_len || len > t->max_len))
    724     return false;
    725   return kw_lookup_hashed(t, kw_fnv_indirect(at, ud, len), NULL, at, ud, len,
    726                           out);
    727 }
    728 
    729 KitGramTokenKind kit_gram_lex_keyword_rewrite(const KitGramLexGrammar* g,
    730                                        KitGramTokenKind kind,
    731                                        const unsigned char* bytes, size_t len) {
    732   const KitGramLexKeywordTable* t = keyword_table_for(g, kind);
    733   if (!t) return kind;
    734   KitGramTokenKind out;
    735   if (kw_lookup_bytes(t, bytes, len, &out)) return out;
    736   return kind;
    737 }
    738 
    739 /* Byte i of a token starting at a span_pos, walking the span list. */
    740 static unsigned char tok_kw_byte(void* ud, size_t i) {
    741   span_pos p = *(const span_pos*)ud;
    742   KitGramLexInputSpan* span = p.span;
    743   size_t off = p.off + i;
    744   while (span && off >= span->len) {
    745     off -= span->len;
    746     span = span->next;
    747   }
    748   return span ? span->bytes[off] : 0;
    749 }
    750 
    751 void kit_gram_lex_input_init(KitGramLexInput* mem,
    752                          const KitGramLexInputConfig* cfg) {
    753   if (!mem) return;
    754   kit_gram_lex_input_impl* in = in_impl(mem);
    755   *in = (kit_gram_lex_input_impl){
    756       .cfg = cfg ? *cfg : (KitGramLexInputConfig){0}, .line = 1, .col = 1};
    757   kit_gram_unicode_pos_init(&in->upos);
    758   if (in->cfg.carry_cap && !in->cfg.carry)
    759     set_error(in, 1, 1, 0, "invalid lexer input configuration");
    760 }
    761 
    762 void kit_gram_lex_input_push(KitGramLexInput* mem, KitGramLexInputSpan* span) {
    763   if (!mem || !span) return;
    764   kit_gram_lex_input_impl* in = in_impl(mem);
    765   if (in->failed) return;
    766   commit_pending(in);
    767   if (in->failed) return;
    768   if (in->finished) {
    769     set_current_error(in, 0, "cannot push after lexer input finish");
    770     return;
    771   }
    772   if (span->len && !span->bytes) {
    773     set_current_error(in, 0, "invalid lexer input span");
    774     return;
    775   }
    776   span->next = NULL;
    777   if (in->tail)
    778     in->tail->next = span;
    779   else
    780     in->head = span;
    781   in->tail = span;
    782   release_consumed_front(in);
    783 }
    784 
    785 void kit_gram_lex_input_finish(KitGramLexInput* mem) {
    786   if (!mem) return;
    787   kit_gram_lex_input_impl* in = in_impl(mem);
    788   if (in->failed) return;
    789   commit_pending(in);
    790   if (!in->failed) {
    791     in->finished = true;
    792     release_consumed_front(in);
    793   }
    794 }
    795 
    796 const KitGramLexError* kit_gram_lex_input_error(const KitGramLexInput* in) {
    797   static const KitGramLexError null_error = {
    798       .line = 1, .col = 1, .message = "null lexer input"};
    799   return in ? &cin_impl(in)->error : &null_error;
    800 }
    801 
    802 void kit_gram_lexer_init(KitGramLexer* mem, const KitGramLexGrammar* g,
    803                      KitGramLexInput* input, const KitGramLexConfig* cfg) {
    804   if (!mem) return;
    805   kit_gram_lexer_impl* lx = lx_impl(mem);
    806   *lx = (kit_gram_lexer_impl){.g = g,
    807                           .input_mem = input,
    808                           .in = input ? in_impl(input) : NULL,
    809                           .cfg = cfg ? *cfg : (KitGramLexConfig){0}};
    810   if (!input) return;
    811   kit_gram_lex_input_impl* in = lx->in;
    812   if (in->failed) return;
    813   if (!g || !g->trans || !g->accept || !g->accepts || g->nclasses == 0 ||
    814       g->nstates == 0 || (g->class_stride && g->class_stride < g->nclasses)) {
    815     set_error(in, cur_line(in), cur_col(in), 0, "invalid lexer configuration");
    816     return;
    817   }
    818   if (!in->mode_set) {
    819     in->mode = g->input;
    820     in->mode_set = true;
    821   } else if (in->mode != g->input) {
    822     set_error(in, cur_line(in), cur_col(in), 0, "lexer input mode mismatch");
    823   }
    824 }
    825 
    826 KitGramLexStatus kit_gram_lexer_next(KitGramLexer* mem, KitGramToken* out) {
    827   kit_gram_lexer_impl* lx = lx_impl(mem);
    828   kit_gram_lex_input_impl* in = lx->in;
    829   if (!in || !lx->g) return KIT_GRAM_LEX_ERROR;
    830   if (in->failed) return KIT_GRAM_LEX_ERROR;
    831   if (!out) {
    832     set_error(in, cur_line(in), cur_col(in), 0, "null token output");
    833     return KIT_GRAM_LEX_ERROR;
    834   }
    835 
    836   commit_pending(in);
    837   if (in->failed) return KIT_GRAM_LEX_ERROR;
    838 
    839   for (;;) {
    840     release_consumed_front(in);
    841     if (!in->head) return in->finished ? KIT_GRAM_LEX_EOF : KIT_GRAM_LEX_NEED_MORE;
    842 
    843     uint16_t accept = KIT_GRAM_LEX_ACCEPT_NONE;
    844     size_t tok_len = 0;
    845     lex_pos tok_end = current_pos(in);
    846     scan_status st = scan_one(lx, &accept, &tok_len, &tok_end);
    847     if (st == SCAN_NEED_MORE) return KIT_GRAM_LEX_NEED_MORE;
    848     if (st == SCAN_ERROR) return KIT_GRAM_LEX_ERROR;
    849     if (accept >= lx->g->naccepts) {
    850       set_error(in, cur_line(in), cur_col(in), 0, "invalid lexer accept table");
    851       return KIT_GRAM_LEX_ERROR;
    852     }
    853 
    854     const KitGramLexAccept* a = &lx->g->accepts[accept];
    855     /* Edge anchors are already resolved inside scan_one (start-state choice
    856      * plus end-context accept selection), so the accepted token is final. */
    857 
    858     if (a->skip) {
    859       if (!consume_committed(in, tok_len, &tok_end)) return KIT_GRAM_LEX_ERROR;
    860       continue;
    861     }
    862 
    863     const unsigned char* lexeme = prepare_lexeme(in, tok_len);
    864     KitGramToken tok = {
    865         .kind = a->tok,
    866         .lexeme = (const char*)lexeme,
    867         .len = tok_len,
    868         .line = cur_line(in),
    869         .col = cur_col(in),
    870     };
    871     in->pending_len = tok_len;
    872     in->pending_pos = tok_end;
    873 
    874     /* Rewrite an extracted keyword's kind on the whole lexeme, before the
    875      * hook, so the hook sees final kinds (as with the in-DFA representation).
    876      */
    877     if (lx->g->nkeyword_tables) {
    878       const KitGramLexKeywordTable* kt = keyword_table_for(lx->g, a->tok);
    879       KitGramTokenKind kw_kind;
    880       if (kt) {
    881         if (lexeme) {
    882           if (kw_lookup_bytes(kt, lexeme, tok_len, &kw_kind))
    883             tok.kind = kw_kind;
    884         } else {
    885           span_pos sp = in->token_start;
    886           if (kw_lookup_indirect(kt, tok_kw_byte, &sp, tok_len, &kw_kind))
    887             tok.kind = kw_kind;
    888         }
    889       }
    890     }
    891 
    892     if (lx->cfg.token_hook) {
    893       KitGramLexError hook_err = {.line = tok.line, .col = tok.col};
    894       KitGramLexHookResult hr =
    895           lx->cfg.token_hook(mem, lx->cfg.hook_ud, &tok, &hook_err);
    896       switch (hr) {
    897         case KIT_GRAM_LEX_HOOK_KEEP:
    898           break;
    899         case KIT_GRAM_LEX_HOOK_SKIP:
    900           commit_pending(in);
    901           if (in->failed) return KIT_GRAM_LEX_ERROR;
    902           continue;
    903         case KIT_GRAM_LEX_HOOK_ERROR:
    904           set_hook_error(in, &tok, &hook_err, "lexer token hook failed");
    905           return KIT_GRAM_LEX_ERROR;
    906         default:
    907           set_hook_error(in, &tok, &hook_err,
    908                          "invalid lexer token hook result");
    909           return KIT_GRAM_LEX_ERROR;
    910       }
    911     }
    912 
    913     *out = tok;
    914     return KIT_GRAM_LEX_TOKEN;
    915   }
    916 }
    917 
    918 void kit_gram_lexer_lexeme(KitGramLexer* mem, KitGramLexLexeme* it_mem) {
    919   if (!it_mem) return;
    920   kit_gram_lex_lexeme_impl* it = it_impl(it_mem);
    921   *it = (kit_gram_lex_lexeme_impl){0};
    922   if (!mem) return;
    923   kit_gram_lexer_impl* lx = lx_impl(mem);
    924   kit_gram_lex_input_impl* in = lx->in;
    925   if (!in || !in->token_valid) return;
    926   it->in = in;
    927   it->remaining = in->token_len;
    928   if (in->token_lexeme) {
    929     it->single = in->token_lexeme;
    930     it->single_len = in->token_len;
    931   } else {
    932     it->span = in->token_start.span;
    933     it->off = in->token_start.off;
    934   }
    935 }
    936 
    937 bool kit_gram_lex_lexeme_next(KitGramLexLexeme* it_mem, KitGramLexSegment* seg) {
    938   if (!it_mem || !seg) return false;
    939   kit_gram_lex_lexeme_impl* it = it_impl(it_mem);
    940   if (it->single) {
    941     if (it->yielded_single) return false;
    942     it->yielded_single = true;
    943     *seg = (KitGramLexSegment){.bytes = it->single, .len = it->single_len};
    944     return true;
    945   }
    946   while (it->span && it->remaining) {
    947     if (it->off >= it->span->len) {
    948       it->span = it->span->next;
    949       it->off = 0;
    950       continue;
    951     }
    952     size_t avail = it->span->len - it->off;
    953     size_t take = it->remaining < avail ? it->remaining : avail;
    954     *seg = (KitGramLexSegment){.bytes = it->span->bytes + it->off, .len = take};
    955     it->off += take;
    956     it->remaining -= take;
    957     return true;
    958   }
    959   return false;
    960 }
    961 
    962 /* ---- Match API (re2::Set, winner-only) -----------------------------------
    963  *
    964  * A read-only view over the input's current span list. Logical offset 0 is the
    965  * first unconsumed byte (in->head at in->off). The view never mutates or
    966  * consumes the input, so the same input may be matched repeatedly. */
    967 typedef struct {
    968   KitGramLexInputSpan* head;
    969   size_t head_off;
    970   size_t total;
    971   KitGramLexInputMode mode;
    972 } match_view;
    973 
    974 /* Locate the span/offset holding logical position pos. Returns false if pos is
    975  * at or beyond the end of available bytes. */
    976 static bool view_locate(const match_view* v, size_t pos,
    977                         KitGramLexInputSpan** span_out, size_t* off_out) {
    978   KitGramLexInputSpan* s = v->head;
    979   size_t off = v->head_off;
    980   size_t remaining = pos;
    981   while (s) {
    982     size_t avail = s->len - off;
    983     if (remaining < avail) {
    984       *span_out = s;
    985       *off_out = off + remaining;
    986       return true;
    987     }
    988     remaining -= avail;
    989     s = s->next;
    990     off = 0;
    991   }
    992   return false;
    993 }
    994 
    995 static bool view_byte(const match_view* v, size_t pos, unsigned char* out) {
    996   KitGramLexInputSpan* s;
    997   size_t off;
    998   if (!view_locate(v, pos, &s, &off)) return false;
    999   *out = s->bytes[off];
   1000   return true;
   1001 }
   1002 
   1003 static size_t view_gather(const match_view* v, size_t pos, unsigned char* out,
   1004                           size_t cap) {
   1005   KitGramLexInputSpan* s;
   1006   size_t off;
   1007   if (!view_locate(v, pos, &s, &off)) return 0;
   1008   return gather_from((span_pos){s, off}, out, cap);
   1009 }
   1010 
   1011 /* True iff a line-break scalar begins exactly at logical position pos. */
   1012 static bool view_break_at(const match_view* v, size_t pos) {
   1013   if (v->mode == KIT_GRAM_LEX_INPUT_UTF8) {
   1014     unsigned char tmp[4];
   1015     size_t n = view_gather(v, pos, tmp, sizeof tmp);
   1016     uint32_t cp = 0;
   1017     size_t nbytes = 0;
   1018     if (kit_gram_utf8_decode_one(tmp, n, &cp, &nbytes) != KIT_GRAM_UTF8_OK)
   1019       return false;
   1020     return kit_gram_unicode_is_line_break(cp);
   1021   }
   1022   unsigned char b = 0;
   1023   return view_byte(v, pos, &b) && b == '\n';
   1024 }
   1025 
   1026 /* True iff a line-break scalar ends exactly at logical position pos (pos > 0),
   1027  * i.e. pos is immediately after a newline. */
   1028 static bool view_break_before(const match_view* v, size_t pos) {
   1029   if (pos == 0) return false;
   1030   if (v->mode != KIT_GRAM_LEX_INPUT_UTF8) {
   1031     unsigned char b = 0;
   1032     return view_byte(v, pos - 1, &b) && b == '\n';
   1033   }
   1034   /* Walk back over UTF-8 continuation bytes to the lead byte, then decode
   1035    * forward and require the scalar to end exactly at pos. */
   1036   size_t start = pos - 1;
   1037   unsigned char b = 0;
   1038   while (start > 0) {
   1039     if (!view_byte(v, start, &b)) return false;
   1040     if ((b & 0xC0u) != 0x80u) break; /* not a continuation byte */
   1041     if (pos - start >= 4) return false;
   1042     start--;
   1043   }
   1044   unsigned char tmp[4];
   1045   size_t n = view_gather(v, start, tmp, sizeof tmp);
   1046   uint32_t cp = 0;
   1047   size_t nbytes = 0;
   1048   if (kit_gram_utf8_decode_one(tmp, n, &cp, &nbytes) != KIT_GRAM_UTF8_OK) return false;
   1049   return start + nbytes == pos && kit_gram_unicode_is_line_break(cp);
   1050 }
   1051 
   1052 /* Longest-match DFA scan from logical position `start`, honoring edge anchors
   1053  * structurally: the start-of-match context picks the start state, and at each
   1054  * length the highest-priority accept whose end context holds wins. Mirrors
   1055  * scan_one over a resident view — running out of bytes is simply end of text,
   1056  * so there is no NEED_MORE. */
   1057 static bool match_scan(const KitGramLexGrammar* g, const match_view* v,
   1058                        size_t start, bool multiline, uint16_t* accept_out,
   1059                        size_t* len_out) {
   1060   KitGramLexInputSpan* span;
   1061   size_t off;
   1062   if (!view_locate(v, start, &span, &off)) return false;
   1063 
   1064   const uint8_t* class_of = g->class_of;
   1065   const uint16_t* trans = g->trans;
   1066   const uint16_t stride = g->class_stride ? g->class_stride : g->nclasses;
   1067   const bool has_end = g->accept_text || g->accept_line;
   1068 
   1069   uint16_t state = 0;
   1070   if (g->start_text || g->start_line) {
   1071     bool ts = start == 0;
   1072     bool ls = ts || view_break_before(v, start);
   1073     state = lex_start_state(g, multiline, ts, ls);
   1074   }
   1075   uint16_t best_accept = KIT_GRAM_LEX_ACCEPT_NONE;
   1076   size_t best_len = 0, seen = 0;
   1077 
   1078   if (!has_end) {
   1079     const uint16_t* accept_tbl = g->accept;
   1080     while (span) {
   1081       const unsigned char* buf = span->bytes;
   1082       size_t len = span->len;
   1083       while (off < len) {
   1084         uint16_t next = trans[(size_t)state * stride + class_of[buf[off]]];
   1085         if (next == KIT_GRAM_LEX_DEAD) goto done;
   1086         state = next;
   1087         off++;
   1088         seen++;
   1089         uint16_t acc = accept_tbl[state];
   1090         if (acc != KIT_GRAM_LEX_ACCEPT_NONE) {
   1091           best_accept = acc;
   1092           best_len = seen;
   1093         }
   1094       }
   1095       span = span->next;
   1096       off = 0;
   1097     }
   1098   done:
   1099     if (best_accept == KIT_GRAM_LEX_ACCEPT_NONE) return false;
   1100     *accept_out = best_accept;
   1101     *len_out = best_len;
   1102     return true;
   1103   }
   1104 
   1105   const bool want_break = g->accept_line && multiline;
   1106   for (;;) {
   1107     bool at_text_end = start + seen == v->total;
   1108     bool before_break =
   1109         !at_text_end && want_break && view_break_at(v, start + seen);
   1110     uint16_t cand =
   1111         lex_pick_accept(g, state, at_text_end, before_break, multiline);
   1112     if (cand != KIT_GRAM_LEX_ACCEPT_NONE) {
   1113       best_accept = cand;
   1114       best_len = seen;
   1115     }
   1116     while (span && off >= span->len) {
   1117       span = span->next;
   1118       off = 0;
   1119     }
   1120     if (!span) break;
   1121     uint16_t next = trans[(size_t)state * stride + class_of[span->bytes[off]]];
   1122     if (next == KIT_GRAM_LEX_DEAD) break;
   1123     state = next;
   1124     off++;
   1125     seen++;
   1126   }
   1127   if (best_accept == KIT_GRAM_LEX_ACCEPT_NONE) return false;
   1128   *accept_out = best_accept;
   1129   *len_out = best_len;
   1130   return true;
   1131 }
   1132 
   1133 static bool match_valid(const KitGramLexGrammar* g, const KitGramLexInput* in) {
   1134   return g && in && g->trans && g->accept && g->accepts && g->nclasses &&
   1135          g->nstates && (!g->class_stride || g->class_stride >= g->nclasses) &&
   1136          !cin_impl(in)->failed;
   1137 }
   1138 
   1139 /* Byte i of a match at logical offset `base` of the view (in range by
   1140  * construction, since the match length came from the same span list). */
   1141 typedef struct {
   1142   const match_view* v;
   1143   size_t base;
   1144 } view_kw_ud;
   1145 static unsigned char view_kw_byte(void* ud, size_t i) {
   1146   const view_kw_ud* u = ud;
   1147   unsigned char b = 0;
   1148   view_byte(u->v, u->base + i, &b);
   1149   return b;
   1150 }
   1151 
   1152 static const unsigned char* view_contiguous(const match_view* v, size_t base,
   1153                                             size_t len) {
   1154   KitGramLexInputSpan* s;
   1155   size_t off;
   1156   if (!view_locate(v, base, &s, &off))
   1157     return len ? NULL : (const unsigned char*)"";
   1158   return off + len <= s->len ? s->bytes + off : NULL;
   1159 }
   1160 
   1161 /* Longest match at exactly logical position `from`, honoring in-pattern
   1162  * anchors. Writes [from, from+len) and kind on success. Extracted keywords are
   1163  * rewritten the same way the tokenizer does, so a Set match reports the same
   1164  * winning kind whether or not the literal was pulled out of the DFA. */
   1165 static bool match_one_at(const KitGramLexGrammar* g, const match_view* v,
   1166                          bool multiline, size_t from, KitGramMatch* out) {
   1167   uint16_t accept = KIT_GRAM_LEX_ACCEPT_NONE;
   1168   size_t len = 0;
   1169   if (!match_scan(g, v, from, multiline, &accept, &len)) return false;
   1170   if (accept >= g->naccepts) return false;
   1171   const KitGramLexAccept* a = &g->accepts[accept];
   1172   KitGramTokenKind kind = a->tok;
   1173   if (g->nkeyword_tables) {
   1174     const KitGramLexKeywordTable* kt = keyword_table_for(g, a->tok);
   1175     KitGramTokenKind kw_kind;
   1176     if (kt) {
   1177       const unsigned char* bytes = view_contiguous(v, from, len);
   1178       if (bytes) {
   1179         if (kw_lookup_bytes(kt, bytes, len, &kw_kind)) kind = kw_kind;
   1180       } else {
   1181         view_kw_ud ud = {v, from};
   1182         if (kw_lookup_indirect(kt, view_kw_byte, &ud, len, &kw_kind))
   1183           kind = kw_kind;
   1184       }
   1185     }
   1186   }
   1187   out->start = from;
   1188   out->end = from + len;
   1189   out->kind = kind;
   1190   return true;
   1191 }
   1192 
   1193 static match_view view_of(const KitGramLexInput* in) {
   1194   const kit_gram_lex_input_impl* imp = cin_impl(in);
   1195   match_view v = {imp->head, imp->off, 0,
   1196                   imp->mode_set ? imp->mode : KIT_GRAM_LEX_INPUT_BYTES};
   1197   for (KitGramLexInputSpan* s = imp->head; s; s = s->next) {
   1198     size_t base = s == imp->head ? imp->off : 0;
   1199     if (s->len > base) v.total += s->len - base;
   1200   }
   1201   return v;
   1202 }
   1203 
   1204 void kit_gram_matcher_bind(KitGramMatcher* m, const KitGramLexGrammar* g) {
   1205   matcher_impl(m)->g = g;
   1206 }
   1207 
   1208 bool kit_gram_match_anchored(KitGramMatcher* m, KitGramLexInput* in,
   1209                          const KitGramMatchOpts* opts, KitGramMatch* out) {
   1210   const KitGramLexGrammar* g = matcher_impl(m)->g;
   1211   if (!out || !match_valid(g, in)) return false;
   1212   bool multiline = opts && opts->multiline;
   1213   match_view v = view_of(in);
   1214   v.mode = g->input; /* the grammar defines byte vs utf8 newline semantics */
   1215   return match_one_at(g, &v, multiline, 0, out);
   1216 }
   1217 
   1218 bool kit_gram_match_full(KitGramMatcher* m, KitGramLexInput* in,
   1219                      const KitGramMatchOpts* opts, KitGramMatch* out) {
   1220   const KitGramLexGrammar* g = matcher_impl(m)->g;
   1221   if (!out || !match_valid(g, in)) return false;
   1222   bool multiline = opts && opts->multiline;
   1223   match_view v = view_of(in);
   1224   v.mode = g->input; /* the grammar defines byte vs utf8 newline semantics */
   1225   KitGramMatch m2;
   1226   if (!match_one_at(g, &v, multiline, 0, &m2)) return false;
   1227   if (m2.end != v.total) return false; /* the whole input must be one match */
   1228   *out = m2;
   1229   return true;
   1230 }
   1231 
   1232 static bool match_find_from(const KitGramLexGrammar* g, const match_view* v,
   1233                             bool multiline, size_t from, KitGramMatch* out) {
   1234   for (size_t p = from; p < v->total; p++)
   1235     if (match_one_at(g, v, multiline, p, out)) return true;
   1236   return false;
   1237 }
   1238 
   1239 bool kit_gram_match_find(KitGramMatcher* m, KitGramLexInput* in,
   1240                      const KitGramMatchOpts* opts, KitGramMatch* out) {
   1241   const KitGramLexGrammar* g = matcher_impl(m)->g;
   1242   if (!out || !match_valid(g, in)) return false;
   1243   bool multiline = opts && opts->multiline;
   1244   match_view v = view_of(in);
   1245   v.mode = g->input; /* the grammar defines byte vs utf8 newline semantics */
   1246   return match_find_from(g, &v, multiline, 0, out);
   1247 }
   1248 
   1249 void kit_gram_match_iter_init(KitGramMatchIter* it, KitGramMatcher* m,
   1250                           KitGramLexInput* in, const KitGramMatchOpts* opts) {
   1251   if (!it) return;
   1252   const KitGramLexGrammar* g = matcher_impl(m)->g;
   1253   iter_impl(it)->g = g;
   1254   iter_impl(it)->in = in;
   1255   iter_impl(it)->from = 0;
   1256   iter_impl(it)->multiline = opts && opts->multiline;
   1257 }
   1258 
   1259 bool kit_gram_match_iter_next(KitGramMatchIter* it, KitGramMatch* out) {
   1260   if (!it || !out || !match_valid(iter_impl(it)->g, iter_impl(it)->in))
   1261     return false;
   1262   match_view v = view_of(iter_impl(it)->in);
   1263   v.mode =
   1264       iter_impl(it)
   1265           ->g->input; /* the grammar defines byte vs utf8 newline semantics */
   1266   KitGramMatch m;
   1267   if (!match_find_from(iter_impl(it)->g, &v, iter_impl(it)->multiline,
   1268                        iter_impl(it)->from, &m))
   1269     return false;
   1270   *out = m;
   1271   iter_impl(it)->from =
   1272       m.end > iter_impl(it)->from
   1273           ? m.end
   1274           : iter_impl(it)->from + 1; /* non-overlap, progress */
   1275   return true;
   1276 }