diff --git a/stl/inc/regex b/stl/inc/regex index 497b80b3492..770f84b7186 100644 --- a/stl/inc/regex +++ b/stl/inc/regex @@ -4861,6 +4861,23 @@ void _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Copy_captures(match_results<_Bid _Matches._Null().second = _End; } +template +void _Advance_at_most(_FwdIt& _Pos, const _FwdIt& _Last, _Int _Amount) { + if constexpr (_Is_ranges_random_iter_v<_FwdIt>) { + auto _Shift_amount = _Last - _Pos; + if (_Shift_amount > _Amount) { + _Shift_amount = static_cast(_Amount); + } + + _Pos += _Shift_amount; + } else { + while (_Amount > 0 && _Pos != _Last) { + ++_Pos; + --_Amount; + } + } +} + template _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip( _It _First, const _It _Last, const _Node_base* const _Node_arg, const unsigned int _Recursion_depth) { @@ -4870,6 +4887,7 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip( static constexpr wchar_t _Line_terminators_wchar_t[] = {static_cast(_Meta_cr), static_cast(_Meta_nl), static_cast(_Meta_ls), static_cast(_Meta_ps)}; constexpr unsigned int _Max_recursion_depth = 50U; + constexpr short _Max_lookahead = 512; const _Node_base* _Nx = _Node_arg ? _Node_arg : _Start; while (_First != _Last && _Nx) { // check current node @@ -4917,9 +4935,21 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip( case _N_str: { // check for string match - const auto _Node = static_cast*>(_Nx); - const auto _Str = _Node->_Data._Str(); - return _STD _Search_translate_left(_First, _Last, _Str, _Str + _Node->_Data._Size(), _Traits, _Sflags); + const auto _Node = static_cast*>(_Nx); + const auto _Str = _Node->_Data._Str(); + const unsigned int _Size = _Node->_Data._Size(); + + auto _Shifted_last = _Last; + if (_Last != _End && _Size > 1U) { // have to continue search beyond the search window + _STD _Advance_at_most(_Shifted_last, _End, static_cast(_Size - 1U)); + } + + auto _Result = _STD _Search_translate_left(_First, _Shifted_last, _Str, _Str + _Size, _Traits, _Sflags); + if (_Shifted_last == _Result) { // correct for search window shift + return _Last; + } else { + return _Result; + } } case _N_class: @@ -4927,7 +4957,7 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip( const auto _Node = static_cast*>(_Nx); for (; _First != _Last; ++_First) { // look for starting match - if (_Do_class(_Node, _First) != _First) { + if (_Do_class(_Node, _First) != _First) { // may read beyond search window return _First; } } @@ -4952,27 +4982,75 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip( case _N_if: { - // GH-5452: If this node has two or more branches, - // examining all these branches has quadratic worst-case complexity. - // Thus, we only continue if this node has a single branch only. + const auto _Node = static_cast(_Nx); + // TRANSITION, ABI: After GH-5539, the parser no longer generates single-branch _N_if nodes. - // But we have to retain this special handling to avoid performance regression + // But we retain this special handling to avoid some performance regression // when an old parser gets mixed with a new matcher. - const auto _Node = static_cast(_Nx); + if (!_Node->_Child) { + break; + } - if (_Node->_Child) { + if (_Recursion_depth >= _Max_recursion_depth) { return _First; } - break; + + // GH-5452: If this node has two or more branches, examining all alternatives in a disjunction + // until the end of the input string could result in quadratic worst-case complexity. + // For this reason, we split the input string into search windows with a constant maximum length. + // This ensures that this heuristic has linear time complexity, because this bounds the number + // of characters that were read beyond the finally determined skip position by a constant. + + for (;;) { + _It _Lookahead_last = _First; + _STD _Advance_at_most(_Lookahead_last, _Last, _Max_lookahead); + + _It _Alt_last = _Lookahead_last; + + for (const _Node_if* _Alternative = _Node; _Alternative && _First != _Alt_last; + _Alternative = _Alternative->_Child) { + _Alt_last = _Skip(_First, _Alt_last, _Alternative->_Next, _Recursion_depth + 1U); + } + + _First = _Alt_last; + if (_First != _Lookahead_last || _First == _Last) { + break; + } + } + + return _First; } case _N_rep: { const auto _Node = static_cast(_Nx); - if (_Node->_Min == 0) { + + if (_Node->_Min > 0) { + break; + } + + if (_Recursion_depth >= _Max_recursion_depth) { return _First; } - break; + + // As in GH-5452, examining the cases with no repetition and at least one repetition + // until the end of the input string could result in quadratic worst-case complexity. + // For this reason, we split the input string into search windows with a constant maximum length. + // This ensures that this heuristic has linear time complexity, because this bounds the number + // of characters that were read beyond the finally determined skip position by a constant. + + for (;;) { + _It _Lookahead_last = _First; + _STD _Advance_at_most(_Lookahead_last, _Last, _Max_lookahead); + const _It _Intermediate = _Skip(_First, _Lookahead_last, _Node->_Next, _Recursion_depth + 1U); + _First = _Skip(_First, _Intermediate, _Node->_End_rep->_Next, _Recursion_depth + 1U); + + if (_First != _Lookahead_last || _First == _Last) { + break; + } + } + + return _First; } case _N_assert: diff --git a/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp b/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp index 687812ce480..25c39fe1584 100644 --- a/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp +++ b/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception #include +#include +#include #include #include #include @@ -656,11 +658,29 @@ void test_gh_5437() { #endif // !defined(SKIP_COLLATE_TESTS) } +void test_gh_6191() { + // GH-6191: Optimize searches for patterns with initial branching + // Check that collating elements are handled correctly at search window boundaries. + const gh_994_regex re("[[.dzs.]]|abc"); + smatch sm; + for (size_t prefix_size = 510; prefix_size < 516; ++prefix_size) { + const string prefix(prefix_size, 'h'); + const string dzs_before_abc = prefix + "dzshhhhhabchh"; + assert(regex_search(dzs_before_abc, sm, re)); + assert(string(sm[0].first, sm[0].second) == "dzs"); + + const string abc_before_dzs = prefix + "abchhhhhdzshh"; + assert(regex_search(abc_before_dzs, sm, re)); + assert(string(sm[0].first, sm[0].second) == "abc"); + } +} + int main() { test_collating_ranges_german(); test_gh_994(); test_gh_5435(); test_gh_5437(); + test_gh_6191(); return g_regexTester.result(); } diff --git a/tests/std/tests/VSO_0000000_regex_use/test.cpp b/tests/std/tests/VSO_0000000_regex_use/test.cpp index a01f4822a9f..1f821999354 100644 --- a/tests/std/tests/VSO_0000000_regex_use/test.cpp +++ b/tests/std/tests/VSO_0000000_regex_use/test.cpp @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception #include +#include #include #include +#include #include #include @@ -2522,6 +2524,61 @@ void test_gh_6189() { re.should_search_fail("d"); } +void test_gh_6191() { + // GH-6191: Optimize searches for patterns with initial branching + // We must check that we handle matches near search window boundaries correctly. + + { + const test_regex test_alt_re(&g_regexTester, "abcdef|uvwxyz"); + test_alt_re.should_search_match("abcdef", "abcdef"); + test_alt_re.should_search_match("uvwxyz", "uvwxyz"); + test_alt_re.should_search_match("hhabcdef", "abcdef"); + test_alt_re.should_search_match("hhuvwxyz", "uvwxyz"); + test_alt_re.should_search_match("hhhhhuvwxyzhhhhhabcdefhhhhh", "uvwxyz"); + for (size_t prefix_size = 510; prefix_size < 516; ++prefix_size) { + const string prefix(prefix_size, 'h'); + test_alt_re.should_search_match(prefix + "abcdef", "abcdef"); + test_alt_re.should_search_match(prefix + "uvwxyz", "uvwxyz"); + test_alt_re.should_search_match(prefix + "abcdefhhhhhhhuvwxyz", "abcdef"); + test_alt_re.should_search_match(prefix + "uvwxyzhhhhhhhabcdef", "uvwxyz"); + test_alt_re.should_search_match(prefix + "abcdefhhhhhhhuvwxyzhhhh", "abcdef"); + test_alt_re.should_search_match(prefix + "uvwxyzhhhhhhhabcdefhhhh", "uvwxyz"); + } + } + + { + const test_regex optional_prefix_re(&g_regexTester, "(abc)?def"); + optional_prefix_re.should_search_match("abcdef", "abcdef"); + optional_prefix_re.should_search_match("def", "def"); + optional_prefix_re.should_search_match("hhabcdef", "abcdef"); + optional_prefix_re.should_search_match("hhdef", "def"); + optional_prefix_re.should_search_match("hhhhabcdefhhhhdefhhh", "abcdef"); + optional_prefix_re.should_search_match("hhhdefhhhhabcdefhhh", "def"); + for (size_t prefix_size = 510; prefix_size < 516; ++prefix_size) { + const string prefix(prefix_size, 'h'); + optional_prefix_re.should_search_match(prefix + "abcdef", "abcdef"); + optional_prefix_re.should_search_match(prefix + "def", "def"); + optional_prefix_re.should_search_match(prefix + "abcdefhhhhhhhdef", "abcdef"); + optional_prefix_re.should_search_match(prefix + "defhhhhhhhabcdef", "def"); + } + } + + // test bidirectional iterators + { + const regex alt_re("abcdef|uvwxyz"); + const string suffix = "abcdefhhhhhhhuvwxyz"; + list input(509, 'h'); + input.insert(input.end(), suffix.begin(), suffix.end()); + + for (size_t prefixes_to_test = 0; prefixes_to_test < 6; ++prefixes_to_test) { + input.push_front('h'); + match_results::const_iterator> results; + assert(regex_search(input.cbegin(), input.cend(), results, alt_re)); + assert(string(results[0].first, results[0].second) == "abcdef"); + } + } +} + int main() { test_dev10_449367_case_insensitivity_should_work(); test_dev11_462743_regex_collate_should_not_disable_regex_icase(); @@ -2588,6 +2645,7 @@ int main() { test_gh_6147(); test_gh_6181(); test_gh_6189(); + test_gh_6191(); return g_regexTester.result(); }