REAL
Regular Expression Algorithmic Library — constexpr C++20 regex
Loading...
Searching...
No Matches
prefilter.hpp
Go to the documentation of this file.
1
12#ifndef REAL_PREFILTER_HPP
13#define REAL_PREFILTER_HPP
14
15// Internal — do not include directly.
16// Users: #include <real/real.hpp> (or the documented opt-ins <real/dfa.hpp>, <real/std/regex.hpp>).
17
18#include "real/version.hpp"
19
20#include <cstdint>
21#include <cstring>
22#include <span>
23#include <string_view>
24#include <type_traits>
25#include <vector>
26
28#include "real/core/program.hpp"
29
30namespace real::detail {
31
44 constexpr bool is_fixed_alternation(std::span<const instr> code)
45 {
46 const std::size_t code_size {code.size()};
47 if (code_size < 7 || code[0].op != opcode::save || code[code_size - 2].op != opcode::save ||
48 code[code_size - 1].op != opcode::match) {
49 return false;
50 }
51 const std::size_t exit {code_size - 2};
52 std::size_t pc {1};
53 std::int32_t branches {};
54 while (true) {
55 const bool is_split {code[pc].op == opcode::split};
56 std::size_t branch_end {is_split ? static_cast<std::size_t>(code[pc].primary_target) : pc};
57 std::int32_t branch_width {};
58 while (branch_end < exit && (code[branch_end].op == opcode::byte || code[branch_end].op == opcode::klass)) {
59 ++branch_end;
60 ++branch_width;
61 }
62 if (branch_width == 0) {
63 return false;
64 }
65 ++branches;
66 if (is_split) {
67 // A non-final branch ends with `jump exit`; continue at the split's y.
68 if (branch_end >= exit || code[branch_end].op != opcode::jump ||
69 code[branch_end].primary_target != static_cast<std::int32_t>(exit)) {
70 return false;
71 }
72 pc = static_cast<std::size_t>(code[pc].secondary_target);
73 if (pc >= exit) {
74 return false;
75 }
76 }
77 else {
78 // The final branch falls straight through to the exit (save 1).
79 return branch_end == exit && branches >= 2;
80 }
81 }
82 }
83
88 constexpr void extract_anchoring(std::span<const instr> code,
89 pattern_hints& hints)
90 {
91 std::size_t pc {};
92 while (code[pc].op == opcode::save) {
93 ++pc;
94 }
95 if (code[pc].op == opcode::assert_position) {
96 const auto kind {static_cast<assert_kind>(code[pc].arg8)};
99 }
100 }
101
112 constexpr void extract_prefix(std::span<const instr> code,
113 pattern_hints& hints)
114 {
115 std::size_t prefix_pc {};
116 while (hints.prefix_size < hints.prefix.size()) {
117 if (code[prefix_pc].op == opcode::save || code[prefix_pc].op == opcode::assert_position) {
118 ++prefix_pc;
119 continue;
120 }
121 if (code[prefix_pc].op != opcode::byte) {
122 break;
123 }
124 hints.prefix[hints.prefix_size] = static_cast<char>(code[prefix_pc].arg8);
125 ++hints.prefix_size;
126 ++prefix_pc;
127 }
128
129 if (hints.prefix_size > 0) {
130 bool has_inter_or_trailing_assert {};
131 bool seen_byte {};
132 for (std::size_t i = 0; i < code.size() && !has_inter_or_trailing_assert; ++i) {
133 if (code[i].op == opcode::byte) {
134 seen_byte = true;
135 }
136 else if (seen_byte && code[i].op == opcode::assert_position) {
137 has_inter_or_trailing_assert = true;
138 }
139 else if (seen_byte && code[i].op == opcode::match) {
140 break;
141 }
142 }
143 if (!has_inter_or_trailing_assert) {
144 std::size_t q {prefix_pc};
145 while (q < code.size() && code[q].op == opcode::save) {
146 ++q;
147 }
148 if (q < code.size() && code[q].op == opcode::match) {
149 hints.exact_literal_len = hints.prefix_size;
150 }
151 }
152 }
153 }
154
162 constexpr void compute_first_bytes(std::span<const instr> code,
163 std::span<const char_class> classes,
164 std::span<const cp_class> cp_classes,
165 pattern_hints& hints)
166 {
167 std::vector<unsigned char> visited(code.size(), 0); // unsigned char, not vector<bool> (constexpr, faster)
168 std::vector<std::int32_t> stack;
169 stack.push_back(0);
170 bool empty_match_possible {};
171 while (!stack.empty()) {
172 const std::int32_t current_pc {stack.back()};
173 stack.pop_back();
174 if (visited[static_cast<std::size_t>(current_pc)] != 0) {
175 continue;
176 }
177 visited[static_cast<std::size_t>(current_pc)] = 1;
178 const instr& instruction {code[static_cast<std::size_t>(current_pc)]};
179 switch (instruction.op) {
180 case opcode::save:
183 stack.push_back(current_pc + 1);
184 break;
185 case opcode::jump:
186 stack.push_back(instruction.primary_target);
187 break;
188 case opcode::split:
189 stack.push_back(instruction.primary_target);
190 stack.push_back(instruction.secondary_target);
191 break;
192 case opcode::byte:
193 hints.first_bytes.set(instruction.arg8);
194 break;
195 case opcode::klass:
196 hints.first_bytes.merge(classes[instruction.arg16]);
197 break;
198 case opcode::klass_cp: {
199 // A code-point predicate: its effective ASCII members (a `\W`-style complement is already
200 // materialised into the bitmap) plus every UTF-8 lead byte a non-ASCII member could begin
201 // with -- a sound superset of the possible first bytes.
202 const cp_class& cc {cp_classes[static_cast<std::size_t>(instruction.arg16)]};
203 hints.first_bytes.merge(cc.ascii);
207 break;
208 }
209 case opcode::match:
210 empty_match_possible = true;
211 break;
212 }
213 }
214 hints.first_bytes_valid = !empty_match_possible && !hints.first_bytes.empty();
215 hints.empty_match_possible = empty_match_possible;
216 }
217
223 constexpr void detect_fast_shapes(std::span<const instr> code,
224 std::span<const char_class> classes,
225 std::int32_t cp_mark_ascii,
226 std::int32_t cp_mark_offset,
227 pattern_hints& hints)
228 {
229 // "class+" shape: save 0, klass, split(back to the klass, exit),
230 // save 1, match -- greedy only (the lazy variant has different
231 // semantics) and no capture groups.
232 // "class+", optionally wrapped in exactly ONE capturing group: save 0, [group-start save,] klass,
233 // split(back to the klass, exit), [group-end save,] save 1, match. Greedy only (the lazy variant
234 // differs). An enveloping group ((\w+), ([a-z]+)) has span == the whole match by
235 // construction, so the fast path mirrors the bounds into the group's slots -- no re-match.
236 {
237 std::size_t p {0};
238 std::int16_t gs {-1};
239 if (p < code.size() && code[p].op == opcode::save) {
240 ++p;
241 if (p < code.size() && code[p].op == opcode::save) {
242 gs = static_cast<std::int16_t>(code[p].arg16);
243 ++p;
244 }
245 if (p + 1 < code.size() && code[p].op == opcode::klass && code[p + 1].op == opcode::split &&
246 code[p + 1].primary_target == static_cast<std::int32_t>(p) &&
247 code[p + 1].secondary_target == static_cast<std::int32_t>(p + 2)) {
248 const std::int32_t cls {code[p].arg16};
249 std::size_t q {p + 2};
250 std::int16_t ge {-1};
251 bool ok {gs < 0};
252 if (gs >= 0 && q < code.size() && code[q].op == opcode::save) {
253 ge = static_cast<std::int16_t>(code[q].arg16);
254 ++q;
255 ok = true;
256 }
257 if (ok && q + 1 < code.size() && code[q].op == opcode::save && code[q + 1].op == opcode::match &&
258 q + 2 == code.size()) {
259 hints.greedy_class_loop = cls;
260 hints.greedy_group_start = gs;
261 hints.greedy_group_end = ge;
262 }
263 }
264 }
265 }
266
267 // Code-point class (klass_cp + its three `klass` continuations), optional greedy `+`, optionally
268 // wrapped in one capturing group: save 0, [group-start save,] klass_cp, klass, klass, klass,
269 // [split back,] [group-end save,] save 1, match. A \w/\d/\s run scanned code point by code point.
270 {
271 std::size_t p {0};
272 std::int16_t gs {-1};
273 if (p < code.size() && code[p].op == opcode::save) {
274 ++p;
275 if (p < code.size() && code[p].op == opcode::save) {
276 gs = static_cast<std::int16_t>(code[p].arg16);
277 ++p;
278 }
279 if (p + 3 < code.size() && code[p].op == opcode::klass_cp && code[p + 1].op == opcode::klass &&
280 code[p + 2].op == opcode::klass && code[p + 3].op == opcode::klass) {
281 const std::int32_t cp_idx {code[p].arg16};
282 const std::size_t loop_pc {p};
283 std::size_t q {p + 4};
284 bool plus {false};
285 if (q < code.size() && code[q].op == opcode::split &&
286 code[q].primary_target == static_cast<std::int32_t>(loop_pc) &&
287 code[q].secondary_target == static_cast<std::int32_t>(q + 1)) {
288 plus = true;
289 ++q;
290 }
291 std::int16_t ge {-1};
292 bool ok {gs < 0};
293 if (gs >= 0 && q < code.size() && code[q].op == opcode::save) {
294 ge = static_cast<std::int16_t>(code[q].arg16);
295 ++q;
296 ok = true;
297 }
298 if (ok && q + 1 < code.size() && code[q].op == opcode::save && code[q + 1].op == opcode::match &&
299 q + 2 == code.size()) {
300 hints.greedy_cp_class = cp_idx;
301 hints.greedy_cp_class_plus = plus;
302 hints.greedy_group_start = gs;
303 hints.greedy_group_end = ge;
304 }
305 }
306 }
307 }
308
309 // "fixed shape": a straight-line run of fixed-width byte/klass consuming ops, possibly interleaved
310 // with capturing saves ((\d{4})-(\d{2})-(\d{2}), (a)(b)), with no branches or assertions. The
311 // whole match is fixed width, so one walk verifies it; because every width is fixed, each save sits
312 // at a compile-time-constant offset from the match start, so the fast path fills each group slot by
313 // that offset (no re-match). Covers class{n} and mixed sequences; pure literals hit the exact-literal
314 // path first. A klass_cp (Unicode shorthand, variable width), split/jump (alternation, {n,m}/+/*/?),
315 // `.` or a negated class (byte-level branches), and lookarounds all break the run, so they never form
316 // this shape -- ASCII / explicit-class fixed widths are what qualify.
317 {
318 std::size_t i {};
319 std::int32_t width {};
320 std::int32_t open_groups {}; // capturing groups (slots >= 2) currently open, for the nesting guard
321 bool closed {}; // saw the closing save (slot 1)
322 bool nested {}; // a group opened inside another -- kept on the general VM (flat only)
323 if (i < code.size() && code[i].op == opcode::save && code[i].arg16 == 0) {
324 ++i; // opening save (slot 0)
325 while (i < code.size()) {
326 const opcode op {code[i].op};
327 if (op == opcode::byte || op == opcode::klass) {
328 ++width;
329 ++i;
330 }
331 else if (op == opcode::save) {
332 const std::int32_t slot {code[i].arg16};
333 if (slot == 1) {
334 closed = true;
335 }
336 else if (slot >= 2 && (slot % 2) == 0) { // an inner group's opening save
337 if (open_groups > 0) {
338 nested = true;
339 }
340 ++open_groups;
341 }
342 else if (slot >= 3) { // an inner group's closing save
343 --open_groups;
344 }
345 ++i;
346 }
347 else {
348 break; // any other op (split/jump/klass_cp/assert/lookaround) disqualifies the shape
349 }
350 }
351 if (width >= 1 && closed && !nested && i + 1 == code.size() && code[i].op == opcode::match) {
352 hints.fixed_shape = true;
353 }
354 }
355 }
356
357 // Whole pattern is a single codepoint class (`.`/negated class), optionally a
358 // greedy `+`. Layout: save 0, the 16-instruction codepoint block (at 1..16),
359 // then either save 1, match (bare, 19 instructions) or split(loop, exit),
360 // save 1, match (the `+`, 20 instructions). No captures; `*` is excluded
361 // because its empty match rules out a consuming fast path.
362 if ((code.size() == 19 || code.size() == 20) && code[0].op == opcode::save) {
363 // The ASCII sub-class index comes from the marker the compiler set when it
364 // emitted the block (emit_codepoint_class) — we no longer reverse-engineer the
365 // 16-instruction bytecode shape here. The whole-program size / `+`-loop checks
366 // are program structure; the ASCII-only test is class content; neither depends
367 // on the block's internal opcode layout.
368 std::int32_t ascii {(cp_mark_ascii >= 0 && cp_mark_offset == 1
369 && static_cast<std::size_t>(cp_mark_ascii) < classes.size())
370 ? cp_mark_ascii
371 : -1};
372 // Content guard: the recorded ASCII sub-class must hold ASCII bytes only.
373 // Provably unreachable today — `ast.hpp::parse_class_item` rejects any class
374 // member >= 0x80 and `char_class::invert_ascii` leaves the high bytes (>= 0x80)
375 // cleared (non-ASCII codepoints are matched via the UTF-8 multi-byte branches),
376 // so the marked sub-class is always pure ASCII. Kept deliberately: unlike the
377 // bytecode-shape recognition this replaced, it is a *content* check that stays
378 // robust to layout changes and becomes load-bearing again if a Unicode
379 // codepoint-class mode is ever added.
380 if (ascii >= 0) {
381 const char_class& ascii_class {classes[static_cast<std::size_t>(ascii)]};
382 for (int byte {0x80}; byte <= 0xFF; ++byte) {
383 if (ascii_class.test(static_cast<std::uint8_t>(byte))) {
384 ascii = -1; // a high byte would mean a non-ASCII sub-class (see guard above)
385 break;
386 }
387 }
388 }
389 const bool bare {code.size() == 19 && code[17].op == opcode::save &&
390 code[18].op == opcode::match};
391 const bool plus {code.size() == 20 && code[17].op == opcode::split && code[17].primary_target == 1 &&
392 code[18].op == opcode::save && code[19].op == opcode::match};
393 if (ascii >= 0 && (bare || plus)) {
395 hints.codepoint_class_plus = plus;
396 }
397 }
398
399 // Whole pattern is an alternation of straight-line branches.
400 if (is_fixed_alternation(code)) {
401 hints.fixed_alternation = true;
402 }
403 }
404
412 constexpr std::uint16_t byte_frequency(std::uint8_t b)
413 {
414 constexpr std::array<std::uint16_t, 26> lower {
415 650, 150, 300, 350, 1000, 200, 180, 450, 550, 15, 80, 350, 250,
416 550, 600, 170, 10, 450, 500, 700, 250, 100, 200, 15, 180, 8};
417 if (b >= 'a' && b <= 'z') {
418 return lower[b - 'a'];
419 }
420 if (b >= 'A' && b <= 'Z') {
421 return static_cast<std::uint16_t>((lower[b - 'A'] / 6) + 3);
422 }
423 if (b >= '0' && b <= '9') {
424 return 120;
425 }
426 switch (b) {
427 case ' ': return 1500;
428 case '.': return 120;
429 case '\n': case ',': return 100;
430 case '/': return 60;
431 case '(': case ')': case '_': return 50;
432 case '"': case '\'': return 45;
433 case '\t': case '=': case '-': return 40;
434 case ':': return 35;
435 case '>': case '<': return 30;
436 case ';': case '*': return 25;
437 case '{': case '}': return 22;
438 case '[': case ']': case '+': return 20;
439 case '?': case '!': case '|': return 15;
440 case '%': return 12;
441 case '&': return 10;
442 case '#': case '\\': case '$': return 8;
443 case '~': case '@': case '^': case '`': return 3;
444 default: return 1; // control and high bytes: assume very rare
445 }
446 }
447
460 constexpr void extract_rare_byte(std::span<const instr> code,
461 pattern_hints& hints)
462 {
463 if (hints.anchored_start || hints.prefix_size >= 2) {
464 return; // anchored needs no scan; a literal prefix is already a stronger filter
465 }
466 std::size_t pc {0};
467 std::size_t offset {0};
468 std::int16_t best_byte {-1};
469 std::uint8_t best_off {0};
470 std::uint16_t best_freq {0xFFFFU};
471 while (pc < code.size() && offset <= 0xFFU) {
472 const opcode op {code[pc].op};
473 if (op == opcode::save || op == opcode::assert_position) {
474 ++pc;
475 continue;
476 }
477 if (op == opcode::byte) {
478 const auto freq {byte_frequency(static_cast<std::uint8_t>(code[pc].arg8))};
479 if (freq < best_freq) {
480 best_freq = freq;
481 best_byte = static_cast<std::int16_t>(static_cast<std::uint8_t>(code[pc].arg8));
482 best_off = static_cast<std::uint8_t>(offset);
483 }
484 ++offset;
485 ++pc;
486 }
487 else if (op == opcode::klass) {
488 ++offset; // a byte class consumes exactly one byte: the offset stays fixed
489 ++pc;
490 }
491 else {
492 break; // klass_cp (variable width) / split / jump / match: the offset is no longer fixed
493 }
494 }
495 if (best_byte < 0) {
496 return;
497 }
498 // Effective commonness of the current first-byte filter: a single byte's frequency, or the sum over a
499 // class (a class is only as selective as the total traffic it stops on).
500 std::uint32_t first_freq {0};
501 if (hints.single_first >= 0) {
502 first_freq = byte_frequency(static_cast<std::uint8_t>(hints.single_first));
503 }
504 else {
505 for (int b = 0; b < 256; ++b) {
506 if (hints.first_bytes.test(static_cast<std::uint8_t>(b))) {
507 first_freq += byte_frequency(static_cast<std::uint8_t>(b));
508 }
509 }
510 }
511 if (best_freq < 100U && static_cast<std::uint32_t>(best_freq) * 4U < first_freq) {
512 hints.rare_byte = best_byte;
513 hints.rare_offset = best_off;
514 }
515 }
516
529 constexpr pattern_hints analyze_program(std::span<const instr> code,
530 std::span<const char_class> classes,
531 std::span<const cp_class> cp_classes,
532 std::int32_t cp_mark_ascii,
533 std::int32_t cp_mark_offset)
534 {
535 pattern_hints hints;
536
537 // A lookaround forces the general Pike VM: no DFA, no fast path. Detected up front;
538 // the fast-path hints are cleared at the end so none can fire even partially. This is a local,
539 // not a persisted hint: nothing past analyze_program ever reads it (the fast paths and the DFA
540 // gate on the individual hints this clears / on the program itself).
541 bool has_lookaround {false};
542 for (const instr& in : code) {
543 if (in.op == opcode::assert_lookaround) {
544 has_lookaround = true;
545 break;
546 }
547 }
548
549 extract_anchoring(code, hints);
550
551 extract_prefix(code, hints);
552
553 compute_first_bytes(code, classes, cp_classes, hints);
554
555 detect_fast_shapes(code, classes, cp_mark_ascii, cp_mark_offset, hints);
556
557 // A lookaround program never takes a fast path or the DFA: the general VM must run so
558 // the sub-VM can evaluate the assertion. Clear every fast-path hint (belt-and-suspenders
559 // — the structural detectors above already miss these shapes). The literal prefix /
560 // first-byte set below stay valid (and sound) filters.
561 if (has_lookaround) {
562 hints.greedy_class_loop = -1;
563 hints.exact_literal_len = 0;
564 hints.fixed_shape = false;
565 hints.codepoint_class_ascii = -1;
566 hints.fixed_alternation = false;
567 }
568
569 if (hints.prefix_size > 0) {
570 hints.single_first = static_cast<unsigned char>(hints.prefix[0]);
571 }
572 else if (hints.first_bytes_valid) {
573 // Enumerate the set, stopping once it exceeds four. A single member drives find_byte (one memchr);
574 // two-to-four members drive the memchr-cascade (small_set); five or more stay on the bitmap loop.
575 std::array<char, 4> members {};
576 int count {0};
577 for (unsigned byte = 0; byte < 256; ++byte) {
578 if (hints.first_bytes.test(static_cast<std::uint8_t>(byte))) {
579 if (count < 4) {
580 members[static_cast<std::size_t>(count)] = static_cast<char>(byte);
581 }
582 ++count;
583 if (count > 4) {
584 break;
585 }
586 }
587 }
588 if (count == 1) {
589 hints.single_first = static_cast<std::int16_t>(static_cast<unsigned char>(members[0]));
590 }
591 else if (count >= 2 && count <= 4) {
592 hints.small_set = members;
593 hints.small_set_size = static_cast<std::uint8_t>(count);
594 }
595 }
596
597 // OPT-C: for a whole-pattern `class+` run (run_class_loop — a byte-wise scan), record the STOP
598 // bytes (the complement of the accepted set) when there are at most six of them, so the run can
599 // advance by a memchr-cascade to the next stop instead of testing every byte. The stops are derived
600 // from the class table and do NOT enter the compiled program, so byte-identity is unaffected.
601 if (hints.greedy_class_loop >= 0) {
602 const char_class& accepted {classes[static_cast<std::size_t>(hints.greedy_class_loop)]};
603 std::array<char, 6> stops {};
604 int stop_count {0};
605 for (unsigned byte = 0; byte < 256; ++byte) {
606 if (!accepted.test(static_cast<std::uint8_t>(byte))) {
607 if (stop_count < 6) {
608 stops[static_cast<std::size_t>(stop_count)] = static_cast<char>(byte);
609 }
610 ++stop_count;
611 if (stop_count > 6) {
612 break;
613 }
614 }
615 }
616 if (stop_count >= 1 && stop_count <= 6) {
617 hints.stop_set = stops;
618 hints.stop_set_size = static_cast<std::uint8_t>(stop_count);
619 }
620 }
621 // OPT-C-1b: a whole-pattern code-point-class run (`.`/`[^x]` in text/ascii mode) accepts EVERY valid
622 // code point >= 0x80 — run_codepoint_class validates the UTF-8 structure but not membership above
623 // ASCII — so once its ASCII complement is small the run can be SWAR-accelerated soundly: memchr the
624 // ASCII stops for an upper bound, high-bit-scan the ASCII stretches, and drop to code-point
625 // validation only across a non-ASCII cluster (so malformed UTF-8 still stops the run, unchanged). The
626 // stops here are only the ASCII bytes the class rejects.
627 else if (hints.codepoint_class_ascii >= 0) {
628 const char_class& accepted {classes[static_cast<std::size_t>(hints.codepoint_class_ascii)]};
629 std::array<char, 6> stops {};
630 int stop_count {0};
631 for (unsigned byte = 0; byte < 0x80; ++byte) {
632 if (!accepted.test(static_cast<std::uint8_t>(byte))) {
633 if (stop_count < 6) {
634 stops[static_cast<std::size_t>(stop_count)] = static_cast<char>(byte);
635 }
636 ++stop_count;
637 if (stop_count > 6) {
638 break;
639 }
640 }
641 }
642 if (stop_count >= 1 && stop_count <= 6) {
643 hints.stop_set = stops;
644 hints.stop_set_size = static_cast<std::uint8_t>(stop_count);
645 }
646 }
647
648 // OPT: a required rare literal byte at a fixed offset (e.g. the `-` in `[0-9]{4}-[0-9]{2}-[0-9]{2}`)
649 // gives a single-byte memchr target far more selective than the first-byte class. Computed last, so it
650 // can compare against the finalized first-byte hints. Sound: it only filters candidate starts.
651 extract_rare_byte(code, hints);
652 return hints;
653 }
654
665 constexpr std::size_t find_byte(std::string_view text,
666 std::size_t pos,
667 char byte)
668 {
669 if (pos >= text.size()) {
670 return npos;
671 }
672 if (!std::is_constant_evaluated()) {
673 const void* hit {std::memchr(text.data() + pos, byte, text.size() - pos)};
674 return hit == nullptr
675 ? npos
676 : static_cast<std::size_t>(static_cast<const char*>(hit) - text.data());
677 }
678 for (std::size_t i = pos; i < text.size(); ++i) {
679 if (text[i] == byte) {
680 return i;
681 }
682 }
683 return npos;
684 }
685
694 constexpr std::size_t find_literal(std::string_view text,
695 std::size_t pos,
696 std::string_view literal)
697 {
698 if (literal.empty()) {
699 return pos <= text.size() ? pos : npos;
700 }
701 if (literal.size() == 1U) {
702 return find_byte(text, pos, literal.front());
703 }
704 if (text.size() < literal.size()) {
705 return npos;
706 }
707 const std::size_t last_start {text.size() - literal.size()};
708 std::size_t i {pos};
709 while (i <= last_start) {
710 const std::size_t hit {find_byte(text, i, literal.front())};
711 if (hit == npos || hit > last_start) {
712 return npos;
713 }
714 if (text.compare(hit, literal.size(), literal) == 0) {
715 return hit;
716 }
717 i = hit + 1U;
718 }
719 return npos;
720 }
721
733 constexpr std::size_t find_prefix(std::string_view text,
734 std::size_t pos,
735 std::string_view prefix)
736 {
737 if (prefix.empty()) {
738 return pos;
739 }
740 if (pos >= text.size()) {
741 return npos;
742 }
743 const auto off {text.substr(pos).find(prefix)};
744 if (off == std::string_view::npos) {
745 return npos;
746 }
747 return pos + off;
748 }
749
766 constexpr std::size_t find_bytes_cascade(std::string_view text,
767 std::size_t pos,
768 const char* set,
769 std::uint8_t n)
770 {
771 if (pos >= text.size()) {
772 return npos;
773 }
774 if (!std::is_constant_evaluated()) {
775 const char* const base {text.data()};
776 std::size_t window {text.size() - pos}; // narrows to [pos, best) as hits are found
777 std::size_t best {npos};
778 for (std::uint8_t i = 0; i < n; ++i) {
779 const void* hit {std::memchr(base + pos, set[i], window)};
780 if (hit != nullptr) {
781 const std::size_t idx {static_cast<std::size_t>(static_cast<const char*>(hit) - base)};
782 if (idx < best) {
783 best = idx;
784 window = idx - pos; // subsequent members need only search before the current best
785 }
786 }
787 }
788 return best;
789 }
790 for (std::size_t i = pos; i < text.size(); ++i) {
791 for (std::uint8_t j = 0; j < n; ++j) {
792 if (text[i] == set[j]) {
793 return i;
794 }
795 }
796 }
797 return npos;
798 }
799
813 constexpr std::size_t first_high_byte(std::string_view text,
814 std::size_t pos,
815 std::size_t end)
816 {
817 if (!std::is_constant_evaluated()) {
818 const char* const base {text.data()};
819 std::size_t i {pos};
820 for (; i + 8 <= end; i += 8) {
821 std::uint64_t word {};
822 std::memcpy(&word, base + i, 8);
823 if ((word & 0x8080808080808080ULL) != 0U) {
824 for (std::size_t b = 0; b < 8; ++b) {
825 if (static_cast<std::uint8_t>(base[i + b]) >= 0x80U) {
826 return i + b;
827 }
828 }
829 }
830 }
831 for (; i < end; ++i) {
832 if (static_cast<std::uint8_t>(base[i]) >= 0x80U) {
833 return i;
834 }
835 }
836 return end;
837 }
838 for (std::size_t i = pos; i < end; ++i) {
839 if (static_cast<std::uint8_t>(text[i]) >= 0x80U) {
840 return i;
841 }
842 }
843 return end;
844 }
845} // namespace real::detail
846
847#endif // REAL_PREFILTER_HPP
256-bit byte set with O(1) membership, fully constexpr.
constexpr char_class utf8_lead3_set()
The lead-byte set of a 3-byte UTF-8 sequence.
constexpr void detect_fast_shapes(std::span< const instr > code, std::span< const char_class > classes, std::int32_t cp_mark_ascii, std::int32_t cp_mark_offset, pattern_hints &hints)
Detects the whole-pattern fast-path shapes and sets their hint flags: class+, fixed-shape straight ru...
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 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.
constexpr char_class utf8_lead2_set()
The lead-byte set of a 2-byte UTF-8 sequence.
constexpr char_class utf8_lead4_set()
The lead-byte set of a 4-byte UTF-8 sequence.
constexpr void extract_anchoring(std::span< const instr > code, pattern_hints &hints)
Records start anchoring: the first non-save instruction tells whether every match must begin at posit...
Definition prefilter.hpp:88
constexpr void extract_rare_byte(std::span< const instr > code, pattern_hints &hints)
Finds a required literal byte at a FIXED offset that is statically far rarer than the pattern's first...
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 void compute_first_bytes(std::span< const instr > code, std::span< const char_class > classes, std::span< const cp_class > cp_classes, pattern_hints &hints)
Computes the possible first-byte set by a DFS over the epsilon closure of pc 0.
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.
constexpr std::uint16_t byte_frequency(std::uint8_t b)
Approximate static frequency of a byte in mixed English + source text (occurrences per 10000; higher ...
constexpr pattern_hints analyze_program(std::span< const instr > code, std::span< const char_class > classes, std::span< const cp_class > cp_classes, std::int32_t cp_mark_ascii, std::int32_t cp_mark_offset)
Walks a compiled program once to derive its search hints.
@ prefix
Anchored at the start position (Python re.match).
assert_kind
Kind of zero-width assertion carried in assert_position's arg8.
Definition program.hpp:208
@ line_start
^ with multiline.
@ text_start
\A, and ^ without multiline.
constexpr void extract_prefix(std::span< const instr > code, pattern_hints &hints)
Collects the required literal prefix and the exact-literal fast-path length.
constexpr bool is_fixed_alternation(std::span< const instr > code)
Tests whether the whole program is an alternation of straight-line branches (e.g. the|fox|dog).
Definition prefilter.hpp:44
opcode
NFA instruction opcodes executed by the Pike VM.
Definition program.hpp:189
@ 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
@ ascii
ASCII mode (re.A): \d \w \s \b stay ASCII and icase folds ASCII only, even in text mode....
Compiled form of a pattern and the public flags / error types.
A set of byte values (0–255) as a 256-bit bitmap.
Definition charclass.hpp:30
constexpr void merge(const char_class &other)
Unions other into this set.
Definition charclass.hpp:73
constexpr bool test(std::uint8_t byte) const
Tests membership of byte byte.
constexpr void set(std::uint8_t byte)
Adds byte byte to the set.
Definition charclass.hpp:37
constexpr bool empty() const
Reports whether the set has no members.
A match-time code-point class for the klass_cp opcode: an ASCII bitmap for code points < 0x80 plus a ...
Definition program.hpp:179
One NFA instruction. Field meaning depends on op.
Definition program.hpp:251
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
bool greedy_cp_class_plus
The greedy_cp_class pattern is a greedy + loop (vs a single code point).
Definition program.hpp:280
std::int16_t rare_byte
A required literal byte at a fixed offset from the match start that is statically rarer than the patt...
Definition program.hpp:308
std::array< char, 16 > prefix
Required literal prefix (possibly truncated).
Definition program.hpp:268
bool fixed_alternation
Whole pattern is an alternation of straight-line branches (no captures/asserts).
Definition program.hpp:286
bool anchored_start
\A / ^ (no multiline): only position 0.
Definition program.hpp:270
std::uint8_t prefix_size
Valid bytes in prefix.
Definition program.hpp:269
std::int16_t greedy_group_start
For a class-loop wrapped in one capturing group ((\w+), ([a-z]+)): the group's start slot to mirror t...
Definition program.hpp:281
std::array< char, 6 > stop_set
For a whole-pattern class+ run whose accepted set has a small (<= 6-byte) complement: the STOP bytes ...
Definition program.hpp:301
bool line_anchored
^ multiline: position 0 or after \n.
Definition program.hpp:271
std::int16_t greedy_group_end
The enveloping group's end slot (mirrors the whole-match end).
Definition program.hpp:282
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
bool empty_match_possible
The pattern can match the empty string (the nullable gate; conservative: assertions/lookarounds pass ...
Definition program.hpp:273
std::int32_t greedy_class_loop
Class index if the whole pattern is "class+", else -1.
Definition program.hpp:278
std::uint8_t small_set_size
Number of valid members in small_set — 0 when not a small set, else 2..4. Drives the memchr-cascade s...
Definition program.hpp:277
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
bool codepoint_class_plus
The codepoint_class_ascii pattern is a greedy + loop (vs a single codepoint).
Definition program.hpp:285
std::uint8_t stop_set_size
Members in stop_set — 0 when the complement is too large, else 1..6.
Definition program.hpp:302
bool first_bytes_valid
False when an empty match is possible.
Definition program.hpp:272
std::array< char, 4 > small_set
The 2..4 possible first bytes, enumerated (the memchr-cascade set). Empty unless small_set_size is se...
Definition program.hpp:276
std::uint8_t rare_offset
The fixed byte offset of rare_byte from the match start.
Definition program.hpp:309
std::int32_t codepoint_class_ascii
ASCII-class index when the whole pattern is ./negated-class (optionally +), else -1.
Definition program.hpp:284
std::int16_t single_first
The unique possible first byte, or -1.
Definition program.hpp:274
REAL's version macros and the C++20 language-standard guard.