20#ifndef REAL_ONEPASS_HPP
21#define REAL_ONEPASS_HPP
58 std::vector<onepass_edge>
edge;
78 static constexpr std::uint32_t
no_node {0xFFFFFFFFU};
93 bail(
"the byte-program is itself ineligible (a lookaround, or a word-ness-flipped assertion)");
119 [[nodiscard]]
const std::vector<onepass_node>&
nodes()
const
125 [[nodiscard]] std::uint8_t
class_of(std::uint8_t
byte)
const
150 template <
typename OutSlots>
151 [[nodiscard]]
bool extract(std::string_view text,
160 std::uint32_t node {0};
161 for (std::size_t pos = s; pos < e; ++pos) {
162 const std::uint8_t cls {
alpha_.
of[
static_cast<std::uint8_t
>(text[pos])]};
164 if (!edge.assigned) {
167 if (edge.assert_mask != 0 && !
asserts_hold(edge.assert_mask, text, pos)) {
170 for (std::uint64_t m = edge.cap_mask; m != 0; m &= m - 1) {
171 out[
static_cast<std::size_t
>(std::countr_zero(m))] = pos;
175 if (!
nodes_[node].matches) {
181 for (std::uint64_t m =
nodes_[node].match_cap_mask; m != 0; m &= m - 1) {
182 out[
static_cast<std::size_t
>(std::countr_zero(m))] = e;
189 std::string_view text,
190 std::size_t pos)
const
192 for (std::uint32_t m = mask; m != 0; m &= m - 1) {
193 const auto kind {
static_cast<assert_kind>(std::countr_zero(m))};
206 constexpr void bail(
const char* reason,
207 std::int32_t node = -1,
208 std::int32_t
klass = -1,
209 std::int32_t pc = -1)
219 constexpr std::uint32_t
node_of(std::int32_t pc,
220 std::vector<std::int32_t>& queue)
222 std::uint32_t&
id {
pc_to_node_[
static_cast<std::size_t
>(pc)]};
224 id =
static_cast<std::uint32_t
>(
nodes_.size());
227 nodes_.push_back(std::move(fresh));
242 class_cover_.assign(classes_.size(), {});
243 for (std::size_t i = 0; i < classes_.size(); ++i) {
244 std::vector<std::uint16_t>& cover {class_cover_[i]};
245 for (
unsigned b = 0; b < 256U; ++b) {
246 if (classes_[i].test(
static_cast<std::uint8_t
>(b))) {
247 cover.push_back(alpha_.of[b]);
250 std::ranges::sort(cover);
251 cover.erase(std::ranges::unique(cover).begin(), cover.end());
254 std::size_t max_slot {0};
257 max_slot = std::max(max_slot,
static_cast<std::size_t
>(in.arg16));
260 slot_count_ = max_slot + 1;
261 if (slot_count_ > max_slots) {
262 bail(
"too many capture slots: the general Pike VM keeps these",
static_cast<std::int32_t
>(slot_count_));
266 pc_to_node_.assign(bp.
code.size(), no_node);
267 std::vector<std::int32_t> queue;
269 while (!queue.empty() && eligible_) {
270 const std::int32_t pc {queue.back()};
272 std::vector<char> on_path(bp.
code.size(), 0);
273 build_edges(pc, 0, 0, on_path, pc_to_node_[
static_cast<std::size_t
>(pc)], queue);
274 if (nodes_.size() > max_nodes) {
275 bail(
"node cap exceeded",
static_cast<std::int32_t
>(nodes_.size()));
284 std::size_t
bytes {0};
288 if (
bytes > max_bytes_) {
289 bail(
"one-pass table too large after minimization (MB)",
static_cast<std::int32_t
>(
bytes >> 20));
301 const std::size_t n {nodes_.size()};
302 std::vector<std::uint32_t> cls(n, 0);
303 std::uint32_t classes {0};
305 std::vector<std::vector<std::uint64_t>> sigs(n);
306 for (std::size_t i = 0; i < n; ++i) {
307 std::vector<std::uint64_t>& s {sigs[i]};
309 s.push_back(nodes_[i].matches ? 1U : 0U);
310 s.push_back(nodes_[i].match_cap_mask);
311 s.push_back(nodes_[i].match_assert_mask);
313 s.push_back(e.assigned ?
static_cast<std::uint64_t
>(cls[e.next]) + 1U : 0U);
314 s.push_back(e.cap_mask);
315 s.push_back(e.assert_mask);
318 std::vector<std::uint32_t> next_cls(n, 0);
319 std::vector<std::vector<std::uint32_t>> buckets(minimize_buckets);
320 std::uint32_t next {0};
321 for (std::size_t i = 0; i < n; ++i) {
322 std::vector<std::uint32_t>& bucket {buckets[sig_hash(sigs[i]) % minimize_buckets]};
323 std::uint32_t
id {no_node};
324 for (
const std::uint32_t j : bucket) {
325 if (sigs[j] == sigs[i]) {
331 next_cls[i] = next++;
332 bucket.push_back(
static_cast<std::uint32_t
>(i));
338 cls = std::move(next_cls);
339 if (next == classes) {
346 std::vector<std::int32_t> rep(classes, -1);
347 for (std::size_t i = 0; i < n; ++i) {
348 if (rep[cls[i]] < 0) {
349 rep[cls[i]] =
static_cast<std::int32_t
>(i);
352 std::vector<std::uint32_t> class_id(classes, no_node);
353 class_id[cls[0]] = 0;
354 std::uint32_t assigned {1};
355 for (std::uint32_t c = 0; c < classes; ++c) {
356 if (class_id[c] == no_node) {
357 class_id[c] = assigned++;
360 std::vector<onepass_node> merged(classes);
361 for (std::uint32_t c = 0; c < classes; ++c) {
362 const onepass_node& r {nodes_[
static_cast<std::size_t
>(rep[c])]};
365 nn.match_cap_mask = r.match_cap_mask;
366 nn.match_assert_mask = r.match_assert_mask;
368 for (std::uint16_t x = 0; x < alpha_.count; ++x) {
369 if (r.edge[x].assigned) {
371 .cap_mask = r.edge[x].cap_mask,
372 .assert_mask = r.edge[x].assert_mask,
377 nodes_ = std::move(merged);
383 static constexpr std::size_t
sig_hash(
const std::vector<std::uint64_t>& v)
385 std::uint64_t h {1469598103934665603ULL};
386 for (
const std::uint64_t x : v) {
387 h = (h ^ x) * 1099511628211ULL;
389 return static_cast<std::size_t
>(h);
395 std::uint64_t cap_mask,
396 std::uint32_t assert_mask,
397 std::vector<char>& on_path,
398 std::uint32_t node_id,
399 std::vector<std::int32_t>& queue)
404 if (on_path[
static_cast<std::size_t
>(pc)] != 0) {
405 bail(
"epsilon cycle (a nullable loop): not one-pass", -1, -1, pc);
408 on_path[
static_cast<std::size_t
>(pc)] = 1;
409 const instr& in {code_[
static_cast<std::size_t
>(pc)]};
412 case opcode::klass: {
413 const std::uint32_t next {node_of(pc + 1, queue)};
418 const auto write_edge {[&](std::uint16_t cls) {
421 && (slot.next != next || slot.cap_mask != cap_mask
422 || slot.assert_mask != assert_mask)) {
423 bail(
"byte-class conflict: not one-pass",
static_cast<std::int32_t
>(node_id),
428 .cap_mask = cap_mask,
429 .assert_mask = assert_mask,
433 if (in.op == opcode::byte) {
434 if (!write_edge(alpha_.of[
static_cast<std::uint8_t
>(in.arg8)])) {
439 for (
const std::uint16_t cls : class_cover_[in.arg16]) {
440 if (!write_edge(cls)) {
447 case opcode::match: {
449 if (node.matches && (node.match_cap_mask != cap_mask || node.match_assert_mask != assert_mask)) {
450 bail(
"second distinct match: not one-pass",
static_cast<std::int32_t
>(node_id));
454 node.match_cap_mask = cap_mask;
455 node.match_assert_mask = assert_mask;
459 build_edges(in.primary_target, cap_mask, assert_mask, on_path, node_id, queue);
460 build_edges(in.secondary_target, cap_mask, assert_mask, on_path, node_id, queue);
463 build_edges(in.primary_target, cap_mask, assert_mask, on_path, node_id, queue);
466 build_edges(pc + 1, cap_mask | (std::uint64_t {1} << in.arg16), assert_mask, on_path, node_id, queue);
468 case opcode::assert_position:
471 build_edges(pc + 1, cap_mask, assert_mask | (std::uint32_t {1} << in.arg8), on_path, node_id, queue);
474 bail(
"unexpected op in byte-program (lookaround/klass_cp should be absent)");
477 on_path[
static_cast<std::size_t
>(pc)] = 0;
486 std::size_t slot_count_ {0};
487 std::size_t max_bytes_ {max_table_bytes};
488 bool ascii_word_ {
true};
489 std::int32_t bail_node_ {-1};
490 std::int32_t bail_class_ {-1};
491 std::int32_t bail_pc_ {-1};
492 bool eligible_ {
true};
507 std::size_t il_min_haystack {};
Zero-width assertion evaluation (^ $ \b \B \A \Z < >) as free functions over a subject and a positi...
Builds and holds the one-pass classification (and table, when eligible) of a byte-program.
std::uint16_t num_classes() const
std::size_t slot_count() const
The number of capture slots (group 0 start/end plus each group's).
const std::string & bail_reason() const
std::uint8_t class_of(std::uint8_t byte) const
The byte-class of byte, for a runtime that walks this table.
bool asserts_hold(std::uint32_t mask, std::string_view text, std::size_t pos) const
Whether every assertion in mask (a set of assert_kind bits) holds at pos in text.
static constexpr std::size_t max_slots
Slot-pointer cap: group 0 + four user groups.
constexpr void build(const byte_program &bp)
std::span< const char_class > classes_
constexpr void build_edges(std::int32_t pc, std::uint64_t cap_mask, std::uint32_t assert_mask, std::vector< char > &on_path, std::uint32_t node_id, std::vector< std::int32_t > &queue)
Walk the epsilon-closure from pc, writing this node's edges. on_path detects epsilon cycles (a nullab...
static constexpr std::size_t sig_hash(const std::vector< std::uint64_t > &v)
FNV-1a hash of a partition signature (for the constexpr-friendly bucket dedup). Accumulates in a fixe...
std::vector< std::uint32_t > pc_to_node_
pc -> node id (or no_node).
std::span< const instr > code_
std::size_t node_count() const
constexpr void bail(const char *reason, std::int32_t node=-1, std::int32_t klass=-1, std::int32_t pc=-1)
Reject as not one-pass, recording a category and the offending node / byte-class / pc (kept as intege...
static constexpr std::size_t minimize_buckets
Hash buckets for the Moore-refinement dedup.
static constexpr std::uint32_t no_node
"No node yet" sentinel in the pc->node map.
std::vector< onepass_node > nodes_
constexpr void minimize()
Moore partition refinement of the one-pass automaton: merge nodes that are behaviourally identical (s...
constexpr onepass(const byte_program &bp, std::size_t max_bytes=max_table_bytes)
const std::vector< onepass_node > & nodes() const
static constexpr std::size_t max_nodes
Node cap (RE2's), a memory/DoS bound.
lazy_byte_alphabet alpha_
bool extract(std::string_view text, std::size_t s, std::size_t e, OutSlots &out) const
Fills out with the capture slots of the one-pass match on text[s, e) — the single left-to- right pass...
constexpr std::uint32_t node_of(std::int32_t pc, std::vector< std::int32_t > &queue)
Get-or-create the node whose entry pc is pc, enqueueing a fresh one for the flood.
static constexpr std::size_t max_table_bytes
Table-memory cap (~8 MB): larger declines to the VM.
std::vector< std::vector< std::uint16_t > > class_cover_
char-class index -> the byte-classes it consumes.
A lazy, priority-preserving forward DFA over the Pike program (the kFirstMatch forward pass + cache).
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.
constexpr lazy_byte_alphabet compute_lazy_alphabet(std::span< const instr > code, std::span< const char_class > classes)
Partition 0..255 by the program's consuming predicates (every klass test, every byte literal)....
@ 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.
constexpr std::size_t npos
Sentinel for "no position" / unset capture slot (akin to std::string::npos).
@ bytes
Binary mode: . and [^…] match raw bytes, not codepoints.
Compiled form of a pattern and the public flags / error types.
A byte-level program derived from a Pike program for the DFA passes: every klass_cp construct is expa...
bool eligible
Representable by the byte DFAs / Tier-A one-pass.
std::vector< char_class > classes
std::vector< instr > code
One NFA instruction. Field meaning depends on op.
Byte-class alphabet over a Pike program: bytes that satisfy exactly the same byte/klass predicates sh...
std::uint16_t count
number of distinct classes.
std::array< std::uint8_t, 256 > of
byte -> class index.
One outgoing edge of a one-pass node, for a byte-class: the next node and the capture slots that take...
std::uint64_t cap_mask
Bit i set => write the current position into slot i on this edge.
bool assigned
Whether this byte-class has an edge from this node.
std::uint32_t assert_mask
Bit k (an assert_kind) set => that assertion must hold at the position to take this edge (Tier-B).
std::uint32_t next
Next node id (valid only when assigned).
A one-pass node: one edge per byte-class, plus whether the run may end here and with what captures....
std::uint32_t match_assert_mask
Assertions that must hold at the end for the match (Tier-B).
std::vector< onepass_edge > edge
Indexed by byte-class.
std::uint64_t match_cap_mask
Slots written when the match is taken.
bool matches
Reaching match from here (via epsilon).
The per-regex immutable cache the router shares across every find_iter on a regex: the byte- program ...
lazy_byte_alphabet alphabet
byte-class alphabet of byte_prog (shared by both DFAs, else recomputed per scan).
std::once_flag once
guards the one-time build.
~regex_immutables()=default
regex_immutables(regex_immutables &&) noexcept
std::optional< onepass > op_table
one-pass extractor, present iff the pattern is one-pass.
regex_immutables & operator=(regex_immutables &&) noexcept
byte_program il_prefix_prog
IL: the inner-literal prefix's byte program (ineligible until built). Per-regex so the reverse DFA th...
regex_immutables & operator=(const regex_immutables &) noexcept
regex_immutables(const regex_immutables &) noexcept
regex_immutables()=default
byte_program byte_prog
klass_cp-expanded byte program (empty until built).