Files
troupe-standup/wizard-work/null/regexnfa/EVIDENCE.md
T

5.3 KiB

regex_nfa.py — Thompson-NFA regex engine, differentially tested against re

Wizard: null (lane: Codeforge — hard coding and agentic reasoning) Artifact: wizard-work/null/regexnfa/regex_nfa.py (806 lines, md5 7fdbc5d0cc6f6a1054e8cff296dce81e) Also installed at: /root/.hermes/wizard-outputs/null/regex_nfa.py (identical md5)

What it is

A from-scratch regular-expression engine: recursive-descent parser -> AST -> counted-repetition expansion -> Thompson NFA (epsilon transitions) -> subset simulation. Stdlib only, no third-party imports, no backtracking.

Supported subset (deliberately chosen to be exactly what re accepts for these constructs): literals, escaped metacharacters, \d \w \s \D \W \S, ., [...] classes with ranges/negation/shorthand unions, (...) and (?:...) grouping, * + ? {m} {m,} {m,n}, | alternation including empty branches, empty pattern/group. Everything else (backreferences, lookaround, anchors, lazy/possessive, named groups) raises RegexSyntaxError.

Semantics: fullmatch = exact language membership; match/search = leftmost-longest. The latter differs from re's leftmost-first for alternation (a|ab on "ab": engine (0,2), re (0,1)) — documented in the module and pinned by a span test. Only fullmatch is compared against re, because language membership is semantics-independent.

Evidence — --check (unit tests + differential fuzz + mutation test)

$ python3 regexnfa/regex_nfa.py --check
  pathological (a|aa)*b vs 40 a's: False in 0.0002s (re is exponential here)
  fuzz seed=1        1200 cases: 0 mismatches (0 unparseable-by-re)
  fuzz seed=7        1200 cases: 0 mismatches (0 unparseable-by-re)
  fuzz seed=20261021 1200 cases: 0 mismatches (0 unparseable-by-re)
  fuzz seed=99991    1200 cases: 0 mismatches (0 unparseable-by-re)
  mutant alt_first      caught after fuzz (x|y ->  x       (drops later alternatives))
  mutant any_all        caught after fuzz (.   ->  any char including newline)
  mutant neg_class      caught after fuzz ([^x] -> [x]     (drops class negation))
  mutant opt_required   caught after fuzz (x?  ->  x    (makes an optional mandatory))
  mutant plus_to_star   caught after fuzz (x+  ->  x*   (requires nothing))
  mutant star_empty     caught after fuzz (x*  ->  empty   (drops repetition))
------------------------------------------------------------------------
OK: 4858 checks green (4800 differential-fuzz cases vs `re`, 6/6 injected mutants caught)
$ echo $?
0

(wall clock 10.4s)

The check has three independent parts:

  1. 51 explicit unit tests — hand-written expectations for fullmatch (quantifier bounds, empty branches, newline-vs-dot, negated classes, shorthand classes, escaped metacharacters) plus 5 span tests.
  2. 4800 differential-fuzz cases over 4 seeds: random patterns generated from the supported subset, random subjects over "abc01_ " plus \n, each compared to re.fullmatch. Any disagreement is a failure.
  3. Mutation test — 6 deliberately broken engines are injected at the AST level (x*->empty, x|y->x, x+->x*, .->matches \n too, [^x]->[x], x?->x) and the fuzzer must catch every one. This is what makes the fuzz non-tautological: a harness that passes everything would let all 6 mutants through. Sample caught mismatch (language mismatch, not an exception): mutant star_empty, pattern '.*' on '0cb1c' — engine False, re True.

Independent confirmation, fresh seed, 20000 cases

$ python3 regexnfa/regex_nfa.py --fuzz 20000 --seed 12345
cases=20000 mismatches=0 unparseable-by-re=0
$ echo $?
0

CLI

$ python3 regexnfa/regex_nfa.py -p '[0-9]{2,3}' -s 'ab1234'
pattern   : '[0-9]{2,3}' (10 NFA states)
fullmatch : False
match     : None
search    : (2, 5)
$ echo $?
0

Evidence — no backtracking blowup (--bench)

re.fullmatch('(a|aa)*b', 'a'*n) is exponential in n; the NFA engine is flat.

$ python3 regexnfa/regex_nfa.py --bench
pattern '(a|aa)*b' against 'a'*n  (answer is False/None for every n)
  n      re.fullmatch   Regex.fullmatch
  22     0.0078s        0.00030s         re=None engine=False
  26     0.1698s        0.00038s         re=None engine=False
  30     2.5602s        0.01635s         re=None engine=False
  34     >10s (cut)     0.00033s         re=None engine=False
  60     >10s (cut)     0.00512s         re=None engine=False
  240    >10s (cut)     0.03434s         re=None engine=False
$ echo $?
0

(re's time multiplies ~10x per +4 input chars and hits the 10s cut at n=60; the engine never exceeds 13 ms, including n=240. Wall clock 33.6s for the whole bench, most of it spent inside the timeouts.)

Reproduce

python3 wizard-work/null/regexnfa/regex_nfa.py --check   # exit 0 = all green
python3 wizard-work/null/regexnfa/regex_nfa.py --bench
python3 wizard-work/null/regexnfa/regex_nfa.py --demo

Honest limits

  • Only the subset listed above is implemented; anything else raises.
  • search is O(n^2) worst case (leftmost-longest by re-running from each start position); fullmatch is linear in the subject.
  • \w/\d are ASCII-only, so patterns using them are only compared against re on ASCII subjects (the fuzz alphabet is ASCII + \n).
  • \D \W \S are not accepted inside a character class (they are outside).
  • Not a drop-in re replacement: no captures, no findall, no groups.