Skip to content

feat: UTF-8 validator product in lazy DFA (phase 3) - #184

Merged
kolkov merged 9 commits into
mainfrom
feature/issue-179-phase3-validator
Oct 9, 2026
Merged

kolkov merged 9 commits into
mainfrom
feature/issue-179-phase3-validator

Conversation

@kolkov

@kolkov kolkov commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 3: UTF-8 validator product replaces temporary 3c quit in lazy DFA.

The temporary 3c fallback (quit on ALL bytes >= 0x80) caused 4-20x regression on Cyrillic/CJK text. The validator product tracks UTF-8 structural validity per byte (9-state automaton), quitting only on structurally invalid bytes. Valid non-ASCII text stays in DFA.

DFA state count (criterion 16: ≤ 2× vs 273ff75 baseline)

Pattern Corpus Baseline (273ff75) Validator Growth
.+ ASCII 4 4 1.00×
.+ Cyrillic 7 7 1.00×
.+ CJK 9 9 1.00×
.+ Mixed 13 13 1.00×
\S+ Cyrillic 9 9 1.00×
\S+ Mixed 16 16 1.00×
[^,]+ all same same 1.00×
ERROR.* Mixed 3 6 2.00×
\d+ms.* CJK 2 4 2.00×
char_class Mixed 73 76 1.04×
(?m)^.*\.php Mixed 7 8 1.14×

All ≤ 2.00×. Zero cache clears on valid text. Max 76 states (28 KB).

Test plan

  • 40 acceptance tests (10 original + 6 truncated sequences + 24 from fix: negated classes (\S, \W, \D, [^a]) match bytes, not codepoints #174)
  • Truncated multi-byte: .x on \xc3x, \xe3x, \xe3\x81x, \xf0\x90x
  • Rune boundary: [^К]b on "Кb" = no match
  • Full suite green, -race clean, lint/gofmt clean
  • Fuzz: 5/7 green without whitelists (remaining = class B/C)
  • regex-bench CI: 4/4 green (Benchmark, LangArena, DNA, Extreme)
  • Cyrillic log perf: 3c regression eliminated (.+ +17%, others within noise)
  • DFA state growth ≤ 2.00× on all patterns (table above)
  • 41 validator unit tests (transitions + full sequences + key identity)

Fixes #179

kolkov added 9 commits October 9, 2026 10:26
Go 1.27.2 uses export data version 5, golangci-lint v2.13 supports
only version 4. CI failed on v0.12.27 merge commit.
Phase 3, step 13: validator state infrastructure.

- State struct: add validatorState uint8 field
- 9 validator states (VS0-VS8) from Björn Höhrmann's UTF-8 DFA, adapted
- ValidatorTransition(vs, byte) → (next, quit): deterministic UTF-8
  structural validation per byte. Quit on invalid sequences.
- ComputeStateKeyFull: includes validatorState in hash (separate byte
  in FNV-1a, distinct from flags). Different validator states =
  different DFA states.
- 41 unit tests: individual transitions, full UTF-8 sequences (2/3/4 byte
  valid, orphan/truncated/overlong/surrogate invalid), state key identity.

No behavioral changes yet — determinize doesn't use validator state.
That comes in task #13 (validator transitions in determinize).
Phase 3, steps 12-13 + task 15: replace temporary 3c quit with
validator-informed transitions.

Validator product (state.go):
- 9 validator states (VS0-VS8) with ValidatorTransition(vs, byte)
- ValidatorState field in State struct, included in ComputeStateKeyFull
- 41 unit tests covering all transitions + full sequences + key identity

Determinize (lazy.go):
- At VS0: ASCII bytes → VS0 (stay in DFA). Lead bytes C2-F4 → VS1-VS8
  (proceed through multi-byte chain). Invalid bytes → QuitState (PikeVM).
- At VS1-VS8: valid continuations → next VS (stay in DFA). Invalid
  continuations → QuitState (PikeVM).
- Replaces blanket 3c quit (ALL bytes >= 0x80 → quit) with targeted
  quit (only structurally invalid bytes → quit).

EOI fallback (lazy.go):
- needsEOIFallback: if validator state != VS0 at end of input, the DFA
  consumed lead bytes of a truncated sequence. Falls back to PikeVM for
  correct U+FFFD handling. Applied to all 6 forward search EOI paths.

Result: DFA now processes valid non-ASCII text (Cyrillic, CJK) without
quitting. Only structurally invalid UTF-8 bytes trigger PikeVM fallback.
Phase 3 correctness fix: truncated multi-byte sequences.

Bug: validator only checked bytes >= 0x80. When validator was in S1-S8
(expecting continuation) and an ASCII byte arrived (e.g., 'x' after
lead byte 0xC3), the code set nextValidatorState=VS0 without calling
ValidatorTransition — no quit, NFA went to dead state, FFFD match lost.

Fix: call ValidatorTransition for ALL bytes when hasInvalidUTF8Look,
not just >= 0x80. ASCII byte in S1-S8 → validator returns quit →
QuitState → PikeVM handles lead byte as U+FFFD correctly.

Also added EOI fallback: needsEOIFallback checks if validator state
!= VS0 at end of input (truncated sequence at EOF). Applied to all
6 forward search EOI paths.

New acceptance tests (6 cases):
- .x on truncated 2/3/4-byte leads: \xc3x, \xe3x, \xe3\x81x, \xf0\x90x
- [^a]x on truncated 2-byte: \xc3x
- a.b on truncated mid-pattern: a\xc3b
- FindAll variants for the above
@codecov

codecov Bot commented Oct 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.34247% with 18 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
dfa/lazy/lazy.go 55.17% 13 Missing ⚠️
dfa/lazy/state.go 88.63% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

Benchmark Comparison

Comparing main → PR #184

Summary: geomean 75.92n 76.22n +0.39%

⚠️ Potential regressions detected:

LazyDFASimpleLiteral-4     47.13n ± ∞ ¹    47.89n ± ∞ ¹  +1.61% (p=0.032 n=5)
LazyDFARepetition-4        77.42n ± ∞ ¹    77.63n ± ∞ ¹  +0.27% (p=0.048 n=5)
geomean                    75.92n          76.22n        +0.39%
geomean                               ³                +0.00%               ³
geomean                               ³                +0.00%               ³
geomean                         ³                +0.00%               ³
geomean                         ³                +0.00%               ³
AhoCorasickLargeInput/stdlib_Find_64KB-4                16.79m ± ∞ ¹   17.19m ± ∞ ¹    +2.38% (p=0.008 n=5)
ASCIIOptimization_Issue79/long_WithoutASCII-4           2.089µ ± ∞ ¹   2.202µ ± ∞ ¹    +5.41% (p=0.016 n=5)
DNA_VsStdlib/stdlib/dna_9-4                             68.15m ± ∞ ¹   69.14m ± ∞ ¹    +1.45% (p=0.016 n=5)

Full results available in workflow artifacts. CI runners have ~10-20% variance.
For accurate benchmarks, run locally: ./scripts/bench.sh --compare

@kolkov
kolkov merged commit 8a0c8e0 into main Oct 9, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: invalid UTF-8 bytes silently skipped instead of matching as U+FFFD

1 participant