/src/hobbes/lib/hobbes/lang/pat/regex.C
Line | Count | Source |
1 | | |
2 | | #include <hobbes/eval/cc.H> |
3 | | #include <hobbes/lang/pat/regex.H> |
4 | | #include <hobbes/util/array.H> |
5 | | #include <hobbes/util/str.H> |
6 | | #include <hobbes/util/rmap.H> |
7 | | |
8 | | #include <algorithm> |
9 | | #include <limits> |
10 | | #include <memory> |
11 | | #include <tuple> |
12 | | #include <unordered_map> |
13 | | #include <unordered_set> |
14 | | |
15 | | namespace hobbes { |
16 | | |
17 | | /****************** |
18 | | * parse regular expressions into an AST |
19 | | ******************/ |
20 | | |
21 | | using rchar_t = uint8_t; |
22 | | |
23 | | // epsilon / the empty string |
24 | | struct REps : public Regex { |
25 | 1.29k | void show(std::ostream& out) const override { out << "`"; } |
26 | | }; |
27 | | |
28 | | // a range of chars [b,e] (inclusive) |
29 | | struct RCharRange : public Regex { |
30 | | rchar_t b, e; |
31 | 1.08M | RCharRange(rchar_t b, rchar_t e) : b(b), e(e) { } |
32 | | |
33 | 8.31k | void show(std::ostream& out) const override { |
34 | 8.31k | if (this->b == this->e) { |
35 | 7.46k | out << this->b; |
36 | 7.46k | } else if (this->b == rchar_t(0) && this->e == rchar_t(255)) { |
37 | 129 | out << "."; |
38 | 725 | } else { |
39 | 725 | out << this->b << "-" << this->e; |
40 | 725 | } |
41 | 8.31k | } |
42 | | }; |
43 | | |
44 | | // E* (zero or more instances of the regex E) |
45 | | struct RStar : public Regex { |
46 | | RegexPtr v; |
47 | 18.2k | RStar(const RegexPtr& v) : v(v) { } |
48 | | |
49 | 500 | void show(std::ostream& out) const override { this->v->show(out); out << "*"; } |
50 | | }; |
51 | | |
52 | | // A|B (either match A, or match B) |
53 | | struct REither : public Regex { |
54 | | RegexPtr lhs; |
55 | | RegexPtr rhs; |
56 | 149k | REither(const RegexPtr& lhs, const RegexPtr& rhs) : lhs(lhs), rhs(rhs) { } |
57 | | |
58 | 1.99k | void show(std::ostream& out) const override { this->lhs->show(out); out << "|"; this->rhs->show(out); } |
59 | | }; |
60 | | |
61 | | // AB (match A, then match B) |
62 | | struct RSeq : public Regex { |
63 | | RegexPtr lhs; |
64 | | RegexPtr rhs; |
65 | 1.01M | RSeq(const RegexPtr& lhs, const RegexPtr& rhs) : lhs(lhs), rhs(rhs) { } |
66 | | |
67 | 7.40k | void show(std::ostream& out) const override { this->lhs->show(out); this->rhs->show(out); } |
68 | | }; |
69 | | |
70 | | // (?<x>E) (match E, bind the substring to a variable "x") |
71 | | struct RBind : public Regex { |
72 | | std::string var; |
73 | | RegexPtr def; |
74 | 7.88k | RBind(const std::string& var, const RegexPtr& def) : var(var), def(def) { } |
75 | | |
76 | 198 | void show(std::ostream& out) const override { |
77 | 198 | out << "(?<" << this->var << ">"; |
78 | 198 | this->def->show(out); |
79 | 198 | out << ")"; |
80 | 198 | } |
81 | | }; |
82 | | |
83 | | template <typename T> |
84 | | struct switchRegex { |
85 | | virtual T with(const REps*) const = 0; |
86 | | virtual T with(const RCharRange*) const = 0; |
87 | | virtual T with(const RStar*) const = 0; |
88 | | virtual T with(const REither*) const = 0; |
89 | | virtual T with(const RSeq*) const = 0; |
90 | | virtual T with(const RBind*) const = 0; |
91 | | }; |
92 | | |
93 | | template <typename T> |
94 | 44.9M | T switchOf(const RegexPtr& p, const switchRegex<T>& f) { |
95 | 44.9M | if (const REps* x = dynamic_cast<REps*>(p.get())) { |
96 | 2.21M | return f.with(x); |
97 | 42.7M | } else if (const RCharRange* x = dynamic_cast<RCharRange*>(p.get())) { |
98 | 20.0M | return f.with(x); |
99 | 22.7M | } else if (const RStar* x = dynamic_cast<RStar*>(p.get())) { |
100 | 776k | return f.with(x); |
101 | 21.9M | } else if (const REither* x = dynamic_cast<REither*>(p.get())) { |
102 | 2.06M | return f.with(x); |
103 | 19.8M | } else if (const RSeq* x = dynamic_cast<RSeq*>(p.get())) { |
104 | 19.7M | return f.with(x); |
105 | 19.7M | } else if (const RBind* x = dynamic_cast<RBind*>(p.get())) { |
106 | 87.0k | return f.with(x); |
107 | 87.0k | } else { |
108 | 0 | throw std::runtime_error("Internal error, not a valid regex case"); |
109 | 0 | } |
110 | 44.9M | } unsigned long hobbes::switchOf<unsigned long>(std::shared_ptr<hobbes::Regex> const&, hobbes::switchRegex<unsigned long> const&) Line | Count | Source | 94 | 4.65M | T switchOf(const RegexPtr& p, const switchRegex<T>& f) { | 95 | 4.65M | if (const REps* x = dynamic_cast<REps*>(p.get())) { | 96 | 379k | return f.with(x); | 97 | 4.27M | } else if (const RCharRange* x = dynamic_cast<RCharRange*>(p.get())) { | 98 | 1.98M | return f.with(x); | 99 | 2.28M | } else if (const RStar* x = dynamic_cast<RStar*>(p.get())) { | 100 | 47.0k | return f.with(x); | 101 | 2.24M | } else if (const REither* x = dynamic_cast<REither*>(p.get())) { | 102 | 390k | return f.with(x); | 103 | 1.85M | } else if (const RSeq* x = dynamic_cast<RSeq*>(p.get())) { | 104 | 1.83M | return f.with(x); | 105 | 1.83M | } else if (const RBind* x = dynamic_cast<RBind*>(p.get())) { | 106 | 20.1k | return f.with(x); | 107 | 20.1k | } else { | 108 | 0 | throw std::runtime_error("Internal error, not a valid regex case"); | 109 | 0 | } | 110 | 4.65M | } |
unsigned char hobbes::switchOf<unsigned char>(std::shared_ptr<hobbes::Regex> const&, hobbes::switchRegex<unsigned char> const&) Line | Count | Source | 94 | 40.3M | T switchOf(const RegexPtr& p, const switchRegex<T>& f) { | 95 | 40.3M | if (const REps* x = dynamic_cast<REps*>(p.get())) { | 96 | 1.83M | return f.with(x); | 97 | 38.4M | } else if (const RCharRange* x = dynamic_cast<RCharRange*>(p.get())) { | 98 | 18.0M | return f.with(x); | 99 | 20.4M | } else if (const RStar* x = dynamic_cast<RStar*>(p.get())) { | 100 | 729k | return f.with(x); | 101 | 19.7M | } else if (const REither* x = dynamic_cast<REither*>(p.get())) { | 102 | 1.67M | return f.with(x); | 103 | 18.0M | } else if (const RSeq* x = dynamic_cast<RSeq*>(p.get())) { | 104 | 17.9M | return f.with(x); | 105 | 17.9M | } else if (const RBind* x = dynamic_cast<RBind*>(p.get())) { | 106 | 66.9k | return f.with(x); | 107 | 66.9k | } else { | 108 | 0 | throw std::runtime_error("Internal error, not a valid regex case"); | 109 | 0 | } | 110 | 40.3M | } |
|
111 | | |
112 | 0 | std::string show(const Regex& rgx) { |
113 | 0 | std::ostringstream ss; |
114 | 0 | rgx.show(ss); |
115 | 0 | return ss.str(); |
116 | 0 | } |
117 | 0 | std::string show(const RegexPtr& rgx) { return show(*rgx); } |
118 | | |
119 | | // shorthand constructors for regex AST forms |
120 | 192k | RegexPtr epsilon() { |
121 | 192k | return RegexPtr(new REps()); |
122 | 192k | } |
123 | | |
124 | 884k | RegexPtr charLit(rchar_t c) { |
125 | 884k | return RegexPtr(new RCharRange(c, c)); |
126 | 884k | } |
127 | | |
128 | 197k | RegexPtr charRange(rchar_t b, rchar_t e) { |
129 | 197k | return RegexPtr(new RCharRange(b, e)); |
130 | 197k | } |
131 | | |
132 | 18.2k | RegexPtr zeroOrMore(const RegexPtr& p) { |
133 | 18.2k | return RegexPtr(new RStar(p)); |
134 | 18.2k | } |
135 | | |
136 | 149k | RegexPtr either(const RegexPtr& p0, const RegexPtr& p1) { |
137 | 149k | return RegexPtr(new REither(p0, p1)); |
138 | 149k | } |
139 | | |
140 | 17.2k | RegexPtr bindTo(const std::string& name, const RegexPtr& p) { |
141 | 17.2k | if (name.empty()) { |
142 | 9.41k | return p; |
143 | 9.41k | } else { |
144 | 7.88k | return RegexPtr(new RBind(name, p)); |
145 | 7.88k | } |
146 | 17.2k | } |
147 | | |
148 | 1.01M | RegexPtr sequence(const RegexPtr& p0, const RegexPtr& p1) { |
149 | 1.01M | return RegexPtr(new RSeq(p0, p1)); |
150 | 1.01M | } |
151 | | |
152 | 100k | RegexPtr anyOf(const Regexes& rs) { |
153 | 100k | if (rs.empty()) { |
154 | 461 | return epsilon(); |
155 | 100k | } else { |
156 | 100k | RegexPtr a = rs[0]; |
157 | 197k | for (size_t i = 1; i < rs.size(); ++i) { |
158 | 97.2k | a = either(a, rs[i]); |
159 | 97.2k | } |
160 | 100k | return a; |
161 | 100k | } |
162 | 100k | } |
163 | | |
164 | | using CharRange = std::pair<rchar_t, rchar_t>; |
165 | | using CharRanges = std::vector<CharRange>; |
166 | | |
167 | 100k | CharRanges toRanges(const std::set<rchar_t>& cs) { |
168 | 100k | if (cs.empty()) { |
169 | 461 | return CharRanges(); |
170 | 100k | } else { |
171 | 100k | auto ci = cs.begin(); |
172 | 100k | rchar_t b = *ci; |
173 | 100k | rchar_t e = b; |
174 | | |
175 | 100k | CharRanges r; |
176 | 4.90M | do { |
177 | 4.90M | ++ci; |
178 | 4.90M | if (ci == cs.end()) { |
179 | 100k | r.push_back(std::make_pair(b, e)); |
180 | 4.80M | } else if (e+1 == *ci) { |
181 | 4.71M | e = *ci; |
182 | 4.71M | } else { |
183 | 97.2k | r.push_back(std::make_pair(b, e)); |
184 | 97.2k | b = e = *ci; |
185 | 97.2k | } |
186 | 4.90M | } while (ci != cs.end()); |
187 | 100k | return r; |
188 | 100k | } |
189 | 100k | } |
190 | | |
191 | 100k | RegexPtr anyOf(const std::set<rchar_t>& cs) { |
192 | 100k | Regexes rs; |
193 | 197k | for (const auto& p : toRanges(cs)) { |
194 | 197k | rs.push_back(charRange(p.first, p.second)); |
195 | 197k | } |
196 | 100k | return anyOf(rs); |
197 | 100k | } |
198 | | |
199 | 80.4k | void unescapeInto(rchar_t e, std::set<rchar_t>* out) { |
200 | 80.4k | switch (e) { |
201 | 1.32k | case 't': out->insert('\t'); break; |
202 | 1.27k | case 'r': out->insert('\r'); break; |
203 | 1.31k | case 'n': out->insert('\n'); break; |
204 | 76.5k | default: out->insert(e); break; |
205 | 80.4k | } |
206 | 80.4k | } |
207 | | |
208 | 75.1k | RegexPtr unescapePatChar(rchar_t x) { |
209 | 75.1k | std::set<rchar_t> cs; |
210 | 75.1k | unescapeInto(x, &cs); |
211 | 75.1k | return anyOf(cs); |
212 | 75.1k | } |
213 | | |
214 | | // read char sets |
215 | | using DCharset = std::pair<size_t, std::set<rchar_t>>; |
216 | | |
217 | 9.69k | void charRange(rchar_t i, rchar_t e, std::set<rchar_t>* out) { |
218 | 112k | for (auto x = static_cast<size_t>(i); x <= static_cast<size_t>(e); ++x) { |
219 | 102k | out->insert(static_cast<rchar_t>(x)); |
220 | 102k | } |
221 | 9.69k | } |
222 | | |
223 | 18.3k | const std::set<rchar_t>& anyChars() { |
224 | 18.3k | static std::set<rchar_t> r; |
225 | 18.3k | if (r.empty()) { |
226 | 2 | charRange(0x00, 0xff, &r); |
227 | 2 | } |
228 | 18.3k | return r; |
229 | 18.3k | } |
230 | | |
231 | 10.2k | DCharset readCharset(const std::string& x, size_t i) { |
232 | 10.2k | DCharset r; |
233 | 10.2k | r.first = i; |
234 | | |
235 | | // lookahead by one char so that we can catch ranges and escapes |
236 | 648k | while (r.first < x.size()) { |
237 | 640k | rchar_t c0 = x[r.first]; |
238 | 640k | ++r.first; |
239 | | |
240 | 640k | rchar_t c1 = (r.first == x.size()) ? '\0' : x[r.first]; |
241 | 640k | if (c0 == ']') { |
242 | 2.39k | return r; |
243 | 638k | } else if (c0 == '\\') { |
244 | | // c1 is the escaped character, and the increment below steps over it -- |
245 | | // but when the backslash is the last character there is no c1 (the read |
246 | | // above substituted a nul for it), and stepping over it lands one past |
247 | | // the end. diffRegex rejects a trailing escape outside a charset; do the |
248 | | // same within one rather than returning a position that cannot be read. |
249 | 5.28k | if (r.first == x.size()) { |
250 | 0 | throw std::runtime_error("Unexpected end in regex (expecting escape code)"); |
251 | 0 | } |
252 | 5.28k | unescapeInto(c1, &r.second); |
253 | 5.28k | ++r.first; |
254 | 633k | } else if (c1 == '-' && (r.first+1) < x.size()) { |
255 | 9.69k | charRange(c0, x[r.first+1], &r.second); |
256 | 9.69k | ++r.first; |
257 | 9.69k | ++r.first; |
258 | 623k | } else { |
259 | 623k | r.second.insert(c0); |
260 | 623k | } |
261 | 640k | } |
262 | 7.86k | return r; |
263 | 10.2k | } |
264 | | |
265 | | // parse a complete regex |
266 | | using DRegex = std::pair<size_t, RegexPtr>; |
267 | | |
268 | | // every term in a regex is a level of nesting in the tree it parses to, and |
269 | | // that tree is built and consumed by recursive functions -- the parser here, |
270 | | // the NFA translation in linkStateF, bindingNames, and the tree's own |
271 | | // destructor. so the term count of a regex literal is stack depth several |
272 | | // times over, and a regex only as exotic as a few thousand characters can run |
273 | | // the stack out where it is read. bound it once, here, where every regex has |
274 | | // to pass, rather than in each of those walks. |
275 | | const size_t maxRegexTerms = 1000; |
276 | | |
277 | | // the term budget above counts the distinct nodes a regex parses to, but a |
278 | | // quantified group shares its sub-tree rather than copying it: 'E+' desugars |
279 | | // to 'E E*', and the two E's are one node pointed at twice (see returnR). So a |
280 | | // literal with quantifiers nested k deep has a term count linear in k while the |
281 | | // tree those terms stand for, walked as a tree, has 2^k nodes -- and every walk |
282 | | // here (bindingNames, the cache key in regexFnKey, the NFA translation in |
283 | | // linkStateF) does walk it as a tree. The term budget is the wrong quantity to |
284 | | // stop that: it is comfortably under 1000 while the walks take exponential time. |
285 | | // Bound the size of the tree once it is unshared instead -- the count of nodes |
286 | | // counting each shared node once per path to it, which is what those walks cost |
287 | | // and what the NFA state count comes to. A regex a person writes is a small |
288 | | // multiple of its term count here (a group is expanded at most once per '+' |
289 | | // applied to it); nothing legitimate approaches this bound, and a regex that |
290 | | // does could not compile in reasonable time even if it were allowed to. |
291 | | const size_t maxRegexExpandedSize = 100000; |
292 | | |
293 | | struct TermBudget { |
294 | | size_t taken = 0; |
295 | | |
296 | 1.07M | void take() { |
297 | 1.07M | if (++this->taken > maxRegexTerms) { |
298 | 209 | throw std::runtime_error("regex is too complex to compile (more than " + str::from(maxRegexTerms) + " terms)"); |
299 | 209 | } |
300 | 1.07M | } |
301 | | }; |
302 | | |
303 | | DRegex diffRegex(const RegexPtr& lhs, const std::string& x, size_t i, TermBudget* tb); |
304 | | |
305 | 1.02M | DRegex returnR(const std::string& x, size_t k, const RegexPtr& r) { |
306 | 1.02M | switch (k==x.size() ? '\0' : x[k]) { |
307 | 9.92k | case '?': return DRegex(k+1, either(epsilon(), r)); |
308 | 3.93k | case '*': return DRegex(k+1, zeroOrMore(r)); |
309 | 14.3k | case '+': return DRegex(k+1, sequence(r, zeroOrMore(r))); |
310 | 995k | default: return DRegex(k, r); |
311 | 1.02M | } |
312 | 1.02M | } |
313 | | |
314 | 984k | DRegex seqR(const RegexPtr& lhs, const RegexPtr& c, const std::string& x, size_t k, TermBudget* tb) { |
315 | 984k | DRegex cm = returnR(x, k, c); |
316 | 984k | return diffRegex(sequence(lhs, cm.second), x, cm.first, tb); |
317 | 984k | } |
318 | | |
319 | 1.20M | DRegex diffRegex(const RegexPtr& lhs, const std::string& x, size_t i, TermBudget* tb) { |
320 | | // a position past the end is a step this function's own cases got wrong, not |
321 | | // something an input can ask for -- each of them advances at most one past |
322 | | // the character it consumed. Nothing downstream rechecks, though: reading |
323 | | // from out there does not stop, it walks further off the end for as many |
324 | | // terms as the budget allows (OSS-Fuzz testcase 4831270021693440 read ~1,000 |
325 | | // bytes past a 3-byte buffer that way). Two steps did get it wrong -- an |
326 | | // unterminated capturing group name below, and a trailing escape in |
327 | | // readCharset -- and both now reject instead. This is what keeps a third |
328 | | // from being an out-of-bounds read rather than an error. |
329 | 1.20M | if (i > x.size()) { |
330 | 0 | throw std::runtime_error("Internal error: regex read position is past the end of the regex"); |
331 | 0 | } |
332 | | |
333 | 1.20M | if (i == x.size()) { |
334 | 129k | return DRegex(i, lhs); |
335 | 1.07M | } else { |
336 | | // one term of the regex is read below, whichever case it takes |
337 | 1.07M | tb->take(); |
338 | | |
339 | 1.07M | rchar_t n = (i+1==x.size()) ? '\0' : x[i+1]; |
340 | | |
341 | 1.07M | switch (x[i]) { |
342 | | // a group ends here; a quantifier after it belongs to the whole group, and |
343 | | // the '(' case applies it once the body has been read (reading it here |
344 | | // would attach it to the alternative being read at the time, so that |
345 | | // '(a|b)*' meant 'a|b*') |
346 | 28.3k | case ')': return DRegex(i+1, lhs); |
347 | 17.9k | case '(': { |
348 | | // maybe read a binding name for this group |
349 | | // (according to typical accepted regex syntax) |
350 | 17.9k | std::string b; |
351 | 17.9k | if (n == '?') { |
352 | 7.99k | size_t k = i+2; |
353 | 7.99k | if (k < x.size() && x[k] == 'P') ++k; |
354 | 7.99k | rchar_t gd = (k < x.size() ? x[k] : '\0'); |
355 | 7.99k | ++k; |
356 | 7.99k | rchar_t ed; |
357 | 7.99k | if (gd == '<') { |
358 | 7.97k | ed = '>'; |
359 | 7.97k | } else if (gd == '\'') { |
360 | 0 | ed = '\''; |
361 | 17 | } else { |
362 | 17 | throw std::runtime_error("Expected capturing group name delimited as 'name' or <name>"); |
363 | 17 | } |
364 | 7.97k | size_t j = k; |
365 | 109k | while (j < x.size() && x[j] != ed) { ++j; } |
366 | | |
367 | | // the scan above stops either on the closing delimiter or on the end of |
368 | | // the regex. Only the first is a group name: the second means there is |
369 | | // no delimiter to resume after, and reading on from it steps past the |
370 | | // end (`i` becomes x.size(), and the recursion below starts at i+1). |
371 | 7.97k | if (j == x.size()) { |
372 | 13 | throw std::runtime_error(std::string("Unexpected end in regex (expecting '") + static_cast<char>(ed) + "' to close a capturing group name)"); |
373 | 13 | } |
374 | | |
375 | 7.96k | b = x.substr(k, j-k); |
376 | 7.96k | i = j; |
377 | 7.96k | } |
378 | | |
379 | | // now the group body just matches as if inline, with any quantifier |
380 | | // after the group applied to the body as a whole |
381 | | // (but we may bind to the group match result) |
382 | 17.8k | DRegex g = diffRegex(epsilon(), x, i+1, tb); |
383 | 17.8k | DRegex q = returnR(x, g.first, g.second); |
384 | 17.8k | return diffRegex(sequence(lhs, bindTo(b, q.second)), x, q.first, tb); |
385 | 17.9k | } |
386 | 44.6k | case '|': { |
387 | 44.6k | DRegex n = diffRegex(epsilon(), x, i+1, tb); |
388 | 44.6k | return DRegex(n.first, either(lhs, n.second)); |
389 | 17.9k | } |
390 | 15.2k | case '.': { |
391 | 15.2k | return seqR(lhs, anyOf(anyChars()), x, i+1, tb); |
392 | 17.9k | } |
393 | 10.3k | case '[': { |
394 | 10.3k | if (i+1 < x.size()) { |
395 | 10.2k | bool invert = x[i+1] == '^'; |
396 | 10.2k | auto p = readCharset(x, invert ? (i+2) : (i+1)); |
397 | 10.2k | auto cs = invert ? setDifference(anyChars(), p.second) : p.second; |
398 | | |
399 | 10.2k | return seqR(lhs, anyOf(cs), x, p.first, tb); |
400 | 10.2k | } else { |
401 | 54 | throw std::runtime_error("Unexpected end in regex (expecting ']')"); |
402 | 54 | } |
403 | 10.3k | } |
404 | 75.1k | case '\\': { |
405 | 75.1k | if (i+1 < x.size()) { |
406 | 75.1k | return seqR(lhs, unescapePatChar(x[i+1]), x, i+2, tb); |
407 | 75.1k | } else { |
408 | 0 | throw std::runtime_error("Unexpected end in regex (expecting escape code)"); |
409 | 0 | } |
410 | 75.1k | } |
411 | 884k | default: { |
412 | 884k | return seqR(lhs, charLit(x[i]), x, i+1, tb); |
413 | 75.1k | }} |
414 | 1.07M | } |
415 | 1.20M | } |
416 | | |
417 | 119k | RegexPtr parseRegex(const std::string& x) { |
418 | 119k | TermBudget tb; |
419 | 119k | DRegex dr = diffRegex(epsilon(), x, 0, &tb); |
420 | 141k | while (dr.first < x.size()) { |
421 | | // only an unmatched ')' stops the read early; it closes an implicit group |
422 | | // around everything read so far, so a quantifier after it applies to all |
423 | | // of that (as it did when the ')' case read the quantifier itself) |
424 | 21.5k | dr = returnR(x, dr.first, dr.second); |
425 | 21.5k | dr = diffRegex(dr.second, x, dr.first, &tb); |
426 | 21.5k | } |
427 | 119k | return dr.second; |
428 | 119k | } |
429 | | |
430 | | // the set of names a regex binds. A quantified group shares its sub-tree |
431 | | // between two parents (see the note on maxRegexExpandedSize), so a plain |
432 | | // recursion over it visits shared nodes once per path -- exponentially often |
433 | | // when groups nest. The names are a set, and a node contributes the same names |
434 | | // however many paths reach it, so each distinct node is visited once here: the |
435 | | // walk skips a node it has already seen. This is what keeps bindingNames linear |
436 | | // in the distinct nodes rather than in the unshared tree. |
437 | | struct bnamesF : public switchRegex<UnitV> { |
438 | | str::set* bnames; |
439 | | std::unordered_set<const Regex*>* seen; |
440 | | |
441 | 80.3k | bnamesF(str::set* bnames, std::unordered_set<const Regex*>* seen) : bnames(bnames), seen(seen) { } |
442 | | |
443 | 4.72M | UnitV visit(const RegexPtr& p) const { |
444 | 4.72M | if (this->seen->insert(p.get()).second) { |
445 | 4.67M | switchOf(p, *this); |
446 | 4.67M | } |
447 | 4.72M | return unitv; |
448 | 4.72M | } |
449 | | |
450 | 317k | UnitV with(const REps*) const override { return unitv; } |
451 | 1.99M | UnitV with(const RCharRange*) const override { return unitv; } |
452 | 65.9k | UnitV with(const RStar* x) const override { return visit(x->v); } |
453 | | |
454 | 391k | UnitV with(const REither* x) const override { |
455 | 391k | visit(x->lhs); |
456 | 391k | return visit(x->rhs); |
457 | 391k | } |
458 | | |
459 | 1.88M | UnitV with(const RSeq* x) const override { |
460 | 1.88M | visit(x->lhs); |
461 | 1.88M | return visit(x->rhs); |
462 | 1.88M | } |
463 | | |
464 | 17.6k | UnitV with(const RBind* x) const override { |
465 | 17.6k | this->bnames->insert(x->var); |
466 | 17.6k | return visit(x->def); |
467 | 17.6k | } |
468 | | }; |
469 | | |
470 | 80.3k | str::seq bindingNames(const RegexPtr& rgx) { |
471 | 80.3k | str::set ns; |
472 | 80.3k | std::unordered_set<const Regex*> seen; |
473 | 80.3k | bnamesF(&ns, &seen).visit(rgx); |
474 | 80.3k | return str::seq(ns.begin(), ns.end()); |
475 | 80.3k | } |
476 | | |
477 | | // the number of nodes in a regex's tree once its shared sub-trees are unshared |
478 | | // -- the size the tree-walking passes over it (bindingNames, regexFnKey, the |
479 | | // NFA translation) actually see. A quantified group shares one sub-tree between |
480 | | // its two uses, so this can be exponentially larger than the term count; it is |
481 | | // computed here in one pass over the distinct nodes by remembering each node's |
482 | | // size the first time it is reached, and saturated at size_t's max so the sum |
483 | | // cannot itself overflow on the way to being rejected. |
484 | | struct expandedSizeF : public switchRegex<size_t> { |
485 | | std::unordered_map<const Regex*, size_t>* memo; |
486 | 181k | explicit expandedSizeF(std::unordered_map<const Regex*, size_t>* memo) : memo(memo) { } |
487 | | |
488 | 4.51M | static size_t satAdd(size_t a, size_t b) { |
489 | 4.51M | size_t s = a + b; |
490 | 4.51M | return s < a ? std::numeric_limits<size_t>::max() : s; |
491 | 4.51M | } |
492 | | |
493 | 4.69M | size_t sizeOf(const RegexPtr& p) const { |
494 | 4.69M | auto i = this->memo->find(p.get()); |
495 | 4.69M | if (i != this->memo->end()) { |
496 | 36.0k | return i->second; |
497 | 36.0k | } |
498 | 4.65M | size_t r = switchOf(p, *this); |
499 | 4.65M | (*this->memo)[p.get()] = r; |
500 | 4.65M | return r; |
501 | 4.69M | } |
502 | | |
503 | 379k | size_t with(const REps*) const override { return 1; } |
504 | 1.98M | size_t with(const RCharRange*) const override { return 1; } |
505 | 47.0k | size_t with(const RStar* x) const override { return satAdd(1, sizeOf(x->v)); } |
506 | 390k | size_t with(const REither* x) const override { return satAdd(1, satAdd(sizeOf(x->lhs), sizeOf(x->rhs))); } |
507 | 1.83M | size_t with(const RSeq* x) const override { return satAdd(1, satAdd(sizeOf(x->lhs), sizeOf(x->rhs))); } |
508 | 20.1k | size_t with(const RBind* x) const override { return satAdd(1, sizeOf(x->def)); } |
509 | | }; |
510 | | |
511 | 181k | size_t expandedRegexSize(const RegexPtr& rgx) { |
512 | 181k | std::unordered_map<const Regex*, size_t> memo; |
513 | 181k | return expandedSizeF(&memo).sizeOf(rgx); |
514 | 181k | } |
515 | | |
516 | 181k | void rejectOversizedRegex(const RegexPtr& rgx) { |
517 | 181k | if (expandedRegexSize(rgx) > maxRegexExpandedSize) { |
518 | 48 | throw std::runtime_error("regex is too complex to compile (expands to more than " + str::from(maxRegexExpandedSize) + " terms)"); |
519 | 48 | } |
520 | 181k | } |
521 | | |
522 | | /****************************** |
523 | | * translate the regex AST to an NFA |
524 | | ******************************/ |
525 | | using state = uint32_t; |
526 | | using stateset = std::set<state>; |
527 | | |
528 | | using result = uint32_t; |
529 | | static const result nullResult = static_cast<result>(-1); |
530 | | |
531 | | struct char_range_ord { |
532 | 714M | static bool lt(rchar_t lhs, rchar_t rhs) { |
533 | 714M | return lhs < rhs; |
534 | 714M | } |
535 | 5.75M | static rchar_t pred(rchar_t x) { |
536 | 5.75M | return x-1; |
537 | 5.75M | } |
538 | 20.2M | static rchar_t succ(rchar_t x) { |
539 | 20.2M | return x+1; |
540 | 20.2M | } |
541 | 254k | static void copyRange(rchar_t t0, rchar_t t1, std::set<rchar_t>* out) { |
542 | 254k | auto t = t0; |
543 | 254k | auto e = t1; |
544 | 280k | while (true) { |
545 | 280k | out->insert(t); |
546 | 280k | if (t == e) { |
547 | 254k | break; |
548 | 254k | } else { |
549 | 26.5k | ++t; |
550 | 26.5k | } |
551 | 280k | } |
552 | 254k | } |
553 | | }; |
554 | | |
555 | | using ntransitions = range_map<rchar_t, stateset, char_range_ord>; |
556 | | |
557 | | using srcmarkers = std::map<result, str::set>; |
558 | | |
559 | | struct NFAState { |
560 | | // transitions to successor states |
561 | | stateset eps; |
562 | | ntransitions chars; |
563 | | |
564 | | // if defined, the accept value for this state |
565 | | result acc = nullResult; |
566 | | |
567 | | // markers to begin and end recording subranges |
568 | | srcmarkers begins, ends; |
569 | 7.95k | void beginMark(result r, const std::string& m) { this->begins[r].insert(m); } |
570 | 7.95k | void endMark (result r, const std::string& m) { this->ends[r].insert(m); } |
571 | | }; |
572 | | using NFA = std::vector<NFAState>; |
573 | | |
574 | | // for a given set of NFA states, find the set of non-overlapping char ranges |
575 | 2.11M | CharRanges usedCharRanges(const NFA& nfa, const stateset& ss) { |
576 | 2.11M | CharRanges rs; |
577 | 27.6M | for (auto s : ss) { |
578 | 27.6M | rs = nfa[s].chars.disjointRanges(rs); |
579 | 27.6M | } |
580 | 2.11M | return rs; |
581 | 2.11M | } |
582 | | |
583 | | // accumulate a regex match into an NFA |
584 | | state regexBefore(const RegexPtr&, state cont, result, NFA*); |
585 | | |
586 | | struct linkStateF : public switchRegex<UnitV> { |
587 | | NFA* nfa; |
588 | | state self; |
589 | | state succ; |
590 | | result id; |
591 | | |
592 | 5.76M | linkStateF(NFA* nfa, state self, state succ, result id) : nfa(nfa), self(self), succ(succ), id(id) { |
593 | 5.76M | } |
594 | | |
595 | 3.39M | NFAState& s(state k) const { return (*this->nfa)[k]; } |
596 | 3.38M | NFAState& s() const { return s(this->self); } |
597 | | |
598 | 3.04M | state ins(const RegexPtr& p) const { return regexBefore(p, this->succ, this->id, this->nfa); } |
599 | | |
600 | 245k | UnitV with(const REps*) const override { |
601 | 245k | s().eps.insert(this->succ); |
602 | 245k | return unitv; |
603 | 245k | } |
604 | | |
605 | 2.58M | UnitV with(const RCharRange* x) const override { |
606 | 2.58M | s().chars.mergeRange(x->b, x->e, [&](stateset& ss){ ss.insert(this->succ); }); |
607 | 2.58M | return unitv; |
608 | 2.58M | } |
609 | | |
610 | 110k | UnitV with(const RStar* x) const override { |
611 | 110k | switchOf(x->v, linkStateF(this->nfa, this->self, this->self, this->id)); |
612 | 110k | s().eps.insert(this->succ); |
613 | 110k | return unitv; |
614 | 110k | } |
615 | | |
616 | 217k | UnitV with(const REither* x) const override { |
617 | 217k | state lstate = ins(x->lhs); |
618 | 217k | state rstate = ins(x->rhs); |
619 | 217k | s().eps.insert(lstate); |
620 | 217k | s().eps.insert(rstate); |
621 | 217k | return unitv; |
622 | 217k | } |
623 | | |
624 | 2.60M | UnitV with(const RSeq* x) const override { |
625 | 2.60M | state rstate = ins(x->rhs); |
626 | 2.60M | switchOf(x->lhs, linkStateF(this->nfa, this->self, rstate, this->id)); |
627 | 2.60M | return unitv; |
628 | 2.60M | } |
629 | | |
630 | 7.95k | UnitV with(const RBind* x) const override { |
631 | 7.95k | s().beginMark(this->id, x->var); |
632 | 7.95k | switchOf(x->def, *this); |
633 | 7.95k | s(this->succ).endMark(this->id, x->var); |
634 | 7.95k | return unitv; |
635 | 7.95k | } |
636 | | }; |
637 | | |
638 | 3.04M | state regexBefore(const RegexPtr& p, state cont, result x, NFA* nfa) { |
639 | 3.04M | state rstate = nfa->size(); |
640 | 3.04M | nfa->resize(rstate+1); |
641 | 3.04M | switchOf(p, linkStateF(nfa, rstate, cont, x)); |
642 | 3.04M | return rstate; |
643 | 3.04M | } |
644 | | |
645 | 6.37k | state accumRegex(const RegexPtr& p, result x, NFA* nfa) { |
646 | 6.37k | state rstate = nfa->size(); |
647 | 6.37k | nfa->resize(rstate+1); |
648 | 6.37k | (*nfa)[rstate].acc = x; |
649 | | |
650 | 6.37k | return regexBefore(p, rstate, x, nfa); |
651 | 6.37k | } |
652 | | |
653 | 0 | std::set<rchar_t> usedChars(const NFA& nfa) { |
654 | 0 | std::set<rchar_t> cs; |
655 | 0 | for (const auto& s : nfa) { |
656 | 0 | s.chars.keys(&cs); |
657 | 0 | } |
658 | 0 | return cs; |
659 | 0 | } |
660 | | |
661 | | /***************************** |
662 | | * print an NFA |
663 | | *****************************/ |
664 | 0 | std::string descStates(const stateset& ss) { |
665 | 0 | std::ostringstream out; |
666 | 0 | if (!ss.empty()) { |
667 | 0 | auto s = ss.begin(); |
668 | 0 | out << *s; |
669 | 0 | ++s; |
670 | 0 | for (; s != ss.end(); ++s) { |
671 | 0 | out << "/" << *s; |
672 | 0 | } |
673 | 0 | } |
674 | 0 | return out.str(); |
675 | 0 | } |
676 | | |
677 | 0 | void print(std::ostream& out, const NFA& nfa) { |
678 | | // make columns for state, eps, and acc |
679 | 0 | str::seq state; |
680 | 0 | state.push_back(""); |
681 | |
|
682 | 0 | str::seq eps; |
683 | 0 | eps.push_back("eps"); |
684 | |
|
685 | 0 | str::seq acc; |
686 | 0 | acc.push_back("acc"); |
687 | |
|
688 | 0 | size_t si = 0; |
689 | 0 | for (const auto& s : nfa) { |
690 | 0 | state.push_back(str::from(si++) + ":"); |
691 | 0 | eps.push_back(descStates(s.eps)); |
692 | 0 | acc.push_back(s.acc == nullResult ? "" : str::from(s.acc)); |
693 | 0 | } |
694 | | |
695 | | // make columns for used chars |
696 | 0 | str::seqs ccols; |
697 | 0 | for (auto c : usedChars(nfa)) { |
698 | 0 | ccols.push_back(str::seq()); |
699 | 0 | str::seq& ccol = ccols.back(); |
700 | |
|
701 | 0 | ccol.push_back(str::from(c)); |
702 | 0 | for (const auto& s : nfa) { |
703 | 0 | if (const auto* ts = s.chars.lookup(c)) { |
704 | 0 | ccol.push_back(descStates(*ts)); |
705 | 0 | } else { |
706 | 0 | ccol.push_back(""); |
707 | 0 | } |
708 | 0 | } |
709 | 0 | } |
710 | | |
711 | | // make columns for begin/end capture markers |
712 | 0 | str::seq mbegin; |
713 | 0 | mbegin.push_back("mbegin"); |
714 | |
|
715 | 0 | str::seq mend; |
716 | 0 | mend.push_back("mend"); |
717 | |
|
718 | 0 | for (const auto& s : nfa) { |
719 | 0 | std::ostringstream mb; |
720 | 0 | for (const auto& b : s.begins) { |
721 | 0 | mb << "{" << b.first << ":"; |
722 | 0 | for (const auto& v : b.second) { |
723 | 0 | mb << " " << v; |
724 | 0 | } |
725 | 0 | mb << "}"; |
726 | 0 | } |
727 | 0 | mbegin.push_back(mb.str()); |
728 | |
|
729 | 0 | std::ostringstream me; |
730 | 0 | for (const auto& e : s.ends) { |
731 | 0 | me << "{" << e.first << ":"; |
732 | 0 | for (const auto& v : e.second) { |
733 | 0 | me << " " << v; |
734 | 0 | } |
735 | 0 | me << "}"; |
736 | 0 | } |
737 | 0 | mend.push_back(me.str()); |
738 | 0 | } |
739 | | |
740 | | // print a table out of all of this |
741 | 0 | str::seqs tbl; |
742 | 0 | tbl.push_back(state); |
743 | 0 | tbl.push_back(eps); |
744 | 0 | tbl.insert(tbl.end(), ccols.begin(), ccols.end()); |
745 | 0 | tbl.push_back(acc); |
746 | 0 | tbl.push_back(mbegin); |
747 | 0 | tbl.push_back(mend); |
748 | 0 | str::printRightAlignedTable(out, tbl); |
749 | 0 | } |
750 | | |
751 | | /********************** |
752 | | * find eps* for an NFA |
753 | | **********************/ |
754 | | using EpsClosure = std::map<state, stateset>; |
755 | | |
756 | | using statemarks = std::vector<bool>; |
757 | | |
758 | | // eps* is kept as a set per NFA state, so what it costs is the sum of those |
759 | | // sets' sizes -- and that is the square of the NFA's size for a regex whose |
760 | | // eps edges make a chain, because then every state's closure is the whole |
761 | | // chain below it. `(a?a?...a?)+` is exactly that regex: 300 of the `a?` under |
762 | | // five nested '+' expand to 28,867 NFA states, every one of them closing over |
763 | | // all the others, for 83M states held at once and ~4.9GB (OSS-Fuzz 557561539 |
764 | | // reported it as an out-of-memory against a 2560MB limit; the same shape one |
765 | | // '+' shallower reaches 2.7GB). |
766 | | // |
767 | | // None of the bounds around this one reaches it. maxRegexTerms bounds the |
768 | | // regex as parsed and maxRegexExpandedSize the tree it expands to, but a tree |
769 | | // of 100,000 terms is a hundredth of what its closures can cost; the DFA |
770 | | // state cap and maxDisambiguationSteps bound the subset construction, which |
771 | | // runs after eps* is already built. So the states carried into closures are |
772 | | // counted here and the regex rejected past a budget, the same way an |
773 | | // oversized DFA or an overlong determinization is. The margin is measured, |
774 | | // not guessed: across the whole test suite the costliest regex holds 6,105 |
775 | | // states this way (the deliberately-huge Matching/ |
776 | | // hugeRegexDFACompilesWithoutQuadraticBlowup one, whose 4,911-state NFA |
777 | | // closes over 1.24 states per state on average), and ordinary regexes hold |
778 | | // tens, so a budget of two million leaves legitimate regexes -- including |
779 | | // wide alternations, whose closures really are large -- a factor of a few |
780 | | // hundred of room while holding this phase to a couple of hundred MB. |
781 | | static const size_t maxEpsClosureStates = 2000000; |
782 | | |
783 | | // the walk is iterative rather than one frame per eps edge: the descent below |
784 | | // reaches the end of a chain of eps edges before it closes over anything, so |
785 | | // a budget on what the closures hold cannot speak until the deepest frame is |
786 | | // already on the stack, and each frame carries a set of its own. The NFA can |
787 | | // have as many states as maxRegexExpandedSize allows, which is far more chain |
788 | | // than a stack holds. (The DFA walk in disambiguate() was made iterative for |
789 | | // the same reason.) |
790 | | // |
791 | | // Order is preserved exactly, and that matters because eps edges can form |
792 | | // cycles: a state on a cycle is combined while a state that reaches it is |
793 | | // still in progress, so neither this walk nor the recursion it replaces |
794 | | // computes a complete closure over a cycle -- what a closure ends up holding |
795 | | // depends on which of its descendants happen to be finished by the time it is |
796 | | // combined. The recursion descended into successors in the ascending order |
797 | | // std::set gave them and combined a state after returning from all of them, |
798 | | // so this pushes successors in reverse to pop them in that same order, and |
799 | | // combines a state only after the ones it descended into. The two therefore |
800 | | // arrive at the same incomplete closure rather than at two different ones. |
801 | 2.46M | void findEpsClosure(const NFA& nfa, state s0, statemarks* sms, EpsClosure* ec, size_t* held) { |
802 | | // false: visit this state and schedule its successors; true: combine it |
803 | 2.46M | std::vector<std::pair<state, bool>> walk(1, std::make_pair(s0, false)); |
804 | | |
805 | 8.46M | while (!walk.empty()) { |
806 | 5.99M | const state s = walk.back().first; |
807 | | |
808 | 5.99M | if (!walk.back().second) { |
809 | 2.99M | if ((*sms)[s]) { |
810 | 0 | walk.pop_back(); |
811 | 0 | continue; |
812 | 0 | } |
813 | 2.99M | (*sms)[s] = true; |
814 | 2.99M | walk.back().second = true; |
815 | | |
816 | 2.99M | const stateset& ets = nfa[s].eps; |
817 | 3.74M | for (auto et = ets.rbegin(); et != ets.rend(); ++et) { |
818 | 744k | if (!(*sms)[*et]) { |
819 | 534k | walk.push_back(std::make_pair(*et, false)); |
820 | 534k | } |
821 | 744k | } |
822 | 2.99M | } else { |
823 | 2.99M | walk.pop_back(); |
824 | | |
825 | 2.99M | stateset stes = (*ec)[s]; |
826 | 2.99M | stes.insert(s); |
827 | 2.99M | for (auto et : nfa[s].eps) { |
828 | 740k | stes.insert(et); |
829 | | |
830 | 740k | const stateset& rstes = (*ec)[et]; |
831 | 740k | stes.insert(rstes.begin(), rstes.end()); |
832 | 740k | } |
833 | | |
834 | 2.99M | *held += stes.size(); |
835 | 2.99M | if (*held > maxEpsClosureStates) { |
836 | 6 | throw std::runtime_error("regex is too complex to compile (needs more than " + str::from(maxEpsClosureStates) + " epsilon-closure states)"); |
837 | 6 | } |
838 | 2.99M | (*ec)[s] = std::move(stes); |
839 | 2.99M | } |
840 | 5.99M | } |
841 | 2.46M | } |
842 | | |
843 | 3.69k | void findEpsClosure(const NFA& nfa, EpsClosure* ec) { |
844 | 3.69k | statemarks ms(nfa.size(), false); |
845 | 3.69k | size_t held = 0; |
846 | | |
847 | 2.99M | for (state s = 0; s < nfa.size(); ++s) { |
848 | 2.98M | if (!ms[s]) { |
849 | 2.46M | findEpsClosure(nfa, s, &ms, ec, &held); |
850 | 2.46M | } |
851 | 2.98M | } |
852 | 3.69k | } |
853 | | |
854 | 47.5M | const stateset& epsState(const EpsClosure& ec, state s) { |
855 | 47.5M | auto eci = ec.find(s); |
856 | 47.5M | if (eci != ec.end()) { |
857 | 47.5M | return eci->second; |
858 | 47.5M | } else { |
859 | 0 | throw std::runtime_error("Internal error, NFA state not in eps*"); |
860 | 0 | } |
861 | 47.5M | } |
862 | | |
863 | 0 | stateset epsState(const EpsClosure& ec, const stateset& ss) { |
864 | 0 | stateset r; |
865 | 0 | for (state s : ss) { |
866 | 0 | auto nss = epsState(ec, s); |
867 | 0 | r.insert(nss.begin(), nss.end()); |
868 | 0 | } |
869 | 0 | return r; |
870 | 0 | } |
871 | | |
872 | 0 | void print(std::ostream& out, const stateset& ss) { |
873 | 0 | out << "{"; |
874 | 0 | auto s = ss.begin(); |
875 | 0 | if (s != ss.end()) { |
876 | 0 | out << *s; |
877 | 0 | ++s; |
878 | 0 | for (; s != ss.end(); ++s) { |
879 | 0 | out << ", " << *s; |
880 | 0 | } |
881 | 0 | } |
882 | 0 | out << "}"; |
883 | 0 | } |
884 | | |
885 | 0 | void print(std::ostream& out, const EpsClosure& ec) { |
886 | 0 | for (const auto& eset : ec) { |
887 | 0 | out << eset.first << " -> "; |
888 | 0 | print(out, eset.second); |
889 | 0 | out << std::endl; |
890 | 0 | } |
891 | 0 | } |
892 | | |
893 | | /********************** |
894 | | * convert an NFA to a DFA |
895 | | **********************/ |
896 | | using dtransitions = range_map<rchar_t, state, char_range_ord>; |
897 | | struct DFAState { |
898 | | dtransitions chars; |
899 | | |
900 | | // if defined, the accept value for this state |
901 | | result acc = nullResult; |
902 | | |
903 | | // markers to begin and end recording subranges |
904 | | srcmarkers begins, ends; |
905 | | }; |
906 | | using DFA = std::vector<DFAState>; |
907 | | |
908 | 47.5M | void insert(stateset* o, const stateset& i) { |
909 | 47.5M | o->insert(i.begin(), i.end()); |
910 | 47.5M | } |
911 | | |
912 | | // find the set of NFA states we'd transition to from a char from within a set of states. |
913 | | // *steps counts every state carried into the union; disambiguate() bounds the |
914 | | // whole walk by that count. |
915 | 6.76M | stateset nfaTransition(const NFA& nfa, const EpsClosure& ec, const stateset& ss, const CharRange& cr, size_t* steps) { |
916 | 6.76M | stateset result; |
917 | 216M | for (state s : ss) { |
918 | 216M | if (const auto* tss = nfa[s].chars.lookupRangeSubset(cr)) { |
919 | 47.5M | for (state t : *tss) { |
920 | 47.5M | const stateset& cts = epsState(ec, t); |
921 | 47.5M | *steps += cts.size(); |
922 | 47.5M | insert(&result, cts); |
923 | 47.5M | } |
924 | 47.5M | } |
925 | 216M | } |
926 | 6.76M | return result; |
927 | 6.76M | } |
928 | | |
929 | | // sets of NFA states are mapped to distinct DFA states |
930 | | using Nss2Ds = std::map<stateset, state>; |
931 | | |
932 | | // the DFA state for a set of NFA states: the one already made for it, or a |
933 | | // new one, allocated here and left on the worklist for its transitions to be |
934 | | // filled in |
935 | 6.76M | state dfaState(const cc* c, const NFA& nfa, Nss2Ds* nss2ds, DFA* dfa, std::vector<std::pair<state, stateset>>* pending, const stateset& ss) { |
936 | | // did we already make this state? if so, just return it |
937 | 6.76M | auto didIt = nss2ds->find(ss); |
938 | 6.76M | if (didIt != nss2ds->end()) { |
939 | 4.58M | return didIt->second; |
940 | 4.58M | } |
941 | | |
942 | | // we need to make this state -- allocate it and remember it |
943 | 2.18M | state result = dfa->size(); |
944 | 2.18M | dfa->resize(dfa->size() + 1); |
945 | | |
946 | | // determinizing is exponential in the worst case, and it doesn't take an |
947 | | // exotic regex to hit it: ' *...........,...............' is 29 characters |
948 | | // that determinize to over 50k states. cap the construction so that a regex |
949 | | // literal can't hang the compiler (or the parser -- regex literals are |
950 | | // compiled where they're read). |
951 | 2.18M | if (dfa->size() > c->regexMaxDFAStates()) { |
952 | 68 | throw std::runtime_error("regex is too complex to compile (needs more than " + str::from(c->regexMaxDFAStates()) + " DFA states)"); |
953 | 68 | } |
954 | | |
955 | 2.18M | if (c->throwOnHugeRegexDFA() and c->regexDFAOverNFAMaxRatio() > 0 and (dfa->size() / nfa.size() > size_t(c->regexDFAOverNFAMaxRatio()))) { |
956 | 0 | throw std::runtime_error("regexes DFA over NFA Max ratio was breached"); |
957 | 0 | } |
958 | | |
959 | 2.18M | (*nss2ds)[ss] = result; |
960 | 2.18M | pending->push_back(std::make_pair(result, ss)); |
961 | 2.18M | return result; |
962 | 2.18M | } |
963 | | |
964 | | // the walk over the DFA costs a product per state made -- the NFA states in |
965 | | // its set, the char ranges they use, and the eps-closed successors unioned |
966 | | // per range -- and the state cap in dfaState() bounds only how many states |
967 | | // there are. 143 bytes of regex (OSS-Fuzz 554287846) held every other factor |
968 | | // high at once: ~35M of the steps counted below and minutes of instrumented |
969 | | // time before the state cap finally spoke, a minute and a half in. So the |
970 | | // steps are counted where they are taken and the regex is rejected past a |
971 | | // budget, the same way an oversized DFA is, while the answer still costs |
972 | | // seconds rather than a fuzzer's whole time budget. The margin is measured, not guessed: the largest |
973 | | // deliberately-huge regex in the test suite (Matching/ |
974 | | // hugeRegexDFACompilesWithoutQuadraticBlowup) costs 1.32M steps and ordinary |
975 | | // regexes cost thousands, so triple the former still rejects the reported |
976 | | // input at an eighth of its budgeted run. |
977 | | static const size_t maxDisambiguationSteps = 4000000; |
978 | | |
979 | 3.69k | void disambiguate(const cc* c, const NFA& nfa, DFA* dfa, RStates* rstates) { |
980 | | // determine eps* for this NFA |
981 | 3.69k | EpsClosure ec; |
982 | 3.69k | findEpsClosure(nfa, &ec); |
983 | | |
984 | | // starting from the eps* start state, follow non-eps transitions to eps* |
985 | | // successor states, making a DFA state for each set of NFA states reached. |
986 | | // |
987 | | // this is a walk over the DFA as it is built, and it is done with an |
988 | | // explicit worklist rather than by recursing into each successor as it is |
989 | | // found: the walk can be as deep as the DFA has states -- a chain of |
990 | | // transitions with no repeats is one frame per state -- and the cap on |
991 | | // state count above bounds how many there are, not how deep a chain of them |
992 | | // goes. A regex well inside that cap ran the stack out this way once its |
993 | | // frames were made large by instrumentation (each carried the sets of NFA |
994 | | // states it was visiting). The order states are made in is not significant |
995 | | // to anything downstream, only that each set of NFA states gets one. |
996 | 3.69k | Nss2Ds nss2ds; |
997 | 3.69k | std::vector<std::pair<state, stateset>> pending; |
998 | 3.69k | dfaState(c, nfa, &nss2ds, dfa, &pending, epsState(ec, 0)); |
999 | | |
1000 | 3.69k | size_t steps = 0; |
1001 | | |
1002 | 2.11M | while (!pending.empty()) { |
1003 | 2.11M | const state result = pending.back().first; |
1004 | 2.11M | const stateset ss = pending.back().second; |
1005 | 2.11M | pending.pop_back(); |
1006 | | |
1007 | | // ok, how can we transition out of here? |
1008 | | // for each case, we'll go to a set of NFA states |
1009 | 2.11M | const CharRanges crs = usedCharRanges(nfa, ss); |
1010 | 2.11M | steps += ss.size() * (crs.size() + 1); |
1011 | 2.11M | if (steps > maxDisambiguationSteps) { |
1012 | 22 | throw std::runtime_error("regex is too complex to compile (needs more than " + str::from(maxDisambiguationSteps) + " determinization steps)"); |
1013 | 22 | } |
1014 | 6.76M | for (const auto& cr : crs) { |
1015 | 6.76M | stateset nss = nfaTransition(nfa, ec, ss, cr, &steps); |
1016 | 6.76M | auto ns = dfaState(c, nfa, &nss2ds, dfa, &pending, nss); |
1017 | 6.76M | (*dfa)[result].chars.insert(cr, ns); |
1018 | 6.76M | } |
1019 | | |
1020 | | // our DFA state accepts if any of its NFA states accept |
1021 | | // we may have multiple potential matches here, so we should |
1022 | | // keep track of every such set so that outer match compilation |
1023 | | // can choose the right one |
1024 | 27.6M | for (state s : ss) { |
1025 | 27.6M | auto nr = nfa[s].acc; |
1026 | 27.6M | if (nr != nullResult) { |
1027 | 59.2k | (*dfa)[result].acc = result; |
1028 | 59.2k | (*rstates)[result].insert(nr); |
1029 | 59.2k | } |
1030 | 27.6M | } |
1031 | | |
1032 | | // our DFA state begins/ends subrange recording for each collapsed NFA state |
1033 | 27.6M | for (state s : ss) { |
1034 | 27.6M | for (const auto& b : nfa[s].begins) { |
1035 | 26.5k | (*dfa)[result].begins[b.first].insert(b.second.begin(), b.second.end()); |
1036 | 26.5k | } |
1037 | 27.6M | for (const auto& e : nfa[s].ends) { |
1038 | 124k | (*dfa)[result].ends[e.first].insert(e.second.begin(), e.second.end()); |
1039 | 124k | } |
1040 | 27.6M | } |
1041 | 2.11M | } |
1042 | 3.69k | } |
1043 | | |
1044 | | /***************************** |
1045 | | * print a DFA |
1046 | | *****************************/ |
1047 | 0 | std::set<rchar_t> usedChars(const DFA& dfa) { |
1048 | 0 | std::set<rchar_t> cs; |
1049 | 0 | for (const auto& s : dfa) { |
1050 | 0 | s.chars.keys(&cs); |
1051 | 0 | } |
1052 | 0 | return cs; |
1053 | 0 | } |
1054 | | |
1055 | 0 | void print(std::ostream& out, const DFA& dfa) { |
1056 | | // make columns for state, eps, and acc |
1057 | 0 | str::seq state; |
1058 | 0 | state.push_back(""); |
1059 | |
|
1060 | 0 | str::seq acc; |
1061 | 0 | acc.push_back("acc"); |
1062 | |
|
1063 | 0 | str::seq mbegin; |
1064 | 0 | mbegin.push_back("mbegin"); |
1065 | |
|
1066 | 0 | str::seq mend; |
1067 | 0 | mend.push_back("mend"); |
1068 | |
|
1069 | 0 | size_t si = 0; |
1070 | 0 | for (const auto& s : dfa) { |
1071 | 0 | state.push_back(str::from(si++) + ":"); |
1072 | 0 | acc.push_back(s.acc == nullResult ? "" : str::from(s.acc)); |
1073 | |
|
1074 | 0 | std::ostringstream mb; |
1075 | 0 | for (const auto& b : s.begins) { |
1076 | 0 | mb << "{" << b.first << ":"; |
1077 | 0 | for (const auto& v : b.second) { |
1078 | 0 | mb << " " << v; |
1079 | 0 | } |
1080 | 0 | mb << "}"; |
1081 | 0 | } |
1082 | 0 | mbegin.push_back(mb.str()); |
1083 | |
|
1084 | 0 | std::ostringstream me; |
1085 | 0 | for (const auto& e : s.ends) { |
1086 | 0 | me << "{" << e.first << ":"; |
1087 | 0 | for (const auto& v : e.second) { |
1088 | 0 | me << " " << v; |
1089 | 0 | } |
1090 | 0 | me << "}"; |
1091 | 0 | } |
1092 | 0 | mend.push_back(me.str()); |
1093 | 0 | } |
1094 | | |
1095 | | // make columns for used chars |
1096 | 0 | str::seqs ccols; |
1097 | 0 | for (auto c : usedChars(dfa)) { |
1098 | 0 | ccols.push_back(str::seq()); |
1099 | 0 | str::seq& ccol = ccols.back(); |
1100 | |
|
1101 | 0 | ccol.push_back(str::from(c)); |
1102 | 0 | for (const auto& s : dfa) { |
1103 | 0 | if (const auto* st = s.chars.lookup(c)) { |
1104 | 0 | ccol.push_back(str::from(*st)); |
1105 | 0 | } else { |
1106 | 0 | ccol.push_back(""); |
1107 | 0 | } |
1108 | 0 | } |
1109 | 0 | } |
1110 | | |
1111 | | // print a table out of all of this |
1112 | 0 | str::seqs tbl; |
1113 | 0 | tbl.push_back(state); |
1114 | 0 | tbl.insert(tbl.end(), ccols.begin(), ccols.end()); |
1115 | 0 | tbl.push_back(acc); |
1116 | 0 | tbl.push_back(mbegin); |
1117 | 0 | tbl.push_back(mend); |
1118 | 0 | str::printRightAlignedTable(out, tbl); |
1119 | 0 | } |
1120 | | |
1121 | | /********************** |
1122 | | * convert a DFA to an equivalent function |
1123 | | **********************/ |
1124 | 978k | static ExprPtr transitionToState(const std::string& fname, int s, const LexicalAnnotation& rootLA) { |
1125 | | // produce an expression fragment to jump to the given state |
1126 | 978k | return fncall(var(fname, rootLA), list(var("cap", rootLA), var("cs", rootLA), var("n", rootLA), var("e", rootLA), constant(s, rootLA)), rootLA); |
1127 | 978k | } |
1128 | | |
1129 | 162k | static ExprPtr transitionAsCharSwitch(const std::string& fname, const DFAState& s, const ExprPtr& charExpr, const ExprPtr& defaultResult, const LexicalAnnotation& rootLA) { |
1130 | 162k | Switch::Bindings cs; |
1131 | 162k | std::set<rchar_t> tcs; |
1132 | 162k | s.chars.keys(&tcs); |
1133 | 280k | for (auto tc : tcs) { |
1134 | 280k | cs.push_back(Switch::Binding(PrimitivePtr(new Char(tc, rootLA)), |
1135 | 280k | transitionToState(fname, *s.chars.lookup(tc), rootLA) |
1136 | 280k | )); |
1137 | 280k | } |
1138 | 162k | return |
1139 | 162k | switchE( |
1140 | 162k | charExpr, |
1141 | 162k | cs, |
1142 | 162k | defaultResult, |
1143 | 162k | rootLA |
1144 | 162k | ); |
1145 | 162k | } |
1146 | | |
1147 | 691k | static ExprPtr charInRange(const ExprPtr& c, const std::pair<rchar_t, rchar_t>& crange, const LexicalAnnotation& rootLA) { |
1148 | 691k | if (crange.first == 0 && crange.second == 255) { |
1149 | 0 | return constant(true, rootLA); |
1150 | 691k | } else if (crange.first == 0) { |
1151 | 95.0k | return fncall(var("blte", rootLA), list(c, constant(static_cast<uint8_t>(crange.second), rootLA)), rootLA); |
1152 | 596k | } else if (crange.first == crange.second) { |
1153 | 335k | return fncall(var("beq", rootLA), list(c, constant(static_cast<uint8_t>(crange.first), rootLA)), rootLA); |
1154 | 335k | } else { |
1155 | 260k | return |
1156 | 260k | fncall( |
1157 | 260k | var("and", rootLA), |
1158 | 260k | list( |
1159 | 260k | fncall(var("blte", rootLA), list(constant(static_cast<uint8_t>(crange.first), rootLA), c), rootLA), |
1160 | 260k | fncall(var("blte", rootLA), list(c, constant(static_cast<uint8_t>(crange.second), rootLA)), rootLA) |
1161 | 260k | ), |
1162 | 260k | rootLA |
1163 | 260k | ); |
1164 | 260k | } |
1165 | 691k | } |
1166 | | |
1167 | 95.3k | static ExprPtr transitionAsRangeChecks(const std::string& fname, const std::vector<std::pair<std::pair<rchar_t,rchar_t>, state>>& ranges, const ExprPtr& charExpr, const ExprPtr& defaultResult, const LexicalAnnotation& rootLA) { |
1168 | 95.3k | std::string n = freshName(); |
1169 | 95.3k | ExprPtr cvar = var(n, rootLA); |
1170 | | |
1171 | 95.3k | ExprPtr ifvn = var("if", rootLA); |
1172 | | |
1173 | 95.3k | ExprPtr result = defaultResult; |
1174 | 786k | for (auto r = ranges.rbegin(); r != ranges.rend(); ++r) { |
1175 | 691k | result = |
1176 | 691k | fncall( |
1177 | 691k | ifvn, |
1178 | 691k | list( |
1179 | 691k | charInRange(cvar, r->first, rootLA), |
1180 | 691k | transitionToState(fname, r->second, rootLA), |
1181 | 691k | result |
1182 | 691k | ), |
1183 | 691k | rootLA |
1184 | 691k | ); |
1185 | 691k | } |
1186 | 95.3k | return |
1187 | 95.3k | let(n, assume(fncall(var("unsafeCast", rootLA), list(charExpr), rootLA), primty("byte"), rootLA), |
1188 | 95.3k | result, |
1189 | 95.3k | rootLA |
1190 | 95.3k | ); |
1191 | 95.3k | } |
1192 | | |
1193 | 271k | static ExprPtr transitionMapping(const std::string& fname, const DFAState& s, const ExprPtr& charExpr, const ExprPtr& defaultResult, const LexicalAnnotation& rootLA) { |
1194 | | // for the full 'range -> state' transition map at this state |
1195 | | // are we mostly looking at single character transitions? |
1196 | | // if so, we're better off using a switch |
1197 | | // else use a sequence of range tests |
1198 | 271k | auto rtns = s.chars.mapping(); |
1199 | | |
1200 | 271k | if (rtns.empty()) { |
1201 | | // shouldn't happen, but it's the right thing to do |
1202 | 7.19k | return defaultResult; |
1203 | 263k | } else if (rtns.size() == 1 && rtns[0].first.first == 0 && rtns[0].first.second == 255) { |
1204 | | // a catch-all case, don't need any special logic |
1205 | 6.31k | return transitionToState(fname, rtns[0].second, rootLA); |
1206 | 6.31k | } |
1207 | | |
1208 | 257k | double sd = 0.0; |
1209 | 945k | for (const auto& rtn : rtns) { |
1210 | 945k | sd += 1 + rtn.first.second - rtn.first.first; |
1211 | 945k | } |
1212 | 257k | double avgs = sd / static_cast<double>(rtns.size()); |
1213 | | |
1214 | 257k | if (avgs <= 2.0) { |
1215 | 162k | return transitionAsCharSwitch(fname, s, charExpr, defaultResult, rootLA); |
1216 | 162k | } else { |
1217 | 95.3k | return transitionAsRangeChecks(fname, rtns, charExpr, defaultResult, rootLA); |
1218 | 95.3k | } |
1219 | 257k | } |
1220 | | |
1221 | 3.29k | void makeExprDFAFunc(cc* c, const std::string& fname, const MonoTypePtr& captureTy, const DFA& dfa, const LexicalAnnotation& rootLA) { |
1222 | | // F(cap,cs,i,e,s) = |
1223 | | // switch (s) { |
1224 | | // ... |
1225 | | // case S: |
1226 | | // cap.N.beginX <- i; |
1227 | | // ... etc for every begin marker |
1228 | | // cap.N.endX <- i; |
1229 | | // ... etc for every end marker |
1230 | | // |
1231 | | // if (i == e) then |
1232 | | // ACC(S) |
1233 | | // else |
1234 | | // switch (c) { |
1235 | | // ... |
1236 | | // case C: |
1237 | | // F(cs, i+1, e, SUCC(S,C)) |
1238 | | // ... |
1239 | | // ... |
1240 | 3.29k | Switch::Bindings bs; |
1241 | 3.29k | MonoTypePtr arrT = freshTypeVar(); |
1242 | 3.29k | QualTypePtr qarrElemTy = qualtype(list(std::make_shared<Constraint>("Array", list(arrT, primty("char")))), functy(list(arrT, primty("long")), primty("char"))); |
1243 | 3.29k | QualTypePtr qarrT = qualtype(list(std::make_shared<Constraint>("Array", list(arrT, primty("char")))), arrT); |
1244 | | |
1245 | 274k | for (size_t s = 0; s < dfa.size(); ++s) { |
1246 | | // all transitions out of this state |
1247 | 271k | ExprPtr dispatchExpr = |
1248 | 271k | transitionMapping( |
1249 | 271k | fname, |
1250 | 271k | dfa[s], |
1251 | 271k | fncall(var("elem", rootLA), list(var("cs", rootLA), var("i", rootLA)), rootLA), |
1252 | 271k | constant(static_cast<int>(-1), rootLA), |
1253 | 271k | rootLA |
1254 | 271k | ); |
1255 | | |
1256 | | // evaluate the input char and transition |
1257 | 271k | ExprPtr evalChar = |
1258 | 271k | fncall( |
1259 | 271k | var("if", rootLA), |
1260 | 271k | list( |
1261 | 271k | fncall(var("leq", rootLA), list(var("i", rootLA), var("e", rootLA)), rootLA), |
1262 | 271k | constant(static_cast<int>(dfa[s].acc), rootLA), |
1263 | 271k | dispatchExpr |
1264 | 271k | ), |
1265 | 271k | rootLA |
1266 | 271k | ); |
1267 | | |
1268 | | // mark begin/end capture ranges as appropriate |
1269 | 271k | for (const auto& b : dfa[s].begins) { |
1270 | 8.69k | for (const auto& v : b.second) { |
1271 | | // cap.N.beginvar <- i |
1272 | | // ... |
1273 | 8.69k | evalChar = |
1274 | 8.69k | let("_", assign(proj(var("cap", rootLA), str::from(b.first) + "_begin_" + v, rootLA), var("i", rootLA), rootLA), |
1275 | 8.69k | evalChar, |
1276 | 8.69k | rootLA |
1277 | 8.69k | ); |
1278 | 8.69k | } |
1279 | 8.53k | } |
1280 | | |
1281 | 271k | for (const auto& e : dfa[s].ends) { |
1282 | 70.6k | for (const auto& v : e.second) { |
1283 | | // cap.N.endvar <- i |
1284 | | // ... |
1285 | 70.6k | evalChar = |
1286 | 70.6k | let("_", assign(proj(var("cap", rootLA), str::from(e.first) + "_end_" + v, rootLA), var("i", rootLA), rootLA), |
1287 | 70.6k | evalChar, |
1288 | 70.6k | rootLA |
1289 | 70.6k | ); |
1290 | 70.6k | } |
1291 | 59.4k | } |
1292 | | |
1293 | | // do all of this when in this state |
1294 | 271k | bs.push_back(Switch::Binding(PrimitivePtr(new Int(static_cast<int>(s), rootLA)), evalChar)); |
1295 | 271k | } |
1296 | | |
1297 | 3.29k | ExprPtr fndef = |
1298 | 3.29k | fn(str::strings("cap", "cs", "i", "e", "s"), |
1299 | 3.29k | let("n", fncall(var("ladd", rootLA), list(var("i", rootLA), constant(static_cast<long>(1), rootLA)), rootLA), |
1300 | 3.29k | let("elem", assume(var("element", rootLA), qarrElemTy, rootLA), |
1301 | 3.29k | switchE( |
1302 | 3.29k | var("s", rootLA), |
1303 | 3.29k | bs, |
1304 | 3.29k | constant(static_cast<int>(-1), rootLA), |
1305 | 3.29k | rootLA |
1306 | 3.29k | ), |
1307 | 3.29k | rootLA),rootLA), |
1308 | 3.29k | rootLA |
1309 | 3.29k | ); |
1310 | | |
1311 | 3.29k | c->define(fname, assume(fndef, qualtype(qarrT->constraints(), functy(list(captureTy, arrT, primty("long"), primty("long"), primty("int")), primty("int"))), rootLA)); |
1312 | 3.29k | } |
1313 | | |
1314 | | using CRange = std::pair<char, char>; |
1315 | | using CTransition = std::pair<CRange, int>; |
1316 | | using CTransitions = array<CTransition>; |
1317 | | |
1318 | | DEFINE_STRUCT( |
1319 | | DFAStateRep, |
1320 | | (CTransitions*, transitions), |
1321 | | (int, acc) |
1322 | | ); |
1323 | | |
1324 | 286 | array<DFAStateRep>* makeDFARep(cc* c, const DFA& dfa) { |
1325 | 286 | auto *result = c->makeArray<DFAStateRep>(dfa.size()); |
1326 | 822k | for (size_t i = 0; i < dfa.size(); ++i) { |
1327 | 822k | DFAStateRep& s = result->data[i]; |
1328 | 822k | auto ctnm = dfa[i].chars.mapping(); |
1329 | 822k | s.transitions = c->makeArray<CTransition>(ctnm.size()); |
1330 | 3.72M | for (size_t j = 0; j < ctnm.size(); ++j) { |
1331 | 2.90M | s.transitions->data[j] = ctnm[j]; |
1332 | 2.90M | } |
1333 | 822k | s.transitions->size = ctnm.size(); |
1334 | 822k | s.acc = dfa[i].acc; |
1335 | 822k | } |
1336 | 286 | result->size = dfa.size(); |
1337 | 286 | return result; |
1338 | 286 | } |
1339 | | |
1340 | 286 | void makeInterpDFAFunc(cc* c, const std::string& fname, const MonoTypePtr& captureTy, const DFA& dfa, const LexicalAnnotation& rootLA) { |
1341 | 286 | MonoTypePtr arrT = freshTypeVar(); |
1342 | 286 | QualTypePtr qarrElemTy = qualtype(list(std::make_shared<Constraint>("Array", list(arrT, primty("char")))), functy(list(arrT, primty("long")), primty("char"))); |
1343 | 286 | QualTypePtr qarrT = qualtype(list(std::make_shared<Constraint>("Array", list(arrT, primty("char")))), arrT); |
1344 | | |
1345 | 286 | std::string regexDFADef = ".regexDFA." + freshName(); |
1346 | 286 | c->bind(regexDFADef, makeDFARep(c, dfa)); |
1347 | | |
1348 | 286 | ExprPtr fndef = |
1349 | 286 | fn(str::strings("cap", "cs", "i", "e", "s"), |
1350 | 286 | fncall(var("runRegexDFA", rootLA), list(var("cs", rootLA), var("i", rootLA), var("e", rootLA), var("s", rootLA), var(regexDFADef, rootLA)), rootLA), |
1351 | 286 | rootLA |
1352 | 286 | ); |
1353 | | |
1354 | 286 | c->define(fname, assume(fndef, qualtype(qarrT->constraints(), functy(list(captureTy, arrT, primty("long"), primty("long"), primty("int")), primty("int"))), rootLA)); |
1355 | 286 | } |
1356 | | |
1357 | | // how many range tests the expression form of a DFA would spell out |
1358 | 3.32k | static size_t dfaTransitions(const DFA& dfa) { |
1359 | 3.32k | size_t n = 0; |
1360 | 316k | for (const auto& s : dfa) { |
1361 | 316k | n += s.chars.size(); |
1362 | 316k | } |
1363 | 3.32k | return n; |
1364 | 3.32k | } |
1365 | | |
1366 | | // The expression form of a DFA (makeExprDFAFunc) is a switch with a case per |
1367 | | // state and a range test per transition, and the compiler types and compiles |
1368 | | // it like any other expression: measured in an optimized build, that costs |
1369 | | // about 270us per state and 50-100us per transition, so a DFA of a few |
1370 | | // thousand states and a few tens of thousands of transitions is seconds of |
1371 | | // compile time for one regex literal. The interpreter (makeInterpDFAFunc) is |
1372 | | // free of that, since its table is built directly, but it knows nothing of |
1373 | | // the markers that record where a capture begins and ends -- so a regex with |
1374 | | // capture groups used to take the expression form at any size, and was |
1375 | | // bounded only by the DFA state cap. OSS-Fuzz 557846266 is a 709 byte |
1376 | | // captured regex that determinizes to 4,906 states and 38,354 transitions: |
1377 | | // 4-6s of type inference in an optimized build, and past the fuzzer's |
1378 | | // minute under ASan. |
1379 | | // |
1380 | | // Both figures are budgeted, because the DFA state cap alone does not bound |
1381 | | // the expression: a state can carry up to 256 disjoint byte ranges, and a |
1382 | | // 257-state DFA with 63 per state (16,191 transitions) took longer to type |
1383 | | // than one with 2,049 states and three each. Past either budget a regex |
1384 | | // without captures is interpreted, as it was past the state cap before, and |
1385 | | // one with captures is rejected. The bounds are cc settings, so a caller that |
1386 | | // wants a larger captured regex compiled, and will wait for it, can raise |
1387 | | // them. |
1388 | 3.59k | void makeDFAFunc(cc* c, const std::string& fname, const MonoTypePtr& captureTy, const DFA& dfa, const LexicalAnnotation& rootLA) { |
1389 | 3.59k | const size_t maxStates = c->regexMaxExprDFASize(); |
1390 | 3.59k | const size_t maxTransitions = c->regexMaxExprDFATransitions(); |
1391 | | |
1392 | | // the transitions are counted once, and only where the count decides |
1393 | | // something (a DFA under the state cap) or is reported (one with captures) |
1394 | 3.59k | const bool underStates = dfa.size() < maxStates; |
1395 | 3.59k | const size_t transitions = (underStates || !isUnit(captureTy)) ? dfaTransitions(dfa) : 0; |
1396 | | |
1397 | 3.59k | if (underStates && transitions < maxTransitions) { |
1398 | 3.29k | makeExprDFAFunc(c, fname, captureTy, dfa, rootLA); |
1399 | 3.29k | } else if (isUnit(captureTy)) { |
1400 | 286 | makeInterpDFAFunc(c, fname, captureTy, dfa, rootLA); |
1401 | 286 | } else { |
1402 | 17 | throw std::runtime_error( |
1403 | 17 | "regex is too complex to compile (its capture groups require it to be compiled as an expression, " |
1404 | 17 | "but its DFA has " + str::from(dfa.size()) + " states and " + str::from(transitions) + " transitions, " |
1405 | 17 | "past the " + str::from(maxStates) + " states or " + str::from(maxTransitions) + " transitions " |
1406 | 17 | "an expression may hold)" |
1407 | 17 | ); |
1408 | 17 | } |
1409 | 3.59k | } |
1410 | | |
1411 | | // merge char-range mappings where possible and conflate duplicate result states |
1412 | 3.59k | void mergeCharRangesAndEqResults(DFA* dfa, const RStates& fstates, RStates* rstates) { |
1413 | 3.59k | std::map<RegexIdxs, size_t> results; |
1414 | | |
1415 | 1.44M | for (auto& s : *dfa) { |
1416 | 1.44M | s.chars.compact(); |
1417 | | |
1418 | 1.44M | if (s.acc != nullResult) { |
1419 | 30.4k | auto fsi = fstates.find(s.acc); |
1420 | 30.4k | if (fsi == fstates.end()) { throw std::runtime_error("Internal error, invalid regex state mapping determined"); } |
1421 | | |
1422 | 30.4k | auto si = results.find(fsi->second); |
1423 | 30.4k | if (si != results.end()) { |
1424 | 22.0k | s.acc = si->second; |
1425 | 22.0k | } else { |
1426 | 8.49k | size_t ns = results.size(); |
1427 | | |
1428 | 8.49k | results[fsi->second] = ns; |
1429 | 8.49k | (*rstates)[ns] = fsi->second; |
1430 | 8.49k | s.acc = ns; |
1431 | 8.49k | } |
1432 | 30.4k | } |
1433 | 1.44M | } |
1434 | 3.59k | } |
1435 | | |
1436 | | /************************** |
1437 | | * compress a DFA by merging equivalent states |
1438 | | **************************/ |
1439 | | using EqStates = std::map<state, state>; |
1440 | | |
1441 | | // a union-find over DFA states where a class is always named by its least |
1442 | | // member (removeEquivStates relies on a representative never being merged away |
1443 | | // itself, and on state 0 staying the start state) |
1444 | | class StatePartition { |
1445 | | public: |
1446 | 3.59k | explicit StatePartition(size_t states) : reps(states) { |
1447 | 1.45M | for (size_t s = 0; s < states; ++s) { |
1448 | 1.44M | this->reps[s] = static_cast<state>(s); |
1449 | 1.44M | } |
1450 | 3.59k | } |
1451 | | |
1452 | 91.6M | state find(state s) { |
1453 | 105M | while (this->reps[s] != s) { |
1454 | 14.0M | this->reps[s] = this->reps[this->reps[s]]; |
1455 | 14.0M | s = this->reps[s]; |
1456 | 14.0M | } |
1457 | 91.6M | return s; |
1458 | 91.6M | } |
1459 | | |
1460 | 314k | bool merge(state s0, state s1) { |
1461 | 314k | state r0 = find(s0); |
1462 | 314k | state r1 = find(s1); |
1463 | 314k | if (r0 == r1) { |
1464 | 0 | return false; |
1465 | 0 | } |
1466 | 314k | this->reps[std::max(r0, r1)] = std::min(r0, r1); |
1467 | 314k | return true; |
1468 | 314k | } |
1469 | | private: |
1470 | | std::vector<state> reps; |
1471 | | }; |
1472 | | |
1473 | | // everything about a state that merging can never change |
1474 | | using Observation = std::tuple<result, srcmarkers, srcmarkers>; |
1475 | | |
1476 | 197M | void appendKey(std::string* k, uint64_t x) { |
1477 | 1.78G | for (size_t b = 0; b < sizeof(x); ++b) { |
1478 | 1.58G | k->push_back(static_cast<char>((x >> (8 * b)) & 0xff)); |
1479 | 1.58G | } |
1480 | 197M | } |
1481 | | |
1482 | | // how a state behaves: what it observes, plus the class each of its char ranges |
1483 | | // leads to (which shifts as states are merged, so this must be recomputed) |
1484 | 24.4M | std::string stateKey(size_t obs, const dtransitions::Mapping& ts, StatePartition* p) { |
1485 | 24.4M | std::string k; |
1486 | 24.4M | k.reserve(sizeof(uint64_t) * (1 + (3 * ts.size()))); |
1487 | 24.4M | appendKey(&k, obs); |
1488 | 57.8M | for (const auto& t : ts) { |
1489 | 57.8M | appendKey(&k, t.first.first); |
1490 | 57.8M | appendKey(&k, t.first.second); |
1491 | 57.8M | appendKey(&k, p->find(t.second)); |
1492 | 57.8M | } |
1493 | 24.4M | return k; |
1494 | 24.4M | } |
1495 | | |
1496 | 3.59k | EqStates findEquivStates(const DFA& dfa) { |
1497 | 3.59k | StatePartition p(dfa.size()); |
1498 | | |
1499 | | // acceptance and capture marking are fixed for the life of a state, so reduce |
1500 | | // them to a single comparable number once rather than per pass |
1501 | 3.59k | std::map<Observation, size_t> obsIdx; |
1502 | 3.59k | std::vector<size_t> obs(dfa.size()); |
1503 | 3.59k | std::vector<dtransitions::Mapping> mappings(dfa.size()); |
1504 | 1.45M | for (size_t s = 0; s < dfa.size(); ++s) { |
1505 | 1.44M | size_t nid = obsIdx.size(); |
1506 | 1.44M | obs[s] = obsIdx.insert(std::make_pair(Observation(dfa[s].acc, dfa[s].begins, dfa[s].ends), nid)).first->second; |
1507 | 1.44M | mappings[s] = dfa[s].chars.mapping(); |
1508 | 1.44M | } |
1509 | | |
1510 | | // two states are equivalent when they observe the same thing and their |
1511 | | // transitions agree on the classes established so far. merging states can |
1512 | | // bring more states into agreement, so keep refining until a pass finds |
1513 | | // nothing new -- a pass that stops short only leaves the DFA larger, never |
1514 | | // wrong, so the states left unmerged in a cycle are safe to keep. |
1515 | 3.59k | bool merging = true; |
1516 | 25.8k | while (merging) { |
1517 | 22.2k | merging = false; |
1518 | | |
1519 | 22.2k | std::unordered_map<std::string, state> keys; |
1520 | 22.2k | std::vector<std::pair<state, state>> merges; |
1521 | 31.7M | for (state s = 0; s < dfa.size(); ++s) { |
1522 | 31.7M | if (p.find(s) != s) continue; // one key per class, not per state |
1523 | | |
1524 | 24.4M | auto k = keys.insert(std::make_pair(stateKey(obs[s], mappings[s], &p), s)); |
1525 | 24.4M | if (!k.second) { |
1526 | 314k | merges.push_back(std::make_pair(k.first->second, s)); |
1527 | 314k | } |
1528 | 24.4M | } |
1529 | | |
1530 | | // deferred so that every key in a pass is read against the same classes |
1531 | 314k | for (const auto& m : merges) { |
1532 | 314k | merging = p.merge(m.first, m.second) || merging; |
1533 | 314k | } |
1534 | 22.2k | } |
1535 | | |
1536 | 3.59k | EqStates result; |
1537 | 1.45M | for (state s = 0; s < dfa.size(); ++s) { |
1538 | 1.44M | state r = p.find(s); |
1539 | 1.44M | if (r != s) { |
1540 | 314k | result[s] = r; |
1541 | 314k | } |
1542 | 1.44M | } |
1543 | 3.59k | return result; |
1544 | 3.59k | } |
1545 | | |
1546 | 3.59k | DFA removeEquivStates(const DFA& dfa, const EqStates& eqs) { |
1547 | | // after removing states, some states will need to be renumbered |
1548 | | // we can infer this mapping from the eqstate mapping |
1549 | 3.59k | std::map<state, state> shifted; |
1550 | 3.59k | size_t accShift = 0; |
1551 | 318k | for (auto eq = eqs.begin(); eq != eqs.end();) { |
1552 | 314k | state s0 = eq->first+1; |
1553 | 314k | ++eq; |
1554 | 314k | state s1 = (eq == eqs.end()) ? dfa.size() : eq->first; |
1555 | 314k | ++accShift; |
1556 | | |
1557 | 1.02M | for (state s = s0; s < s1; ++s) { |
1558 | 706k | shifted[s] = s - accShift; |
1559 | 706k | } |
1560 | 314k | } |
1561 | | |
1562 | | // now make a new DFA with eq states removed |
1563 | | // all references to states need to be |
1564 | | // patched up to account for merging/shifting |
1565 | 3.59k | DFA result; |
1566 | 3.59k | result.reserve(dfa.size()-eqs.size()); |
1567 | | |
1568 | 1.45M | for (size_t s = 0; s < dfa.size(); ++s) { |
1569 | 1.44M | const auto& sd = dfa[s]; |
1570 | | |
1571 | | // don't include eliminated states |
1572 | 1.44M | if (eqs.find(s) != eqs.end()) continue; |
1573 | | |
1574 | | // bring this state over to the result |
1575 | 1.13M | result.push_back(dfa[s]); |
1576 | 1.13M | auto& rsd = result.back(); |
1577 | | |
1578 | | // patch its transitions to account for merged and shifted states |
1579 | 3.94M | for (const auto& m : sd.chars.mapping()) { |
1580 | 3.94M | auto eq = eqs.find(m.second); |
1581 | 3.94M | state tgt = (eq == eqs.end()) ? m.second : eq->second; |
1582 | 3.94M | auto shift = shifted.find(tgt); |
1583 | 3.94M | state stgt = (shift == shifted.end()) ? tgt : shift->second; |
1584 | | |
1585 | 3.94M | if (stgt != m.second) { |
1586 | 2.03M | rsd.chars.insert(m.first, stgt); |
1587 | 2.03M | } |
1588 | 3.94M | } |
1589 | 1.13M | } |
1590 | 3.59k | return result; |
1591 | 3.59k | } |
1592 | | |
1593 | | /************************** |
1594 | | * make an expression to allocate capture group data for a set of regular expressions |
1595 | | **************************/ |
1596 | 31.5k | MonoTypePtr regexCaptureBufferType(const Regexes& regexes) { |
1597 | 31.5k | Record::Members ms; |
1598 | 68.5k | for (size_t r = 0; r < regexes.size(); ++r) { |
1599 | 36.9k | for (const auto& b : bindingNames(regexes[r])) { |
1600 | 9.19k | ms.push_back(Record::Member(str::from(r) + "_begin_" + b, primty("long"))); |
1601 | 9.19k | ms.push_back(Record::Member(str::from(r) + "_end_" + b, primty("long"))); |
1602 | 9.19k | } |
1603 | 36.9k | } |
1604 | 31.5k | return ms.empty() ? primty("unit") : MonoTypePtr(Record::make(ms)); |
1605 | 31.5k | } |
1606 | | |
1607 | 27.9k | ExprPtr makeRegexCaptureBuffer(const Regexes& regexes, const LexicalAnnotation& rootLA) { |
1608 | 27.9k | return |
1609 | 27.9k | assume( |
1610 | 27.9k | fncall( |
1611 | 27.9k | var("newPrimZ", rootLA), |
1612 | 27.9k | list(mktunit(rootLA)), |
1613 | 27.9k | rootLA |
1614 | 27.9k | ), |
1615 | 27.9k | regexCaptureBufferType(regexes), |
1616 | 27.9k | rootLA |
1617 | 27.9k | ); |
1618 | 27.9k | } |
1619 | | |
1620 | | /************************** |
1621 | | * make a function to determine which among the input regexes here a later string matches |
1622 | | **************************/ |
1623 | | // an encoding of a regex that two regexes share only if they are the same |
1624 | | // regex: every node is tagged, and every name is length-prefixed, so the |
1625 | | // encodings of different trees cannot run together. (show() is not that: it |
1626 | | // prints characters raw, so 'a|b' and the three literals a, |, b look alike.) |
1627 | | struct regexKeyF : public switchRegex<UnitV> { |
1628 | | std::string* out; |
1629 | 213k | explicit regexKeyF(std::string* out) : out(out) { } |
1630 | | |
1631 | 1.27M | UnitV with(const REps*) const override { *this->out += "e;"; return unitv; } |
1632 | 13.4M | UnitV with(const RCharRange* x) const override { |
1633 | 13.4M | *this->out += "r" + str::from(static_cast<int>(x->b)) + "," + str::from(static_cast<int>(x->e)) + ";"; |
1634 | 13.4M | return unitv; |
1635 | 13.4M | } |
1636 | 553k | UnitV with(const RStar* x) const override { |
1637 | 553k | *this->out += "*("; |
1638 | 553k | switchOf(x->v, *this); |
1639 | 553k | *this->out += ")"; |
1640 | 553k | return unitv; |
1641 | 553k | } |
1642 | 1.06M | UnitV with(const REither* x) const override { |
1643 | 1.06M | *this->out += "|("; |
1644 | 1.06M | switchOf(x->lhs, *this); |
1645 | 1.06M | *this->out += ","; |
1646 | 1.06M | switchOf(x->rhs, *this); |
1647 | 1.06M | *this->out += ")"; |
1648 | 1.06M | return unitv; |
1649 | 1.06M | } |
1650 | 13.4M | UnitV with(const RSeq* x) const override { |
1651 | 13.4M | *this->out += ".("; |
1652 | 13.4M | switchOf(x->lhs, *this); |
1653 | 13.4M | *this->out += ","; |
1654 | 13.4M | switchOf(x->rhs, *this); |
1655 | 13.4M | *this->out += ")"; |
1656 | 13.4M | return unitv; |
1657 | 13.4M | } |
1658 | 41.4k | UnitV with(const RBind* x) const override { |
1659 | 41.4k | *this->out += "b" + str::from(x->var.size()) + ":" + x->var + "("; |
1660 | 41.4k | switchOf(x->def, *this); |
1661 | 41.4k | *this->out += ")"; |
1662 | 41.4k | return unitv; |
1663 | 41.4k | } |
1664 | | }; |
1665 | | |
1666 | 213k | std::string regexKey(const RegexPtr& r) { |
1667 | 213k | std::string k; |
1668 | 213k | switchOf(r, regexKeyF(&k)); |
1669 | 213k | return k; |
1670 | 213k | } |
1671 | | |
1672 | 27.9k | std::string regexFnKey(const Regexes& regexes) { |
1673 | 27.9k | std::string k; |
1674 | 30.6k | for (const auto& r : regexes) { |
1675 | 30.6k | k += regexKey(r); |
1676 | 30.6k | k += "\n"; |
1677 | 30.6k | } |
1678 | 27.9k | return k; |
1679 | 27.9k | } |
1680 | | |
1681 | 27.9k | CRegexes makeRegexFn(cc* c, const Regexes& regexes, const LexicalAnnotation& rootLA) { |
1682 | 27.9k | CRegexes result; |
1683 | | |
1684 | | // reject a regex whose tree is too large before anything walks it: every pass |
1685 | | // below (the capture buffer's bindingNames, the cache key, the NFA build) is |
1686 | | // linear in the unshared tree size, which a quantified group can blow up far |
1687 | | // past the term count. Checked here, once, ahead of the first of those walks. |
1688 | 30.6k | for (const auto& r : regexes) { |
1689 | 30.6k | rejectOversizedRegex(r); |
1690 | 30.6k | } |
1691 | | |
1692 | | // save capturing-group settings |
1693 | 27.9k | result.captureBuffer = makeRegexCaptureBuffer(regexes, rootLA); |
1694 | 58.6k | for (size_t i = 0; i < regexes.size(); ++i) { |
1695 | 30.6k | result.captureVarsAt[i] = bindingNames(regexes[i]); |
1696 | 30.6k | } |
1697 | | |
1698 | | // if these regexes have been compiled into this compiler before, the |
1699 | | // function and the result mapping made then serve now: the function is |
1700 | | // pure in its input and the mapping is a property of the regexes, so |
1701 | | // neither depends on where the match that uses them is. (The capture |
1702 | | // buffer expression above is remade with this match's annotation, and the |
1703 | | // binding names recomputed, because they are cheap and carry a location.) |
1704 | 27.9k | const std::string key = regexFnKey(regexes); |
1705 | 27.9k | auto cached = c->regexFnCache.find(key); |
1706 | 27.9k | if (cached != c->regexFnCache.end()) { |
1707 | 24.2k | result.fname = cached->second.fname; |
1708 | 24.2k | result.rstates = cached->second.rstates; |
1709 | 24.2k | return result; |
1710 | 24.2k | } |
1711 | | |
1712 | | // our NFA will non-deterministically jump to every possible start state |
1713 | 3.69k | NFA nfa; |
1714 | 3.69k | nfa.resize(1); |
1715 | 10.0k | for (size_t i = 0; i < regexes.size(); ++i) { |
1716 | 6.37k | auto s = accumRegex(regexes[i], i, &nfa); |
1717 | 6.37k | nfa[0].eps.insert(s); |
1718 | 6.37k | } |
1719 | | |
1720 | | // now map this NFA to a DFA |
1721 | 3.69k | DFA dfa; |
1722 | 3.69k | RStates fstates; |
1723 | 3.69k | disambiguate(c, nfa, &dfa, &fstates); |
1724 | | |
1725 | | // make all char ranges compact and minimize the results to avoid redundant work in the caller |
1726 | 3.69k | mergeCharRangesAndEqResults(&dfa, fstates, &result.rstates); |
1727 | | |
1728 | | // in case of blowup, minimize the size of this DFA |
1729 | 3.69k | dfa = removeEquivStates(dfa, findEquivStates(dfa)); |
1730 | | |
1731 | | // translate this DFA to a function |
1732 | 3.69k | std::string fname = ".regex." + freshName(); |
1733 | 3.69k | makeDFAFunc(c, fname, regexCaptureBufferType(regexes), dfa, rootLA); |
1734 | | |
1735 | | // and that's the function that the outer match logic should use |
1736 | 3.69k | result.fname = fname; |
1737 | | |
1738 | | // remember the function and the result mapping for the next match on these |
1739 | | // regexes; not the capture buffer expression, which carries this match's |
1740 | | // source location and would pin it for the life of the compiler |
1741 | 3.69k | CRegexes& kept = c->regexFnCache[key]; |
1742 | 3.69k | kept.fname = result.fname; |
1743 | 3.69k | kept.rstates = result.rstates; |
1744 | 3.69k | return result; |
1745 | 27.9k | } |
1746 | | |
1747 | | /************************** |
1748 | | * produce code to load capture vars out of a buffer for a given source regex |
1749 | | **************************/ |
1750 | 53.1k | CVarDefs unpackCaptureVars(const std::string& strVar, const std::string& bufferVar, const CRegexes& crgxs, size_t regex, const LexicalAnnotation& rootLA) { |
1751 | 53.1k | CVarDefs result; |
1752 | 53.1k | auto cvars = crgxs.captureVarsAt.find(regex); |
1753 | 53.1k | if (cvars == crgxs.captureVarsAt.end()) return result; |
1754 | | |
1755 | 53.1k | for (const auto& vn : cvars->second) { |
1756 | 27.4k | result.push_back(CVarDef(vn, |
1757 | 27.4k | fncall( |
1758 | 27.4k | var("slice", rootLA), |
1759 | 27.4k | list( |
1760 | 27.4k | var(strVar, rootLA), |
1761 | 27.4k | proj(var(bufferVar, rootLA), str::from(regex) + "_begin_" + vn, rootLA), |
1762 | 27.4k | proj(var(bufferVar, rootLA), str::from(regex) + "_end_" + vn, rootLA) |
1763 | 27.4k | ), |
1764 | 27.4k | rootLA |
1765 | 27.4k | ) |
1766 | 27.4k | )); |
1767 | 27.4k | } |
1768 | 53.1k | return result; |
1769 | 53.1k | } |
1770 | | |
1771 | | } |