Coverage Report

Created: 2026-10-11 13:54

real-regex/tests/compat/test_compat_noncontiguous.cpp
Line
Count
Source (jump to first uncovered line)
1
//! A non-contiguous range -- a deque, a list, a reverse iterator -- is searched on REAL over one contiguous copy, with
2
//! the positions mapped back to the caller's own iterators: the same answers as on a string, linear time, and
3
//! O(n) memory for the copy. These pin the answers, the iterators handed back, the cost by counting the caller's
4
//! iterator steps, and which types compile.
5
#include <cstdint>
6
#include <deque>
7
#include <iterator>
8
#include <list>
9
#include <memory>
10
#include <regex>
11
#include <string>
12
#include <type_traits>
13
#include <utility>
14
#include <vector>
15
16
#include <sciforge/test/framework.hpp>
17
#include "real/compat/std/regex.hpp"
18
19
namespace rc = real::compat;
20
21
namespace {
22
23
  using DqIt = std::deque<char>::const_iterator;
24
  using LsIt = std::list<char>::const_iterator;
25
  using StIt = std::string::const_iterator;
26
  using RvIt = std::string::const_reverse_iterator;
27
28
  template <typename It>
29
  concept searches = requires(It a, rc::match_results<It>& m, const rc::regex& re) {
30
    rc::regex_search(a, a, m, re);
31
    rc::regex_match(a, a, m, re);
32
    rc::regex_search(a, a, re);
33
    rc::regex_match(a, a, re);
34
    rc::regex_iterator<It>(a, a, re);
35
    rc::regex_token_iterator<It>(a, a, re, -1);
36
  };
37
38
  template <typename It>
39
  concept has_view = requires(const rc::sub_match<It>& s) {
40
    s.view();
41
  };
42
43
  // Every operation takes any bidirectional iterator; view() only exists over contiguous storage, where it cannot
44
  // dangle; a contiguous iterator carries nothing for the copy.
45
  static_assert(searches<DqIt> && searches<LsIt> && searches<RvIt> && searches<StIt> && searches<const char*>);
46
  static_assert(!has_view<LsIt> && !has_view<DqIt> && !has_view<RvIt> && has_view<StIt> && has_view<const char*>);
47
  static_assert(std::is_same_v<decltype(std::declval<const rc::sub_match<LsIt>&>().str()), std::string>);
48
  static_assert(std::is_same_v<rc::detail::subject_for<StIt>, rc::detail::in_place_subject>);
49
  static_assert(sizeof(rc::regex_iterator<StIt>) < sizeof(rc::regex_iterator<LsIt>));
50
51
  //! (position, length) of every group of \p m, -1 for an unmatched one.
52
  template <typename It>
53
  std::vector<std::pair<long long, long long>> groups_of(const rc::match_results<It>& m)
54
164
  {
55
164
    std::vector<std::pair<long long, long long>> out;
56
368
    for (std::size_t g {0}; g < m.size(); 
++g204
) {
57
204
      out.emplace_back(m[g].matched ? static_cast<long long>(m.position(g)) : 
-1LL0
,
58
204
                       m[g].matched ? static_cast<long long>(m.length(g)) : 
-1LL0
);
59
204
    }
60
164
    return out;
61
164
  }
test_compat_noncontiguous.cpp:_ZN12_GLOBAL__N_19groups_ofISt15_Deque_iteratorIcRKcPS2_EEESt6vectorISt4pairIxxESaIS8_EERKN4real6compat13match_resultsIT_SaINSC_9sub_matchISE_EEEEE
Line
Count
Source
54
41
  {
55
41
    std::vector<std::pair<long long, long long>> out;
56
92
    for (std::size_t g {0}; g < m.size(); 
++g51
) {
57
51
      out.emplace_back(m[g].matched ? static_cast<long long>(m.position(g)) : 
-1LL0
,
58
51
                       m[g].matched ? static_cast<long long>(m.length(g)) : 
-1LL0
);
59
51
    }
60
41
    return out;
61
41
  }
test_compat_noncontiguous.cpp:_ZN12_GLOBAL__N_19groups_ofIN9__gnu_cxx17__normal_iteratorIPKcNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEEEEESt6vectorISt4pairIxxESaISE_EERKN4real6compat13match_resultsIT_SaINSI_9sub_matchISK_EEEEE
Line
Count
Source
54
82
  {
55
82
    std::vector<std::pair<long long, long long>> out;
56
184
    for (std::size_t g {0}; g < m.size(); 
++g102
) {
57
102
      out.emplace_back(m[g].matched ? static_cast<long long>(m.position(g)) : 
-1LL0
,
58
102
                       m[g].matched ? static_cast<long long>(m.length(g)) : 
-1LL0
);
59
102
    }
60
82
    return out;
61
82
  }
test_compat_noncontiguous.cpp:_ZN12_GLOBAL__N_19groups_ofISt20_List_const_iteratorIcEEESt6vectorISt4pairIxxESaIS5_EERKN4real6compat13match_resultsIT_SaINS9_9sub_matchISB_EEEEE
Line
Count
Source
54
41
  {
55
41
    std::vector<std::pair<long long, long long>> out;
56
92
    for (std::size_t g {0}; g < m.size(); 
++g51
) {
57
51
      out.emplace_back(m[g].matched ? static_cast<long long>(m.position(g)) : 
-1LL0
,
58
51
                       m[g].matched ? static_cast<long long>(m.length(g)) : 
-1LL0
);
59
51
    }
60
41
    return out;
61
41
  }
62
63
  //! Every match of \p re over [first, last) by the iterator, as (position, length) of each group.
64
  template <typename It>
65
  std::vector<std::vector<std::pair<long long, long long>>> walk(It               first,
66
                                                                 It               last,
67
                                                                 const rc::regex& re)
68
20
  {
69
20
    std::vector<std::vector<std::pair<long long, long long>>> out;
70
164
    for (rc::regex_iterator<It> it {first, last, re}, end; it != end; 
it++144
) { // it++: a copy every step
71
144
      out.push_back(groups_of(*it));
72
144
    }
73
20
    return out;
74
20
  }
test_compat_noncontiguous.cpp:_ZN12_GLOBAL__N_14walkISt15_Deque_iteratorIcRKcPS2_EEESt6vectorIS6_ISt4pairIxxESaIS8_EESaISA_EET_SD_RKN4real6compat11basic_regexIcNSt7__cxx1112regex_traitsIcEEEE
Line
Count
Source
68
5
  {
69
5
    std::vector<std::vector<std::pair<long long, long long>>> out;
70
41
    for (rc::regex_iterator<It> it {first, last, re}, end; it != end; 
it++36
) { // it++: a copy every step
71
36
      out.push_back(groups_of(*it));
72
36
    }
73
5
    return out;
74
5
  }
test_compat_noncontiguous.cpp:_ZN12_GLOBAL__N_14walkIN9__gnu_cxx17__normal_iteratorIPKcNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEEEEESt6vectorISC_ISt4pairIxxESaISE_EESaISG_EET_SJ_RKN4real6compat11basic_regexIcNS5_12regex_traitsIcEEEE
Line
Count
Source
68
10
  {
69
10
    std::vector<std::vector<std::pair<long long, long long>>> out;
70
82
    for (rc::regex_iterator<It> it {first, last, re}, end; it != end; 
it++72
) { // it++: a copy every step
71
72
      out.push_back(groups_of(*it));
72
72
    }
73
10
    return out;
74
10
  }
test_compat_noncontiguous.cpp:_ZN12_GLOBAL__N_14walkISt20_List_const_iteratorIcEEESt6vectorIS3_ISt4pairIxxESaIS5_EESaIS7_EET_SA_RKN4real6compat11basic_regexIcNSt7__cxx1112regex_traitsIcEEEE
Line
Count
Source
68
5
  {
69
5
    std::vector<std::vector<std::pair<long long, long long>>> out;
70
41
    for (rc::regex_iterator<It> it {first, last, re}, end; it != end; 
it++36
) { // it++: a copy every step
71
36
      out.push_back(groups_of(*it));
72
36
    }
73
5
    return out;
74
5
  }
75
76
  long long steps {0};
77
78
  //! A list iterator that counts every step, so the cost is checked by the caller's own walk, not by a clock.
79
  class counted
80
  {
81
  public:
82
83
    using base              = std::list<char>::const_iterator;
84
    using iterator_category = std::bidirectional_iterator_tag;
85
    using value_type        = char;
86
    using difference_type   = std::ptrdiff_t;
87
    using pointer           = const char*;
88
    using reference         = const char&;
89
90
48
    counted() = default;
91
92
6
    explicit counted(base it) : it_ {it}
93
6
    {}
94
95
    reference operator*() const
96
80.0k
    {
97
80.0k
      return *it_;
98
80.0k
    }
99
100
    pointer operator->() const
101
0
    {
102
0
      return &*it_;
103
0
    }
104
105
    counted& operator++()
106
220k
    {
107
220k
      ++steps;
108
220k
      ++it_;
109
220k
      return *this;
110
220k
    }
111
112
    counted operator++(int)
113
0
    {
114
0
      const counted old {*this};
115
0
      ++*this;
116
0
      return old;
117
0
    }
118
119
    counted& operator--()
120
0
    {
121
0
      ++steps;
122
0
      --it_;
123
0
      return *this;
124
0
    }
125
126
    counted operator--(int)
127
0
    {
128
0
      const counted old {*this};
129
0
      --*this;
130
0
      return old;
131
0
    }
132
133
    bool operator==(const counted& other) const
134
220k
    {
135
220k
      return it_ == other.it_;
136
220k
    }
137
138
  private:
139
140
    base it_;
141
  };
142
143
  static_assert(std::bidirectional_iterator<counted> && !std::random_access_iterator<counted>);
144
} // namespace
145
146
// Search and match, with and without results, answer on a deque, a list and a reverse range as on a string -- the
147
// deque crossing one of its blocks -- and the iterators handed back are the caller's.
148
TEST(compat_non_contiguous_answers_as_on_a_string)
149
1
{
150
1
  std::string subject(4093, 'x');
151
1
  subject += " foo=bar baz=42 qux bar=foo ";
152
1
  const std::deque<char> deque(subject.begin(), subject.end());
153
1
  const std::list<char>  list(subject.begin(), subject.end());
154
1
  const char* const      patterns[] {R"((\w+)=(\w+))", R"(\bbaz\b)", R"((?<=x )foo)", R"(q(u)?x)", "x*"};
155
5
  for (const char* const p : patterns) {
156
5
    const rc::regex         re    {p};
157
5
    rc::smatch              on_string;
158
5
    const bool              found {rc::regex_search(subject, on_string, re)};
159
5
    rc::match_results<DqIt> on_deque;
160
5
    rc::match_results<LsIt> on_list;
161
5
    EXPECT_EQ(rc::regex_search(deque.begin(), deque.end(), on_deque, re), found);
162
5
    EXPECT_EQ(rc::regex_search(list.begin(), list.end(), on_list, re), found);
163
5
    EXPECT_EQ(rc::regex_search(deque.begin(), deque.end(), re), found);
164
5
    EXPECT_EQ(rc::regex_search(list.begin(), list.end(), re), found);
165
5
    if (found) {
166
5
      EXPECT(groups_of(on_deque) == groups_of(on_string));
167
5
      EXPECT(groups_of(on_list) == groups_of(on_string));
168
5
      EXPECT_EQ(on_list.str(0), on_string.str(0));
169
5
      const auto at {std::next(list.begin(), static_cast<long>(on_string.position(0)))};
170
5
      EXPECT(&*on_list[0].first == &*at); // the caller's element, not one of the copy
171
5
    }
172
5
    EXPECT(walk(deque.begin(), deque.end(), re) == walk(subject.cbegin(), subject.cend(), re));
173
5
    EXPECT(walk(list.begin(), list.end(), re) == walk(subject.cbegin(), subject.cend(), re));
174
5
  }
175
1
  const rc::regex whole {R"(x+ foo=bar.*)"};
176
1
  EXPECT(rc::regex_match(list.begin(), list.end(), whole));
177
1
  rc::match_results<LsIt> whole_list;
178
1
  EXPECT(rc::regex_match(list.begin(), list.end(), whole_list, whole));
179
1
  EXPECT_EQ(whole_list.length(0), static_cast<long long>(subject.size()));
180
1
  const std::string       reversed {"rab=oof"};
181
1
  rc::match_results<RvIt> backwards;
182
1
  EXPECT(rc::regex_search(reversed.crbegin(), reversed.crend(), backwards, rc::regex {R"((\w+)=(\w+))"}));
183
1
  EXPECT_EQ(backwards.str(1), "foo");
184
1
}
185
186
// An iterator copied, moved to the heap or post-incremented keeps walking the one copy it made; token iterators
187
// and the range form of replace answer as on a string.
188
TEST(compat_non_contiguous_iterators_tokens_and_replace)
189
1
{
190
1
  const std::string              subject {"a=1, b=22, c=333"};
191
1
  const std::list<char>          list(subject.begin(), subject.end());
192
1
  const rc::regex                re      {R"((\w)=(\d+))"};
193
1
  rc::regex_iterator<LsIt>       it      {list.begin(), list.end(), re};
194
1
  const rc::regex_iterator<LsIt> copy    {it};
195
1
  auto                           moved   {std::make_unique<rc::regex_iterator<LsIt>>(std::move(it))};
196
1
  ++*moved;
197
1
  EXPECT_EQ((**moved).str(1), "b");
198
1
  EXPECT_EQ((*copy).str(1), "a");
199
1
  std::vector<std::string> tokens;
200
10
  for (rc::regex_token_iterator<LsIt> t {list.begin(), list.end(), re, {-1, 1, 2}}, end; t != end; 
++t9
) {
201
9
    tokens.push_back(t->str());
202
9
  }
203
1
  const std::vector<std::string> want {"", "a", "1", ", ", "b", "22", ", ", "c", "333"};
204
1
  EXPECT(tokens == want);
205
1
  std::string replaced;
206
1
  rc::regex_replace(std::back_inserter(replaced), list.begin(), list.end(), re, std::string {"$2$1"});
207
1
  EXPECT_EQ(replaced, rc::regex_replace(subject, re, std::string {"$2$1"}));
208
1
}
209
210
// A pattern that backtracks exponentially stays linear on a non-contiguous range, where std's would not: the copy
211
// runs on REAL.
212
TEST(compat_non_contiguous_runs_in_linear_time)
213
1
{
214
1
  std::string            subject(20000, 'a');
215
1
  const std::deque<char> deque(subject.begin(), subject.end());
216
1
  const rc::regex        re {"(a+)+b"};
217
1
  EXPECT(re.uses_real());
218
1
  EXPECT(!rc::regex_search(deque.begin(), deque.end(), re));
219
1
}
220
221
// The cost, by the caller's own steps: the copy is 2n on a list (measured, then copied) and mapping the positions
222
// back at most n more, for a search and for a whole iteration alike -- never a walk from the start per group or
223
// per match.
224
TEST(compat_non_contiguous_cost_is_linear_in_steps)
225
1
{
226
1
  constexpr long long n {20000};
227
1
  {
228
1
    std::string nested(static_cast<std::size_t>(n), 'a');
229
1
    nested += 'b';
230
1
    const std::list<char>      list(nested.begin(), nested.end());
231
1
    rc::match_results<counted> m;
232
1
    steps = 0;
233
1
    EXPECT(rc::regex_search(counted {list.begin()}, counted {list.end()}, m, rc::regex {"((a+)b)"}));
234
1
    EXPECT(steps <= (3 * n) + 16);
235
1
    EXPECT_EQ(m.str(2).size(), static_cast<std::size_t>(n));
236
1
  }
237
1
  std::string pairs;
238
10.0k
  for (long long i {0}; i < n / 2; 
++i10.0k
) {
239
10.0k
    pairs += i % 7 == 0 ? 
"a-"1.42k
:
"ab"8.57k
;
240
10.0k
  }
241
1
  const std::list<char> list(pairs.begin(), pairs.end());
242
1
  const rc::regex       pair_re {"(a)(b)?"};
243
1
  steps = 0;
244
1
  long long count               {0};
245
10.0k
  for (rc::regex_iterator<counted> it {counted {list.begin()}, counted {list.end()}, pair_re}, end; it != end; 
it++10.0k
) {
246
10.0k
    ++count;
247
10.0k
  }
248
1
  EXPECT_EQ(count, n / 2);
249
1
  EXPECT(steps <= (3 * n) + 16);
250
1
  const rc::regex       star {"a*"};
251
1
  const std::string     dashes(static_cast<std::size_t>(n), '-');
252
1
  const std::list<char> empties(dashes.begin(), dashes.end());
253
1
  steps = 0;
254
1
  count = 0;
255
1
  for (rc::regex_iterator<counted> it {counted {empties.begin()}, counted {empties.end()}, star}, end;
256
20.0k
       it != end; 
++it20.0k
) {
257
20.0k
    ++count;
258
20.0k
  }
259
1
  EXPECT_EQ(count, n + 1);
260
1
  EXPECT(steps <= (3 * n) + 16);
261
1
}
262
263
// match_prev_avail on a sub-range of a deque or a list reads the context from the caller's sequence (std's route,
264
// on the caller's iterators), as std does.
265
TEST(compat_non_contiguous_prev_avail_reads_the_callers_context)
266
1
{
267
1
  const std::string                                   subject {"ab ab"};
268
1
  const std::list<char>                               list(subject.begin(), subject.end());
269
1
  const std::deque<char>                              deque(subject.begin(), subject.end());
270
1
  const rc::regex                                     re    {R"(\bb)"};
271
1
  const std::regex                                    ref   {R"(\bb)"};
272
1
  const auto                                          flags {rc::regex_constants::match_prev_avail};
273
1
  std::match_results<std::list<char>::const_iterator> want_list;
274
1
  const bool                                          want  {std::regex_search(std::next(list.begin()), list.end(), want_list, ref, std::regex_constants::match_prev_avail)};
275
1
  EXPECT_EQ(rc::regex_search(std::next(list.begin()), list.end(), re, flags), want);
276
1
  EXPECT_EQ(rc::regex_search(std::next(deque.begin()), deque.end(), re, flags), want);
277
1
  EXPECT(!want); // 'a' before the 'b': no boundary there, and the second 'b' follows an 'a' too
278
1
}