Skip to content

Releases

v0.12.30: (?s).* prefix clipped to the suffix's line

Choose a tag to compare

@kolkov kolkov released this 17 Oct 14:14

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 the s flag, (?s).*\.txt on "a\nb.txt" returned [2 7] instead of
    [0 7] (regression in v0.12.15). The line-bounded shortcut of ReverseSuffix
    and ReverseSuffixSet now 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 in regexp
  • Find returned the rightmost match for ReverseSuffixSet patterns: \w+\.(txt|log)
    on "a.txt b.log" returned [6 11] instead of [0 5], and .*\.txt on
    "q.txt\nz\nb.txt" returned [8 13] instead of [0 5]. Find now uses the
    same leftmost search as FindAll. ReverseSuffixSet also 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 of ReverseSuffix, ReverseSuffixSet or ReverseInner
    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.rs does; callers fall back to the PikeVM
  • Performance on 1 MB of log text (local, noisy laptop): Find for
    .*\.(txt|log|md) and \w+\.(txt|log) drops from ~4 ms to under 1 µs, since it
    no longer scans every candidate; (?s).*\.txt Find now reads up to the last
    suffix (~9 ms, previously a wrong early result). FindAll on non-dotall .*
    patterns takes the same code path as before. ReverseSuffixSet patterns
    without a .* prefix (e.g. \w+\.(txt|log)) now pay for the forward search in
    FindAll: local median +15%, equal minimums

Validation

go get github.com/coregx/coregex@v0.12.30

Full Changelog: v0.12.29...v0.12.30

v0.12.29: concurrent search safety

Choose a tag to compare

@kolkov kolkov released this 10 Oct 17:49
d61c865

Fixed

  • Concurrent use of one compiled Regex returned 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 as a+?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-search Cache).

Deprecated

  • lazy.(*DFA).SetPikeVM is 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

Choose a tag to compare

@kolkov kolkov released this 10 Oct 10:34
cdde6fa

What's Changed

Fixed

  • FindAll/Count/ReplaceAll/Split O(n²) for $-anchored patterns (#183): all iteration APIs fell through to PikeVM loop for UseReverseAnchored. Fix: single dispatch — one reverse searcher call. ERROR.*$ on 4800-line log: 18 s → instant.

  • Reverse searcher returned empty/short matches for *$ patterns (#183): Find on a*$/"aaa" returned [3,3] instead of [0,3]. Root cause: fillStartStateWithIncoming in 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 new LookInvalidUTF8 assertion. 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)

Choose a tag to compare

@kolkov kolkov released this 09 Oct 07:08
709119d

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

  • UseBranchDispatch strategy (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)

Choose a tag to compare

@kolkov kolkov released this 08 Oct 21:42
b996be3

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: cloneRegexp cloned both Sub and Sub0 arrays, 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 ReplaceAllString on 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 via unicode.SimpleFold
  • expand() templates: ${N}, ${name}, $name, multi-digit $10 — ported from stdlib
  • CompilePOSIX: 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]
  • AllIndex iterator: \b produced duplicate zero-width matches
  • Split(n=1), Split("","",n), LiteralPrefix FoldCase, Find* cap>len (three-index slicing), repeated captures (aa)*$
  • debugStrategy: re.String() evaluated unconditionally — uri compile 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

Choose a tag to compare

@kolkov kolkov released this 06 Sep 10:50
de416f9

Changed

  • Go toolchain: 1.25.4 → 1.27.1 — compiler improvements (known-bits dataflow, LICM, switch-to-lookup-table), simd/archsimd fixes, 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

Choose a tag to compare

@kolkov kolkov released this 06 Sep 09:45
4569222

Changed

  • Dependencies: github.com/coregx/ahocorasick v0.3.0 → v0.3.1 (LeftmostLongest early return at maxPatternLen), golang.org/x/sys v0.40.0 → v0.47.0

v0.12.23 — ahocorasick v0.3.0

Choose a tag to compare

@kolkov kolkov released this 05 Aug 17:45
b7f16a0

Changed

  • Dependency: github.com/coregx/ahocorasick v0.2.1 → v0.3.0 — zero-allocation Find/FindAt API (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

Choose a tag to compare

@kolkov kolkov released this 15 Jun 15:23
2812db7

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

Choose a tag to compare

@kolkov kolkov released this 27 Mar 16:54
87d600b

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 version
  • All(b []byte) iter.Seq[[]byte] — match content iterator
  • AllString(s string) iter.Seq[string] — string version
  • AppendAllIndex(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
  • isMatchWithPrefilter pfSkip — zx+ on "zzx" now correct

Full Changelog

v0.12.20...v0.12.21