REAL
Regular Expression Algorithmic Library — constexpr C++20 regex
Loading...
Searching...
No Matches
pike.hpp
Go to the documentation of this file.
1
14#ifndef REAL_PIKE_HPP
15#define REAL_PIKE_HPP
16
17// Internal — do not include directly.
18// Users: #include <real/real.hpp> (or the documented opt-ins <real/dfa.hpp>, <real/std/regex.hpp>).
19
20#include "real/version.hpp"
21
22#include <array>
23#include <cassert>
24#include <cstdint>
25#include <string_view>
26#include <type_traits>
27#include <utility>
28#include <vector>
29
32#include <mutex>
33#include <optional>
34
37#include "real/core/program.hpp"
39#include "real/unicode/utf8.hpp"
40
41namespace real::detail {
42
46 enum class run_mode : std::uint8_t
47 {
48 prefix,
49 full,
50 search,
51 };
52
58 struct eps_entry
59 {
60 std::int32_t pc;
61 std::uint32_t block;
62 };
63
77 template <typename DataVec, typename RefVec, typename FreeVec>
79 {
80 DataVec data;
81 RefVec refcount;
82 FreeVec free_list;
83 std::uint16_t width {0};
84
85 static constexpr std::uint32_t npos_block {0};
86
90 constexpr void reset(std::uint16_t slot_count)
91 {
92 width = slot_count;
93 data.assign(slot_count, npos);
94 refcount.assign(1, 1); // block 0, sentinel refcount 1 (never freed)
95 free_list.clear();
96 }
97
99 [[nodiscard]] constexpr std::size_t* slots(std::uint32_t b)
100 {
101 return &data[static_cast<std::size_t>(b) * width];
102 }
103
105 [[nodiscard]] constexpr std::uint32_t allocate()
106 {
107 if (!free_list.empty()) {
108 const std::uint32_t b {free_list.back()};
109 free_list.pop_back();
110 refcount[b] = 1;
111 return b;
112 }
113 const auto b {static_cast<std::uint32_t>(refcount.size())};
114 refcount.push_back(1);
115 for (std::uint16_t s = 0; s < width; ++s) {
116 data.push_back(npos);
117 }
118 return b;
119 }
120
121 constexpr void incref(std::uint32_t b)
122 {
123 ++refcount[b];
124 }
125
126 constexpr void decref(std::uint32_t b)
127 {
128 if (--refcount[b] == 0) {
129 free_list.push_back(b);
130 }
131 }
132
135 [[nodiscard]] constexpr std::uint32_t cow_write(std::uint32_t b,
136 std::uint16_t slot,
137 std::size_t value)
138 {
139 if (refcount[b] > 1) {
140 const std::uint32_t b2 {allocate()}; // may grow data — recompute both pointers after
141 std::size_t* const dst {slots(b2)};
142 const std::size_t* const src {slots(b)};
143 for (std::uint16_t s = 0; s < width; ++s) {
144 dst[s] = src[s];
145 }
146 dst[slot] = value;
147 decref(b);
148 return b2;
149 }
150 slots(b)[slot] = value;
151 return b;
152 }
153
156 [[nodiscard]] constexpr long long total_refs() const
157 {
158 long long sum {0};
159 for (std::size_t b = 0; b < refcount.size(); ++b) {
160 // free blocks sit at refcount 0; the sentinel block 0 carries its permanent +1.
161 sum += refcount[b] > 0 ? refcount[b] : 0;
162 }
163 return sum;
164 }
165 };
166
169 std::vector<std::int32_t>,
170 std::vector<std::uint32_t>>;
171
181 template <typename PcVec, typename SlotVec, typename MarkVec>
183 {
184 PcVec pcs;
185 SlotVec slots;
186 MarkVec mark;
187 std::uint64_t generation {};
188
193 constexpr void reset(std::size_t code_size)
194 {
195 if (mark.size() != code_size) {
196 mark.assign(code_size, 0);
197 generation = 0;
198 }
199 ++generation;
200 pcs.clear();
201 slots.clear();
202 }
203
209 [[nodiscard]] constexpr bool seen(std::int32_t pc) const
210 {
211 return mark[static_cast<std::size_t>(pc)] == generation;
212 }
213
218 constexpr void mark_seen(std::int32_t pc)
219 {
220 mark[static_cast<std::size_t>(pc)] = generation;
221 }
222 };
223
234 template <typename ThreadList, typename EpsVec>
236 {
237 ThreadList lists[2];
238 EpsVec stack;
239
251 std::int32_t table_class {-1};
252 std::array<std::uint8_t, 256> table {};
253
262 std::int32_t cp_page_class {-1};
263 std::array<std::uint64_t, 30> cp_page {};
264 };
265
269 using thread_list = basic_thread_list<std::vector<std::int32_t>, std::vector<std::size_t>, std::vector<std::uint64_t>>;
279 {
281 std::vector<eps_entry> stack;
282 };
283
287 struct pike_state : basic_pike_state<thread_list, std::vector<eps_entry>>
288 {
291 std::optional<lazy_dfa> fwd_dfa;
292 std::optional<reverse_dfa> rev_dfa;
293 const void* dfa_program {nullptr};
294 std::optional<reverse_dfa> il_prefix_rev;
295 const void* il_prefix_for {nullptr};
296 const void* il_text {nullptr};
297 bool il_abandoned {false};
298 };
299
304 template <typename State>
306 {
307 public:
308
314 constexpr pike_vm(const program_view& prog,
315 State& state)
316 : prog_(prog),
317 state_(state)
318 {}
319
341 template <bool Cascade = false, typename OutSlots>
342 constexpr bool run(std::string_view text,
343 std::size_t start,
344 run_mode mode,
345 OutSlots& out_slots,
346 std::size_t forbid_empty_until = 0,
348 {
349 text_ = text;
350 forbid_empty_until_ = forbid_empty_until;
351 sem_ = sem;
352 // Fast paths only fire for patterns that always consume (literal /
353 // class+), which can never produce the empty match the flag guards.
355 // OPT-C: the memchr-cascade instantiation (Cascade) is selected ONCE by the caller (a whole
356 // find_iter/search) from stop_set_size, never per match — so when it is off this run is byte-for-
357 // byte the pre-OPT-C per-byte loop and the hot path pays nothing. The caller only sets Cascade
358 // when stop_set_size >= 1, so the cascade tail always has real stop bytes.
359 return run_class_loop<Cascade>(text, start, mode, out_slots);
360 }
362 return run_cp_class_loop(text, start, mode, out_slots);
363 }
365 return run_exact_literal(text, start, mode, out_slots);
366 }
367 // OPT inner-literal: memmem a required inner literal and reverse/confirm the match around it — the
368 // most selective prefilter for a pattern whose match does not begin with a literal (the date `-`, the
369 // `@`). Placed AFTER the literal / class-loop fast paths (an exact-literal `dog` must keep its own path)
370 // but BEFORE the fixed-shape / DFA scans it beats. Search mode, runtime, dynamic-only. On a linearity
371 // guard it abandons and falls through to the scans below.
372 if constexpr (requires(State & s) {
373 s.il_prefix_rev;
374 }) {
375 if (!std::is_constant_evaluated() && !inner_literal_route_disabled() && mode == run_mode::search
376 && sem_ == match_semantics::first // longest semantics need the general loop (these routes are kFirstMatch)
378 && (prog_.hints.inner_literal_prefix == 0 || !prog_.prefix_code.empty())) {
379 // No size guard: on a no-match haystack the route is memmem-only (the reverse setup is lazy, built on
380 // the first candidate, never here), so it wins at every size; and the prefix byte-program is a
381 // per-regex immutable (built once, amortized by any later use — the lazy-DFA warmup's own contract).
382 if (state_.il_text != static_cast<const void*>(text.data())) {
383 state_.il_abandoned = false; // a fresh haystack: re-enable the route and re-evaluate its guards
384 state_.il_text = static_cast<const void*>(text.data());
385 }
386 if (!state_.il_abandoned) {
387 bool abandon {false};
388 const bool matched {run_inner_literal(text, start, out_slots, abandon)};
389 if (!abandon) {
390 return matched;
391 }
392 state_.il_abandoned = true; // a linearity guard tripped: stay on the core for the rest of this haystack
393 }
394 }
395 }
397 return run_fixed_shape(text, start, mode, out_slots);
398 }
400 // OPT-C-1b: the SWAR variant (Cascade) is chosen once per walk, like the class-loop cascade.
401 return run_codepoint_class<Cascade>(text, start, mode, out_slots);
402 }
404 return run_alternation(text, start, mode, out_slots);
405 }
406 // OPT inner-literal: memmem a required literal and reverse/confirm the match around it — for patterns
407 // whose match need not begin with a literal (a leading class/quantifier), so no prefix skip applies but
408 // a rarer INNER literal does (the date `-`, the `@`). Search mode, runtime, dynamic-only (it needs the
409 // prefix sub-program). Placed before the DFA: a memchr skip to a rare byte beats a per-byte DFA scan.
410 // On a linearity guard it abandons and falls through to the DFA / core VM below.
411 // OPT lazy-DFA: for an eligible pattern on a large enough input, a forward DFA finds the match end
412 // (capture-free, ~12x a Pike no-match scan) and a reverse DFA its start; the Pike VM then runs only on
413 // the [s, e] window for the captures and the empty-match rule (the DFA supplies the span, nothing
414 // else). Ineligible patterns, a tripped thrash flag, small inputs, and non-search modes stay on the VM.
415 if constexpr (requires(State & s) {
416 s.fwd_dfa;
417 }) {
418 // Direct anchored routing: a full-match's window is exactly [start, text.size()] — no search, no DFA.
419 // A one-pass pattern (Tier A, or Tier B now that assertions are edge conditions) fills its captures in
420 // a single pass over that window; extract returns false (and we fall to the VM) if it does not one-pass
421 // or the span does not in fact match there. Only the immutables are needed, so no DFA build is paid.
422 if (!std::is_constant_evaluated() && !lazy_dfa_route_disabled() && mode == run_mode::full) {
424 if (prog_.immut != nullptr && prog_.immut->op_table.has_value() && prog_.immut->op_table->eligible()
425 && prog_.immut->op_table->extract(text, start, text.size(), out_slots)) {
426 return true;
427 }
428 }
429 // forbid_empty_until_ != 0 means the iterator just yielded an empty match and the next may not be
430 // empty at the same spot; forward_end does not model that rule, so those searches stay on the Pike
431 // VM (which does). Empty-matching patterns thus alternate DFA/VM across a find_iter; all others route.
432 if (!std::is_constant_evaluated() && !lazy_dfa_route_disabled() && mode == run_mode::search
433 && sem_ == match_semantics::first // kFirstMatch forward pass; longest uses the general loop below
434 && forbid_empty_until_ == 0 && text.size() - start >= lazy_dfa_min_input) {
435 if (state_.dfa_program != static_cast<const void*>(prog_.code.data())) {
436 ensure_lazy_dfa(); // once per iterator, not per match — skips the call_once atomic load on the hot path
437 }
438 if (state_.fwd_dfa.has_value() && state_.rev_dfa.has_value() && state_.fwd_dfa->eligible()
439 && !state_.fwd_dfa->thrashing()) {
440 const std::size_t match_end {state_.fwd_dfa->forward_end(text.substr(start))};
441 if (match_end == npos) {
442 out_slots.assign(prog_.slot_count, npos);
443 return false; // the forward DFA rejected the whole suffix at DFA speed
444 }
445 const std::size_t abs_end {start + match_end};
446 const std::size_t abs_start {state_.rev_dfa->reverse_start(text, abs_end, start)};
447 // OPT onepass (Tier A): a one-pass pattern fills captures in a single pass over [s, e] with no
448 // thread lists (the shared per-regex table). Otherwise the window-Pike runs the general loop
449 // there. Both give the same slots.
450 if (prog_.immut->op_table.has_value() && prog_.immut->op_table->eligible()
451 && prog_.immut->op_table->extract(text, abs_start, abs_end, out_slots)) {
452 return true; // extract filled out_slots directly — no intermediate buffer or copy
453 }
454 return run_general<Cascade>(text.substr(0, abs_end), abs_start, mode, out_slots);
455 }
456 }
457 }
459 // The longest path uses the plain general loop (the memchr-cascade OPT-C variant is a first-match
460 // acceleration; correctness, not throughput, is what the experimental mode needs).
461 return run_general<false>(text, start, mode, out_slots);
462 }
463 return run_general<Cascade>(text, start, mode, out_slots);
464 }
465
470 template <bool Cascade = false, typename OutSlots>
471 constexpr bool run_general(std::string_view text,
472 std::size_t start,
473 run_mode mode,
474 OutSlots& out_slots,
475 std::size_t* forward_stop = nullptr) // IL: how far the forward scan reached
476 {
477 text_ = text;
478 const std::size_t code_size {prog_.code.size()};
479 auto* clist {&state_.lists[0]};
480 auto* nlist {&state_.lists[1]};
481 clist->reset(code_size);
482 nlist->reset(code_size);
483 out_slots.assign(prog_.slot_count, npos);
484 state_.pool.reset(prog_.slot_count); // OPT D1: fresh COW pool (block 0 = all-npos, per this run)
485
486 bool matched {};
487 std::size_t pos {start};
488 while (pos <= text.size()) {
489 const bool seeding = (pos == start) || (mode == run_mode::search && !matched);
490 if (seeding && mode == run_mode::search && !matched && clist->pcs.empty()) {
491 // No thread is alive: jump straight to the next position
492 // that could start a match (prefilter). Single pass, so the
493 // linear-time guarantee is unaffected.
494 pos = next_candidate(text, pos, start);
495 if (pos > text.size()) {
496 break; // no further start is possible (includes npos)
497 }
498 // Fresh generation before seeding at the jumped position: the list
499 // may still carry `seen` marks from a previous position's epsilon
500 // exploration whose threads all died, which would otherwise dedup
501 // away (drop) the seed's own threads here.
502 clist->reset(code_size);
503 }
504 if (seeding && seed_viable(text, pos, start)) {
505 // OPT D1: a seed shares the canonical all-npos block (one incref, no allocation); the first save
506 // in its closure copies-on-write off it, so block 0 is never mutated.
507 state_.pool.incref(pool_type::npos_block);
508 add_thread(*clist, 0, pos, pool_type::npos_block);
509 }
510 if (clist->pcs.empty()) {
511 // The seed itself may die in the closure (failed assertion):
512 // later positions must still be tried while searching. The
513 // dead seed's seen-marks must not block the next one.
514 if (matched || mode != run_mode::search || pos >= text.size()) {
515 break;
516 }
517 clist->reset(code_size);
518 ++pos;
519 continue;
520 }
521 step(*clist, *nlist, pos, mode, matched, out_slots);
522 auto* swap {clist};
523 clist = nlist;
524 nlist = swap;
525 cow_release_blocks(*nlist); // OPT D1: the old clist was consumed by step — drop its block refs
526 nlist->reset(code_size);
527 ++pos;
528 }
529 cow_release_blocks(*clist); // OPT D1: drain both surviving lists on exit (the leak class the review named)
530 cow_release_blocks(*nlist);
531 // Σ-invariant: after a full drain only the canonical npos block's sentinel ref remains. A leaked
532 // block (missing decref) or a double-free (underflow) breaks it. Debug/sanitize builds only.
533 assert(state_.pool.total_refs() == 1 && "OPT-D1 capture-block refcount leak or imbalance");
534 if (forward_stop != nullptr) {
535 *forward_stop = pos; // the position the forward scan reached — IL's min_pre_start on a failed confirm
536 }
537 return matched;
538 }
539
548 template <typename OutSlots>
549 bool confirm_at(std::string_view text,
550 std::size_t s,
551 OutSlots& out_slots,
552 std::size_t& stop)
553 {
554 stop = s;
555 if constexpr (requires(State & st) {
556 st.fwd_dfa;
557 }) {
559 if (state_.dfa_program != static_cast<const void*>(prog_.code.data())) {
561 }
562 if (state_.fwd_dfa.has_value() && state_.fwd_dfa->eligible() && !state_.fwd_dfa->thrashing()) {
563 const std::size_t match_end {state_.fwd_dfa->forward_end(text.substr(s))};
564 if (match_end == npos) {
565 stop = text.size();
566 out_slots.assign(prog_.slot_count, npos);
567 return false; // the forward DFA rejects the whole suffix
568 }
569 const std::size_t e {s + match_end};
570 stop = e;
572 if (prog_.immut != nullptr && prog_.immut->op_table.has_value() && prog_.immut->op_table->eligible()
573 && prog_.immut->op_table->extract(text, s, e, out_slots)) {
574 return true; // one-pass filled the captures for [s, e] anchored at s
575 }
576 // Not one-pass, or the DFA's leftmost match did not begin at s: the anchored window Pike decides.
577 return run_general<false>(text.substr(0, e), s, run_mode::prefix, out_slots, &stop);
578 }
579 }
580 }
581 return run_general<false>(text, s, run_mode::prefix, out_slots, &stop);
582 }
583
593 template <typename OutSlots>
594 bool run_inner_literal(std::string_view text,
595 std::size_t start,
596 OutSlots& out_slots,
597 bool& abandon)
598 {
599 abandon = false;
600 std::array<char, 16> lit_buf {}; // copy the literal into char storage (no pointer cast to appease both lints)
601 for (std::size_t i = 0; i < prog_.hints.inner_literal_len; ++i) {
602 lit_buf[i] = static_cast<char>(prog_.hints.inner_literal[i]);
603 }
604 const std::string_view lit {lit_buf.data(), prog_.hints.inner_literal_len};
605 const std::int32_t boundary {prog_.hints.inner_literal_prefix};
606
607 std::size_t pos {start};
608 const std::size_t min_match_start {start}; // reverse floor = this search's start (the finditer resume); never advances mid-call
609 std::size_t min_pre_start {start}; // literal-scan floor (last confirm's reach) — the linearity backstop
610 bool first_candidate {true};
611 while (true) {
612 const std::size_t h {find_literal(text, pos, lit)};
613 if (h == npos) {
614 out_slots.assign(prog_.slot_count, npos);
615 return false; // no more candidates (no-match): memmem-only — the guard below was never reached
616 }
617 if (first_candidate) {
618 first_candidate = false;
619 // Small-haystack guard, decided ONCE at the first candidate (per-scan, made sticky by the caller's
620 // il_abandoned). It therefore applies only to a haystack that HAS a match — a no-match scan returns
621 // above, memmem-only, and is never gated (its huge win is safe by construction). Below the
622 // prefix-scaled threshold the reverse DFA's per-iterator cache does not amortize, so the core (the
623 // pre-IL baseline, with its own one-pass/lazy-DFA) is faster: hand it the whole scan.
625 if (!inner_literal_guard_disabled() && prog_.immut != nullptr && text.size() < prog_.immut->il_min_haystack) {
626 abandon = true;
627 return false;
628 }
629 }
630 if (h < min_pre_start) {
631 abandon = true; // guard 2: the scan is regressing into confirmed territory -> retry on the core
632 return false;
633 }
634 std::size_t s {h}; // boundary 0 = head literal: the reverse is the identity
635 if (boundary >= 1) {
636 // The prefix's byte program lives in the per-regex immutables — built once (call_once, already done
637 // by the first-candidate guard above), not per find_iter; the expensive klass_cp expansion is what a
638 // small-input regex must not pay repeatedly. The reverse DFA that spans it is a cheap per-iterator
639 // wrapper, (re)created when this iterator binds a new program.
640 if (prog_.immut == nullptr || !prog_.immut->il_prefix_prog.eligible) {
641 abandon = true; // no per-regex cache, or the prefix is not byte-DFA-eligible — let the core VM handle it
642 return false;
643 }
644 if (state_.il_prefix_for != static_cast<const void*>(prog_.immut->il_prefix_prog.code.data())) {
646 state_.il_prefix_for = static_cast<const void*>(prog_.immut->il_prefix_prog.code.data());
647 }
648 if (state_.il_prefix_rev.has_value()) {
649 s = state_.il_prefix_rev->reverse_start(text, h, min_match_start);
650 }
651 }
652 if (s == npos) {
653 pos = h + 1; // the prefix reaches no start within [min_match_start, h] -> next candidate
654 }
655 else {
656 std::size_t stop {s};
657 if (confirm_at(text, s, out_slots, stop)) {
658 return true; // confirmed: out_slots holds [s, e]
659 }
660 if (stop > min_pre_start) {
661 min_pre_start = stop; // the failed forward's reach bounds future candidates
662 }
663 pos = h + 1;
664 }
665 // min_match_start does NOT advance within a call: it advances only on a YIELD (the finditer's next
666 // start). Advancing it per candidate (to the previous literal's end) would bound the next reverse too
667 // tightly and miss a leftmost match whose start precedes a failed candidate — e.g. `((.))a` on "aaaab",
668 // where the "a" at 0 fails but the match [0,2) is found from the "a" at 1 only if the reverse may still
669 // reach 0. The min_pre_start backstop (a candidate before the last confirm's forward reach) keeps it
670 // linear instead.
671 }
672 }
673
674 private:
675
677 State& state_;
678 std::string_view text_;
679
689 std::size_t forbid_empty_until_ {};
690
694
698 using list_type = std::remove_reference_t<decltype(std::declval<State&>().lists[0])>;
699
702 static constexpr std::size_t lazy_dfa_min_input {512};
703
714 {
715 detail::regex_immutables* const immut {prog_.immut};
716 if (immut == nullptr) {
717 return; // no per-regex cache (not the dynamic storage) — the caller keeps the VM
718 }
719 std::call_once(immut->once, [&] {
720 immut->byte_prog = build_byte_program(prog_); // Tier-A: ineligible if assert/lookaround
721 if (immut->byte_prog.eligible) {
722 immut->alphabet =
723 compute_lazy_alphabet(immut->byte_prog.code, immut->byte_prog.classes); // shared by both DFAs
724 }
725 const byte_program tier_b {build_byte_program(prog_, /*keep_assertions=*/ true)};
726 if (tier_b.eligible) {
727 immut->op_table.emplace(tier_b); // one-pass extractor: Tier-A window + Tier-B anchored
728 }
729 if (!prog_.prefix_code.empty()) { // IL: expand the inner-literal prefix once per regex (not per find_iter)
730 program_view pv {};
732 pv.classes = prog_.prefix_classes;
733 pv.cp_classes = prog_.prefix_cp_classes;
734 pv.cp_ranges = prog_.prefix_cp_ranges;
735 pv.unicode_word = prog_.unicode_word;
736 immut->il_prefix_prog = build_byte_program(pv);
737 // The reverse DFA's per-iterator cache re-warms per find_iter; below a size scaled by
738 // the prefix byte-program (its cache size) that cost does not amortize on a haystack
739 // that HAS candidates, and the core is faster. Measured crossover (route on vs core):
740 // ~158 KB for the email \w+ (3436 instr, dense — the harder density), <64 KB for the
741 // date \d{4} (1031 instr). N = size * 64, clamped [64 KB, 512 KB] — ~40% above the
742 // email crossover (220 KB) so the mid-size win at 256 KB+ is kept, while every size
743 // below stays on the core. Checked ONLY after the first memmem hit (see
744 // run_inner_literal), so no-match — memmem-only, a win at every size — is never gated.
745 const std::size_t sz {immut->il_prefix_prog.code.size()};
746 immut->il_min_haystack = std::min<std::size_t>(512UL * 1024, std::max<std::size_t>(64UL * 1024, sz * 64));
747 }
748 });
749 }
750
757 {
758 detail::regex_immutables* const immut {prog_.immut};
759 if (immut == nullptr) {
760 return; // no per-regex cache (not the dynamic storage) — the caller keeps the VM
761 }
762 ensure_immutables();
763 // The DFA transition caches are mutable (warm per scan): they stay per-iterator, spanning the shared
764 // byte-program, rebuilt only if this state is now bound to a different program.
765 const auto* const program {static_cast<const void*>(prog_.code.data())};
766 if (state_.dfa_program != program) {
767 if (immut->byte_prog.eligible) {
768 state_.fwd_dfa.emplace(immut->byte_prog.code, immut->byte_prog.classes, lazy_dfa::state_budget,
769 &immut->alphabet);
770 state_.rev_dfa.emplace(immut->byte_prog.code, immut->byte_prog.classes, reverse_dfa::state_budget,
771 &immut->alphabet);
772 }
773 else {
774 state_.fwd_dfa.reset();
775 state_.rev_dfa.reset();
776 }
777 state_.dfa_program = program;
778 }
779 }
780
783 using pool_type = std::remove_reference_t<decltype(std::declval<State&>().pool)>;
784
797 constexpr const std::uint8_t* class_table(std::size_t class_index)
798 {
799 if (state_.table_class != static_cast<std::int32_t>(class_index)) {
800 const char_class& klass {prog_.classes[class_index]};
801 for (std::size_t b {0}; b < 256; ++b) {
802 state_.table[b] = klass.test(static_cast<std::uint8_t>(b)) ? 1U : 0U;
803 }
804 state_.table_class = static_cast<std::int32_t>(class_index);
805 }
806 return state_.table.data();
807 }
808
817 constexpr const std::uint8_t* cp_ascii_table(std::size_t cp_index)
818 {
819 const std::int32_t key {-2 - static_cast<std::int32_t>(cp_index)};
820 if (state_.table_class != key) {
821 const char_class& klass {prog_.cp_classes[cp_index].ascii};
822 for (std::size_t b {0}; b < 256; ++b) {
823 state_.table[b] = klass.test(static_cast<std::uint8_t>(b)) ? 1U : 0U;
824 }
825 state_.table_class = key;
826 }
827 return state_.table.data();
828 }
829
831 static constexpr std::uint32_t cp_page_max {0x7FFU};
832
835 static constexpr int max_loop_hops {8};
836
840 static constexpr std::size_t cascade_run_threshold {32};
841
849 constexpr const std::uint64_t* cp_page_table(std::size_t cp_index)
850 {
851 const std::int32_t key {-2 - static_cast<std::int32_t>(cp_index)};
852 if (state_.cp_page_class != key) {
853 state_.cp_page.fill(0);
854 const detail::cp_class& cc {prog_.cp_classes[cp_index]};
855 for (std::uint32_t k {0}; k < cc.range_count; ++k) {
856 const detail::code_range& r {prog_.cp_ranges[cc.range_begin + k]};
857 if (r.lo > cp_page_max) {
858 break; // ranges are sorted: nothing more falls in the page
859 }
860 const std::uint32_t lo {r.lo < 0x80U ? 0x80U : r.lo};
861 const std::uint32_t hi {r.hi > cp_page_max ? cp_page_max : r.hi};
862 for (std::uint32_t c {lo}; c <= hi; ++c) {
863 const std::uint32_t bit {c - 0x80U};
864 state_.cp_page[bit >> 6U] |= std::uint64_t {1} << (bit & 63U);
865 }
866 }
867 state_.cp_page_class = key;
868 }
869 return state_.cp_page.data();
870 }
871
878 template <typename OutSlots>
879 constexpr void fill_span_slots(OutSlots& out_slots,
880 std::size_t match_start,
881 std::size_t match_end) const
882 {
883 out_slots[0] = match_start;
884 out_slots[1] = match_end;
885 if (prog_.hints.greedy_group_start >= 0) {
886 out_slots[static_cast<std::size_t>(prog_.hints.greedy_group_start)] = match_start;
887 out_slots[static_cast<std::size_t>(prog_.hints.greedy_group_end)] = match_end;
888 }
889 }
890
895 [[nodiscard]] constexpr std::size_t run_cascade_stop(std::string_view text,
896 std::size_t from) const
897 {
898 const std::size_t stop {
899 find_bytes_cascade(text, from, prog_.hints.stop_set.data(), prog_.hints.stop_set_size)};
900 return stop == npos ? text.size() : stop;
901 }
902
917 template <bool Cascade, typename OutSlots>
918 constexpr bool run_class_loop(std::string_view text,
919 std::size_t start,
920 run_mode mode,
921 OutSlots& out_slots)
922 {
923 const std::uint8_t* const tbl =
924 class_table(static_cast<std::size_t>(prog_.hints.greedy_class_loop));
925 const auto in_class = [&](std::size_t i) {
926 return tbl[static_cast<std::uint8_t>(text[i])] != 0U;
927 };
928 std::size_t match_start {start};
929 if (mode == run_mode::search) {
930 while (match_start < text.size() && !in_class(match_start)) {
931 ++match_start;
932 }
933 }
934 if (match_start >= text.size() || !in_class(match_start)) {
935 out_slots.assign(prog_.slot_count, npos);
936 return false;
937 }
938 // OPT-C: this is a byte-wise run whose accepted set may have a small complement (the STOP bytes).
939 // Only the Cascade instantiation carries the memchr-cascade; it advances per byte until a run
940 // passes a 32-byte threshold — a genuinely long run — then jumps to the next stop by a cascade, so
941 // a stop-dense stream of short runs stays on the per-byte path. The constant-evaluation guard is a
942 // per-call property, hoisted out of the loop; at compile time the plain loop runs. The common
943 // classes (`[a-z]+`, `\d+`) compile to the pristine per-byte loop with none of the cascade code.
944 // Sound because run_class_loop never validates UTF-8 (the OPT-C perimeter pin, test_utf8.cpp).
945 std::size_t match_end {match_start + 1};
946 if constexpr (Cascade) {
947 if (!std::is_constant_evaluated()) {
948 while (match_end < text.size() && in_class(match_end)) {
949 ++match_end;
950 if (match_end - match_start == cascade_run_threshold) {
951 match_end = run_cascade_stop(text, match_end);
952 break;
953 }
954 }
955 }
956 else {
957 while (match_end < text.size() && in_class(match_end)) {
958 ++match_end;
959 }
960 }
961 }
962 else {
963 while (match_end < text.size() && in_class(match_end)) {
964 ++match_end;
965 }
966 }
967 if (mode == run_mode::full && match_end != text.size()) {
968 out_slots.assign(prog_.slot_count, npos);
969 return false;
970 }
971 out_slots.assign(prog_.slot_count, npos);
972 fill_span_slots(out_slots, match_start, match_end);
973 return true;
974 }
975
991 template <typename OutSlots>
992 constexpr bool run_cp_class_loop(std::string_view text,
993 std::size_t start,
994 run_mode mode,
995 OutSlots& out_slots)
996 {
997 const std::size_t cp_index {static_cast<std::size_t>(prog_.hints.greedy_cp_class)};
998 const detail::cp_class& cc {prog_.cp_classes[cp_index]};
999 const std::uint8_t* const asc {cp_ascii_table(cp_index)};
1000 // Membership of a non-ASCII code point (>= 0x80): a one-load page-bitmap test over the two-byte
1001 // range, the range search only for CJK / astral code points beyond it. The page is built
1002 // lazily on the first non-ASCII code point, so a pure-ASCII scan never pays for it.
1003 const auto member_hi = [&](char32_t cp) -> bool {
1004 if (cp <= cp_page_max) {
1005 const std::uint64_t* const page {cp_page_table(cp_index)};
1006 const std::uint32_t bit {static_cast<std::uint32_t>(cp) - 0x80U};
1007 return ((page[bit >> 6U] >> (bit & 63U)) & std::uint64_t {1}) != 0U;
1008 }
1009 return cp_class_matches(cc, cp);
1010 };
1011 // Byte width of a matching code point at i, or 0. Used for the leftmost-scan step and the first
1012 // code point; the hot greedy run is scanned inline below.
1013 const auto width = [&](std::size_t i) -> std::size_t {
1015 if (!dc.valid) {
1016 return 0;
1017 }
1018 const bool m {dc.cp < 0x80U ? asc[dc.cp] != 0U : member_hi(dc.cp)};
1019 return m ? dc.length : 0;
1020 };
1021 out_slots.assign(prog_.slot_count, npos);
1022 std::size_t match_start {start};
1023 if (mode == run_mode::search) {
1024 while (match_start < text.size() && width(match_start) == 0) {
1025 ++match_start;
1026 }
1027 }
1028 if (match_start >= text.size()) {
1029 return false;
1030 }
1031 // The first code point must match: this path is only chosen for `\w`/`\w+` (never nullable), so
1032 // it reports a non-empty match or none — it can never produce the empty match `forbid_empty_until_`
1033 // guards, which is why that state is not consulted here (see the fast-path dispatch in run()).
1034 const std::size_t first {width(match_start)};
1035 if (first == 0) {
1036 return false;
1037 }
1038 std::size_t match_end {match_start + first};
1039 if (prog_.hints.greedy_cp_class_plus) {
1040 while (match_end < text.size()) {
1041 // Tight ASCII inner loop: a byte-indexed table lookup with no decode and no call,
1042 // the same one-load trick the byte-NFA scan loop uses; only a non-ASCII lead decodes.
1043 const auto lead {static_cast<std::uint8_t>(text[match_end])};
1044 if (lead < 0x80U) {
1045 if (asc[lead] == 0U) {
1046 break;
1047 }
1048 ++match_end;
1049 continue;
1050 }
1052 if (!dc.valid || !member_hi(dc.cp)) {
1053 break;
1054 }
1055 match_end += dc.length;
1056 }
1057 }
1058 if (mode == run_mode::full && match_end != text.size()) {
1059 return false;
1060 }
1061 fill_span_slots(out_slots, match_start, match_end);
1062 return true;
1063 }
1064
1077 template <bool SkipSaves = false>
1078 [[nodiscard]] constexpr std::size_t match_byte_klass_run(std::string_view text,
1079 std::size_t pc,
1080 std::size_t s) const
1081 {
1082 std::size_t consumed {};
1083 while (pc < prog_.code.size()) {
1084 const instr& instruction {prog_.code[pc]};
1085 if constexpr (SkipSaves) {
1086 // Grouped fixed shape: interleaved capturing saves are epsilon here (slots filled
1087 // separately). if constexpr keeps this branch out of the no-group tight loop entirely.
1088 if (instruction.op == opcode::save) {
1089 ++pc;
1090 continue;
1091 }
1092 }
1093 if (instruction.op != opcode::byte && instruction.op != opcode::klass) {
1094 break;
1095 }
1096 if (s + consumed >= text.size()) {
1097 return npos;
1098 }
1099 const auto byte_value {static_cast<std::uint8_t>(text[s + consumed])};
1100 const bool ok {instruction.op == opcode::byte ? byte_value == instruction.arg8
1101 : prog_.classes[instruction.arg16].test(byte_value)};
1102 if (!ok) {
1103 return npos;
1104 }
1105 ++consumed;
1106 ++pc;
1107 }
1108 return s + consumed;
1109 }
1110
1127 template <typename MatchAt, typename OutSlots>
1128 constexpr bool fast_search(std::string_view text,
1129 std::size_t start,
1130 MatchAt match_at,
1131 OutSlots& out_slots)
1132 {
1133 std::size_t match_start {start};
1134 while (match_start <= text.size()) {
1135 match_start = next_candidate(text, match_start, start);
1136 if (match_start > text.size()) {
1137 break;
1138 }
1139 const std::size_t match_end {match_at(match_start)};
1140 if (match_end != npos) {
1141 out_slots[0] = match_start;
1142 out_slots[1] = match_end;
1143 return true;
1144 }
1145 ++match_start;
1146 }
1147 return false;
1148 }
1149
1165 template <typename OutSlots>
1166 constexpr bool run_fixed_shape(std::string_view text,
1167 std::size_t start,
1168 run_mode mode,
1169 OutSlots& out_slots)
1170 {
1171 // No inner groups (slot_count 2): a contiguous byte/klass run, the original tight path unchanged.
1172 if (prog_.slot_count <= 2) {
1173 out_slots.assign(2, npos);
1174 const auto at {[&](std::size_t s) { return match_byte_klass_run<false>(text, 1, s); }};
1175 if (mode != run_mode::search) {
1176 const std::size_t match_end {at(start)};
1177 if (match_end == npos || (mode == run_mode::full && match_end != text.size())) {
1178 return false;
1179 }
1180 out_slots[0] = start;
1181 out_slots[1] = match_end;
1182 return true;
1183 }
1184 return fast_search(text, start, at, out_slots);
1185 }
1186
1187 // Inner capturing groups: the run has interleaved saves, so the verify walk skips them
1188 // (SkipSaves) and the group slots are filled from their constant offsets on success only (not per
1189 // failed candidate). A separate body keeps the no-group loop above free of any grouping branch.
1190 out_slots.assign(prog_.slot_count, npos);
1191 const auto at {[&](std::size_t s) { return match_byte_klass_run<true>(text, 1, s); }};
1192 if (mode != run_mode::search) {
1193 const std::size_t match_end {at(start)};
1194 if (match_end == npos || (mode == run_mode::full && match_end != text.size())) {
1195 return false;
1196 }
1197 out_slots[0] = start;
1198 out_slots[1] = match_end;
1199 fill_fixed_saves(start, out_slots);
1200 return true;
1201 }
1202 if (!fast_search(text, start, at, out_slots)) {
1203 return false;
1204 }
1205 fill_fixed_saves(out_slots[0], out_slots); // out_slots[0] is the winning match start
1206 return true;
1207 }
1208
1217 template <typename OutSlots>
1218 constexpr void fill_fixed_saves(std::size_t match_start,
1219 OutSlots& out_slots) const
1220 {
1221 if (prog_.slot_count <= 2) {
1222 return;
1223 }
1224 std::size_t offset {};
1225 for (std::size_t pc {1}; pc < prog_.code.size(); ++pc) {
1226 const instr& instruction {prog_.code[pc]};
1227 if (instruction.op == opcode::byte || instruction.op == opcode::klass) {
1228 ++offset;
1229 }
1230 else if (instruction.op == opcode::save) {
1231 out_slots[static_cast<std::size_t>(instruction.arg16)] = match_start + offset;
1232 }
1233 else {
1234 break; // reached match
1235 }
1236 }
1237 }
1238
1255 template <bool Cascade, typename OutSlots>
1256 constexpr bool run_codepoint_class(std::string_view text,
1257 std::size_t start,
1258 run_mode mode,
1259 OutSlots& out_slots)
1260 {
1261 const std::uint8_t* const ascii {
1262 class_table(static_cast<std::size_t>(prog_.hints.codepoint_class_ascii))};
1263 out_slots.assign(2, npos);
1264
1265 const auto cont = [&](std::size_t i) {
1266 const auto cont_byte {static_cast<std::uint8_t>(text[i])};
1267 return cont_byte >= 0x80 && cont_byte <= 0xBF;
1268 };
1269 // Byte length of a matching codepoint at i, or 0 for no match.
1270 const auto width = [&](std::size_t i) -> std::size_t {
1271 const auto byte_value {static_cast<std::uint8_t>(text[i])};
1272 if (byte_value < 0x80) {
1273 return ascii[byte_value] != 0U ? 1 : 0;
1274 }
1275 if (byte_value >= 0xC2 && byte_value <= 0xDF) {
1276 return i + 1 < text.size() && cont(i + 1) ? 2 : 0;
1277 }
1278 if (byte_value >= 0xE0 && byte_value <= 0xEF) {
1279 return i + 2 < text.size() && cont(i + 1) && cont(i + 2) ? 3 : 0;
1280 }
1281 if (byte_value >= 0xF0 && byte_value <= 0xF4) {
1282 return i + 3 < text.size() && cont(i + 1) && cont(i + 2) && cont(i + 3) ? 4 : 0;
1283 }
1284 return 0;
1285 };
1286
1287 std::size_t match_start {start};
1288 if (mode == run_mode::search) {
1289 while (match_start < text.size() && width(match_start) == 0) {
1290 ++match_start;
1291 }
1292 }
1293 if (match_start >= text.size()) {
1294 return false;
1295 }
1296 const std::size_t first_width {width(match_start)};
1297 if (first_width == 0) {
1298 return false;
1299 }
1300 std::size_t match_end {match_start + first_width};
1301 if (prog_.hints.codepoint_class_plus) {
1302 const auto scalar_scan = [&]() {
1303 while (match_end < text.size()) {
1304 const std::size_t codepoint_width {width(match_end)};
1305 if (codepoint_width == 0) {
1306 break;
1307 }
1308 match_end += codepoint_width;
1309 }
1310 };
1311 if constexpr (Cascade) {
1312 if (!std::is_constant_evaluated()) {
1313 // OPT-C-1b SWAR: the next ASCII stop bounds the whole run (an ASCII byte can never lie inside
1314 // a multi-byte cluster), so memchr it ONCE. Then walk [match_end, p1): the high-bit scan
1315 // skips ASCII stretches eight bytes at a time, and only a non-ASCII cluster drops to code-
1316 // point validation. A pure-ASCII stretch to the stop is exact (ASCII text == bytes); a
1317 // malformed sequence still stops the run via width() == 0 — the C-0 property, preserved.
1318 const std::size_t stop {find_bytes_cascade(text, match_end, prog_.hints.stop_set.data(),
1319 prog_.hints.stop_set_size)};
1320 const std::size_t p1 {stop == npos ? text.size() : stop};
1321 bool malformed {false};
1322 while (match_end < p1) {
1323 const std::size_t high {first_high_byte(text, match_end, p1)};
1324 if (high == p1) {
1325 break; // the rest of [match_end, p1) is pure ASCII
1326 }
1327 match_end = high; // validate the non-ASCII cluster at `high`
1328 const std::size_t w {width(match_end)};
1329 if (w == 0) {
1330 malformed = true;
1331 break;
1332 }
1333 match_end += w;
1334 }
1335 if (!malformed) {
1336 match_end = p1; // the whole run up to the stop / text end is accepted
1337 }
1338 }
1339 else {
1340 scalar_scan();
1341 }
1342 }
1343 else {
1344 scalar_scan();
1345 }
1346 }
1347 if (mode == run_mode::full && match_end != text.size()) {
1348 return false;
1349 }
1350 out_slots[0] = match_start;
1351 out_slots[1] = match_end;
1352 return true;
1353 }
1354
1370 template <typename OutSlots>
1371 constexpr bool run_alternation(std::string_view text,
1372 std::size_t start,
1373 run_mode mode,
1374 OutSlots& out_slots)
1375 {
1376 out_slots.assign(2, npos);
1377 const auto& code {prog_.code};
1378
1379 // First branch that matches at \p s (and, for full, spans to the end). The
1380 // branches are read from the split chain in source order (highest priority
1381 // first), mirroring the VM's thread priority.
1382 const auto match_at = [&](std::size_t match_start, bool require_full) -> std::size_t {
1383 std::size_t pc {1};
1384 while (true) {
1385 const bool is_split {code[pc].op == opcode::split};
1386 const std::size_t branch {is_split ? static_cast<std::size_t>(code[pc].primary_target) : pc};
1387 const std::size_t match_end {match_byte_klass_run(text, branch, match_start)};
1388 if (match_end != npos && (!require_full || match_end == text.size())) {
1389 return match_end;
1390 }
1391 if (!is_split) {
1392 return npos;
1393 }
1394 pc = static_cast<std::size_t>(code[pc].secondary_target);
1395 }
1396 };
1397
1398 if (mode != run_mode::search) {
1399 const std::size_t match_end {match_at(start, mode == run_mode::full)};
1400 if (match_end == npos) {
1401 return false;
1402 }
1403 out_slots[0] = start;
1404 out_slots[1] = match_end;
1405 return true;
1406 }
1407 return fast_search(text, start, [&](std::size_t match_start) { return match_at(match_start, false); }, out_slots);
1408 }
1409
1417 [[nodiscard]] constexpr bool literal_at(std::string_view text,
1418 std::size_t cand,
1419 std::size_t len) const
1420 {
1421 if (cand + len > text.size()) {
1422 return false;
1423 }
1424 const auto pfx {std::string_view(prog_.hints.prefix.data(), len)};
1425 if (std::is_constant_evaluated()) {
1426 return text.substr(cand, len) == pfx;
1427 }
1428 return std::memcmp(text.data() + cand, pfx.data(), len) == 0;
1429 }
1430
1444 template <typename OutSlots>
1445 constexpr bool replay_literal(std::size_t cand,
1446 std::size_t len,
1447 OutSlots& out_slots) const
1448 {
1449 out_slots.assign(prog_.slot_count, npos);
1450 std::size_t consumed {};
1451 for (std::size_t pc = 0; pc < prog_.code.size(); ++pc) {
1452 const instr& instruction {prog_.code[pc]};
1453 if (instruction.op == opcode::save) {
1454 out_slots[instruction.arg16] = cand + consumed;
1455 }
1456 else if (instruction.op == opcode::assert_position) {
1457 if (!assertion_holds(static_cast<assert_kind>(instruction.arg8), cand + consumed, instruction.arg16 != 0U)) {
1458 out_slots.assign(prog_.slot_count, npos);
1459 return false;
1460 }
1461 }
1462 else if ((instruction.op == opcode::byte || instruction.op == opcode::klass) && consumed < len) {
1463 ++consumed;
1464 }
1465 else if (instruction.op == opcode::match) {
1466 break;
1467 }
1468 }
1469 if (prog_.slot_count >= 2 && out_slots[1] == npos) {
1470 out_slots[1] = cand + len; // group 0 end, even if replay ended early
1471 }
1472 return true;
1473 }
1474
1492 template <typename OutSlots>
1493 constexpr bool run_exact_literal(std::string_view text,
1494 std::size_t start,
1495 run_mode mode,
1496 OutSlots& out_slots)
1497 {
1498 const std::size_t len {static_cast<std::size_t>(prog_.hints.exact_literal_len)};
1499 if (len == 0) {
1500 out_slots.assign(prog_.slot_count, npos);
1501 return false;
1502 }
1503 if (mode != run_mode::search) {
1504 const bool full_ok = mode != run_mode::full || start + len == text.size();
1505 const bool ok = literal_at(text, start, len) && full_ok &&
1506 replay_literal(start, len, out_slots);
1507 if (!ok) {
1508 out_slots.assign(prog_.slot_count, npos);
1509 }
1510 return ok;
1511 }
1512 std::size_t from {start};
1513 while (true) {
1514 const std::size_t cand {next_candidate(text, from, start)};
1515 if (cand > text.size() || cand + len > text.size()) {
1516 out_slots.assign(prog_.slot_count, npos);
1517 return false;
1518 }
1519 if (literal_at(text, cand, len) && replay_literal(cand, len, out_slots)) {
1520 return true;
1521 }
1522 from = cand + 1; // assertion failed here; try the next occurrence
1523 }
1524 }
1525
1538 [[nodiscard]] constexpr std::size_t next_candidate(std::string_view text,
1539 std::size_t pos,
1540 std::size_t start) const
1541 {
1542 const pattern_hints& hints {prog_.hints};
1543 if (hints.anchored_start) {
1544 return pos == start ? pos : npos; // one shot at the start
1545 }
1546 if (hints.prefix_size >= 2) {
1547 return find_prefix(text, pos, std::string_view(hints.prefix.data(), hints.prefix_size));
1548 }
1549 if (hints.rare_byte >= 0) {
1550 // A required rare byte sits `rare_offset` into every match: memchr it (SIMD), then back up to the
1551 // candidate start. Far more selective than scanning a common first-byte class per byte. The VM
1552 // still verifies the candidate, so a false back-up is simply rejected there.
1553 const std::size_t from {pos + hints.rare_offset};
1554 if (from > text.size()) {
1555 return npos;
1556 }
1557 const std::size_t hit {find_byte(text, from, static_cast<char>(hints.rare_byte))};
1558 return hit == npos ? npos : hit - hints.rare_offset;
1559 }
1560 if (hints.single_first >= 0) {
1561 return find_byte(text, pos, static_cast<char>(hints.single_first));
1562 }
1563 if (hints.line_anchored && pos != start) {
1564 const std::size_t nl {find_byte(text, pos - 1, '\n')};
1565 return nl == npos ? npos : nl + 1;
1566 }
1567 if (hints.small_set_size >= 2) {
1568 // Adaptive: probe a short window with the bitmap loop first (one test per byte — the baseline
1569 // cost), so a near hit on dense text is found without paying the cascade's per-member memchr
1570 // overhead. Only when the window is clean (the set bytes are sparse) does the vectorised cascade
1571 // take over the long scan. The threshold is where measurement put the crossover.
1572 constexpr std::size_t probe {32};
1573 const std::size_t window_end {pos + probe < text.size() ? pos + probe : text.size()};
1574 std::size_t p {pos};
1575 while (p < window_end && !hints.first_bytes.test(static_cast<std::uint8_t>(text[p]))) {
1576 ++p;
1577 }
1578 if (p < window_end) {
1579 return p; // a near (dense) hit — the bitmap probe found it at baseline cost
1580 }
1581 if (window_end == text.size()) {
1582 return npos;
1583 }
1584 return find_bytes_cascade(text, window_end, hints.small_set.data(), hints.small_set_size);
1585 }
1586 if (hints.first_bytes_valid) {
1587 while (pos < text.size() &&
1588 !hints.first_bytes.test(static_cast<std::uint8_t>(text[pos]))) {
1589 ++pos;
1590 }
1591 return pos < text.size() ? pos : npos;
1592 }
1593 return pos;
1594 }
1595
1609 [[nodiscard]] constexpr bool seed_viable(std::string_view text,
1610 std::size_t pos,
1611 std::size_t start) const
1612 {
1613 const pattern_hints& hints {prog_.hints};
1614 if (hints.anchored_start && pos != start) {
1615 return false;
1616 }
1617 // A match can never start inside a multi-byte codepoint: in non-byte mode
1618 // a UTF-8 continuation byte (10xxxxxx) is not a valid start position. This
1619 // keeps zero-width matches (\b, \B, ^, $, empty) codepoint-aligned, like a
1620 // codepoint-based engine — bytes mode seeds every byte.
1621 if (!prog_.byte_mode && pos < text.size() &&
1622 (static_cast<std::uint8_t>(text[pos]) & 0xC0U) == 0x80U) {
1623 return false;
1624 }
1625 if (!hints.first_bytes_valid) {
1626 return true;
1627 }
1628 return pos < text.size() && hints.first_bytes.test(static_cast<std::uint8_t>(text[pos]));
1629 }
1630
1638 [[nodiscard]] constexpr bool word_before(std::size_t pos,
1639 bool ascii_word) const
1640 {
1641 return real::detail::word_before(text_, pos, ascii_word); // shared free function (assert_eval.hpp)
1642 }
1643
1646 [[nodiscard]] constexpr bool word_after(std::size_t pos,
1647 bool ascii_word) const
1648 {
1649 return real::detail::word_after(text_, pos, ascii_word); // shared free function (assert_eval.hpp)
1650 }
1651
1660 [[nodiscard]] constexpr bool assertion_holds(assert_kind kind,
1661 std::size_t pos,
1662 bool word_ness_flipped) const
1663 {
1664 // A word assert's word-ness is the program default (\ref program_view::unicode_word), flipped by
1665 // the instruction's flip bit for a scoped (?a:...) / (?-a:...) island — so non-scoped programs
1666 // keep flip == 0 and are byte-identical. ascii_word == unicode default matches iff not flipped.
1667 const bool ascii_word {prog_.unicode_word == word_ness_flipped};
1668 return real::detail::assertion_holds(kind, text_, pos, ascii_word); // shared free function
1669 }
1670
1686 template <typename OutSlots>
1687 constexpr void step(list_type& clist,
1688 list_type& nlist,
1689 std::size_t pos,
1690 run_mode mode,
1691 bool& matched,
1692 OutSlots& out_slots)
1693 {
1694 const std::uint16_t slot_count {prog_.slot_count};
1695 for (std::size_t i = 0; i < clist.pcs.size(); ++i) {
1696 const std::int32_t pc {clist.pcs[i]};
1697 const instr& instruction {prog_.code[static_cast<std::size_t>(pc)]};
1698 switch (instruction.op) {
1699 case opcode::byte:
1700 if (pos < text_.size() &&
1701 static_cast<std::uint8_t>(text_[pos]) == instruction.arg8) {
1702 advance_thread(clist, nlist, i, pc + 1, pos + 1);
1703 }
1704 break;
1705 case opcode::klass:
1706 if (pos < text_.size() &&
1707 prog_.classes[instruction.arg16].test(static_cast<std::uint8_t>(text_[pos]))) {
1708 advance_thread(clist, nlist, i, pc + 1, pos + 1);
1709 }
1710 break;
1711 case opcode::klass_cp:
1712 if (pos < text_.size()) {
1714 if (dc.valid &&
1715 cp_class_matches(prog_.cp_classes[instruction.arg16], dc.cp)) {
1716 advance_thread(clist, nlist, i,
1717 pc + 1 + static_cast<std::int32_t>(4 - dc.length), pos + 1);
1718 }
1719 }
1720 break;
1721 case opcode::match:
1722 {
1723 if (mode == run_mode::full && pos != text_.size()) {
1724 break; // must consume the whole text: thread dies
1725 }
1726 // The winning thread's capture slots — its COW block.
1727 const std::size_t* const won {thread_slots(clist, i)};
1728 // Reject an empty match forbidden at this position; a lower-priority
1729 // thread may still consume a byte and win a non-empty match here.
1730 if (pos == won[0] && won[0] < forbid_empty_until_) {
1731 break;
1732 }
1733 if (sem_ == match_semantics::longest) {
1734 // Leftmost-longest (POSIX / RE2 set_longest_match): keep the leftmost start, then the longest end
1735 // at that start. Record only a strictly-better match and do NOT cut — a lower-priority or later
1736 // thread may still extend it. Seeding has already stopped (matched), so no start past the
1737 // leftmost survives. A lazy quantifier therefore behaves greedily here (the longest end wins).
1738 const bool better {!matched
1739 || won[0] < out_slots[0]
1740 || (won[0] == out_slots[0] && won[1] > out_slots[1])};
1741 if (better) {
1742 for (std::uint16_t s = 0; s < slot_count; ++s) {
1743 out_slots[s] = won[s];
1744 }
1745 }
1746 matched = true;
1747 break; // the match thread dies; the rest of the list and later positions may lengthen it
1748 }
1749 for (std::uint16_t s = 0; s < slot_count; ++s) {
1750 out_slots[s] = won[s];
1751 }
1752 matched = true;
1753 return; // drop lower-priority threads
1754 }
1755 case opcode::split:
1756 case opcode::jump:
1757 case opcode::save:
1760 break; // epsilon ops never appear in a stepped list
1761 }
1762 }
1763 }
1764
1770 constexpr void advance_thread(list_type& clist,
1771 list_type& nlist,
1772 std::size_t i,
1773 std::int32_t next_pc,
1774 std::size_t next_pos)
1775 {
1776 const std::uint32_t block {static_cast<std::uint32_t>(clist.slots[i])};
1777 state_.pool.incref(block); // the new closure holds its own ref (paired with cow_release_blocks)
1778 add_thread(nlist, next_pc, next_pos, block);
1779 }
1780
1785 [[nodiscard]] constexpr const std::size_t* thread_slots(list_type& clist,
1786 std::size_t i)
1787 {
1788 return state_.pool.slots(static_cast<std::uint32_t>(clist.slots[i]));
1789 }
1790
1799 [[nodiscard]] constexpr bool cp_class_matches(const detail::cp_class& cc,
1800 char32_t cp) const
1801 {
1802 bool member {};
1803 if (cp < 0x80U) {
1804 member = cc.ascii.test(static_cast<std::uint8_t>(cp));
1805 }
1806 else {
1807 std::size_t lo {cc.range_begin};
1808 std::size_t hi {static_cast<std::size_t>(cc.range_begin) + cc.range_count};
1809 while (lo < hi) {
1810 const std::size_t mid {lo + ((hi - lo) / 2)};
1811 if (prog_.cp_ranges[mid].hi < cp) {
1812 lo = mid + 1;
1813 }
1814 else {
1815 hi = mid;
1816 }
1817 }
1818 member = lo < static_cast<std::size_t>(cc.range_begin) + cc.range_count &&
1819 cp >= prog_.cp_ranges[lo].lo && cp <= prog_.cp_ranges[lo].hi;
1820 }
1821 return member;
1822 }
1823
1840 constexpr void add_thread(list_type& list,
1841 std::int32_t pc0,
1842 std::size_t pos,
1843 std::uint32_t initial_block)
1844 {
1845 auto& pool {state_.pool};
1846 auto& stack {state_.stack};
1847 stack.clear();
1848 stack.push_back({.pc = pc0, .block = initial_block});
1849 while (!stack.empty()) {
1850 const auto entry {stack.back()};
1851 stack.pop_back();
1852 const std::int32_t pc {entry.pc};
1853 const std::uint32_t block {entry.block}; // this frame owns 1 ref
1854 if (list.seen(pc)) {
1855 pool.decref(block);
1856 continue;
1857 }
1858 list.mark_seen(pc);
1859 const instr& instruction {prog_.code[static_cast<std::size_t>(pc)]};
1860 switch (instruction.op) {
1861 case opcode::jump:
1862 {
1863 // Identical FIX-1/2 loop-exit routing as add_thread; only the ref travels along.
1864 std::int32_t head {instruction.primary_target};
1865 for (int hops = 0; hops < max_loop_hops && list.seen(head)
1866 && prog_.code[static_cast<std::size_t>(head)].op == opcode::jump; ++hops) {
1867 head = prog_.code[static_cast<std::size_t>(head)].primary_target;
1868 }
1869 const instr& head_instruction {prog_.code[static_cast<std::size_t>(head)]};
1870 const std::int32_t target {list.seen(head) && head_instruction.op == opcode::split
1871 ? head_instruction.secondary_target
1872 : instruction.primary_target};
1873 stack.push_back({.pc = target, .block = block}); // transfer the ref
1874 }
1875 break;
1876 case opcode::split:
1877 pool.incref(block); // one held ref -> two pushed frames
1878 stack.push_back({.pc = instruction.secondary_target, .block = block});
1879 stack.push_back({.pc = instruction.primary_target, .block = block});
1880 break;
1881 case opcode::save:
1882 {
1883 // The one write: copy-on-write off the block if shared, then record pos in the slot.
1884 const std::uint32_t written {pool.cow_write(block, instruction.arg16, pos)};
1885 stack.push_back({.pc = pc + 1, .block = written});
1886 }
1887 break;
1889 if (assertion_holds(static_cast<assert_kind>(instruction.arg8), pos, instruction.arg16 != 0U)) {
1890 stack.push_back({.pc = pc + 1, .block = block});
1891 }
1892 else {
1893 pool.decref(block); // thread dies here
1894 }
1895 break;
1897 if constexpr (requires(State & s) {
1898 s.lookaround;
1899 }) {
1900 if (lookaround_holds(instruction.arg16, pos)) {
1901 stack.push_back({.pc = pc + 1, .block = block});
1902 }
1903 else {
1904 pool.decref(block);
1905 }
1906 }
1907 else {
1908 pool.decref(block); // unreachable (this walk is instantiated only for the pool-bearing state)
1909 }
1910 break;
1911 case opcode::byte:
1912 case opcode::klass:
1913 case opcode::klass_cp:
1914 case opcode::match:
1915 list.pcs.push_back(pc);
1916 list.slots.push_back(block); // transfer the ref to the thread: one block handle per pc
1917 break;
1918 }
1919 }
1920 }
1921
1927 constexpr void cow_release_blocks(list_type& list)
1928 {
1929 for (std::size_t i = 0; i < list.pcs.size(); ++i) {
1930 state_.pool.decref(static_cast<std::uint32_t>(list.slots[i]));
1931 }
1932 }
1933
1948 [[nodiscard]] constexpr bool lookaround_holds(std::uint16_t sub_id,
1949 std::size_t pos)
1950 {
1951 const lookaround_sub& sub {prog_.lookarounds[sub_id]};
1952 const bool matched {sub.direction == look_dir::behind
1953 ? lookbehind_matches(sub, pos)
1954 : lookahead_matches(sub, pos)};
1955 return sub.negative ? !matched : matched;
1956 }
1957
1964 [[nodiscard]] constexpr bool lookahead_matches(const lookaround_sub& sub,
1965 std::size_t pos)
1966 {
1967 const std::size_t code_size {prog_.code.size()};
1968 thread_list* clist {&state_.lookaround.lists[0]};
1969 thread_list* nlist {&state_.lookaround.lists[1]};
1970 clist->reset(code_size);
1971 nlist->reset(code_size);
1972 bool matched {};
1973 sub_add_thread(*clist, sub.code_offset, pos, matched);
1974 const std::size_t limit {pos + static_cast<std::size_t>(sub.l_max)};
1975 for (std::size_t p {pos}; !matched && !clist->pcs.empty() && p < text_.size() && p < limit; ++p) {
1976 const auto byte_value {static_cast<std::uint8_t>(text_[p])};
1977 for (const std::int32_t pc : clist->pcs) {
1978 const instr& in {prog_.code[static_cast<std::size_t>(pc)]};
1979 if (in.op == opcode::klass_cp) {
1980 // A code-point predicate inside the lookahead: decode once, then enter the continuation
1981 // chain via the computed skip (same mechanism as the main VM's step).
1983 if (dc.valid && cp_class_matches(prog_.cp_classes[in.arg16], dc.cp)) {
1984 sub_add_thread(*nlist, pc + 1 + static_cast<std::int32_t>(4 - dc.length), p + 1, matched);
1985 }
1986 continue;
1987 }
1988 // Otherwise the parked pc is a byte/klass; the ternary's else assumes klass.
1989 assert((in.op == opcode::byte || in.op == opcode::klass) && "lookaround parked a non-consuming op");
1990 const bool consume {in.op == opcode::byte ? byte_value == in.arg8
1991 : prog_.classes[in.arg16].test(byte_value)};
1992 if (consume) {
1993 sub_add_thread(*nlist, pc + 1, p + 1, matched);
1994 }
1995 }
1996 thread_list* const done {clist};
1997 clist = nlist;
1998 nlist = done;
1999 nlist->reset(code_size);
2000 }
2001 return matched;
2002 }
2003
2013 [[nodiscard]] constexpr bool lookbehind_matches(const lookaround_sub& sub,
2014 std::size_t pos)
2015 {
2016 const std::size_t lmax {static_cast<std::size_t>(sub.l_max)};
2017 const std::size_t window_start {pos > lmax ? pos - lmax : 0};
2018 for (std::size_t s {pos};; --s) {
2019 // s == pos reads nothing (always a valid boundary); for s < pos the start must begin
2020 // a codepoint — not a 0x80–0xBF continuation byte — unless we are in raw-bytes mode.
2021 const bool aligned {prog_.byte_mode || s >= pos
2022 || (static_cast<std::uint8_t>(text_[s]) & 0xC0U) != 0x80U};
2023 if (aligned && sub_fullmatch_window(sub.code_offset, s, pos)) {
2024 return true;
2025 }
2026 if (s == window_start) {
2027 break; // reached the far edge; stop before s underflows past 0
2028 }
2029 }
2030 return false;
2031 }
2032
2040 [[nodiscard]] constexpr bool sub_fullmatch_window(std::int32_t code_offset,
2041 std::size_t start,
2042 std::size_t pos)
2043 {
2044 const std::size_t code_size {prog_.code.size()};
2045 thread_list* clist {&state_.lookaround.lists[0]};
2046 thread_list* nlist {&state_.lookaround.lists[1]};
2047 clist->reset(code_size);
2048 nlist->reset(code_size);
2049 bool here {false};
2050 sub_add_thread(*clist, code_offset, start, here);
2051 if (start == pos) {
2052 return here; // empty window: the sub must match the empty string exactly at pos
2053 }
2054 bool sink {false}; // matches reached before pos: collected then ignored
2055 for (std::size_t p {start}; p < pos; ++p) {
2056 if (clist->pcs.empty()) {
2057 return false;
2058 }
2059 const bool last {p + 1 == pos};
2060 bool at_pos {false};
2061 const auto byte_value {static_cast<std::uint8_t>(text_[p])};
2062 for (const std::int32_t pc : clist->pcs) {
2063 const instr& in {prog_.code[static_cast<std::size_t>(pc)]};
2064 if (in.op == opcode::klass_cp) {
2066 if (dc.valid && cp_class_matches(prog_.cp_classes[in.arg16], dc.cp)) {
2067 sub_add_thread(*nlist, pc + 1 + static_cast<std::int32_t>(4 - dc.length), p + 1,
2068 last ? at_pos : sink);
2069 }
2070 continue;
2071 }
2072 // Otherwise the parked pc is a byte/klass; the ternary's else assumes klass.
2073 assert((in.op == opcode::byte || in.op == opcode::klass) && "lookaround parked a non-consuming op");
2074 const bool consume {in.op == opcode::byte ? byte_value == in.arg8
2075 : prog_.classes[in.arg16].test(byte_value)};
2076 if (consume) {
2077 sub_add_thread(*nlist, pc + 1, p + 1, last ? at_pos : sink);
2078 }
2079 }
2080 if (last) {
2081 return at_pos; // a match counts only when it ends exactly at pos
2082 }
2083 thread_list* const done {clist};
2084 clist = nlist;
2085 nlist = done;
2086 nlist->reset(code_size);
2087 }
2088 return false; // intentionally uncovered: the p+1==pos iteration always returns above
2089 }
2090
2107 constexpr void sub_add_thread(thread_list& list,
2108 std::int32_t pc0,
2109 std::size_t pos,
2110 bool& matched)
2111 {
2112 auto& stack {state_.lookaround.stack};
2113 stack.clear();
2114 stack.push_back({.pc = pc0, .block = 0});
2115 while (!stack.empty()) {
2116 const std::int32_t pc {stack.back().pc};
2117 stack.pop_back();
2118 if (list.seen(pc)) {
2119 continue;
2120 }
2121 list.mark_seen(pc);
2122 const instr& in {prog_.code[static_cast<std::size_t>(pc)]};
2123 switch (in.op) {
2124 case opcode::jump:
2125 stack.push_back({.pc = in.primary_target, .block = 0});
2126 break;
2127 case opcode::split:
2128 stack.push_back({.pc = in.secondary_target, .block = 0});
2129 stack.push_back({.pc = in.primary_target, .block = 0});
2130 break;
2131 case opcode::save:
2132 // intentionally uncovered: a capture-free sub emits no `save`; kept as an
2133 // epsilon arm for completeness should the emission ever change.
2134 stack.push_back({.pc = pc + 1, .block = 0});
2135 break;
2137 if (assertion_holds(static_cast<assert_kind>(in.arg8), pos, in.arg16 != 0U)) {
2138 stack.push_back({.pc = pc + 1, .block = 0});
2139 }
2140 break;
2141 case opcode::match:
2142 matched = true;
2143 break;
2144 case opcode::byte:
2145 case opcode::klass:
2146 case opcode::klass_cp:
2147 list.pcs.push_back(pc);
2148 break;
2150 // intentionally uncovered: -Wswitch exhaustiveness arm; nesting is rejected at
2151 // parse time, so a sub-program never contains an assert_lookaround.
2152 break;
2153 }
2154 }
2155 }
2156 };
2157} // namespace real::detail
2158
2159#endif // REAL_PIKE_HPP
256-bit byte set with O(1) membership, fully constexpr.
static constexpr std::size_t state_budget
Cached states before a flush (the memory cap).
Definition lazy_dfa.hpp:496
The Pike VM, generic over the scratch-state container policy.
Definition pike.hpp:306
void ensure_lazy_dfa()
Ensure this iterator's lazy DFA caches are warm for the current program. Builds the shared immutables...
Definition pike.hpp:756
std::remove_reference_t< decltype(std::declval< State & >().lists[0])> list_type
The concrete thread-list type taken from the bound State.
Definition pike.hpp:698
bool confirm_at(std::string_view text, std::size_t s, OutSlots &out_slots, std::size_t &stop)
Confirm a match anchored at s: find its end with the forward DFA and fill captures with the one-pass ...
Definition pike.hpp:549
constexpr const std::uint64_t * cp_page_table(std::size_t cp_index)
Builds (once, cached) and returns the cp_class's membership bitmap over [U+0080, U+07FF] — a one-load...
Definition pike.hpp:849
constexpr bool lookbehind_matches(const lookaround_sub &sub, std::size_t pos)
Lookbehind: does the sub-pattern match a window ENDING EXACTLY at pos?
Definition pike.hpp:2013
constexpr bool word_after(std::size_t pos, bool ascii_word) const
Word-ness of the code point starting at pos — the right side of a boundary. False at the text end or ...
Definition pike.hpp:1646
constexpr pike_vm(const program_view &prog, State &state)
Binds the VM to a program and caller-owned scratch state.
Definition pike.hpp:314
constexpr bool run_codepoint_class(std::string_view text, std::size_t start, run_mode mode, OutSlots &out_slots)
Fast path for . / a negated class, optionally a greedy +.
Definition pike.hpp:1256
constexpr std::size_t next_candidate(std::string_view text, std::size_t pos, std::size_t start) const
First position >= pos that could start a match, per the hints.
Definition pike.hpp:1538
match_semantics sem_
Match semantics for the current run (match_semantics::first by default; match_semantics::longest is t...
Definition pike.hpp:693
constexpr bool lookahead_matches(const lookaround_sub &sub, std::size_t pos)
Lookahead: does the sub-pattern match a prefix starting at pos?
Definition pike.hpp:1964
constexpr void add_thread(list_type &list, std::int32_t pc0, std::size_t pos, std::uint32_t initial_block)
Adds pc0 and its whole epsilon closure to list — the one closure walk (OPT D1). Each DFS frame carrie...
Definition pike.hpp:1840
constexpr bool run_exact_literal(std::string_view text, std::size_t start, run_mode mode, OutSlots &out_slots)
Fast path for a pure-literal pattern.
Definition pike.hpp:1493
std::remove_reference_t< decltype(std::declval< State & >().pool)> pool_type
The capture-block pool type of the bound State (OPT D1) — heap-backed for dynamic,...
Definition pike.hpp:783
constexpr const std::uint8_t * class_table(std::size_t class_index)
Returns a flat 256-byte membership table for class class_index.
Definition pike.hpp:797
constexpr void fill_span_slots(OutSlots &out_slots, std::size_t match_start, std::size_t match_end) const
Writes a class-loop fast-path result into out_slots: the whole-match span in slots 0/1,...
Definition pike.hpp:879
std::string_view text_
The subject text for the current run.
Definition pike.hpp:678
constexpr bool lookaround_holds(std::uint16_t sub_id, std::size_t pos)
Evaluates a bounded lookaround at pos (true if the thread should proceed).
Definition pike.hpp:1948
constexpr std::size_t run_cascade_stop(std::string_view text, std::size_t from) const
OPT-C run tail: the next stop byte at or after from, or the text end. Kept in its own function so the...
Definition pike.hpp:895
constexpr void cow_release_blocks(list_type &list)
Releases the block references a list's threads hold (OPT D1), before the list is reset or the run ret...
Definition pike.hpp:1927
constexpr bool seed_viable(std::string_view text, std::size_t pos, std::size_t start) const
Cheap pre-check before seeding a new thread at pos.
Definition pike.hpp:1609
constexpr bool run_fixed_shape(std::string_view text, std::size_t start, run_mode mode, OutSlots &out_slots)
Fast path for a whole-pattern fixed-width byte/klass sequence.
Definition pike.hpp:1166
constexpr void step(list_type &clist, list_type &nlist, std::size_t pos, run_mode mode, bool &matched, OutSlots &out_slots)
Advances every thread of clist by the byte at pos.
Definition pike.hpp:1687
constexpr std::size_t match_byte_klass_run(std::string_view text, std::size_t pc, std::size_t s) const
Matches the run of byte/klass instructions starting at pc.
Definition pike.hpp:1078
constexpr bool fast_search(std::string_view text, std::size_t start, MatchAt match_at, OutSlots &out_slots)
Leftmost search by scanning candidate positions (first-byte hints).
Definition pike.hpp:1128
constexpr bool run_cp_class_loop(std::string_view text, std::size_t start, run_mode mode, OutSlots &out_slots)
Fast path for a whole-pattern code-point class klass_cp, optionally a greedy +.
Definition pike.hpp:992
static constexpr std::size_t lazy_dfa_min_input
Below this input length the lazy-DFA routing is skipped (the two-pass setup does not amortise on a sh...
Definition pike.hpp:702
constexpr bool run_class_loop(std::string_view text, std::size_t start, run_mode mode, OutSlots &out_slots)
Fast path for a whole-pattern "class+".
Definition pike.hpp:918
constexpr bool replay_literal(std::size_t cand, std::size_t len, OutSlots &out_slots) const
Fills capture slots for a literal match at cand.
Definition pike.hpp:1445
constexpr bool run_general(std::string_view text, std::size_t start, run_mode mode, OutSlots &out_slots, std::size_t *forward_stop=nullptr)
The general Pike VM search loop (the match semantics), factored so the lazy-DFA routing can run it on...
Definition pike.hpp:471
constexpr void sub_add_thread(thread_list &list, std::int32_t pc0, std::size_t pos, bool &matched)
Epsilon-closure for the lookaround sub-VM, on the isolated sub-scratch.
Definition pike.hpp:2107
State & state_
Borrowed reusable scratch state.
Definition pike.hpp:677
constexpr const std::size_t * thread_slots(list_type &clist, std::size_t i)
Pointer to thread i's slot_count capture values — its COW block's slots (OPT D1). Used by the match c...
Definition pike.hpp:1785
const program_view & prog_
The program being executed (borrowed; a stable lvalue that outlives the VM).
Definition pike.hpp:676
constexpr bool sub_fullmatch_window(std::int32_t code_offset, std::size_t start, std::size_t pos)
Reports whether the sub-program, run from start, reaches match EXACTLY at pos (a fullmatch of [start,...
Definition pike.hpp:2040
constexpr const std::uint8_t * cp_ascii_table(std::size_t cp_index)
Byte-indexed membership table for a cp_class's ASCII bitmap — the same one-load trick as class_table,...
Definition pike.hpp:817
constexpr bool assertion_holds(assert_kind kind, std::size_t pos, bool word_ness_flipped) const
Evaluates a zero-width assertion at pos in the current text.
Definition pike.hpp:1660
constexpr void fill_fixed_saves(std::size_t match_start, OutSlots &out_slots) const
Fills the capturing-group slots of a fixed-shape match. Every consuming op is one byte wide,...
Definition pike.hpp:1218
bool run_inner_literal(std::string_view text, std::size_t start, OutSlots &out_slots, bool &abandon)
The inner-literal search: memmem a required literal, reverse-match the prefix to the match start,...
Definition pike.hpp:594
constexpr bool word_before(std::size_t pos, bool ascii_word) const
Word-ness of the code point ending exactly at pos — the left side of a \b/\B/ </> boundary....
Definition pike.hpp:1638
constexpr bool cp_class_matches(const detail::cp_class &cc, char32_t cp) const
Tests a decoded code point against a klass_cp class: ASCII bitmap below 0x80, a binary search of the ...
Definition pike.hpp:1799
std::size_t forbid_empty_until_
Reject empty matches whose start is below this offset.
Definition pike.hpp:689
constexpr bool literal_at(std::string_view text, std::size_t cand, std::size_t len) const
Tests whether the fixed literal prefix occurs at cand.
Definition pike.hpp:1417
constexpr bool run_alternation(std::string_view text, std::size_t start, run_mode mode, OutSlots &out_slots)
Fast path for an alternation of straight-line branches.
Definition pike.hpp:1371
void ensure_immutables()
Build the forward/reverse DFAs into the reusable state on first eligible use, or rebuild them if the ...
Definition pike.hpp:713
constexpr bool run(std::string_view text, std::size_t start, run_mode mode, OutSlots &out_slots, std::size_t forbid_empty_until=0, match_semantics sem=match_semantics::first)
Runs the VM over text starting at start.
Definition pike.hpp:342
constexpr void advance_thread(list_type &clist, list_type &nlist, std::size_t i, std::int32_t next_pc, std::size_t next_pos)
Advances thread i of clist by one consumed byte, seeding its continuation's closure into nlist (OPT D...
Definition pike.hpp:1770
static constexpr std::size_t state_budget
Definition lazy_dfa.hpp:873
A lazy, priority-preserving forward DFA over the Pike program (the kFirstMatch forward pass + cache).
constexpr std::size_t find_literal(std::string_view text, std::size_t pos, std::string_view literal)
Index of the first occurrence of literal in text[pos..), or real::npos.
constexpr std::size_t find_prefix(std::string_view text, std::size_t pos, std::string_view prefix)
First position >= pos where prefix occurs in text, or npos.
constexpr decoded_codepoint decode_codepoint_strict(std::string_view text, std::size_t pos)
Strictly decodes and validates the UTF-8 sequence at text[pos].
Definition utf8.hpp:44
constexpr std::size_t find_bytes_cascade(std::string_view text, std::size_t pos, const char *set, std::uint8_t n)
Index of the first byte in text[pos..) that belongs to a small (2..4) first-byte set.
@ behind
(?<= / (?<! — the sub matches ending exactly at the position.
constexpr bool word_after(std::string_view text, std::size_t pos, bool ascii_word)
Word-ness of the code point starting at pos — the right side of a boundary. False at the text end or ...
constexpr bool word_before(std::string_view text, std::size_t pos, bool ascii_word)
Word-ness of the code point ending at pos — the left side of a boundary. False at the text start or o...
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
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
constexpr std::size_t first_high_byte(std::string_view text, std::size_t pos, std::size_t end)
Index of the first byte >= 0x80 in text[pos, end), or end if the range is pure ASCII.
constexpr std::size_t find_byte(std::string_view text, std::size_t pos, char byte)
Index of byte in text[pos..), or real::npos.
run_mode
How a VM run is anchored.
Definition pike.hpp:47
@ search
First match anywhere (Python re.search).
@ prefix
Anchored at the start position (Python re.match).
@ full
Anchored at both ends (Python re.fullmatch).
constexpr bool assertion_holds(assert_kind kind, std::string_view text, std::size_t pos, bool ascii_word)
Evaluates a zero-width assertion at pos in text.
assert_kind
Kind of zero-width assertion carried in assert_position's arg8.
Definition program.hpp:208
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
match_semantics
Which match a search returns among those starting at the leftmost position (an experimental,...
Definition program.hpp:57
@ longest
Leftmost-longest (POSIX / RE2 set_longest_match): the longest overall match wins the bounds.
@ first
Leftmost-first (Perl / Python re / the crate): source-order thread priority decides....
@ ascii
ASCII mode (re.A): \d \w \s \b stay ASCII and icase folds ASCII only, even in text mode....
The one-pass builder: decides whether a pattern is one-pass and, if so, tabulates a deterministic cap...
Search acceleration: pattern analysis and candidate-finding.
Compiled form of a pattern and the public flags / error types.
Copy-on-write pool of capture blocks (OPT D1) — the one capture-slot mechanism for both storages.
Definition pike.hpp:79
constexpr std::uint32_t cow_write(std::uint32_t b, std::uint16_t slot, std::size_t value)
Write slots(b)[slot] = value, detaching first if b is shared. Returns the block that now holds the wr...
Definition pike.hpp:135
std::uint16_t width
slot_count (values per block).
Definition pike.hpp:83
constexpr long long total_refs() const
Sum of all live refcounts — the debug Σ-invariant checks this equals the references the VM actually h...
Definition pike.hpp:156
constexpr void decref(std::uint32_t b)
Definition pike.hpp:126
FreeVec free_list
Recycled block indices (refcount hit 0).
Definition pike.hpp:82
RefVec refcount
Live references per block (non-atomic — the VM is single-threaded).
Definition pike.hpp:81
static constexpr std::uint32_t npos_block
Canonical all-npos block, shared by every seed.
Definition pike.hpp:85
constexpr std::uint32_t allocate()
A fresh block with refcount 1 (a recycled index if available, else a grown one).
Definition pike.hpp:105
constexpr std::size_t * slots(std::uint32_t b)
Pointer to block b's width slots. Invalidated by any allocate that grows data.
Definition pike.hpp:99
constexpr void reset(std::uint16_t slot_count)
Reset for a new match run: block 0 = all-npos, held by a permanent sentinel ref. The storage grows by...
Definition pike.hpp:90
constexpr void incref(std::uint32_t b)
Definition pike.hpp:121
DataVec data
Flat slot storage: block b's slots at [b*width, b*width+width).
Definition pike.hpp:80
Reusable VM scratch state.
Definition pike.hpp:236
std::int32_t table_class
Flat 256-byte membership table for the hot single-class scan, and the class index it was built for (-...
Definition pike.hpp:251
std::array< std::uint64_t, 30 > cp_page
1 where the code point (U+0080..U+07FF) is a member.
Definition pike.hpp:263
std::array< std::uint8_t, 256 > table
1 where the byte is in table_class.
Definition pike.hpp:252
EpsVec stack
Epsilon-closure DFS stack.
Definition pike.hpp:238
std::int32_t cp_page_class
Membership bitmap for a cp_class over the 2-byte UTF-8 range [U+0080, U+07FF], and the class it was b...
Definition pike.hpp:262
ThreadList lists[2]
Current and next thread lists (flipped by index).
Definition pike.hpp:237
One priority-ordered list of NFA threads (leftmost-greedy semantics).
Definition pike.hpp:183
constexpr bool seen(std::int32_t pc) const
Returns true if pc is already in this generation.
Definition pike.hpp:209
PcVec pcs
Live program counters, in priority order.
Definition pike.hpp:184
SlotVec slots
Flattened capture slots, parallel to pcs.
Definition pike.hpp:185
std::uint64_t generation
Current generation; bumped by reset.
Definition pike.hpp:187
MarkVec mark
Per-pc generation stamp (see seen).
Definition pike.hpp:186
constexpr void reset(std::size_t code_size)
Clears the list in O(1) by bumping the generation.
Definition pike.hpp:193
constexpr void mark_seen(std::int32_t pc)
Marks pc as present in the current generation.
Definition pike.hpp:218
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
std::vector< char_class > classes
Definition lazy_dfa.hpp:74
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 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
std::uint32_t lo
First code point (inclusive).
Definition program.hpp:169
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
The result of a strict UTF-8 decode: the code point, its byte length, and validity.
Definition utf8.hpp:25
std::uint32_t cp
The decoded code point (meaningful only when valid).
Definition utf8.hpp:26
std::size_t length
Bytes consumed by the sequence (1–4), or the bytes examined on failure.
Definition utf8.hpp:27
One frame on the epsilon-closure DFS stack (OPT D1): a program counter to explore,...
Definition pike.hpp:59
std::int32_t pc
The program counter to explore.
Definition pike.hpp:60
std::uint32_t block
Index of the capture block this branch carries (into the capture pool).
Definition pike.hpp:61
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
Reusable, isolated scratch for one level of lookaround evaluation (dynamic only).
Definition pike.hpp:279
thread_list lists[2]
Sub-VM thread lists (pcs only; the sub is capture-free).
Definition pike.hpp:280
std::vector< eps_entry > stack
Sub-VM epsilon-closure stack.
Definition pike.hpp:281
A bounded lookaround sub-program, referenced by assert_lookaround's arg16.
Definition program.hpp:239
std::int32_t code_offset
First instruction of the sub-program in code.
Definition program.hpp:240
std::int32_t l_max
Max bytes the sub-pattern can consume (bounded).
Definition program.hpp:242
look_dir direction
Ahead or behind.
Definition program.hpp:243
Search-acceleration hints extracted from a compiled program.
Definition program.hpp:267
std::uint8_t exact_literal_len
Length of the pure-literal match, or 0.
Definition program.hpp:296
std::int32_t inner_literal_prefix
Definition program.hpp:319
bool fixed_alternation
Whole pattern is an alternation of straight-line branches (no captures/asserts).
Definition program.hpp:286
std::int32_t greedy_cp_class
cp_class index if the whole pattern is a code-point class klass_cp (optionally +),...
Definition program.hpp:279
std::int32_t greedy_class_loop
Class index if the whole pattern is "class+", else -1.
Definition program.hpp:278
bool fixed_shape
Whole pattern is a fixed-width byte/klass sequence (no branches/asserts/captures).
Definition program.hpp:283
char_class first_bytes
All possible first bytes.
Definition program.hpp:275
std::uint8_t inner_literal_len
Definition program.hpp:318
std::array< std::uint8_t, 16 > inner_literal
A required inner literal every match must contain (the memmem candidate the inner-literal prefilter s...
Definition program.hpp:317
std::int32_t codepoint_class_ascii
ASCII-class index when the whole pattern is ./negated-class (optionally +), else -1.
Definition program.hpp:284
VM scratch state for the dynamic storage mode, plus the lookaround sub-scratch.
Definition pike.hpp:288
lookaround_scratch lookaround
Isolated sub-scratch for bounded lookaround evaluation.
Definition pike.hpp:289
std::optional< reverse_dfa > rev_dfa
OPT lazy-DFA: the reverse start-finder.
Definition pike.hpp:292
capture_pool pool
OPT D1: copy-on-write capture blocks (heap-backed).
Definition pike.hpp:290
const void * il_text
IL: the haystack il_abandoned refers to (reset the flag when it changes).
Definition pike.hpp:296
const void * il_prefix_for
IL: the prefix program il_prefix_rev was built for.
Definition pike.hpp:295
std::optional< lazy_dfa > fwd_dfa
OPT lazy-DFA: forward pass (built lazily, cache persists across a find_iter).
Definition pike.hpp:291
bool il_abandoned
IL: a linearity guard tripped on this haystack — stay on the core.
Definition pike.hpp:297
std::optional< reverse_dfa > il_prefix_rev
IL: the inner-literal prefix reverse DFA (built once per program).
Definition pike.hpp:294
const void * dfa_program
The program the DFAs were built for (rebuild if it changes).
Definition pike.hpp:293
std::span< const instr > prefix_code
IL: inner-literal prefix sub-program (the reverse start-finder). Empty unless there is a required lit...
Definition program.hpp:353
regex_immutables * immut
Per-regex DFA/one-pass cache (dynamic storage only; else null).
Definition program.hpp:361
std::span< const code_range > prefix_cp_ranges
IL: flat range buffer for prefix_cp_classes.
Definition program.hpp:356
std::uint16_t slot_count
2 * (capture groups + 1).
Definition program.hpp:357
std::span< const cp_class > prefix_cp_classes
IL: code-point classes for prefix_code.
Definition program.hpp:355
pattern_hints hints
Search-acceleration hints.
Definition program.hpp:360
bool unicode_word
\b \B < > use Unicode word-ness (text mode, not bytes / re.A).
Definition program.hpp:359
std::span< const char_class > prefix_classes
IL: classes for prefix_code.
Definition program.hpp:354
std::span< const instr > code
The instruction stream (main + lookaround regions).
Definition program.hpp:347
The per-regex immutable cache the router shares across every find_iter on a regex: the byte- program ...
Definition onepass.hpp:502
std::optional< onepass > op_table
one-pass extractor, present iff the pattern is one-pass.
Definition onepass.hpp:505
byte_program il_prefix_prog
IL: the inner-literal prefix's byte program (ineligible until built). Per-regex so the reverse DFA th...
Definition onepass.hpp:506
std::size_t il_min_haystack
IL: on a haystack that HAS a candidate, the route only fires at or above this size (0 = always)....
Definition onepass.hpp:507
Unicode \w / \d / \s property ranges and their lookups.
UTF-8 position arithmetic for match iteration.
REAL's version macros and the C++20 language-standard guard.