Skip to content

v2026.7.24

Choose a tag to compare

@github-actions github-actions released this 07 Jul 01:55

POSIX Extended (ERE) grammar moves onto the linear engine — the compat layer's first ReDoS hole closed for
extended, on every operation — and a P1 parity bug shipped since 2026.7.3 is fixed: mixed character classes
now union their members regardless of order.

The std::regex compat layer routed every POSIX grammar to std::regex's backtracker — the one non-linear path
left in the library. extended (ERE) now closes it. A translatable ERE pattern is translated to REAL and run on
the linear engine with leftmost-longest bounds — the POSIX semantics — instead of delegating. This holds for
every operation on a translated non-nullable ERE: regex_search / regex_match run on search_longest,
and regex_replace / the iterators run on find_iter_longest. So (a+)+b under an ERE grammar matches in linear
time where std::regex would blow up — verified: 200k a iterates and replaces in ~9 ms.

regex.posix_longest() reports the routing. POSIX bracket classes ([[:alpha:]][[:xdigit:]]) become their
C-locale ASCII ranges. The translator declines — falling back to std::regex, never a silent wrong-match — on the
constructs the two grammars read differently: an ECMAScript shorthand (\d \w \s \b, undefined or literal in
ERE), an ambiguous {, an unknown or collating [[:…:]] / [.x.] / [=x=]. A nullable ERE (x*) keeps its
search/match on REAL but delegates replace/iterate to std (the empty-match traversal differs, exactly as on
the ECMAScript path), with POSIX-correct bounds. Captures are the winning thread's, not POSIX submatch (documented).
The other POSIX grammars — basic, awk, grep, egrep — still delegate; they are next.

In a text-mode character class, a non-ASCII member — a literal, a range, or a second predicate — that followed
a code-point predicate (\w \d \s \p{…}) was silently lost. [\dЩ] did not match Щ while [Щ\d] did;
[\dа-я], [\w\W] (which dropped U+0382), and [\p{L}\p{N}] (which dropped the Nd digits) all under-matched.
The members accumulated in parse order, but the match-time class binary-searches them and so requires them sorted —
a member sorting below the predicate's ranges landed out of order and was missed. Members now union regardless of
order (Python re and the regex crate co-arbitrated the fix). This closes a parity gap widened by the \p{}
in-class work of 2026.7.23; anyone matching mixed classes on 2026.7.3–2026.7.23 should update.

The POSIX bounds are backed by a new, opt-in match_semantics { first, longest }. The default (leftmost-first) is
untouched and byte-identical. The longest mode is reachable directly via search_longest(text[, pos, endpos]) and
its iteration twin find_iter_longest(text[, pos, endpos]). Among matches at the leftmost start it returns the
longest (a lazy quantifier behaves greedily); captures are the winning thread's, not POSIX submatch. These are a
prototype for the match_semantics arc and are not yet a stable API. The default first-match fast paths are
bypassed under longest; first mode keeps them, proven byte-identical by the exhaustive and 4-D matrix gates.