|
REAL
Regular Expression Algorithmic Library — constexpr C++20 regex
|
Search-acceleration hints extracted from a compiled program. More...
Public Attributes | |
| std::array< char, 16 > | prefix {} |
| Required literal prefix (possibly truncated). | |
| std::uint8_t | prefix_size {} |
| Valid bytes in prefix. | |
| bool | anchored_start {} |
\A / ^ (no multiline): only position 0. | |
| bool | line_anchored {} |
^ multiline: position 0 or after \n. | |
| bool | first_bytes_valid {} |
| False when an empty match is possible. | |
| bool | empty_match_possible {} |
The pattern can match the empty string (the nullable gate; conservative: assertions/lookarounds pass through, so e.g. ^$ is flagged nullable). | |
| bool | nullable_captured_repeat {} |
The pattern contains a CAPTURING group (not (?:...)) whose body is nullable, transitively under a quantifier (any quantifier, ? included) — e.g. (ab|)+a. AST-derived (compiler.hpp, not analyze_program/prefilter.hpp): real's engine captures the loop's last CONSUMING iteration (RE2/Rust/Go lineage) while an ECMAScript backtracker captures an extra empty final one, so real::compat routes replace/iterate to std for these patterns (real::compat::basic_regex::uses_real_traversal); conservative over-approximation (assertions/lookarounds count as nullable), so sur-flagging is safe. | |
| std::int16_t | single_first {-1} |
| The unique possible first byte, or -1. | |
| char_class | first_bytes |
| All possible first bytes. | |
| std::int32_t | greedy_class_loop {-1} |
| std::uint8_t | greedy_class_loop_end {} |
Trailing end anchor on greedy_class_loop — 0 none, 1 \Z, 2 $ (which also accepts one final newline). The match must END at that limit; the route walks the run BACKWARD from it rather than scanning forward and retrying. | |
| std::uint16_t | greedy_class_loop_min {1} |
Minimum run length (bytes) for greedy_class_loop – 1 for a bare X+, k for the X{k,} desugaring (k mandatory copies of the same atom then a loop of it, compiler.hpp's emit_repeat). | |
| std::int32_t | greedy_cp_class {-1} |
cp_class index if the whole pattern is a code-point class klass_cp (optionally +), else -1. | |
| bool | greedy_cp_class_plus {} |
| std::uint8_t | greedy_cp_class_end {} |
Trailing end anchor on greedy_cp_class — 0 none, 1 \Z, 2 $ (which also accepts one final newline). Same meaning as greedy_class_loop_end. | |
| std::uint16_t | greedy_cp_class_min {1} |
| Minimum run length (CODE POINTS, not bytes) for greedy_cp_class when greedy_cp_class_plus is set – see greedy_class_loop_min. | |
| std::int16_t | greedy_group_start {-1} |
For a class-loop wrapped in one capturing group ((\w+), ([a-z]+)): the group's start slot to mirror the whole-match start into (-1 = no enveloping group). | |
| std::int16_t | greedy_group_end {-1} |
| The enveloping group's end slot (mirrors the whole-match end). | |
| bool | fixed_shape {} |
| Whole pattern is a fixed-width byte/klass sequence (no branches/asserts/captures). | |
| std::int32_t | codepoint_class_ascii {-1} |
ASCII-class index when the whole pattern is ./negated-class (optionally +), else -1. | |
| bool | codepoint_class_plus {} |
The codepoint_class_ascii pattern is a greedy + loop (vs a single codepoint). | |
| bool | fixed_alternation {} |
| Whole pattern is an alternation of straight-line branches (no captures/asserts). | |
| std::uint8_t | exact_literal_len {} |
| Length of the pure-literal match, or 0. | |
| std::array< char, 6 > | stop_set {} |
For a whole-pattern class+ run whose accepted set has a small (<= 6-byte) complement: the STOP bytes (the complement), driving the memchr-cascade run scan. Empty unless stop_set_size is set. Placed last so it never shifts the hot fields' offsets. | |
| std::uint8_t | stop_set_size {} |
| Members in stop_set — 0 when the complement is too large, else 1..6. | |
| std::array< char, 8 > | small_set {} |
| The 2..8 possible first bytes, enumerated (the memchr-cascade set for alternation search mode: the L-SIMD masked block scan) in place of the bitmap loop. Empty unless small_set_size is set. Placed here (cold, alongside stop_set — the same memchr-cascade family) rather than near first_bytes, its earlier position: that spot was ahead of the greedy_class_loop* fields the class-loop fast path reads every call, and raising this array's cap from four to eight moved those hot fields across a cache line – a pure layout effect, not a logic change, but measurable. This array is written once at compile and read only by the alternation route, so it costs nothing to keep it out of the hot prefix. | |
| std::uint8_t | small_set_size {} |
| Members in small_set — 0 when not a small set, else 2..8. | |
| std::int16_t | rare_byte {-1} |
A required literal byte at a fixed offset from the match start that is statically rarer than the pattern's first-byte set (the date - at offset 4). The search jumps by memchr-ing this one byte (SIMD) and back-verifies from found - rare_offset, instead of the per-byte first-byte bitmap loop on a common class. -1 when no such byte was found. | |
| std::uint8_t | rare_offset {} |
| The fixed byte offset of rare_byte from the match start. | |
| std::int16_t | rare_disc {-1} |
Rare discriminant with optional mono-byte before it (URL https?://…). Search: memchr(rare_disc) (SIMD single-byte) → back-verify [prefix][opt?][disc][after] → return match start. Prefer this over a weak literal prefix (http) when the disc is rarer than the first-byte filter. Unlike rare_byte, the disc offset from match start is not fixed when rare_disc_opt is set (s? → colon at 4 or 5). -1 = unarmed. | |
| std::array< char, 8 > | rare_disc_prefix {} |
Required bytes before the optional (e.g. http). | |
| std::uint8_t | rare_disc_prefix_len {} |
| Length of rare_disc_prefix. | |
| std::int16_t | rare_disc_opt {-1} |
Optional mono-byte before the disc (s), or -1. | |
| std::array< char, 4 > | rare_disc_after {} |
Fixed bytes after the disc (e.g. //). | |
| std::uint8_t | rare_disc_after_len {} |
| Length of rare_disc_after. | |
| std::array< std::uint8_t, 16 > | inner_literal {} |
A required inner literal every match must contain (the memmem candidate the inner-literal prefilter scans for), and how many top-level children precede it — the prefix the prefilter reverse-matches from a candidate back to the match start. inner_literal_len == 0 = none; inner_literal_prefix == 0 = the literal is at the head (reverse is the identity), -1 = it is nested with no clean prefix boundary. Filled at compile from the AST (raw bytes, so pattern_hints — a core type — need not know the frontend literal type). pike_vm::run dispatches on these through its inner-literal route. | |
| std::uint8_t | inner_literal_len {} |
| Bytes held in inner_literal; 0 means the pattern has no required inner literal. | |
| std::int32_t | inner_literal_prefix {-1} |
| Top-level children before the literal; 0 = at the head, -1 = nested with no clean boundary. | |
| bool | capture_free_walk {false} |
Every save in this program writes slot 0 or slot 1, so a thread's whole capture state is the group-0 START and nothing else. | |
| std::uint8_t | fixed_shape_lo0 {} |
For a fixed_shape run that is also HOMOGENEOUS – every position accepts the identical byte set, itself expressible as <= 2 contiguous ranges ([0-9a-f]{8}, \d{4}) – the shared range bounds and run length, driving the SIMD scan+verify (SSE2/NEON) in run_fixed_shape. fixed_shape_simd_len is 0 when not eligible (mixed-class shapes, a run > 16 bytes, or a class needing > 2 ranges stay scalar). | |
| std::uint8_t | fixed_shape_hi0 {} |
| First range's high byte. | |
| std::uint8_t | fixed_shape_lo1 {1} |
| lo1 > hi1 (default 1 > 0) encodes "no second range". | |
| std::uint8_t | fixed_shape_hi1 {} |
| Second range's high byte, when one is present. | |
| std::uint8_t | fixed_shape_simd_len {} |
| The run length (1..16) when eligible, else 0. | |
| std::uint8_t | fs_end_anchor {} |
A fixed_shape whose trailing end anchor was peeled — 0 none, 1 \Z (strict end), 2 $ (end, or just before ONE final newline: Python's semantics, and why ^X$ is NOT fullmatch(X)). | |
| std::uint8_t | reserved_layout_hold [1] {} |
| The remaining held byte; see fs_end_anchor. | |
| std::int16_t | trailing_lookaround {-1} |
Trailing lookaround on a groupless greedy class+ body ([a-z]+(?=[a-z]), [0-9]+(?![0-9]), …). Index into lookarounds; -1 = not this shape. trailing_la_class holds the body's class index. Intentionally does not set greedy_class_loop (left −1) so the pure class+ call site stays a single compare — sharing that selector would make every [a-z]+ search branch on this shape too, which was measured to cost more than the route saves. | |
| std::int32_t | trailing_la_class {-1} |
| Class index for trailing_lookaround body; −1 if unset. | |
| std::array< char, 8 > | possessive_prefix {} |
possessive fast-path hints – additive, mirrors greedy_class_loop / greedy_cp_class / prefix. Scope: UNBOUNDED possessive loops only (X*+/X++, an opcode-level self-loop via jump back to itself) – a bounded count (X{n,m}+) has no "any
start within the run reaches the identical body end" invariant (the upper bound can cut different start positions at genuinely different lengths – see the per-attempt-independence divergence pinned in test_static.cpp's (a){2,4}+b), so a linear-time skip-based search cannot be built for it here; it stays on the general VM. At most one leading mandatory copy (min in {0,1}); min >= 2 also stays on the general VM. A non-empty possessive_prefix additionally requires (checked at recognition time, prefilter.hpp) that the loop's class excludes the prefix's AND suffix's leading byte – the invariant that makes the delimited/"quoted" runner's skip-to-body-end retry provably linear rather than quadratic on adversarial input (id=[a-z0-9]*+; fails this and stays general – alphanumeric prefix bytes are members of the loop's own class). | |
| std::uint8_t | possessive_prefix_size {} |
| Meaningful bytes of possessive_prefix; 0 selects the bare/suffixed shape, non-zero the delimited one. | |
| std::array< char, 8 > | possessive_suffix {} |
Required literal AFTER the loop (0 len = none; e.g. the 'x' in \d++x). | |
| std::uint8_t | possessive_suffix_size {} |
| Meaningful bytes of possessive_suffix; 0 means the loop has no required suffix. | |
| class_ref | possessive_class {} |
| The loop body's class, typed by opcode. | |
| std::int16_t | possessive_group_start {-1} |
| Enveloping single capture group's start slot (mirrors greedy_group_start), -1 = none. | |
| std::int16_t | possessive_group_end {-1} |
| The enveloping group's end slot. | |
| bool | possessive_min_nonzero {} |
True for X++/X{1,}+ (one mandatory copy present) — a search candidate MUST be in-class; false for X*+ (min 0), where a zero-length body is also a valid candidate anywhere. | |
| std::uint8_t | wb_lead {} |
Optional leading/trailing word-boundary wrap on fixed_shape / fixed_alternation / exact_literal. 0 = none; 1 = \b (assert_kind::word_boundary); 2 = \B (assert_kind::not_word_boundary). Verified in O(1) at the match start/end after the body fast-path accepts a candidate. | |
| std::uint8_t | wb_trail {} |
| bool | wb_lead_maximal_run {} |
True when a genuine leading \b was dropped by the DROP rule (a maximal greedy/possessive run can only legitimately START where the preceding character is non-word, so the runtime check is redundant – resolve_class_wb_hints). That argument silently assumes "preceding character absent" means the TRUE start of the text – sound for mode::full/mode::prefix and for search()'s own internal position-0 seed, but NOT for a caller-supplied search(text, pos, ...) with pos > 0: pos restricts where a match may START, it does not assert that text[pos - 1] is absent or non-word (Python's own re.search(pat, text, pos) does not treat pos as a virtual string start for \b purposes – verified against the live oracle, and confirmed asymmetric with endpos, which DOES act as a virtual end for a trailing \b, so only the lead side needs this guard). When true, a fast-path runner whose very first search-mode candidate coincides with start itself (no forward scan past a genuine non-word byte occurred) must fall back to an explicit assertion_holds check at that one position before accepting it — found live by differential fuzzing (\b\w+ search'd from pos>0 wrongly matched with no boundary at pos). | |
| std::uint8_t | body_pc {1} |
First consuming (byte/klass) or branch pc for fixed_shape / alternation after save 0 and an optional lead \b/\B. Default 1 (no lead wrap). | |
| std::uint16_t | alternation_branch_count {} |
| Branch count for fixed_alternation (0 when unset). Read only by the alternation route to pick a runtime STRATEGY past a literal-count threshold (Aho-Corasick) – small_set is untouched either way (still capped 2..8, still the sub-threshold scan). Placed LAST (own reason small_set / stop_set are also placed away from the hot prefix): inserting anywhere earlier reflows every field after it, including wb_lead / body_pc themselves, which fixed_shape / alternation / exact_literal all read on every match. An insertion right after small_set_size does worse than shift them: it grows the whole struct through an alignment cascade off inner_literal_prefix. Appending after the last field leaves sizeof and every existing offset untouched. | |
| std::uint8_t | inner_literal_prefix_skip {} |
Number of leading top-level children peeled before the IL reverse-prefix (a lead \b/\B on \b\w+@\w+\b). build_prefix_ast skips this many children then takes inner_literal_prefix body children. Appended last (same placement rule as alternation_branch_count) so hot-field offsets stay put. | |
| bool | literal_one_search {} |
One find_prefix answers the whole exact-literal search: every per-match step the general run_exact_literal loop takes is provably redundant for this program. | |
| std::uint8_t | fs_pair_width {} |
| HETEROGENEOUS fixed-shape pair filter: two positions, each with its own <= 2-range accepted set, used to reject 16 candidate starts per vector compare. | |
| std::uint8_t | fs_pair_off_a {} |
| Offset of the first probed position within the shape. | |
| std::uint8_t | fs_pair_off_b {} |
| Offset of the second probed position. | |
| std::uint8_t | fs_pair_a_lo0 {} |
| Position A, first range's low byte. | |
| std::uint8_t | fs_pair_a_hi0 {} |
| Position A, first range's high byte. | |
| std::uint8_t | fs_pair_a_lo1 {1} |
Position A, second range's low byte; lo1 > hi1 encodes "no second range". | |
| std::uint8_t | fs_pair_a_hi1 {} |
| Position A, second range's high byte. | |
| std::uint8_t | fs_pair_b_lo0 {} |
| Position B, first range's low byte. | |
| std::uint8_t | fs_pair_b_hi0 {} |
| Position B, first range's high byte. | |
| std::uint8_t | fs_pair_b_lo1 {1} |
| Position B, second range's low byte; same "no second range" convention. | |
| std::uint8_t | fs_pair_b_hi1 {} |
| Position B, second range's high byte. | |
| std::int32_t | il_rev_class {-1} |
IL reverse-by-class: set when the inner-literal PREFIX is exactly one greedy class loop ([a-z]+@…, \w+-…, \d+\.…), so the match start for a candidate literal at h is the start of the class run ending at h — a backward scan, no automaton. | |
| bool | il_rev_is_cp {} |
il_rev_class indexes cp_classes (a klass_cp loop) rather than classes. | |
| std::int32_t | il_fwd_class {-1} |
IL two-run confirm: set when the WHOLE pattern is class+ <literal> class+ (capture groups around either run are transparent), so a confirmed candidate needs no match engine at all. | |
| bool | il_fwd_is_cp {} |
il_fwd_class indexes cp_classes rather than classes. | |
| bool | il_cp_shape_eligible {} |
IL fixed code-point shape: the whole pattern is a fixed SEQUENCE of code-point atoms and literal bytes — \d{4}-\d{2}-\d{2} and its kin — with no loop anywhere. | |
| std::uint8_t | il_cp_prefix_cps {} |
| Code points before the literal in il_cp_shape_eligible. | |
| std::uint16_t | greedy_cp_class_max {} |
| Upper bound, in CODE POINTS, on greedy_cp_class's run, or 0 for unbounded. | |
| std::int32_t | single_class {-1} |
Class index when the WHOLE pattern is a bare single byte-class – [a-z], [aeiou], [0-9] with no quantifier at all – else -1. | |
Search-acceleration hints extracted from a compiled program.
Filled by analyze_program (prefilter.hpp). The engine consults them to skip positions that cannot start a match and to take fast paths; they never change what matches, only how fast.
greedy_* selectors, fixed_shape, fixed_alternation. The remaining ~145 bytes are per-route: inner_literal, the fixed_shape_* widths, possessive_prefix, the rare_* discriminants. A given pattern takes one route, so that tail is cold for it while being hot for whichever route it does take — which is why the tail is ordered away from the prefix rather than moved behind an indirection.Two consequences, both easy to undo by accident. Add new fields at the END: an insertion higher up reflows everything after it, and both small_set's growth and alternation_branch_count's placement record a case where doing otherwise moved hot fields across a cache line — a pure layout effect, no logic change, and measurable on patterns that read none of the new field. And do not "split this into hot and cold structs" as a fresh idea: that split is what this order already is. Removing the tail from program_view would shrink the view at the price of an indirection on every route that reads it, where materialising the dynamic view removes the whole per-search construction and costs none.
| bool real::detail::pattern_hints::capture_free_walk {false} |
Every save in this program writes slot 0 or slot 1, so a thread's whole capture state is the group-0 START and nothing else.
STRUCTURAL, not empirical: compile() emits save 0 at pc 0 and save 1 immediately before match, and emits no other save unless the pattern has a capture GROUP. So "no save past slot 1" is exactly "no groups" – but it is derived here by SCANNING the code rather than from group_count, so it holds for any producer of a program, including one added later.
What it licenses: the epsilon walk need not carry a refcounted capture block per branch. save 0 sits at pc 0, so every thread one add_thread call adds shares one start – either the one passed in, or pos if the walk crossed the head – and both are single std::size_t values in the call frame. That is why this must be a GUARD and not an assumption: a program whose save 0 sat behind a split would let one branch inherit its sibling's start, which is a wrong ANSWER rather than a slow one (tests/frontend/test_index_and_range_limits.cpp).
| std::uint8_t real::detail::pattern_hints::exact_literal_len {} |
Length of the pure-literal match, or 0.
Non-zero when the whole pattern is a fixed literal (the prefix bytes are the entire match content, possibly with internal group saves but no branches or further consuming ops). Enables a direct slot-replay bypass of the full Pike VM — the major win for "search for a fixed string".
| std::uint8_t real::detail::pattern_hints::fixed_shape_lo0 {} |
For a fixed_shape run that is also HOMOGENEOUS – every position accepts the identical byte set, itself expressible as <= 2 contiguous ranges ([0-9a-f]{8}, \d{4}) – the shared range bounds and run length, driving the SIMD scan+verify (SSE2/NEON) in run_fixed_shape. fixed_shape_simd_len is 0 when not eligible (mixed-class shapes, a run > 16 bytes, or a class needing > 2 ranges stay scalar).
First range's low byte.
| std::uint8_t real::detail::pattern_hints::fs_end_anchor {} |
A fixed_shape whose trailing end anchor was peeled — 0 none, 1 \Z (strict end), 2 $ (end, or just before ONE final newline: Python's semantics, and why ^X$ is NOT fullmatch(X)).
Only ever set together with anchored_start, and that pairing is the whole reason the end test is cheap: with the start pinned there is exactly ONE candidate position, so a failed end test owes no retry. A trailing $ WITHOUT ^ stays on the general VM.
This byte and the one below it are the pair held where retired fields sat, so that removing dead code did not reflow every field after it — see this struct's layout note. One of the two is now spent on something real; reclaiming the other is a measurement, not a cleanup.
| std::uint8_t real::detail::pattern_hints::fs_pair_width {} |
HETEROGENEOUS fixed-shape pair filter: two positions, each with its own <= 2-range accepted set, used to reject 16 candidate starts per vector compare.
fixed_shape_simd_len only fires when EVERY position accepts the identical set (a homogeneous shape like [0-9a-f]{8}), because there the "all L lanes in range" test is the verify. A shape whose positions differ — (?i)cafe (position 0 accepts {c,C}, position 3 {e,E}), [0-9]{2}:[0-9]{2}, \w\d\w\d — got no vector help at all and scanned one byte at a time. This pair filters instead of verifying: the two chosen positions must both match, then the ordinary per-position walk confirms the candidate.
The pair is the two most selective positions (smallest accepted-set cardinality, ties broken toward the widest separation, so the two probes decorrelate). fs_pair_width is the shape's byte width, 0 when ineligible — set only when fixed_shape_simd_len is 0 (the homogeneous path already has its own, better, fused scan), the width is 2..16, and every position resolved to 1..2 ranges. lo1 > hi1 encodes "no second range", the same convention as fixed_shape_lo1. Appended last (same placement rule as alternation_branch_count). The shape's byte width (2..16); 0 when the pair filter is ineligible.
| std::int32_t real::detail::pattern_hints::greedy_class_loop {-1} |
Class index if the whole pattern is "class+", else -1.
| std::uint16_t real::detail::pattern_hints::greedy_cp_class_max {} |
Upper bound, in CODE POINTS, on greedy_cp_class's run, or 0 for unbounded.
\w+ and \w{8,} are unbounded and leave this 0; \w{8} sets it to 8, which is what lets the route accept a counted repeat at all. Without it the recognizer had to decline X{k} — the route knew how to extend greedily and bound the result from BELOW, and an exact count needs the run stopped from above, since \w{8} over a nine-letter word matches the first eight and not the nine.
APPENDED LAST, and that placement is the change rather than a detail: this struct's hot fields are read per call on the class-loop path, and inserting a field mid-struct has twice cost this project double-digit regressions on patterns that read none of it (see alternation_branch_count and the six IL fields above).
| bool real::detail::pattern_hints::greedy_cp_class_plus {} |
The greedy_cp_class pattern is a greedy + loop (vs a single code point).
| bool real::detail::pattern_hints::il_cp_shape_eligible {} |
IL fixed code-point shape: the whole pattern is a fixed SEQUENCE of code-point atoms and literal bytes — \d{4}-\d{2}-\d{2} and its kin — with no loop anywhere.
fixed_shape and its own route already cover the case where that sequence is fixed-width in BYTES, which a klass_cp never is (a Unicode \d matches multi-byte digits). But the code-point COUNT is fixed, so the match start is still arithmetic: step il_cp_prefix_cps code points back from the candidate literal, then one forward walk verifies every atom and fills every capture. No loop means no reverse walk to bound and no engine to run.
| std::int32_t real::detail::pattern_hints::il_fwd_class {-1} |
IL two-run confirm: set when the WHOLE pattern is class+ <literal> class+ (capture groups around either run are transparent), so a confirmed candidate needs no match engine at all.
With il_rev_class placing the start by walking the prefix class back from the literal, the rest of the match is the suffix class run forward from the literal's end — and every capture slot is one of four positions (s, the literal's start, the literal's end, e). What this buys is aimed at a storage with NO per-regex cache: a dynamic regex confirms a candidate through its lazy DFA and one-pass table, while static_regex has neither and falls back to the general VM – most of its work then being thread management and the copy-on-write capture pool, on a pattern that may have no capture groups at all. This shape confirms without an engine, so that cost disappears rather than being reduced.
-1 when the suffix is not a single greedy class loop reaching the end of the pattern.
| std::int32_t real::detail::pattern_hints::il_rev_class {-1} |
IL reverse-by-class: set when the inner-literal PREFIX is exactly one greedy class loop ([a-z]+@…, \w+-…, \d+\.…), so the match start for a candidate literal at h is the start of the class run ending at h — a backward scan, no automaton.
This is the middle case between a fixed_shape program (fixed-width throughout, so the start is arithmetic) and the general reverse pass (a reverse DFA over the prefix sub-program, which lives in the per-regex immutables). It needs neither: no sub-program, no DFA, no allocation, so a storage with no immutables — static_regex — can run it, which is the point. Leftmost semantics hold because the run's beginning IS the leftmost start for that candidate, and confirm_at verifies forward regardless, so a rejected candidate costs an advance and never a wrong match.
-1 when the prefix is not that shape (a fixed repeat count has no split, so \d{4}-… is excluded here and stays on the general path).
| std::array<std::uint8_t, 16> real::detail::pattern_hints::inner_literal {} |
A required inner literal every match must contain (the memmem candidate the inner-literal prefilter scans for), and how many top-level children precede it — the prefix the prefilter reverse-matches from a candidate back to the match start. inner_literal_len == 0 = none; inner_literal_prefix == 0 = the literal is at the head (reverse is the identity), -1 = it is nested with no clean prefix boundary. Filled at compile from the AST (raw bytes, so pattern_hints — a core type — need not know the frontend literal type). pike_vm::run dispatches on these through its inner-literal route.
The literal's bytes, the first inner_literal_len of which are meaningful.
| bool real::detail::pattern_hints::literal_one_search {} |
One find_prefix answers the whole exact-literal search: every per-match step the general run_exact_literal loop takes is provably redundant for this program.
Set when ALL of: exact_literal_len >= 2 (a 1-byte literal goes through find_byte, not find_prefix); prefix_size == exact_literal_len (so find_prefix matches the whole literal, making literal_at's re-compare redundant); the program carries no assert_position (an exact-literal program legally may — \bdog, ^dog, dog\b, see extract_prefix — and each occurrence must then be checked at its own position, which is exactly the retry the general loop exists for); and neither anchored_start, line_anchored nor rare_disc is armed (each takes an earlier branch in next_candidate, so the candidate would not come from find_prefix).
Folded into one bit deliberately: every term is a property of the compiled program, never of the subject, so evaluating them per match charges the shapes that FAIL the guard for a decision that cannot change between matches. The caller adds only slot_count == 2, which lives on the program rather than here. Assertion-freeness is computed directly from code and deliberately not inferred from wb_lead / anchored_start / line_anchored — an assert kind those hints do not represent would make that inference silently unsound. Appended last (same placement rule as alternation_branch_count).
| std::array<char, 8> real::detail::pattern_hints::possessive_prefix {} |
possessive fast-path hints – additive, mirrors greedy_class_loop / greedy_cp_class / prefix. Scope: UNBOUNDED possessive loops only (X*+/X++, an opcode-level self-loop via jump back to itself) – a bounded count (X{n,m}+) has no "any
start within the run reaches the identical body end" invariant (the upper bound can cut different start positions at genuinely different lengths – see the per-attempt-independence divergence pinned in test_static.cpp's (a){2,4}+b), so a linear-time skip-based search cannot be built for it here; it stays on the general VM. At most one leading mandatory copy (min in {0,1}); min >= 2 also stays on the general VM. A non-empty possessive_prefix additionally requires (checked at recognition time, prefilter.hpp) that the loop's class excludes the prefix's AND suffix's leading byte – the invariant that makes the delimited/"quoted" runner's skip-to-body-end retry provably linear rather than quadratic on adversarial input (id=[a-z0-9]*+; fails this and stays general – alphanumeric prefix bytes are members of the loop's own class).
Required literal BEFORE the loop (0 len = none; the delimited/"quoted" shape).
| std::int16_t real::detail::pattern_hints::rare_disc {-1} |
Rare discriminant with optional mono-byte before it (URL https?://…). Search: memchr(rare_disc) (SIMD single-byte) → back-verify [prefix][opt?][disc][after] → return match start. Prefer this over a weak literal prefix (http) when the disc is rarer than the first-byte filter. Unlike rare_byte, the disc offset from match start is not fixed when rare_disc_opt is set (s? → colon at 4 or 5). -1 = unarmed.
Discriminant byte (e.g. :).
| std::int32_t real::detail::pattern_hints::single_class {-1} |
Class index when the WHOLE pattern is a bare single byte-class – [a-z], [aeiou], [0-9] with no quantifier at all – else -1.
greedy_class_loop describes class+ and carries no "single" flag, unlike its two neighbours (greedy_cp_class_plus, codepoint_class_plus). Without this field the unquantified form matches no batchable selector and crosses a full route entry PER match, where all three class routes cross one per BATCH – which made a single-byte class slower per byte than ., a pattern that matches at every position.
A SEPARATE field rather than a flag on greedy_class_loop, and that is not a stylistic choice: sharing that selector forces every pure class+ call site to branch on the other shape too, which was measured to cost more than it saves (see trailing_lookaround, which declines to arm it for the same reason).
Scope is the 4-opcode program exactly (save 0, klass, save 1, match): no capture wrap, no \b wrap, no anchor. A single literal BYTE (a) is excluded – it takes exact_literal, a different route with its own memchr scan.
APPENDED LAST, per this struct's placement rule.
| std::uint8_t real::detail::pattern_hints::wb_lead {} |
Optional leading/trailing word-boundary wrap on fixed_shape / fixed_alternation / exact_literal. 0 = none; 1 = \b (assert_kind::word_boundary); 2 = \B (assert_kind::not_word_boundary). Verified in O(1) at the match start/end after the body fast-path accepts a candidate.
Leading wrap: 0 none, 1 \b, 2 \B — asserted at the match start.
| std::uint8_t real::detail::pattern_hints::wb_trail {} |
Trailing wrap, same encoding as wb_lead — asserted at the match end.