/** * Hand-rolled first-set analysis for `regex()` terminals — the interpreter's * replacement for the old `regexp-tree`-backed analyzer (~264 KB). A `regex()` * terminal's first-set is used ONLY to let `choice()` do first-char dispatch * (see `choice.ts`); it is never consulted for whether a match succeeds. So this * analyzer only has to be SOUND as an OVER-approximation: every char the pattern * can actually start with must be in the returned set. Any construct it can't * pin down widens to `any()` (— "could start with anything" —), which only * disables a dispatch fast path, never changes a parse. Under-approximating * (dropping a real start char) WOULD be a bug: dispatch would skip a matching * arm. The soundness fuzz test (`test/unit/regex-first-set.test.ts`) checks this * against the real `RegExp` engine as an oracle. * * It parses the regex into a tiny AST (single-char matchers, sequence, * alternation, repetition, zero-width) reusing the shared char-class primitives * in `./classes.ts`, then walks the AST unioning first-sets through the nullable * prefix — the same shape the `regexp-tree` walker used. * * Note: flags are not consulted (matching the prior `regexp-tree` seam, which * parsed `/${source}/` with no flags), so an `/i` pattern's first-set is its * literal-case set. Sound for dispatch because the interpreter's `/i` regex arms * are compared case-sensitively at the same seam. */ import type { FirstSet } from '../types.ts'; /** * Compute a regex terminal's first-set and newline-start flag from its SOURCE * (no delimiters, no flags). Over-approximates: an unparseable pattern degrades * to `{ any, canMatchNewline: true }` — the same conservative value the prior * `regexp-tree` seam returned on a parse failure. */ /** * Can `source` match ZERO characters? * * Structural, over the same AST `firstSetFromRegex` uses — which already models * every zero-width construct as `EMPTY`: anchors (`^`/`$`), lookaround * (`(?=…)`/`(?!…)`/`(?<=…)`/`(?