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 | } |