Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
055f619
refactor: split DFA into immutable DFA + mutable DFACache (Phase 1)
kolkov Mar 20, 2026
7fad89b
refactor: integrate DFACache into SearchState pool (Phase 2)
kolkov Mar 20, 2026
44f6a76
fix: pass StartTable byteMap by pointer (lint gocritic hugeParam)
kolkov Mar 20, 2026
732c72c
perf: pre-compute word boundary match flags — eliminate 30% CPU overhead
kolkov Mar 20, 2026
10300ef
perf: eliminate double prefilter scan in isMatchDFA
kolkov Mar 20, 2026
bf121ba
perf: replace FatTeddy with AC DFA prefilter for >32 literals
kolkov Mar 20, 2026
0138487
docs: update CHANGELOG, ROADMAP, README for v0.12.15
kolkov Mar 20, 2026
ed4d6ff
perf: integrated prefilter+DFA loop (Rust approach)
kolkov Mar 20, 2026
7c9e311
fix: restore FatTeddy for 33-64 patterns, AC only for >64
kolkov Mar 20, 2026
ceeac11
docs: fix CHANGELOG/ROADMAP/README — AC prefilter >64, FatTeddy 33-64
kolkov Mar 20, 2026
14a2e08
fix: eliminate double pool.Get in BoundedBacktracker DFA fallback
kolkov Mar 20, 2026
7b455e9
perf: reduce DFACache initial allocation from 10K to 64 entries
kolkov Mar 20, 2026
a18c2bf
perf: pass strategy DFA caches through SearchState — eliminate per-ca…
kolkov Mar 20, 2026
3db7962
test: add stdlib compatibility test for all regex-bench patterns
kolkov Mar 21, 2026
5904aa5
fix: .* newline boundary in ReverseSuffix/ReverseSuffixSet + stdlib c…
kolkov Mar 21, 2026
41ceb1e
fix: Teddy ignores anchors + .* newline boundary + stdlib compat test
kolkov Mar 21, 2026
635f29c
fix: 5 correctness bugs — anchors in Teddy, .* newline, canMatchEmpty…
kolkov Mar 21, 2026
b18645a
fix: (?m)^ multiline anchor verification — WrapIncomplete + NFA fallback
kolkov Mar 21, 2026
97fcf47
fix: partial prefilter on (?i) alternation overflow — return empty Seq
kolkov Mar 21, 2026
4bab181
docs: update CHANGELOG for v0.12.15 — 7 fixes, stdlib compat test
kolkov Mar 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,82 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- ARM NEON SIMD support (Go 1.26 `simd/archsimd` intrinsics — [#120](https://github.com/coregx/coregex/issues/120))
- SIMD prefilter for CompositeSequenceDFA (#83)

## [0.12.15] - 2026-03-21

### Performance
- **Per-goroutine DFA cache** (Rust approach) — split lazy DFA into immutable `DFA`
(shared across goroutines) + mutable `DFACache` (pooled via `sync.Pool`).
Eliminates all data races on concurrent `IsMatch`/`Find`/`FindSubmatch`.
Concurrent case-insensitive IsMatch: data race → **450 ns/op** (zero-contention).

- **Pre-computed word boundary match flags** — `matchAtWordBoundary` /
`matchAtNonWordBoundary` flags computed during `determinize()`, replacing
per-byte `checkWordBoundaryMatch` (was 30% CPU). Word boundary check:
**30% → 0.3% CPU**.

- **Aho-Corasick DFA prefilter for >64 literals** — for patterns exceeding
FatTeddy's 64-pattern limit, AC DFA provides zero false positives with
O(n) flat transition table scan. FatTeddy retained for 33-64 patterns.

- **Integrated prefilter+DFA loop** (Rust approach) — replaced two-pass
`prefilter.Find → DFA.searchAnchored → repeat` with single DFA loop where
dead-state transitions trigger prefilter skip-ahead. Eliminates function call
overhead between passes.

- **Eliminated double prefilter scan** — `isMatchDFA` called prefilter externally,
then `DFA.IsMatch` called it again internally. Removed redundant external call.

- **Strategy DFA caches in SearchState** — reverse searchers (ReverseSuffix,
ReverseInner, etc.) had per-call `sync.Pool` access for DFA caches. Now
strategy-specific caches live in `SearchState`, eliminating N×pool.Get/Put
per FindAll iteration.

### Fixed
- **`.*` newline boundary** — ReverseSuffix/ReverseSuffixSet `matchStartZero`
path crossed `\n` boundaries. Pattern `.*@example.com` on multi-line input
matched from line 1 across newline into line 2. Fix: `lineStartBefore()`
constrains `.*` to single line (matching stdlib behavior).

- **Teddy ignoring anchors** — `(?m)^(GET|POST|...)` selected UseTeddy which
doesn't verify `^` anchor, returning matches mid-line. Fix:
`hasAnchorAssertions()` prevents UseTeddy for anchored patterns.

- **`(?m)^` multiline anchor in DFA** — DFA can't verify multiline line-start
assertions. Fix: `hasMultilineLineAnchor()` routes to UseNFA with prefilter
skip-ahead. `http_methods` on 6MB: 73ms (DFA) → 1.56ms (NFA+prefilter).

- **`.*` FindAll with DFA cache clear** — `UseBoth` strategy caused incorrect
match positions when DFA cache cleared mid-FindAll for empty-matchable
patterns. Fix: `canMatchEmpty()` routes to UseNFA.

- **Partial prefilter on `(?i)` alternation overflow** — literal extractor
truncated alternation branches on cross-product overflow (>250), producing
a partial prefilter that missed uncovered branches. Fix: return empty Seq
on overflow (Rust approach) — NFA handles all branches without prefilter.

- **`expandCaseFoldLiteral` incomplete foldSets** — early exit on
CrossProductLimit left trailing nil entries in foldSets array.
`generateCaseFoldVariants` produced 0 results. Fix: track `filledCount`.

- **Prefilter `IsComplete` with anchors** — prefilter marked `complete=true`
skipped anchor verification. Fix: `WrapIncomplete()` forces engine
verification when pattern has anchors.

### Added
- **Stdlib compatibility test** — 38 patterns (regex-bench + LangArena + edge
cases) compared against `regexp` for IsMatch, Find, FindAllIndex, Count.
Runs in CI on every PR. 38/38 PASS.

### Refactored
- `dfa/lazy/cache.go` — `Cache` renamed to `DFACache`, `sync.RWMutex` removed
(single-owner per goroutine, no locking needed).
- `dfa/lazy/start.go` — `StartTable` moved into `DFACache` (mutable start states).
- `meta/search_state.go` — `SearchState` carries `dfaCache`, `revDFACache`,
and strategy-specific caches (`stratFwdCache`, `stratRevCache`).
- All DFA call sites in `meta/` updated to pass pooled cache (14+ call sites).
- `prefilter/ahocorasick.go` — AC DFA prefilter for >64 patterns.
- `prefilter/wrap.go` — `WrapIncomplete()` for anchor-aware prefilter gating.

## [0.12.14] - 2026-03-19

### Fixed
Expand Down
19 changes: 14 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> **Strategic Focus**: Production-grade regex engine with RE2/rust-regex level optimizations

**Last Updated**: 2026-03-19 | **Current Version**: v0.12.14 | **Target**: v1.0.0 stable
**Last Updated**: 2026-03-20 | **Current Version**: v0.12.15 | **Target**: v1.0.0 stable

---

Expand All @@ -12,7 +12,7 @@ Build a **production-ready, high-performance regex engine** for Go that matches

### Current State vs Target

| Metric | Current (v0.12.14) | Target (v1.0.0) |
| Metric | Current (v0.12.15) | Target (v1.0.0) |
|--------|-------------------|-----------------|
| Inner literal speedup | **280-3154x** | ✅ Achieved |
| Case-insensitive speedup | **263x** | ✅ Achieved |
Expand All @@ -23,7 +23,8 @@ Build a **production-ready, high-performance regex engine** for Go that matches
| OnePass DFA | **Yes** | ✅ Achieved |
| Slim Teddy (2-32 patterns) | **Yes (SSSE3, 9GB/s)** | ✅ Achieved |
| Fat Teddy (33-64 patterns) | **Yes (AVX2, 9GB/s)** | ✅ Achieved |
| Aho-Corasick (>64 patterns) | **Yes** | ✅ Achieved |
| Aho-Corasick (>64 patterns) | **Yes (AC DFA prefilter)** | ✅ Achieved |
| Per-goroutine DFA cache | **Yes (Rust approach)** | ✅ Achieved |
| BoundedBacktracker | **Yes** | ✅ Achieved |
| CharClassSearcher | **Yes (35% faster than Rust!)** | ✅ Achieved |
| **Patterns faster than Rust** | **5 patterns** | ✅ Achieved |
Expand Down Expand Up @@ -84,7 +85,9 @@ v0.12.12 ✅ → Prefix trimming for case-fold literals (Teddy instead of AC)
↓
v0.12.13 ✅ → FatTeddy fix, prefilter acceleration, AC v0.2.1
↓
v0.12.14 (Current) ✅ → Concurrent safety fix for isMatchDFA prefilter (#137)
v0.12.14 ✅ → Concurrent safety fix for isMatchDFA prefilter (#137)
↓
v0.12.15 (Current) ✅ → Per-goroutine DFA cache, word boundary 30%→0.3% CPU, AC prefilter
↓
v1.0.0-rc → Feature freeze, API locked
↓
Expand Down Expand Up @@ -121,7 +124,13 @@ v1.0.0 STABLE → Production release with API stability guarantee
- ✅ **v0.12.6**: BoundedBacktracker span-based CanHandle, ReplaceAllStringFunc O(n) (#127)
- ✅ **v0.12.7**: PikeVM sparse-dispatch for `.` patterns, 2.8-4.8x speedup (#132)
- ✅ **v0.12.8**: Streaming ReplaceAll + DFA-first FindSubmatchAt, Rust-style two-phase search (#135)
- ✅ **v0.12.8**: Streaming ReplaceAll + DFA-first FindSubmatchAt, Rust-style two-phase search (#135)
- ✅ **v0.12.9**: Bidirectional DFA, Teddy/reverse NFA fixes
- ✅ **v0.12.10**: Case-insensitive literals, DigitPrefilter O(n²) fix (#137)
- ✅ **v0.12.11**: ReverseSuffix multi-wildcard, COREGEX_DEBUG logging
- ✅ **v0.12.12**: Prefix trimming for case-fold literals
- ✅ **v0.12.13**: FatTeddy fix (ANDL→ORL, VPTEST), prefilter acceleration, AC v0.2.1
- ✅ **v0.12.14**: Concurrent safety fix for isMatchDFA prefilter (#137)
- ✅ **v0.12.15**: Per-goroutine DFA cache (Rust approach), word boundary 30%→0.3% CPU, AC DFA prefilter for >32 literals (7-13x faster)

---

Expand Down
5 changes: 4 additions & 1 deletion dfa/lazy/accel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ func TestDFAAccelerate(t *testing.T) {
if err != nil {
t.Fatalf("Failed to compile DFA: %v", err)
}
_ = dfa.NewCache() // accelerate is an immutable DFA method, cache not needed here

haystack := []byte("aaaaaaaaaafoobar")

Expand Down Expand Up @@ -175,6 +176,7 @@ func TestAccelerableStateInSearch(t *testing.T) {
if err != nil {
t.Fatalf("Failed to compile DFA: %v", err)
}
cache := dfa.NewCache()

// Test that search still works correctly
tests := []struct {
Expand All @@ -188,7 +190,7 @@ func TestAccelerableStateInSearch(t *testing.T) {
}

for _, tc := range tests {
got := dfa.Find([]byte(tc.haystack))
got := dfa.Find(cache, []byte(tc.haystack))
if got != tc.want {
t.Errorf("Find(%q) = %d, want %d", tc.haystack, got, tc.want)
}
Expand All @@ -200,6 +202,7 @@ func BenchmarkAccelerate(b *testing.B) {
if err != nil {
b.Fatalf("Failed to compile DFA: %v", err)
}
_ = dfa.NewCache() // accelerate is an immutable DFA method

haystack := make([]byte, 4096)
for i := range haystack {
Expand Down
Loading
Loading