Repository navigation
Releases
v0.12.30: (?s).* prefix clipped to the suffix's line
Fixes #191: with the s flag, a .* prefix was clipped to the suffix's line (regression in v0.12.15). Found by uawk, which enables (?s) for every AWK regex.
Fixed
(?s).*prefix clipped to the suffix's line (#191):
with thesflag,(?s).*\.txton"a\nb.txt"returned[2 7]instead of
[0 7](regression in v0.12.15). The line-bounded shortcut ofReverseSuffix
andReverseSuffixSetnow applies only to a non-dotall.*;(?s).*takes the
reverse DFA path and gets its greedy end from a forward search, so it ends after
the last suffix in the input, as inregexpFindreturned the rightmost match forReverseSuffixSetpatterns:\w+\.(txt|log)
on"a.txt b.log"returned[6 11]instead of[0 5], and.*\.txton
"q.txt\nz\nb.txt"returned[8 13]instead of[0 5].Findnow uses the
same leftmost search asFindAll.ReverseSuffixSetalso takes the match end
from a forward search instead of ending at the first suffix candidate- Reverse scan stopped by the anti-quadratic guard returned a clipped start:
when the reverse DFA ofReverseSuffix,ReverseSuffixSetorReverseInner
reached the guard position (the end of the previous suffix candidate) still
alive, it returned the match start found so far instead of asking the caller
to retry:\w+\.(txt|log)on"11@.logx.txtbar.txtt"gave[7 12]instead
of[4 12]. It now signals the retry in that case and when it reaches the
search start alive with a match that begins later, as Rust's
meta/limited.rsdoes; callers fall back to the PikeVM - Performance on 1 MB of log text (local, noisy laptop):
Findfor
.*\.(txt|log|md)and\w+\.(txt|log)drops from ~4 ms to under 1 µs, since it
no longer scans every candidate;(?s).*\.txtFindnow reads up to the last
suffix (~9 ms, previously a wrong early result).FindAllon non-dotall.*
patterns takes the same code path as before.ReverseSuffixSetpatterns
without a.*prefix (e.g.\w+\.(txt|log)) now pay for the forward search in
FindAll: local median +15%, equal minimums
Validation
- regex-bench on AMD EPYC, paired with
main; match counts identical on every pattern: Benchmark (main), Extreme (main), LangArena, DNA - uawk test suite and an AWK-level comparison with gawk pass on this commit
go get github.com/coregx/coregex@v0.12.30Full Changelog: v0.12.29...v0.12.30
v0.12.29: concurrent search safety
Fixed
- Concurrent use of one compiled
Regexreturned wrong results or panicked
(regression of #78):
several search paths kept mutable search state in objects shared by all
goroutines using a compiled pattern. With 8 goroutines, searches with
patterns such asa+?b,(\w+)@(\w+)\.(\w+)and^(\w+): (.+)$
returned wrong matches or panicked. Single-goroutine use was not affected.
Compiled objects are now immutable; every search takes its mutable state
from the pattern's state pool (the model Rust regex uses with its
per-searchCache).
Deprecated
lazy.(*DFA).SetPikeVMis a no-op: the DFA no longer holds a PikeVM
Performance
Measured across 4 randomized link layouts on AMD EPYC CI: no regressions.
MatchString on a short BoundedBacktracker pattern: +5% (+3% to +12%
depending on layout). All other paths within noise.
regex-bench CI (AMD EPYC):
Benchmark ·
Extreme ·
LangArena ·
DNA
v0.12.28: invalid UTF-8 handling, reverse searcher fix, O(n²) elimination
What's Changed
Fixed
-
FindAll/Count/ReplaceAll/Split O(n²) for
$-anchored patterns (#183): all iteration APIs fell through to PikeVM loop forUseReverseAnchored. Fix: single dispatch — one reverse searcher call.ERROR.*$on 4800-line log: 18 s → instant. -
Reverse searcher returned empty/short matches for
*$patterns (#183):Findona*$/"aaa"returned[3,3]instead of[0,3]. Root cause:fillStartStateWithIncomingin reverse NFA converted ByteRange edges to epsilon for patterns with*-loops at the anchored start state. Pre-existing since v0.12.0. -
Invalid UTF-8 bytes now match as U+FFFD width 1 (#179):
.,\S,\D,\W,[^x],\P{Han}, and literal\x{FFFD}now match invalid UTF-8 bytes as U+FFFD, matching stdlib semantics. All four engines handle the newLookInvalidUTF8assertion. Lazy DFA uses UTF-8 validator product (9-state automaton). On valid text: zero overhead on ASCII, ≤ 17% on.-heavy patterns. Harness divergence: 14 → 1. -
Negated classes matched bytes, not runes (#174):
\S{2}matched single CyrillicКby treating each byte as a separate character. Random-fuzz divergence: 0.75% → 0.21%.
Performance
- regex-bench CI: all patterns within noise vs v0.12.27 on ASCII
- Cyrillic/CJK: 3c-quit regression from early #179 phases eliminated by UTF-8 validator product
$-anchored FindAll/ReplaceAll: from O(n²) to O(1) reverse scan
Full Changelog: v0.12.27...v0.12.28
v0.12.27: fix anchored alternation correctness (#166)
Hotfix for #166 — anchored alternation patterns produced wrong results.
Fixed
^x(a|bc)on"xbc"returned false — BranchDispatch strategy ignored prefix/suffix around alternation. Also produced false positives (^(GET|POST) /on"GET x")^[Vv]\d+rejected"v1"— ExtractFirstBytes ignored FoldCase, took only uppercase rune without SimpleFold orbit. Non-ASCII runes now use UTF-8 lead-byte correctly
Removed
UseBranchDispatchstrategy (nfa/branch_dispatch.go, -1077 lines): O(1) branch dispatch for anchored alternations. Removed due to fundamental correctness issues — the simple branch matcher could not safely verify complex AST shapes. The lazy DFA and BoundedBacktracker already provide correct O(1) first-byte dispatch inherently (as Rust regex does). Performance: 13→66 ns on short strings, still 1.5× faster than stdlib
Reported by @vmihailenco (go-redis, bun).
v0.12.26: 14 correctness fixes, compile-time DoS, Unicode (?i)
14 correctness bugs fixed, compile-time DoS eliminated. Random-fuzz divergence vs stdlib reduced from 6.0% to 0.7%. Zero performance regressions.
Security
- Compile-time DoS:
cloneRegexpcloned bothSubandSub0arrays, causing 2^depth blowup. A 15-character pattern (.(?:ac*d|ac)xy*) caused >3 GB allocation and >15 s compile time. Now instant.
Fixed
- DFA cache overflow:
\pL+FindAll on inputs ≥10 KB returned only 1 match instead of thousands — reverse DFA NFA fallback ran PikeVM forward on reversed UTF-8 - UTF-8 empty match: empty matches advanced by byte instead of rune, producing invalid UTF-8 from
ReplaceAllStringon multibyte input (8 functions, 22 call sites) - Unicode
(?i)case folding: NFA compiler only folded ASCII — Cyrillic, Greek sigma (Σ/σ/ς), Kelvin (K/k/K), long s (ſ/s/S) now handled viaunicode.SimpleFold expand()templates:${N},${name},$name, multi-digit$10— ported from stdlibCompilePOSIX: compiled with Perl flags instead of POSIX —^/$multiline and[^a]newline semantics now correct. Rejects\d,\w,(?i),\pL- Empty capture at end:
()on"abc"last FindAll match gave[-1,-1]instead of[3,3] AllIndexiterator:\bproduced duplicate zero-width matchesSplit(n=1),Split("","",n),LiteralPrefixFoldCase,Find*cap>len (three-index slicing), repeated captures(aa)*$debugStrategy:re.String()evaluated unconditionally —uricompile 9ms→0.1ms- DFA/NFA threshold 200→250 for SimpleFold state growth
276 new test assertions. Validated on AMD EPYC via regex-bench CI (Benchmark, LangArena, DNA, Extreme) — no regressions.
v0.12.25 — Go 1.27.1 toolchain
Changed
- Go toolchain: 1.25.4 → 1.27.1 — compiler improvements (known-bits dataflow, LICM, switch-to-lookup-table),
simd/archsimdfixes, Green Tea GC - CI: job names no longer include Go version (stable branch protection), golangci-lint v2.10 → v2.13, branch protection migrated to GitHub Rulesets
Fixed
- Remove private path references from public documentation (ROADMAP.md, CONTRIBUTING.md)
v0.12.24 — dependency updates
Changed
- Dependencies:
github.com/coregx/ahocorasickv0.3.0 → v0.3.1 (LeftmostLongest early return at maxPatternLen),golang.org/x/sysv0.40.0 → v0.47.0
v0.12.23 — ahocorasick v0.3.0
Changed
- Dependency:
github.com/coregx/ahocorasickv0.2.1 → v0.3.0 — zero-allocationFind/FindAtAPI (returns(Match, bool)instead of*Match)
Adapted 7 call sites across prefilter/ahocorasick.go, meta/find.go, and meta/find_indices.go.
No behavioral changes. All benchmarks clean (regex-bench: Benchmark, LangArena, DNA, Extreme — no regressions).
v0.12.22: Lazy memory architecture — 5-7x memory reduction per pattern
Lazy Memory Architecture (#158)
Adopts Rust regex Cache separation model to dramatically reduce memory per compiled pattern — unblocks WAF adoption (Coraza with 900 OWASP CRS patterns).
Changes
- PikeVM lazy init — defers thread queues/sparse set allocation to first search (~10 KB saved per unused PikeVM)
- Shared DFA PikeVM — eliminates duplicate PikeVM per DFA (~15-20 KB per DFA pattern)
- Deferred SearchState — removed eager allocation at compile time (~15-50 KB per pattern)
- Strategy-aware caches — only allocates caches needed by active strategy (30-70% fewer allocations)
- DFA cache initCap 64→16 — smaller initial maps/slices (~3 MB across 900 patterns)
Impact
For WAF workloads compiling 900+ patterns:
- Memory per pattern: 152 KB → ~30 KB (5x reduction)
- No search performance regression (verified via regex-bench on AMD EPYC)
Other
- Open Collective sponsorship badges in README
- CI: benchmark workflow skips docs-only PRs
go get github.com/coregx/coregex@v0.12.22
v0.12.21: Tagged start states, zero-alloc API, 5 patterns faster than Rust
Performance
DFA Engine
- Tagged start states (Rust
LazyStateID) — prefilter skip-ahead only at start state, fast transition in slow path (no getState/acceleration overhead) - DFA multiline
$fix — EndLine look-ahead (Rust determinize mod.rs:131-212) - Dead-state prefilter restart in searchEarliestMatch
- Tiny NFA → UseDFA — patterns with <20 NFA states use bidirectional DFA (was PikeVM, 7x faster)
Allocation
- 1100x fewer mallocs — flat buffer for FindAllIndex/FindAllSubmatchIndex
- Local SearchState cache — atomic.Pointer, survives GC
- Pool round-trip elimination in FindAll/Count
New Public API (zero-alloc)
AllIndex(b []byte) iter.Seq[[2]int]— zero-alloc iterator (Go proposal #61902)AllStringIndex(s string) iter.Seq[[2]int]— string versionAll(b []byte) iter.Seq[[]byte]— match content iteratorAllString(s string) iter.Seq[string]— string versionAppendAllIndex(dst [][2]int, b []byte, n int) [][2]int— buffer-reuse (strconv.Append*pattern)AppendAllStringIndex(dst [][2]int, s string, n int) [][2]int— string version
Benchmarks (EPYC CI, 6MB input)
5 patterns faster than Rust (was 3 in v0.12.20):
| Pattern | vs stdlib | vs Rust |
|---|---|---|
| IP address | 685x | 18.8x faster |
| Multiline PHP | 299x | 2.2x faster |
| Char class | 11x | 1.3x faster |
| Inner literal | 881x | 1.2x faster (NEW) |
| Version | 263x | 1.2x faster (NEW) |
Zero-Alloc API (new methods vs stdlib-compat)
| Method | errors (33K matches) | Alloc |
|---|---|---|
| FindAllStringIndex (stdlib) | 8.2ms / 3890 KB | 19 mallocs |
| AllIndex (iter.Seq) | 5.9ms / 0 KB | 0 mallocs |
| AppendAllIndex | 5.5ms / 0 KB | 0 mallocs |
emails with AppendAllIndex: 2.0ms vs Rust 2.6ms — faster than Rust!
Fixed
- DFA multiline
$—(?m)hello$now matches before\n isMatchWithPrefilterpfSkip —zx+on "zzx" now correct