REAL
Regular Expression Algorithmic Library — constexpr C++20 regex
Loading...
Searching...
No Matches
lazy_dfa.hpp
Go to the documentation of this file.
1
16#ifndef REAL_LAZY_DFA_HPP
17#define REAL_LAZY_DFA_HPP
18
19// Internal — do not include directly.
20// Users: #include <real/real.hpp> (or the documented opt-ins <real/dfa.hpp>, <real/std/regex.hpp>).
21
22#include <algorithm>
23#include <array>
24#include <cstddef>
25#include <cstdint>
26#include <span>
27#include <string>
28#include <string_view>
29#include <vector>
30
31#include "real/core/program.hpp"
33
34namespace real::detail {
35
40 {
41 static bool disabled {false};
42 return disabled;
43 }
44
50 {
51 static bool disabled {false};
52 return disabled;
53 }
54
61 {
62 static bool disabled {false};
63 return disabled;
64 }
65
72 {
73 std::vector<instr> code;
74 std::vector<char_class> classes;
75 bool eligible {true};
76 bool has_assertions {false};
77 bool unicode_word {false};
78 };
79
85 {
86 std::vector<std::pair<utf8_byte_range, std::int32_t>> trans;
87 };
88
90 struct utf8_trie
91 {
92 std::vector<utf8_trie_node> nodes;
93 std::int32_t root {-1};
94 };
95
108 std::span<const code_range> cp_ranges)
109 {
110 std::vector<std::vector<utf8_byte_range>> seqs;
111 for (int b = 0; b < 0x80;) { // ASCII: each contiguous run of set bits is a one-byte sequence
112 if (cc.ascii.test(static_cast<std::uint8_t>(b))) {
113 const int lo {b};
114 while (b < 0x80 && cc.ascii.test(static_cast<std::uint8_t>(b))) {
115 ++b;
116 }
117 seqs.push_back({utf8_byte_range {.lo = static_cast<std::uint8_t>(lo), .hi = static_cast<std::uint8_t>(b - 1)}});
118 }
119 else {
120 ++b;
121 }
122 }
123 for (std::uint32_t k = 0; k < cc.range_count; ++k) { // non-ASCII: canonical byte-range sequences
124 const code_range& r {cp_ranges[cc.range_begin + k]};
125 for (const utf8_byte_seq& s : utf8_range_sequences(r.lo, r.hi)) {
126 std::vector<utf8_byte_range> seq;
127 for (std::size_t j = 0; j < s.length; ++j) {
128 seq.push_back(s.parts[j]);
129 }
130 seqs.push_back(std::move(seq));
131 }
132 }
133
134 utf8_trie trie;
135 if (seqs.empty()) {
136 return trie;
137 }
138 struct builder
139 {
140 std::vector<utf8_trie_node>& nodes;
141 std::vector<std::vector<std::int32_t>>& memo; // hash-cons index: FNV(trans) bucket -> node ids. The
142 // engine headers avoid std::hash / std::unordered_map,
143 // whose out-of-line libc++ symbols (e.g. __hash_memory)
144 // drift across toolchains; an in-house FNV over the
145 // transitions hash-conses with no string key per node.
146
147 static constexpr std::uint64_t hash_trans(const std::vector<std::pair<utf8_byte_range, std::int32_t>>& trans)
148 {
149 std::uint64_t h {1469598103934665603ULL};
150 for (const std::pair<utf8_byte_range, std::int32_t>& t : trans) {
151 h = (h ^ t.first.lo) * 1099511628211ULL;
152 h = (h ^ t.first.hi) * 1099511628211ULL;
153 h = (h ^ static_cast<std::uint32_t>(t.second)) * 1099511628211ULL;
154 }
155 return h;
156 }
157
158 static bool trans_equal(const std::vector<std::pair<utf8_byte_range, std::int32_t>>& a,
159 const std::vector<std::pair<utf8_byte_range, std::int32_t>>& b)
160 {
161 if (a.size() != b.size()) {
162 return false;
163 }
164 for (std::size_t i = 0; i < a.size(); ++i) {
165 if (a[i].first.lo != b[i].first.lo || a[i].first.hi != b[i].first.hi || a[i].second != b[i].second) {
166 return false;
167 }
168 }
169 return true;
170 }
171
172 std::int32_t build(const std::vector<std::vector<utf8_byte_range>>& in) // all non-empty sequences
173 {
174 std::vector<int> bounds;
175 for (const std::vector<utf8_byte_range>& s : in) {
176 bounds.push_back(s[0].lo);
177 bounds.push_back(s[0].hi + 1);
178 }
179 std::sort(bounds.begin(), bounds.end());
180 bounds.erase(std::unique(bounds.begin(), bounds.end()), bounds.end());
181
182 utf8_trie_node node;
183 for (std::size_t i = 0; i + 1 < bounds.size(); ++i) {
184 const int lo {bounds[i]};
185 const int hi {bounds[i + 1] - 1};
186 std::vector<std::vector<utf8_byte_range>> tails;
187 bool all_empty {true};
188 for (const std::vector<utf8_byte_range>& s : in) {
189 if (s[0].lo <= lo && hi <= s[0].hi) { // this disjoint interval sits inside sequence s's first range
190 std::vector<utf8_byte_range> tail(s.begin() + 1, s.end());
191 all_empty = all_empty && tail.empty();
192 tails.push_back(std::move(tail));
193 }
194 }
195 if (tails.empty()) {
196 continue;
197 }
198 std::int32_t child {-1};
199 if (!all_empty) { // UTF-8 is prefix-free, so within one interval the tails share a length
200 std::vector<std::vector<utf8_byte_range>> nonempty;
201 for (std::vector<utf8_byte_range>& t : tails) {
202 if (!t.empty()) {
203 nonempty.push_back(std::move(t));
204 }
205 }
206 child = build(nonempty);
207 }
208 node.trans.emplace_back(utf8_byte_range {.lo = static_cast<std::uint8_t>(lo), .hi = static_cast<std::uint8_t>(hi)}, child);
209 }
210
211 std::vector<std::int32_t>& bucket {memo[hash_trans(node.trans) % memo.size()]};
212 for (const std::int32_t existing : bucket) {
213 if (trans_equal(nodes[static_cast<std::size_t>(existing)].trans, node.trans)) {
214 return existing; // an identical suffix sub-trie already exists (Daciuk sharing)
215 }
216 }
217 const auto id {static_cast<std::int32_t>(nodes.size())};
218 nodes.push_back(std::move(node));
219 bucket.push_back(id);
220 return id;
221 }
222 };
223 constexpr std::size_t trie_memo_buckets {1024}; // chained buckets, sized for the bounded trie
224 std::vector<std::vector<std::int32_t>> memo(trie_memo_buckets);
225 builder b {trie.nodes, memo};
226 trie.root = b.build(seqs);
227 return trie;
228 }
229
232 inline std::size_t utf8_trie_emit_size(const utf8_trie& trie)
233 {
234 if (trie.root < 0) {
235 return 1;
236 }
237 std::size_t n {0};
238 for (const utf8_trie_node& node : trie.nodes) {
239 n += (3 * node.trans.size()) - 1;
240 }
241 return n;
242 }
243
248 const utf8_trie& trie,
249 std::int32_t after)
250 {
251 if (trie.root < 0) {
252 bp.code.push_back({.op = opcode::klass, .arg16 = static_cast<std::uint16_t>(bp.classes.size())});
253 bp.classes.emplace_back(); // matches no byte: the run dies (an empty class matches nothing)
254 return;
255 }
256 const auto base {static_cast<std::int32_t>(bp.code.size())};
257 std::vector<std::int32_t> order {trie.root}; // root first, so the entry pc is `base`
258 for (std::int32_t i = 0; i < static_cast<std::int32_t>(trie.nodes.size()); ++i) {
259 if (i != trie.root) {
260 order.push_back(i);
261 }
262 }
263 std::vector<std::int32_t> node_pc(trie.nodes.size(), 0);
264 std::int32_t off {base};
265 for (const std::int32_t id : order) {
266 node_pc[static_cast<std::size_t>(id)] = off;
267 off += (3 * static_cast<std::int32_t>(trie.nodes[static_cast<std::size_t>(id)].trans.size())) - 1;
268 }
269 for (const std::int32_t id : order) {
270 const utf8_trie_node& node {trie.nodes[static_cast<std::size_t>(id)]};
271 const auto k {static_cast<std::int32_t>(node.trans.size())};
272 for (std::int32_t j = 0; j < k; ++j) {
273 const auto& edge {node.trans[static_cast<std::size_t>(j)]};
274 const std::int32_t target {edge.second < 0 ? after : node_pc[static_cast<std::size_t>(edge.second)]};
275 const auto here {static_cast<std::int32_t>(bp.code.size())};
276 if (j + 1 < k) {
277 bp.code.push_back({.op = opcode::split, .primary_target = here + 1, .secondary_target = here + 3});
278 }
279 char_class cc;
280 cc.set_range(edge.first.lo, edge.first.hi);
281 bp.code.push_back({.op = opcode::klass, .arg16 = static_cast<std::uint16_t>(bp.classes.size())});
282 bp.classes.push_back(cc);
283 bp.code.push_back({.op = opcode::jump, .primary_target = target});
284 }
285 }
286 }
287
296 bool keep_assertions = false)
297 {
298 byte_program bp;
299 bp.unicode_word = prog.unicode_word;
300 for (const instr& in : prog.code) {
301 if (in.op == opcode::assert_lookaround) {
302 bp.eligible = false; // no byte automaton can carry a bounded lookaround (Tier-B stops at assertions)
303 return bp;
304 }
305 if (in.op == opcode::assert_position) {
306 if (!keep_assertions || in.arg16 != 0) {
307 bp.eligible = false; // Tier-A declines any assertion; Tier-B declines a word-ness-flipped one (scoped (?a:))
308 return bp;
309 }
310 bp.has_assertions = true; // Tier-B: kept, to become an edge condition in the one-pass table
311 }
312 }
313 bp.classes.assign(prog.classes.begin(), prog.classes.end()); // original classes keep their indices
314
315 const std::size_t n {prog.code.size()};
316 std::vector<std::int32_t> map(n + 1, 0); // old pc -> new pc (n = the one-past end)
317 std::vector<utf8_trie> tries(n); // the trie for each klass_cp pc (built once, reused when emitting)
318 std::size_t cur {0};
319 for (std::size_t pc = 0; pc < n; ++pc) {
320 map[pc] = static_cast<std::int32_t>(cur);
321 if (prog.code[pc].op == opcode::klass_cp) {
322 tries[pc] = build_utf8_trie(prog.cp_classes[prog.code[pc].arg16], prog.cp_ranges);
323 cur += utf8_trie_emit_size(tries[pc]);
324 map[pc + 1] = map[pc + 2] = map[pc + 3] = static_cast<std::int32_t>(cur); // continuation slots absorbed
325 pc += 3; // skip the construct's tail
326 }
327 else {
328 ++cur;
329 }
330 }
331 map[n] = static_cast<std::int32_t>(cur);
332
333 const auto remap {[&](std::int32_t t) {
334 return (t >= 0 && static_cast<std::size_t>(t) <= n) ? map[static_cast<std::size_t>(t)] : t;
335 }};
336 for (std::size_t pc = 0; pc < n; ++pc) {
337 const instr& in {prog.code[pc]};
338 if (in.op == opcode::klass_cp) {
339 emit_utf8_trie(bp, tries[pc], map[pc + 4]);
340 pc += 3;
341 }
342 else {
343 instr out {in};
344 out.primary_target = remap(in.primary_target);
345 out.secondary_target = remap(in.secondary_target);
346 bp.code.push_back(out);
347 }
348 }
349 return bp;
350 }
351
358 {
359 std::array<std::uint8_t, 256> of {};
360 std::uint16_t count {0};
361 };
362
365 inline constexpr lazy_byte_alphabet compute_lazy_alphabet(std::span<const instr> code,
366 std::span<const char_class> classes)
367 {
368 std::vector<char_class> class_preds;
369 std::vector<std::uint16_t> literal_preds;
370 const auto push_unique {[](auto& vec, const auto& value) {
371 for (const auto& existing : vec) {
372 if (existing == value) {
373 return;
374 }
375 }
376 vec.push_back(value);
377 }};
378 for (const instr& in : code) {
379 if (in.op == opcode::klass && in.arg16 < classes.size()) {
380 push_unique(class_preds, classes[in.arg16]);
381 }
382 else if (in.op == opcode::byte) {
383 push_unique(literal_preds, static_cast<std::uint16_t>(in.arg8));
384 }
385 }
386 const auto sig_equal {[&](unsigned a, unsigned b) {
387 for (const char_class& cc : class_preds) {
388 if (cc.test(static_cast<std::uint8_t>(a)) != cc.test(static_cast<std::uint8_t>(b))) {
389 return false;
390 }
391 }
392 for (const std::uint16_t lit : literal_preds) {
393 if ((a == lit) != (b == lit)) {
394 return false;
395 }
396 }
397 return true;
398 }};
399 lazy_byte_alphabet alpha;
400 std::array<std::uint8_t, 256> rep {};
401 for (unsigned b = 0; b < 256U; ++b) {
402 bool assigned {false};
403 for (std::uint16_t c = 0; c < alpha.count; ++c) {
404 if (sig_equal(b, rep[c])) {
405 alpha.of[b] = static_cast<std::uint8_t>(c);
406 assigned = true;
407 break;
408 }
409 }
410 if (!assigned) {
411 rep[alpha.count] = static_cast<std::uint8_t>(b);
412 alpha.of[b] = static_cast<std::uint8_t>(alpha.count);
413 ++alpha.count;
414 }
415 }
416 return alpha;
417 }
418
426 {
427 static constexpr std::size_t bucket_count {2048};
428 static constexpr std::uint32_t not_found {0xFFFFFFFFU};
429
430 std::vector<std::vector<std::uint32_t>> buckets;
431
432 constexpr pc_set_cache()
434 {}
435
436 static constexpr std::size_t hash(const std::vector<std::int32_t>& v)
437 {
438 // FNV-1a computed in a fixed 64-bit accumulator, truncated to size_t on return: the 64-bit
439 // offset-basis brace-initialised straight into size_t narrows (an error) where size_t is 32-bit
440 // (win32). Truncating a full FNV-64 keeps a well-distributed hash without width-specific constants.
441 std::uint64_t h {1469598103934665603ULL};
442 for (const std::int32_t x : v) {
443 h = (h ^ static_cast<std::uint64_t>(static_cast<std::uint32_t>(x))) * 1099511628211ULL;
444 }
445 return static_cast<std::size_t>(h);
446 }
447
448 [[nodiscard]] constexpr std::uint32_t find(const std::vector<std::int32_t>& pcs,
449 const std::vector<std::vector<std::int32_t>>& state_pcs) const
450 {
451 for (const std::uint32_t id : buckets[hash(pcs) % bucket_count]) {
452 if (state_pcs[id] == pcs) {
453 return id;
454 }
455 }
456 return not_found;
457 }
458
459 constexpr void insert(const std::vector<std::int32_t>& pcs,
460 std::uint32_t id)
461 {
462 buckets[hash(pcs) % bucket_count].push_back(id);
463 }
464
465 constexpr void clear()
466 {
467 for (std::vector<std::uint32_t>& b : buckets) {
468 b.clear();
469 }
470 }
471 };
472
490 {
491 public:
492
493 static constexpr std::uint32_t dead_state {0};
494 static constexpr std::uint32_t no_transition {0xFFFFFFFFU};
495 static constexpr std::uint32_t no_match_idx {0xFFFFFFFFU};
496 static constexpr std::size_t state_budget {4096};
497 static constexpr std::size_t thrash_flushes {2};
498
500 struct counters
501 {
502 std::size_t hits {0};
503 std::size_t misses {0};
504 std::size_t flushes {0};
505 std::size_t scan_flushes {0};
506 };
507
518 explicit constexpr lazy_dfa(std::span<const instr> code,
519 std::span<const char_class> classes,
520 std::size_t budget = state_budget,
521 const lazy_byte_alphabet* shared_alpha = nullptr)
522 : code_ {code}, classes_ {classes},
523 alpha_ {shared_alpha != nullptr ? *shared_alpha : compute_lazy_alphabet(code, classes)},
524 eligible_ {compute_eligibility(code)}, budget_ {budget}
525 {
526 flush(); // seeds the dead state (0) and the start state (1)
527 }
528
529 [[nodiscard]] bool eligible() const
530 {
531 return eligible_;
532 }
533
534 [[nodiscard]] std::uint16_t num_classes() const
535 {
536 return alpha_.count;
537 }
538
539 [[nodiscard]] std::uint32_t start_state() const
540 {
541 return start_state_;
542 }
543
547 {
549 thrashing_ = false;
550 }
551
552 [[nodiscard]] bool thrashing() const
553 {
554 return thrashing_;
555 }
556
557 [[nodiscard]] const counters& stats() const
558 {
559 return stats_;
560 }
561
573 [[nodiscard]] std::size_t forward_end(std::string_view text)
574 {
575 if (!eligible_) {
576 return npos;
577 }
578 begin_scan();
579 std::uint32_t state {start_state_}; // the seed at position 0 (a re-seeding state)
580 std::size_t best_end {npos};
581 bool matched {false};
582 std::size_t pos {0};
583 while (true) {
584 const std::uint32_t midx {state_match_idx_[state]};
585 if (midx != no_match_idx) {
586 best_end = pos; // the highest-priority accept lives at index midx; a higher thread may extend it
587 matched = true;
588 state = cut_cached(state); // drop the accept and every lower-priority thread after it (memoized)
589 if (state == dead_state) {
590 break; // nothing higher-priority survives to extend the match
591 }
592 }
593 if (pos >= text.size() || state == dead_state) {
594 break;
595 }
596 const std::uint8_t byte {static_cast<std::uint8_t>(text[pos])};
597 // pre-match transitions re-seed (unanchored search continues); post-match ones do not (leftmost).
598 state = matched ? step(state, byte) : step_seeded(state, byte);
599 ++pos;
600 }
601 return best_end;
602 }
603
605 [[nodiscard]] bool is_match(std::uint32_t state) const
606 {
607 return state_match_idx_[state] != no_match_idx;
608 }
609
614 std::uint32_t step(std::uint32_t state,
615 std::uint8_t byte)
616 {
617 const std::uint8_t cls {alpha_.of[byte]};
618 const std::uint32_t cached {trans_[(static_cast<std::size_t>(state) * alpha_.count) + cls]}; // by value: intern() below may realloc
619 if (cached != no_transition) {
620 ++stats_.hits;
621 return cached;
622 }
623 ++stats_.misses;
624 std::vector<std::int32_t> next;
625 std::vector<char> seen(code_.size(), 0);
626 for (const std::int32_t pc : state_pcs_[state]) {
627 if (consumes(pc, byte)) {
628 close_into(pc + consumed_width(pc), next, seen);
629 }
630 }
631 const std::size_t flushes_before {stats_.flushes};
632 const std::uint32_t result {intern(next)}; // may grow/flush the tables — do not hold a reference
633 if (stats_.flushes == flushes_before) {
634 trans_[(static_cast<std::size_t>(state) * alpha_.count) + cls] = result; // no flush: `state` is still valid, so cache the edge
635 }
636 // On a flush mid-step the caller's `state` id is stale; `result` is a fresh post-flush id, and the
637 // caller re-seeds. (An eventual forward pass falls back to Pike once \ref thrashing trips.)
638 return result;
639 }
640
641 private:
642
646 std::uint32_t step_seeded(std::uint32_t state,
647 std::uint8_t byte)
648 {
649 const std::uint8_t cls {alpha_.of[byte]};
650 const std::uint32_t cached {trans_seeded_[(static_cast<std::size_t>(state) * alpha_.count) + cls]};
651 if (cached != no_transition) {
652 ++stats_.hits;
653 return cached;
654 }
655 ++stats_.misses;
656 const std::vector<std::int32_t> pcs {state_pcs_[state]}; // copy: intern() below may realloc state_pcs_
657 std::vector<std::int32_t> next;
658 std::vector<char> seen(code_.size(), 0);
659 for (const std::int32_t pc : pcs) {
660 if (consumes(pc, byte)) {
661 close_into(pc + consumed_width(pc), next, seen);
662 }
663 }
664 close_into(0, next, seen); // re-seed at the lowest priority (deduped against the advanced threads)
665 const std::size_t flushes_before {stats_.flushes};
666 const std::uint32_t result {intern(next)};
667 if (stats_.flushes == flushes_before) {
668 trans_seeded_[(static_cast<std::size_t>(state) * alpha_.count) + cls] = result;
669 }
670 return result;
671 }
672
675 std::uint32_t cut(std::uint32_t state,
676 std::uint32_t m)
677 {
678 // Copy the prefix element-by-element before interning, which may reallocate state_pcs_ (the intern
679 // dangling-ref trap). A loop rather than an iterator-range copy — the latter trips a g++ false
680 // -Werror=free-nonheap-object here.
681 std::vector<std::int32_t> prefix;
682 prefix.reserve(m);
683 for (std::uint32_t i = 0; i < m; ++i) {
684 prefix.push_back(state_pcs_[state][i]);
685 }
686 return intern(prefix);
687 }
688
693 std::uint32_t cut_cached(std::uint32_t state)
694 {
695 const std::uint32_t memo {state_cut_[state]};
696 if (memo != no_transition) {
697 return memo;
698 }
699 const std::size_t flushes_before {stats_.flushes};
700 const std::uint32_t result {cut(state, state_match_idx_[state])}; // intern may flush/realloc
701 if (stats_.flushes == flushes_before) {
702 state_cut_[state] = result; // no flush: `state` is still valid, memoise the edge
703 }
704 return result;
705 }
706
707 static constexpr bool compute_eligibility(std::span<const instr> code)
708 {
709 for (const instr& in : code) {
711 || in.op == opcode::klass_cp) {
712 return false; // position assertions / variable-width classes: no forward-DFA representation
713 }
714 }
715 return true;
716 }
717
718 [[nodiscard]] bool consumes(std::int32_t pc,
719 std::uint8_t byte) const
720 {
721 const instr& in {code_[static_cast<std::size_t>(pc)]};
722 if (in.op == opcode::byte) {
723 return static_cast<std::uint8_t>(in.arg8) == byte;
724 }
725 if (in.op == opcode::klass) {
726 return classes_[in.arg16].test(byte);
727 }
728 return false; // match / anything else: not a consuming edge
729 }
730
731 [[nodiscard]] static std::int32_t consumed_width(std::int32_t /*pc*/)
732 {
733 return 1;
734 }
735
738 constexpr void close_into(std::int32_t pc,
739 std::vector<std::int32_t>& out,
740 std::vector<char>& seen) const
741 {
742 if (pc < 0 || static_cast<std::size_t>(pc) >= code_.size()) {
743 return;
744 }
745 std::vector<std::int32_t> stack {pc};
746 while (!stack.empty()) {
747 const std::int32_t cur {stack.back()};
748 stack.pop_back();
749 if (cur < 0 || static_cast<std::size_t>(cur) >= code_.size() || seen[static_cast<std::size_t>(cur)] != 0) {
750 continue;
751 }
752 seen[static_cast<std::size_t>(cur)] = 1;
753 const instr& in {code_[static_cast<std::size_t>(cur)]};
754 switch (in.op) {
755 case opcode::byte:
756 case opcode::klass:
757 case opcode::match:
758 out.push_back(cur);
759 break;
760 case opcode::split:
761 stack.push_back(in.secondary_target); // secondary pushed first -> primary explored first
762 stack.push_back(in.primary_target);
763 break;
764 case opcode::jump:
765 stack.push_back(in.primary_target);
766 break;
767 case opcode::save:
768 stack.push_back(cur + 1);
769 break;
770 default:
771 break; // assert / klass_cp / lookaround: only reachable for ineligible programs (not built)
772 }
773 }
774 }
775
777 constexpr std::uint32_t intern(const std::vector<std::int32_t>& pcs)
778 {
779 if (pcs.empty()) {
780 return dead_state;
781 }
782 const std::uint32_t found {cache_.find(pcs, state_pcs_)};
783 if (found != pc_set_cache::not_found) {
784 return found;
785 }
786 if (state_pcs_.size() >= budget_) {
787 flush();
788 return intern_fresh(pcs); // rebuild from empty; the seeded start remains reachable
789 }
790 return intern_fresh(pcs);
791 }
792
793 constexpr std::uint32_t intern_fresh(const std::vector<std::int32_t>& pcs)
794 {
795 const auto id {static_cast<std::uint32_t>(state_pcs_.size())};
796 state_pcs_.push_back(pcs);
797 trans_.insert(trans_.end(), alpha_.count, no_transition);
799 std::uint32_t match_idx {no_match_idx};
800 for (std::size_t i = 0; i < pcs.size(); ++i) {
801 if (code_[static_cast<std::size_t>(pcs[i])].op == opcode::match) {
802 match_idx = static_cast<std::uint32_t>(i); // the highest-priority accept in this state
803 break;
804 }
805 }
806 state_match_idx_.push_back(match_idx);
807 state_cut_.push_back(no_transition); // the priority-cut result, memoized lazily on first use
808 cache_.insert(pcs, id);
809 return id;
810 }
811
813 constexpr void flush()
814 {
815 const bool first {state_pcs_.empty()};
816 if (!first) {
817 ++stats_.flushes;
820 thrashing_ = true;
821 }
822 }
823 state_pcs_.clear();
824 trans_.clear();
825 trans_seeded_.clear();
826 state_match_idx_.clear();
827 state_cut_.clear();
828 cache_.clear();
829 // state 0 = dead (empty, self-looping), state 1 = start (closure of pc 0).
830 state_pcs_.emplace_back();
831 trans_.insert(trans_.end(), alpha_.count, dead_state);
834 state_cut_.push_back(no_transition);
835 std::vector<std::int32_t> start;
836 std::vector<char> seen(code_.size(), 0);
837 close_into(0, start, seen);
838 start_state_ = intern_fresh(start);
839 }
840
841 std::span<const instr> code_;
842 std::span<const char_class> classes_;
844 bool eligible_ {false};
845 std::uint32_t start_state_ {0};
846
847 std::vector<std::vector<std::int32_t>> state_pcs_;
848 std::vector<std::uint32_t> trans_;
849 std::vector<std::uint32_t> trans_seeded_;
850 std::vector<std::uint32_t> state_match_idx_;
851 std::vector<std::uint32_t> state_cut_;
853
854 std::size_t budget_ {state_budget};
856 bool thrashing_ {false};
857 };
858
868 {
869 public:
870
871 static constexpr std::uint32_t dead_state {0};
872 static constexpr std::uint32_t no_transition {0xFFFFFFFFU};
873 static constexpr std::size_t state_budget {4096};
874
875 explicit constexpr reverse_dfa(std::span<const instr> code,
876 std::span<const char_class> classes,
877 std::size_t budget = state_budget,
878 const lazy_byte_alphabet* shared_alpha = nullptr)
879 : code_ {code}, classes_ {classes},
880 alpha_ {shared_alpha != nullptr ? *shared_alpha : compute_lazy_alphabet(code, classes)},
881 eligible_ {compute_eligibility(code)}, budget_ {budget}
882 {
883 // Transpose the program: rev_eps_[x] = the pcs with a forward epsilon edge to x; rev_consume_[x] = the
884 // consuming pcs whose successor is x (a byte/klass at pc goes to pc+1).
885 rev_eps_.assign(code.size(), {});
886 rev_consume_.assign(code.size(), {});
887 for (std::int32_t pc = 0; pc < static_cast<std::int32_t>(code.size()); ++pc) {
888 const instr& in {code[static_cast<std::size_t>(pc)]};
889 switch (in.op) {
890 case opcode::byte:
891 case opcode::klass:
892 rev_consume_[static_cast<std::size_t>(pc) + 1].push_back(pc);
893 break;
894 case opcode::split:
895 rev_eps_[static_cast<std::size_t>(in.primary_target)].push_back(pc);
896 rev_eps_[static_cast<std::size_t>(in.secondary_target)].push_back(pc);
897 break;
898 case opcode::jump:
899 rev_eps_[static_cast<std::size_t>(in.primary_target)].push_back(pc);
900 break;
901 case opcode::save:
902 rev_eps_[static_cast<std::size_t>(pc) + 1].push_back(pc);
903 break;
904 case opcode::match:
905 match_pc_ = pc; // the reverse start
906 break;
907 default:
908 break;
909 }
910 }
911 flush();
912 }
913
914 [[nodiscard]] bool eligible() const
915 {
916 return eligible_;
917 }
918
924 [[nodiscard]] std::size_t reverse_start(std::string_view text,
925 std::size_t e,
926 std::size_t resume)
927 {
928 std::uint32_t state {start_state_}; // rev-closure of the forward `match`
929 std::size_t best {npos};
930 std::size_t pos {e};
931 while (true) {
932 if (state_has_start_[state] != 0) {
933 best = pos; // reached the original start: [pos, e] matches; kLongest keeps the smallest pos
934 }
935 if (pos <= resume || state == dead_state) {
936 break;
937 }
938 --pos;
939 state = step(state, static_cast<std::uint8_t>(text[pos]));
940 }
941 return best;
942 }
943
944 private:
945
946 constexpr void rev_closure(std::vector<std::int32_t>& set,
947 std::vector<char>& seen) const
948 {
949 std::vector<std::int32_t> stack {set};
950 while (!stack.empty()) {
951 const std::int32_t pc {stack.back()};
952 stack.pop_back();
953 for (const std::int32_t pred : rev_eps_[static_cast<std::size_t>(pc)]) {
954 if (seen[static_cast<std::size_t>(pred)] == 0) {
955 seen[static_cast<std::size_t>(pred)] = 1;
956 set.push_back(pred);
957 stack.push_back(pred);
958 }
959 }
960 }
961 std::sort(set.begin(), set.end()); // unordered: a canonical (sorted) key, no priority
962 }
963
964 std::uint32_t step(std::uint32_t state,
965 std::uint8_t byte)
966 {
967 const std::uint8_t cls {alpha_.of[byte]};
968 const std::uint32_t cached {trans_[(static_cast<std::size_t>(state) * alpha_.count) + cls]};
969 if (cached != no_transition) {
970 return cached;
971 }
972 const std::vector<std::int32_t> pcs {state_pcs_[state]}; // copy: intern may realloc
973 std::vector<std::int32_t> next;
974 std::vector<char> seen(code_.size(), 0);
975 for (const std::int32_t pc : pcs) {
976 for (const std::int32_t pred : rev_consume_[static_cast<std::size_t>(pc)]) {
977 if (consumes(pred, byte) && seen[static_cast<std::size_t>(pred)] == 0) {
978 seen[static_cast<std::size_t>(pred)] = 1;
979 next.push_back(pred);
980 }
981 }
982 }
983 rev_closure(next, seen);
984 const std::uint32_t result {intern(next)};
985 trans_[(static_cast<std::size_t>(state) * alpha_.count) + cls] = result;
986 return result;
987 }
988
989 [[nodiscard]] bool consumes(std::int32_t pc,
990 std::uint8_t byte) const
991 {
992 const instr& in {code_[static_cast<std::size_t>(pc)]};
993 if (in.op == opcode::byte) {
994 return static_cast<std::uint8_t>(in.arg8) == byte;
995 }
996 return in.op == opcode::klass && classes_[in.arg16].test(byte);
997 }
998
999 static constexpr bool compute_eligibility(std::span<const instr> code)
1000 {
1001 for (const instr& in : code) {
1002 if (in.op == opcode::assert_position || in.op == opcode::assert_lookaround
1003 || in.op == opcode::klass_cp) {
1004 return false;
1005 }
1006 }
1007 return true;
1008 }
1009
1010 constexpr std::uint32_t intern(const std::vector<std::int32_t>& pcs)
1011 {
1012 if (pcs.empty()) {
1013 return dead_state;
1014 }
1015 const std::uint32_t found {cache_.find(pcs, state_pcs_)};
1016 if (found != pc_set_cache::not_found) {
1017 return found;
1018 }
1019 if (state_pcs_.size() >= budget_) {
1020 flush();
1021 }
1022 const auto id {static_cast<std::uint32_t>(state_pcs_.size())};
1023 state_pcs_.push_back(pcs);
1024 trans_.insert(trans_.end(), alpha_.count, no_transition);
1025 bool has_start {false};
1026 for (const std::int32_t pc : pcs) {
1027 if (pc == 0) { // pc 0 is the program's save-0 start
1028 has_start = true;
1029 break;
1030 }
1031 }
1032 state_has_start_.push_back(has_start ? 1 : 0);
1033 cache_.insert(pcs, id);
1034 return id;
1035 }
1036
1037 constexpr void flush()
1038 {
1039 state_pcs_.clear();
1040 trans_.clear();
1041 state_has_start_.clear();
1042 cache_.clear();
1043 state_pcs_.emplace_back(); // dead state 0
1044 trans_.insert(trans_.end(), alpha_.count, dead_state);
1045 state_has_start_.push_back(0);
1046 std::vector<std::int32_t> start;
1047 std::vector<char> seen(code_.size(), 0);
1048 if (match_pc_ >= 0) {
1049 seen[static_cast<std::size_t>(match_pc_)] = 1;
1050 start.push_back(match_pc_);
1051 rev_closure(start, seen);
1052 }
1053 start_state_ = intern(start);
1054 }
1055
1056 std::span<const instr> code_;
1057 std::span<const char_class> classes_;
1059 bool eligible_ {false};
1060 std::int32_t match_pc_ {-1};
1061 std::uint32_t start_state_ {0};
1062 std::size_t budget_ {state_budget};
1063 std::vector<std::vector<std::int32_t>> rev_eps_;
1064 std::vector<std::vector<std::int32_t>> rev_consume_;
1065 std::vector<std::vector<std::int32_t>> state_pcs_;
1066 std::vector<std::uint32_t> trans_;
1067 std::vector<char> state_has_start_;
1069 };
1070} // namespace real::detail
1071
1072#endif // REAL_LAZY_DFA_HPP
A lazy priority-preserving forward DFA over a Pike program (the kFirstMatch forward pass).
Definition lazy_dfa.hpp:490
std::span< const char_class > classes_
Definition lazy_dfa.hpp:842
std::vector< std::uint32_t > state_cut_
state id -> memoized priority-cut result (no_transition = not yet computed).
Definition lazy_dfa.hpp:851
static constexpr bool compute_eligibility(std::span< const instr > code)
Definition lazy_dfa.hpp:707
static constexpr std::uint32_t no_transition
A not-yet-computed cached transition.
Definition lazy_dfa.hpp:494
constexpr std::uint32_t intern_fresh(const std::vector< std::int32_t > &pcs)
Definition lazy_dfa.hpp:793
std::uint32_t step(std::uint32_t state, std::uint8_t byte)
Transition state on byte to the next DFA state, computing and caching it on first use....
Definition lazy_dfa.hpp:614
std::size_t forward_end(std::string_view text)
The end offset of the leftmost-first match in text (kFirstMatch), or real::npos.
Definition lazy_dfa.hpp:573
std::span< const instr > code_
Definition lazy_dfa.hpp:841
std::uint32_t cut(std::uint32_t state, std::uint32_t m)
The priority-cut at an accept: intern the prefix of state's ordered pc-set before index m (dropping t...
Definition lazy_dfa.hpp:675
static constexpr std::size_t state_budget
Cached states before a flush (the memory cap).
Definition lazy_dfa.hpp:496
std::uint32_t cut_cached(std::uint32_t state)
The priority-cut of state at its own accept, memoized. cut is O(state size) — it rebuilds and re-inte...
Definition lazy_dfa.hpp:693
std::uint32_t start_state_
Definition lazy_dfa.hpp:845
std::vector< std::vector< std::int32_t > > state_pcs_
state id -> ordered pc-set.
Definition lazy_dfa.hpp:847
static std::int32_t consumed_width(std::int32_t)
Definition lazy_dfa.hpp:731
bool thrashing() const
Definition lazy_dfa.hpp:552
std::vector< std::uint32_t > trans_
flat [state*stride + class] -> next, unseeded (post-match); stride = alpha_.count.
Definition lazy_dfa.hpp:848
bool eligible() const
Definition lazy_dfa.hpp:529
void begin_scan()
Begin a search: clear the per-scan flush counter and the thrash flag. (No cache flush — the states ca...
Definition lazy_dfa.hpp:546
bool is_match(std::uint32_t state) const
Whether state accepts here (its ordered set contains a match PC).
Definition lazy_dfa.hpp:605
constexpr void flush()
Empty the cache back to the dead + start states (the eviction: bounded memory).
Definition lazy_dfa.hpp:813
constexpr std::uint32_t intern(const std::vector< std::int32_t > &pcs)
Intern an ordered pc-set into a state id (cached). Flushes the cache when the budget is hit.
Definition lazy_dfa.hpp:777
lazy_byte_alphabet alpha_
Definition lazy_dfa.hpp:843
std::uint32_t step_seeded(std::uint32_t state, std::uint8_t byte)
Like step, but re-seeds: the unanchored-search variant appends pc 0's closure at the lowest priority,...
Definition lazy_dfa.hpp:646
static constexpr std::uint32_t no_match_idx
A state whose ordered set holds no accept.
Definition lazy_dfa.hpp:495
constexpr void close_into(std::int32_t pc, std::vector< std::int32_t > &out, std::vector< char > &seen) const
Append the ordered epsilon-closure of pc to out (split priority; save/jump crossed),...
Definition lazy_dfa.hpp:738
bool consumes(std::int32_t pc, std::uint8_t byte) const
Definition lazy_dfa.hpp:718
std::vector< std::uint32_t > trans_seeded_
flat [state*stride + class] -> next, re-seeding (pre-match).
Definition lazy_dfa.hpp:849
std::uint16_t num_classes() const
Definition lazy_dfa.hpp:534
std::vector< std::uint32_t > state_match_idx_
state id -> index of its first accept, or no_match_idx.
Definition lazy_dfa.hpp:850
std::uint32_t start_state() const
Definition lazy_dfa.hpp:539
const counters & stats() const
Definition lazy_dfa.hpp:557
static constexpr std::size_t thrash_flushes
Flushes within one scan that trip thrashing.
Definition lazy_dfa.hpp:497
constexpr lazy_dfa(std::span< const instr > code, std::span< const char_class > classes, std::size_t budget=state_budget, const lazy_byte_alphabet *shared_alpha=nullptr)
Builds the (initially empty) lazy DFA over a Pike program.
Definition lazy_dfa.hpp:518
static constexpr std::uint32_t dead_state
The empty state: every transition from it stays here.
Definition lazy_dfa.hpp:493
The start-finder companion to lazy_dfa. Given a match end, it finds the leftmost start (the design gu...
Definition lazy_dfa.hpp:868
bool consumes(std::int32_t pc, std::uint8_t byte) const
Definition lazy_dfa.hpp:989
std::vector< std::vector< std::int32_t > > state_pcs_
static constexpr std::uint32_t dead_state
Definition lazy_dfa.hpp:871
std::size_t reverse_start(std::string_view text, std::size_t e, std::size_t resume)
The leftmost start of the match ending at e, not before resume. Scans the text backward from e over t...
Definition lazy_dfa.hpp:924
std::span< const instr > code_
static constexpr bool compute_eligibility(std::span< const instr > code)
Definition lazy_dfa.hpp:999
std::vector< std::vector< std::int32_t > > rev_eps_
transposed epsilon edges.
constexpr reverse_dfa(std::span< const instr > code, std::span< const char_class > classes, std::size_t budget=state_budget, const lazy_byte_alphabet *shared_alpha=nullptr)
Definition lazy_dfa.hpp:875
constexpr std::uint32_t intern(const std::vector< std::int32_t > &pcs)
std::uint32_t step(std::uint32_t state, std::uint8_t byte)
Definition lazy_dfa.hpp:964
lazy_byte_alphabet alpha_
std::vector< std::uint32_t > trans_
flat [state*stride + class] -> next.
static constexpr std::size_t state_budget
Definition lazy_dfa.hpp:873
std::vector< char > state_has_start_
state -> reaches the program start (an accept).
constexpr void rev_closure(std::vector< std::int32_t > &set, std::vector< char > &seen) const
Definition lazy_dfa.hpp:946
static constexpr std::uint32_t no_transition
Definition lazy_dfa.hpp:872
constexpr void flush()
std::span< const char_class > classes_
std::vector< std::vector< std::int32_t > > rev_consume_
transposed consuming edges (the pred consuming pcs).
constexpr std::vector< utf8_byte_seq > utf8_range_sequences(std::uint32_t lo, std::uint32_t hi)
Canonical UTF-8 byte-range sequences for the code-point range [lo, hi], excluding the surrogate block...
std::size_t utf8_trie_emit_size(const utf8_trie &trie)
The instruction count emit_utf8_trie writes: an empty class is one dead klass; otherwise each node is...
Definition lazy_dfa.hpp:232
bool & lazy_dfa_route_disabled()
Test seam: force the matcher off the lazy-DFA route onto the pure Pike VM, so a differential can asse...
Definition lazy_dfa.hpp:39
void emit_utf8_trie(byte_program &bp, const utf8_trie &trie, std::int32_t after)
Emits trie into bp as a deterministic split/klass/jump fragment; accept edges jump to after (the cons...
Definition lazy_dfa.hpp:247
bool & inner_literal_guard_disabled()
Test seam: force the inner-literal small-haystack guard off, so the route fires on any size....
Definition lazy_dfa.hpp:60
bool & inner_literal_route_disabled()
Test seam: force the matcher off the inner-literal search route (IL.2) onto the core search,...
Definition lazy_dfa.hpp:49
utf8_trie build_utf8_trie(const cp_class &cc, std::span< const code_range > cp_ranges)
Builds the minimal deterministic trie recognising a code-point class's UTF-8 byte sequences.
Definition lazy_dfa.hpp:107
@ prefix
Anchored at the start position (Python re.match).
constexpr lazy_byte_alphabet compute_lazy_alphabet(std::span< const instr > code, std::span< const char_class > classes)
Partition 0..255 by the program's consuming predicates (every klass test, every byte literal)....
Definition lazy_dfa.hpp:365
byte_program build_byte_program(const program_view &prog, bool keep_assertions=false)
Builds the byte-level DFA program for prog (see byte_program). A klass_cp at P (a four- instruction c...
Definition lazy_dfa.hpp:295
@ klass_cp
Consume one code point tested against cp_classes[arg16] (decode + range bsearch); enters a 3-instr co...
@ byte
Consume one byte equal to arg8; fall through to pc+1.
@ save
Store current position in slot arg16; fall through (epsilon).
@ klass
Consume one byte in classes[arg16]; fall through to pc+1.
@ assert_lookaround
Epsilon; proceeds only if the lookaround sub-program arg16 holds here.
@ assert_position
Epsilon; proceeds only if assertion arg8 holds here.
@ jump
Epsilon-jump to x.
@ split
Epsilon-branch to x (preferred) and y.
constexpr std::size_t npos
Sentinel for "no position" / unset capture slot (akin to std::string::npos).
Definition program.hpp:33
@ first
Leftmost-first (Perl / Python re / the crate): source-order thread priority decides....
Compiled form of a pattern and the public flags / error types.
A byte-level program derived from a Pike program for the DFA passes: every klass_cp construct is expa...
Definition lazy_dfa.hpp:72
bool eligible
Representable by the byte DFAs / Tier-A one-pass.
Definition lazy_dfa.hpp:75
bool has_assertions
A Tier-B build kept assert_position ops (else stripped/declined).
Definition lazy_dfa.hpp:76
std::vector< char_class > classes
Definition lazy_dfa.hpp:74
bool unicode_word
Program default word-ness for \b \B < > (Tier-B edge conditions).
Definition lazy_dfa.hpp:77
std::vector< instr > code
Definition lazy_dfa.hpp:73
A set of byte values (0–255) as a 256-bit bitmap.
Definition charclass.hpp:30
constexpr void set_range(std::uint8_t low, std::uint8_t high)
Adds the inclusive byte range [low, high] to the set.
Definition charclass.hpp:48
constexpr bool test(std::uint8_t byte) const
Tests membership of byte byte.
An inclusive code-point range [lo, hi]. Shared by character classes (ast.hpp) and the generated Unico...
Definition program.hpp:168
A match-time code-point class for the klass_cp opcode: an ASCII bitmap for code points < 0x80 plus a ...
Definition program.hpp:179
std::uint32_t range_count
Number of ranges belonging to this class.
Definition program.hpp:182
std::uint32_t range_begin
First range in the program's cp_ranges buffer.
Definition program.hpp:181
char_class ascii
Members < 0x80.
Definition program.hpp:180
One NFA instruction. Field meaning depends on op.
Definition program.hpp:251
opcode op
The operation.
Definition program.hpp:252
std::int32_t primary_target
Primary branch target (split/jump).
Definition program.hpp:255
Byte-class alphabet over a Pike program: bytes that satisfy exactly the same byte/klass predicates sh...
Definition lazy_dfa.hpp:358
std::uint16_t count
number of distinct classes.
Definition lazy_dfa.hpp:360
std::array< std::uint8_t, 256 > of
byte -> class index.
Definition lazy_dfa.hpp:359
Cache-behaviour counters, for the policy tests and later tuning.
Definition lazy_dfa.hpp:501
std::size_t scan_flushes
flushes in the current scan (reset by begin_scan).
Definition lazy_dfa.hpp:505
A tiny open-chaining hash set of interned PC-set state ids, keyed by their pc-set....
Definition lazy_dfa.hpp:426
constexpr void clear()
Definition lazy_dfa.hpp:465
std::vector< std::vector< std::uint32_t > > buckets
Definition lazy_dfa.hpp:430
constexpr std::uint32_t find(const std::vector< std::int32_t > &pcs, const std::vector< std::vector< std::int32_t > > &state_pcs) const
Definition lazy_dfa.hpp:448
static constexpr std::size_t hash(const std::vector< std::int32_t > &v)
Definition lazy_dfa.hpp:436
constexpr void insert(const std::vector< std::int32_t > &pcs, std::uint32_t id)
Definition lazy_dfa.hpp:459
static constexpr std::size_t bucket_count
Definition lazy_dfa.hpp:427
static constexpr std::uint32_t not_found
Definition lazy_dfa.hpp:428
std::span< const code_range > cp_ranges
Flat range buffer the cp_class slices index into.
Definition program.hpp:352
bool unicode_word
\b \B < > use Unicode word-ness (text mode, not bytes / re.A).
Definition program.hpp:359
std::span< const char_class > classes
Interned character classes.
Definition program.hpp:348
std::span< const cp_class > cp_classes
Match-time code-point classes (for klass_cp).
Definition program.hpp:351
std::span< const instr > code
The instruction stream (main + lookaround regions).
Definition program.hpp:347
One byte-range step [lo, hi] of a UTF-8 sequence produced by the code-point-range algorithm.
std::uint8_t lo
Low byte (inclusive).
A canonical UTF-8 byte-range sequence (1–4 steps) covering part of a code-point range.
One node of a minimal deterministic UTF-8 trie for a code-point class. Its transitions are byte range...
Definition lazy_dfa.hpp:85
std::vector< std::pair< utf8_byte_range, std::int32_t > > trans
Definition lazy_dfa.hpp:86
A minimal deterministic UTF-8 trie for a code-point class. root == -1 means the class is empty.
Definition lazy_dfa.hpp:91
std::vector< utf8_trie_node > nodes
Definition lazy_dfa.hpp:92
Code-point range → canonical UTF-8 byte-range sequences (RE2 / rust regex-syntax Utf8Sequences).