|
REAL
Regular Expression Algorithmic Library — constexpr C++20 regex
|
The per-regex immutable cache the router shares across every find_iter on a regex: the byte program (klass_cp expanded to the deterministic trie) and, when the pattern is one-pass, the extractor table. More...
#include <onepass.hpp>
Public Member Functions | |
| bool | row_ready (std::size_t i) const noexcept |
Reads the "filled" flag for flag index i, acquiring what the filling thread released. | |
| void | set_row_ready (std::size_t i) noexcept |
Publishes the "filled" flag for flag index i. | |
| regex_immutables ()=default | |
| An empty cache: every identity key null, nothing built. | |
| regex_immutables (const regex_immutables &) noexcept | |
| Copies as an EMPTY cache: a copied regex is an independent regex. | |
| regex_immutables (regex_immutables &&) noexcept | |
| Moves as an empty cache, for the same reason as the copy constructor. | |
| void | invalidate_all () noexcept |
| Clears EVERY identity key, so nothing built for the old program survives an assignment. | |
| regex_immutables & | operator= (const regex_immutables &) noexcept |
| Keeps this object's cache STORAGE but marks it invalid. | |
| regex_immutables & | operator= (regex_immutables &&) noexcept |
| Invalidates as the copy assignment does, and for the same reason. | |
| constexpr | ~regex_immutables () |
Runtime erase of this regex's shared DFA slot (reclaims match-time caches). Constexpr paths skip the map entirely — is_constant_evaluated so dynamic_storage::compile / static_assert stay valid. | |
Public Attributes | |
| byte_program | byte_prog |
| klass_cp-expanded byte program (empty until built). | |
| lazy_byte_alphabet | alphabet |
| byte-class alphabet of byte_prog (shared by both DFAs, else recomputed per scan). | |
| std::optional< onepass > | op_table |
| one-pass extractor, present iff the pattern is one-pass. | |
| byte_program | il_prefix_prog |
| IL: the inner-literal prefix's byte program (ineligible until built). Per-regex so the reverse DFA that spans it is a cheap shared wrapper, not a per-find_iter rebuild. | |
| std::size_t | il_min_haystack {} |
| IL cold floor: first candidate-scan on this regex only fires at or above this size when the haystack HAS a match (0 = always). Warm scans use il_warm_floor (shared reverse DFA in shared_dfa_slot). Checked ONLY after the first memmem hit — no-match is never gated. Scaled by prefix byte-program size; see pike_vm::run_inner_literal. | |
| std::vector< std::uint8_t > | class_rows |
| Byte-indexed membership rows, filled ON FIRST USE of each class and kept for the regex's life. | |
| std::vector< std::uint8_t > | cp_ascii_rows |
| One 256-byte row per cp_class: its ASCII half. | |
| std::vector< std::uint64_t > | cp_page_rows |
One 30-word bitmap per cp_class: [U+0080, U+07FF]. | |
| std::atomic< std::uint64_t > | row_ready_bits {0} |
| "This row is filled" flags: one bit per row, three runs packed into one word, plus an overflow vector for the runs that do not fit. | |
| std::vector< std::atomic< char > > | row_ready_overflow |
| Flags for row indices at or past row_ready_bit_capacity. | |
| std::size_t | cp_ascii_ready_at {0} |
| Where the cp_ascii run starts in the flag index space. | |
| std::size_t | cp_page_ready_at {0} |
| std::atomic< const void * > | rows_for {nullptr} |
prog.code.data() the rows above were SIZED for, or null. Same identity discipline as built_for, and independent of it: the rows are needed by scan routes that never build the DFA caches. | |
| std::optional< ac_automaton > | ac |
The multi-literal automaton for a fixed_alternation past the branch threshold, or empty when never built or declined (a pathological icase-fold expansion). Per REGEX, not per state: a state is fresh per search(), so holding it there rebuilds the whole automaton on every call — a fast path costing orders of magnitude more than the route it replaces. | |
| std::atomic< const void * > | ac_for {nullptr} |
prog.code.data() ac was built for, or null. Its OWN identity atomic, deliberately not folded into built_for — only the alternation route consults the automaton, and this cache's own history records what bundling a route-specific product into the shared flag cost every other route (see op_table_for). | |
| std::atomic< const void * > | op_table_for {nullptr} |
prog.code.data() op_table was built for, or null. Same identity discipline as rows_for and for the same reason: the extractor is needed only by the routes that fill captures through it, and it is by far the most expensive thing this cache holds — more than the byte program and the lazy DFA together. Bundling it into built_for makes every route that needs only the byte program pay for it, including a 2-slot pattern with no capture to extract at all. | |
| std::atomic< const void * > | built_for {nullptr} |
prog.code.data() this cache was built for, or null if never built / invalidated. Hot path: one atomic load. Not once_flag — assignment reuses this object under a new program; a spent once_flag would never rebuild (silent wrong matches). | |
Static Public Attributes | |
| static constexpr std::size_t | row_ready_bit_capacity {64} |
| How many flag indices row_ready_bits covers; the rest live in row_ready_overflow. | |
The per-regex immutable cache the router shares across every find_iter on a regex: the byte program (klass_cp expanded to the deterministic trie) and, when the pattern is one-pass, the extractor table.
Built under program-identity invalidation (built_for), so a const regex used from many threads builds race-free, and assigning onto a warmed regex rebuilds for the new program.
The mutable lazy-DFA transition caches live in a process-wide side table (shared_dfa_slot), keyed by this object's address and guarded by a per-slot mutex — so this struct stays free of std::mutex and of the members one would bring. Address reuse of the immutables object invalidates the slot via reset_shared_dfas; a program change at the same address is caught by built_for (see pike_vm::ensure_immutables). The destructor erases this address's map entry, so match-time caches never outlive the regex.
|
inlinenoexcept |
|
inlinenoexcept |
Clears EVERY identity key, so nothing built for the old program survives an assignment.
There are four keys and every one of them must be cleared, because the identity check they perform can PASS on a stale cache: copy-assigning the program's vector reuses its buffer, so code.data() is unchanged and a key left pointing at it still matches. A cache kept that way then serves the PREVIOUS pattern's product – for ac_for, the previous alternation's automaton, which answers matches on a subject the new pattern does not match and none on a subject it does. Silent wrong answers in both directions, not a crash.
That is why the invariant is "an assignment invalidates every cache" rather than "every cache with a known reproducer": one of the four has no reproducer today and is cleared all the same.
|
inlinenoexcept |
Keeps this object's cache STORAGE but marks it invalid.
The destination's program is already the new one by the time storage assignment reaches here, so this cannot rebuild from the source's program and must not inherit its built state: clearing built_for forces pike_vm's ensure_immutables to rebuild. Self-assignment-safe.
*this.
|
inlinenoexcept |
Invalidates as the copy assignment does, and for the same reason.
*this.
|
inlinenoexcept |
Reads the "filled" flag for flag index i, acquiring what the filling thread released.
| [in] | i | Flag index: a class row, or a cp_ascii / cp_page row at its own run's offset. |
true once the row's contents are visible to this thread.
|
inlinenoexcept |
Publishes the "filled" flag for flag index i.
Called only by the thread holding immut_build_mu, so the read-modify-write on the bit word cannot race another writer.
| [in] | i | Flag index, in the same space row_ready reads. |
| std::vector<std::uint8_t> real::detail::regex_immutables::class_rows |
Byte-indexed membership rows, filled ON FIRST USE of each class and kept for the regex's life.
Deriving a 256-entry row into the VM state charges every short search, because search() builds a fresh state and the derivation lands in it. Filling every row at compile instead charges patterns that never touch most of their classes – a negated class interns a dozen and a short search reads one. Per class, on demand, cached per regex is the only arrangement that pays for neither.
THREAD SAFETY. rows_for identifies the program the rows were sized for, exactly as built_for does for the rest of this cache, so a copied or reassigned regex re-sizes rather than reading a stale table. A row's flag is release-stored after the row is filled under immut_build_mu and acquire-loaded before the row is read; only the lock holder that observed the flag clear ever writes a row, so a published row is immutable and readers race with no one. One 256-byte row per interned BYTE class.
| std::size_t real::detail::regex_immutables::cp_page_ready_at {0} |
As cp_ascii_ready_at, for cp_page.
| std::atomic<std::uint64_t> real::detail::regex_immutables::row_ready_bits {0} |
"This row is filled" flags: one bit per row, three runs packed into one word, plus an overflow vector for the runs that do not fit.
A fresh real::regex pays this whole block per construction, so the bit word matters more than it looks: as three separate std::vector<std::atomic<char>>, a pattern with no cp_class still allocates two one-element vectors, and first use pays for both. Bits cover the runs that fit in one word and allocate nothing; row_ready_overflow carries the rest. The check is not on a hot path — the VM state remembers its last verified row, so this is read on a miss, not per call.
A std::vector of atomics rather than a unique_ptr array: unique_ptr's destructor is not constexpr, and this struct must stay a literal type for real::regex to be usable in a constant expression. std::atomic_ref over a plain vector would say it more directly but is absent from this clang's libc++.