REAL
Regular Expression Algorithmic Library — constexpr C++20 regex
Loading...
Searching...
No Matches
dfa.hpp
Go to the documentation of this file.
1
28#ifndef REAL_DFA_HPP
29#define REAL_DFA_HPP
30
31#include "real/version.hpp"
32
33#include <array>
34#include <cstddef>
35#include <cstdint>
36#include <limits>
37#include <optional>
38#include <span>
39#include <stdexcept>
40#include <string>
41#include <string_view>
42#include <vector>
43
44#include "real/core/config.hpp"
45#include "real/real.hpp"
46
47namespace real {
48
57 class dfa_error : public std::runtime_error
58 {
59 public:
60
65 explicit dfa_error(const std::string& message)
66 : std::runtime_error(message)
67 {}
68 };
69
73 struct dfa_match
74 {
75 std::uint32_t rule_index;
76 std::size_t length;
77 };
78
79 namespace detail {
80
82 struct dfa_instr
83 {
85 std::uint8_t arg8 {};
86 std::uint32_t klass {0};
87 std::int64_t primary {-1};
88 std::int64_t secondary {-1};
89 };
90
92 struct dfa_nfa
93 {
94 std::vector<dfa_instr> code;
95 std::vector<char_class> classes;
96 std::vector<std::int64_t> accept_rule;
97 std::vector<std::size_t> entry;
98 std::size_t rule_count {0};
99 };
100
105 inline dfa_nfa dfa_flatten(std::span<const program_view> programs)
106 {
107 dfa_nfa nfa;
108 nfa.rule_count = programs.size();
109 for (std::size_t r = 0; r < programs.size(); ++r) {
110 const program_view& prog {programs[r]};
111 const std::size_t base {nfa.code.size()};
112 const std::size_t cbase {nfa.classes.size()};
113 nfa.entry.push_back(base);
114 for (const char_class& cls : prog.classes) {
115 nfa.classes.push_back(cls);
116 }
117 for (std::size_t i = 0; i < prog.code.size(); ++i) {
118 const instr& in {prog.code[i]};
119 dfa_instr out {.op = in.op, .arg8 = in.arg8};
120 if (in.op == opcode::klass) {
121 out.klass = static_cast<std::uint32_t>(cbase + in.arg16);
122 }
123 else if (in.op == opcode::split) {
124 out.primary = in.primary_target + static_cast<std::int64_t>(base);
125 out.secondary = in.secondary_target + static_cast<std::int64_t>(base);
126 }
127 else if (in.op == opcode::jump) {
128 out.primary = in.primary_target + static_cast<std::int64_t>(base);
129 }
130 else if (in.op == opcode::assert_position
131 && in.arg8 != static_cast<std::uint8_t>(assert_kind::text_start)) {
132 // text_start (`\A`/`^`) is handled as a conditional ε in the closure (true
133 // at the cursor, false after any byte — exactly its anchored meaning). Any
134 // other assertion ($, \b, \B, multiline ^/$, …) cannot be a pure DFA.
135 throw dfa_error("pattern has a zero-width assertion that no DFA can represent "
136 "(only \\A/^ is allowed)");
137 }
138 else if (in.op == opcode::assert_lookaround) {
139 throw dfa_error("pattern has a lookaround, which no DFA can represent");
140 }
141 else if (in.op == opcode::klass_cp) {
142 // A code-point predicate matches whole code points via a decode + range search, not a
143 // byte transition, so it has no place in a byte-transition DFA.
144 throw dfa_error("pattern has a Unicode code-point class (\\w/\\d/\\s in text mode), "
145 "which no DFA can represent");
146 }
147 nfa.code.push_back(out);
148 nfa.accept_rule.push_back(in.op == opcode::match ? static_cast<std::int64_t>(r) : -1);
149 }
150 }
151 return nfa;
152 }
153
155 using dfa_set = std::vector<std::uint64_t>;
156
157 inline void dfa_set_bit(dfa_set& s,
158 std::size_t i)
159 {
160 const std::size_t word {i >> 6U};
161 if (word < s.size()) { // defensive bound, mirroring dfa_test_bit (the set is sized to fit)
162 s[word] |= (std::uint64_t {1} << (i & 63U));
163 }
164 }
165
166 inline bool dfa_test_bit(const dfa_set& s,
167 std::size_t i)
168 {
169 const std::size_t word {i >> 6U};
170 return word < s.size() && ((s[word] >> (i & 63U)) & 1U) != 0; // beyond the set ⇒ absent
171 }
172
175 inline dfa_set dfa_closure(const dfa_nfa& nfa,
176 const std::vector<std::uint32_t>& seeds,
177 bool at_start)
178 {
179 const std::size_t words {(nfa.code.size() + 63U) / 64U};
180 dfa_set present(words, 0);
181 std::vector<std::uint32_t> stack;
182 for (const std::uint32_t pc : seeds) {
183 if (!dfa_test_bit(present, pc)) {
184 dfa_set_bit(present, pc);
185 stack.push_back(pc);
186 }
187 }
188 while (!stack.empty()) {
189 const std::uint32_t pc {stack.back()};
190 stack.pop_back();
191 const dfa_instr& in {nfa.code[pc]};
192 const auto visit {[&](std::int64_t target) {
193 if (target >= 0 && !dfa_test_bit(present, static_cast<std::size_t>(target))) {
194 dfa_set_bit(present, static_cast<std::size_t>(target));
195 stack.push_back(static_cast<std::uint32_t>(target));
196 }
197 }};
198 switch (in.op) {
199 case opcode::split: visit(in.primary); visit(in.secondary); break;
200 case opcode::jump: visit(in.primary); break;
201 case opcode::save: visit(static_cast<std::int64_t>(pc) + 1); break;
203 if (at_start) { visit(static_cast<std::int64_t>(pc) + 1); } // text_start: ε only at offset 0
204 break;
205 case opcode::byte:
206 case opcode::klass:
207 case opcode::klass_cp:
208 case opcode::match:
209 case opcode::assert_lookaround: break; // terminal / unreachable (dfa_flatten rejects lookaround & klass_cp)
210 }
211 }
212 return present;
213 }
214
217 inline dfa_set dfa_move(const dfa_nfa& nfa,
218 const dfa_set& set,
219 std::uint8_t rep)
220 {
221 std::vector<std::uint32_t> seeds;
222 for (std::size_t pc = 0; pc < nfa.code.size(); ++pc) {
223 if (!dfa_test_bit(set, pc)) {
224 continue;
225 }
226 const dfa_instr& in {nfa.code[pc]};
227 const bool consume {(in.op == opcode::byte && in.arg8 == rep)
228 || (in.op == opcode::klass && nfa.classes[in.klass].test(rep))};
229 if (consume) {
230 seeds.push_back(static_cast<std::uint32_t>(pc + 1));
231 }
232 }
233 return dfa_closure(nfa, seeds, false); // post-consumption: text_start is false here
234 }
235
238 inline std::int64_t dfa_accept_of(const dfa_nfa& nfa,
239 const dfa_set& set)
240 {
241 std::int64_t best {-1};
242 for (std::size_t pc = 0; pc < nfa.code.size(); ++pc) {
243 if (dfa_test_bit(set, pc) && nfa.accept_rule[pc] >= 0
244 && (best < 0 || nfa.accept_rule[pc] < best)) {
245 best = nfa.accept_rule[pc];
246 }
247 }
248 return best;
249 }
250
255 {
256 std::array<std::uint8_t, 256> of {};
257 std::array<std::uint8_t, 256> rep {};
258 std::size_t count {0};
259 };
260
262 {
263 // Predicates as VALUES (the char_class itself, and byte literals), deduped —
264 // self-contained, so the signature loop indexes only its own vectors (no
265 // cross-vector nfa.classes[idx] the analyzer cannot prove in bounds).
266 std::vector<char_class> class_preds;
267 std::vector<std::uint16_t> literal_preds;
268 const auto push_unique {[](auto& vec, const auto& value) {
269 for (std::size_t i = 0; i < vec.size(); ++i) {
270 if (vec[i] == value) { return; }
271 }
272 vec.push_back(value);
273 }};
274 for (std::size_t pc = 0; pc < nfa.code.size(); ++pc) {
275 const dfa_instr& in {nfa.code[pc]};
276 if (in.op == opcode::klass && in.klass < nfa.classes.size()) {
277 push_unique(class_preds, nfa.classes[in.klass]);
278 }
279 else if (in.op == opcode::byte) {
280 push_unique(literal_preds, static_cast<std::uint16_t>(in.arg8));
281 }
282 }
283 const auto sig_equal {[&](unsigned a, unsigned b) {
284 for (std::size_t i = 0; i < class_preds.size(); ++i) {
285 if (class_preds[i].test(static_cast<std::uint8_t>(a))
286 != class_preds[i].test(static_cast<std::uint8_t>(b))) {
287 return false;
288 }
289 }
290 for (std::size_t i = 0; i < literal_preds.size(); ++i) {
291 const unsigned lit {literal_preds[i]};
292 if ((a == lit) != (b == lit)) { return false; }
293 }
294 return true;
295 }};
297 for (unsigned b = 0; b < 256U; ++b) {
298 bool assigned {false};
299 for (std::size_t c = 0; c < bc.count; ++c) {
300 if (sig_equal(b, bc.rep[c])) {
301 bc.of[b] = static_cast<std::uint8_t>(c);
302 assigned = true;
303 break;
304 }
305 }
306 if (!assigned) {
307 bc.rep[bc.count] = static_cast<std::uint8_t>(b);
308 bc.of[b] = static_cast<std::uint8_t>(bc.count);
309 ++bc.count;
310 }
311 }
312 return bc;
313 }
314
317 {
318 std::array<std::uint8_t, 256> byte_class {};
319 std::size_t num_classes {0};
320 std::vector<std::uint32_t> trans;
321 std::vector<std::uint32_t> accept;
322 std::uint32_t start {0};
323 std::size_t num_states {0};
324 std::size_t rule_count {0};
325 };
326
327 inline constexpr std::uint32_t dfa_no_rule {std::numeric_limits<std::uint32_t>::max()};
328
334 inline dfa_tables dfa_build(std::span<const program_view> programs,
335 std::size_t state_cap = max_dfa_states)
336 {
337 const dfa_nfa nfa {dfa_flatten(programs)};
339 const std::size_t nc {bc.count};
340 dfa_tables out;
341 out.byte_class = bc.of;
342 out.num_classes = nc;
343 out.rule_count = nfa.rule_count;
344
345 std::vector<dfa_set> sets; // sets[s] = the NFA state set of DFA state s
346 std::vector<std::int64_t> accept_pre; // accept of each pre-min state
347 const auto find_or_add {[&](dfa_set s) -> std::uint32_t { // by value: a definite object
348 for (std::size_t i = 0; i < sets.size(); ++i) {
349 if (sets[i] == s) { return static_cast<std::uint32_t>(i); }
350 }
351 // False positive: `nfa` and `s` are live locals bound by
352 // const-ref; the analyzer mis-models the std::vector here.
353 // NOLINTNEXTLINE(clang-analyzer-core.NonNullParamChecker)
354 const std::int64_t acc {dfa_accept_of(nfa, s)};
355 sets.push_back(std::move(s));
356 accept_pre.push_back(acc);
357 if (sets.size() > state_cap) { // bound the 2^NFA worst case
358 throw dfa_error("DFA state count exceeded max_dfa_states; "
359 "pattern is too complex for a DFA");
360 }
361 return static_cast<std::uint32_t>(sets.size() - 1);
362 }};
363
364 const std::size_t words {(nfa.code.size() + 63U) / 64U};
365 sets.emplace_back(words, 0); // state 0 = dead (empty set)
366 accept_pre.push_back(-1);
367 std::vector<std::uint32_t> entry_seeds;
368 for (const std::size_t e : nfa.entry) {
369 entry_seeds.push_back(static_cast<std::uint32_t>(e));
370 }
371 out.start = find_or_add(dfa_closure(nfa, entry_seeds, true)); // offset 0: text_start holds
372
373 std::vector<std::uint32_t> trans_pre; // [s*nc + c]
374 for (std::size_t s = 0; s < sets.size(); ++s) { // sets grows as states are discovered
375 for (std::size_t c = 0; c < nc; ++c) {
376 trans_pre.push_back(find_or_add(dfa_move(nfa, sets[s], bc.rep[c])));
377 }
378 }
379 const std::size_t n_pre {sets.size()};
380
381 // Moore partition refinement; initial block by accept tag.
382 std::vector<std::int64_t> block(n_pre, 0);
383 for (std::size_t s = 0; s < n_pre; ++s) {
384 block[s] = accept_pre[s] < 0 ? 0 : accept_pre[s] + 1;
385 }
386 std::size_t num_blocks {0};
387 {
388 std::vector<std::int64_t> seen;
389 for (std::size_t s = 0; s < n_pre; ++s) {
390 bool found {false};
391 for (const std::int64_t b : seen) {
392 if (b == block[s]) { found = true; break; }
393 }
394 if (!found) { seen.push_back(block[s]); }
395 }
396 num_blocks = seen.size();
397 }
398 for (bool changed = true; changed;) {
399 changed = false;
400 std::vector<std::vector<std::int64_t>> sigs;
401 std::vector<std::int64_t> new_block(n_pre, 0);
402 for (std::size_t s = 0; s < n_pre; ++s) {
403 std::vector<std::int64_t> sig;
404 sig.reserve(nc + 1);
405 sig.push_back(block[s]);
406 for (std::size_t c = 0; c < nc; ++c) {
407 sig.push_back(block[trans_pre[(s * nc) + c]]);
408 }
409 std::int64_t id {-1};
410 for (std::size_t i = 0; i < sigs.size(); ++i) {
411 if (sigs[i] == sig) { id = static_cast<std::int64_t>(i); break; }
412 }
413 if (id < 0) {
414 id = static_cast<std::int64_t>(sigs.size());
415 sigs.push_back(std::move(sig));
416 }
417 new_block[s] = id;
418 }
419 if (sigs.size() != num_blocks) {
420 changed = true;
421 num_blocks = sigs.size();
422 block = std::move(new_block);
423 }
424 }
425
426 // Emit minimized tables: one state per block, fields from a representative.
427 out.num_states = num_blocks;
428 std::vector<std::int64_t> rep_of_block(num_blocks, -1);
429 for (std::size_t s = 0; s < n_pre; ++s) {
430 std::int64_t& slot {rep_of_block[static_cast<std::size_t>(block[s])]};
431 if (slot < 0) { slot = static_cast<std::int64_t>(s); }
432 }
433 out.trans.reserve(num_blocks * nc);
434 out.accept.reserve(num_blocks);
435 for (std::size_t b = 0; b < num_blocks; ++b) {
436 const std::size_t rep {static_cast<std::size_t>(rep_of_block[b])};
437 out.accept.push_back(accept_pre[rep] < 0 ? dfa_no_rule : static_cast<std::uint32_t>(accept_pre[rep]));
438 for (std::size_t c = 0; c < nc; ++c) {
439 out.trans.push_back(static_cast<std::uint32_t>(block[trans_pre[(rep * nc) + c]]));
440 }
441 }
442 out.start = static_cast<std::uint32_t>(block[out.start]);
443 return out;
444 }
445 } // namespace detail
446
454 class dfa
455 {
456 public:
457
463 explicit dfa(std::span<const detail::program_view> programs)
464 : tables_(detail::dfa_build(programs))
465 {}
466
472 explicit dfa(std::span<const regex> patterns)
473 : dfa(views_of(patterns))
474 {}
475
486 [[nodiscard]] std::optional<dfa_match> match(std::string_view rest) const noexcept
487 {
488 std::uint32_t state {tables_.start};
489 std::optional<dfa_match> best;
490 for (std::size_t i = 0; i < rest.size();) {
491 const auto byte {static_cast<std::uint8_t>(rest[i])};
492 const std::size_t cls {tables_.byte_class[byte]};
493 state = tables_.trans[(static_cast<std::size_t>(state) * tables_.num_classes) + cls];
494 if (state == 0U) { // dead state
495 break;
496 }
497 ++i;
498 const std::uint32_t rule {tables_.accept[state]};
499 if (rule != detail::dfa_no_rule) {
500 best = dfa_match {.rule_index = rule, .length = i};
501 }
502 }
503 return best;
504 }
505
507 [[nodiscard]] std::size_t state_count() const noexcept
508 {
509 return tables_.num_states;
510 }
511
513 [[nodiscard]] std::size_t rule_count() const noexcept
514 {
515 return tables_.rule_count;
516 }
517
519 [[nodiscard]] std::size_t class_count() const noexcept
520 {
521 return tables_.num_classes;
522 }
523
524 private:
525
527 static std::vector<detail::program_view> views_of(std::span<const regex> patterns)
528 {
529 std::vector<detail::program_view> views;
530 views.reserve(patterns.size());
531 for (const regex& pattern : patterns) {
532 views.push_back(pattern.raw_program());
533 }
534 return views;
535 }
536
538 };
539} // namespace real
540
541#endif // REAL_DFA_HPP
A compiled regular expression, parameterized on its storage policy.
Definition real.hpp:468
Thrown when a pattern cannot be represented as a DFA.
Definition dfa.hpp:58
dfa_error(const std::string &message)
Builds the error.
Definition dfa.hpp:65
A maximal-munch DFA over an ordered set of patterns.
Definition dfa.hpp:455
static std::vector< detail::program_view > views_of(std::span< const regex > patterns)
Materializes program views from patterns (helper for the regex ctor).
Definition dfa.hpp:527
detail::dfa_tables tables_
The immutable baked tables.
Definition dfa.hpp:537
std::size_t rule_count() const noexcept
The number of patterns the DFA was built from.
Definition dfa.hpp:513
dfa(std::span< const detail::program_view > programs)
Builds the DFA from compiled programs (the embedder path).
Definition dfa.hpp:463
std::optional< dfa_match > match(std::string_view rest) const noexcept
Matches the longest pattern anchored at the start of rest.
Definition dfa.hpp:486
std::size_t class_count() const noexcept
The number of byte-equivalence classes (the reduced alphabet width).
Definition dfa.hpp:519
std::size_t state_count() const noexcept
The number of states in the minimized automaton (includes the dead state).
Definition dfa.hpp:507
dfa(std::span< const regex > patterns)
Builds the DFA from regexes (a convenience over regex::raw_program).
Definition dfa.hpp:472
Resource limits guarding against pattern-driven resource exhaustion.
void dfa_set_bit(dfa_set &s, std::size_t i)
Definition dfa.hpp:157
bool dfa_test_bit(const dfa_set &s, std::size_t i)
Definition dfa.hpp:166
std::vector< std::uint64_t > dfa_set
A set of NFA PCs as a bitset (one per DFA state during construction).
Definition dfa.hpp:155
std::int64_t dfa_accept_of(const dfa_nfa &nfa, const dfa_set &set)
The accepting rule of a state set: the SMALLEST rule index among its match PCs (the order tie-break),...
Definition dfa.hpp:238
dfa_tables dfa_build(std::span< const program_view > programs, std::size_t state_cap=max_dfa_states)
Subset construction over byte-classes, then Moore minimization (initial partition by accept tag,...
Definition dfa.hpp:334
constexpr std::uint32_t dfa_no_rule
Definition dfa.hpp:327
dfa_byte_classes dfa_compute_classes(const dfa_nfa &nfa)
Definition dfa.hpp:261
dfa_nfa dfa_flatten(std::span< const program_view > programs)
Flattens programs into one union NFA, auditing DFA-ability.
Definition dfa.hpp:105
constexpr std::size_t max_dfa_states
Maximum DFA states (opt-in real::dfa). Subset construction is 2^NFA in the worst case; this caps it s...
Definition config.hpp:47
@ text_start
\A, and ^ without multiline.
dfa_set dfa_closure(const dfa_nfa &nfa, const std::vector< std::uint32_t > &seeds, bool at_start)
The epsilon-closure of seeds (a PC list), as a canonical PC bitset. at_start follows a text_start ass...
Definition dfa.hpp:175
dfa_set dfa_move(const dfa_nfa &nfa, const dfa_set &set, std::uint8_t rep)
The move on the byte rep: ε-closure of the successors of every PC in set that consumes rep.
Definition dfa.hpp:217
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.
The public API: real::regex, real::static_regex and results.
A set of byte values (0–255) as a 256-bit bitmap.
Definition charclass.hpp:30
Computes byte-equivalence classes: two bytes are equivalent iff they satisfy the same consuming predi...
Definition dfa.hpp:255
std::array< std::uint8_t, 256 > rep
Definition dfa.hpp:257
std::array< std::uint8_t, 256 > of
Definition dfa.hpp:256
A flattened NFA instruction (global PCs, global class index).
Definition dfa.hpp:83
std::uint8_t arg8
Definition dfa.hpp:85
std::uint32_t klass
Global class index (op == klass).
Definition dfa.hpp:86
std::int64_t primary
Global target (split/jump).
Definition dfa.hpp:87
std::int64_t secondary
Global secondary target (split).
Definition dfa.hpp:88
The union NFA over all the patterns, flattened into one address space.
Definition dfa.hpp:93
std::vector< char_class > classes
Definition dfa.hpp:95
std::size_t rule_count
Definition dfa.hpp:98
std::vector< std::int64_t > accept_rule
accept_rule[pc] = rule index if match, else -1.
Definition dfa.hpp:96
std::vector< std::size_t > entry
entry[rule] = global pc of its pc 0.
Definition dfa.hpp:97
std::vector< dfa_instr > code
Definition dfa.hpp:94
The baked DFA tables produced by dfa_build.
Definition dfa.hpp:317
std::uint32_t start
Definition dfa.hpp:322
std::vector< std::uint32_t > trans
[state*num_classes + cls] -> next state (0 = dead).
Definition dfa.hpp:320
std::array< std::uint8_t, 256 > byte_class
Definition dfa.hpp:318
std::size_t rule_count
Definition dfa.hpp:324
std::size_t num_states
Definition dfa.hpp:323
std::size_t num_classes
Definition dfa.hpp:319
std::vector< std::uint32_t > accept
accept[state] = rule index, or NO_RULE.
Definition dfa.hpp:321
One NFA instruction. Field meaning depends on op.
Definition program.hpp:251
The outcome of dfa::match — which rule won, and how many bytes it spans.
Definition dfa.hpp:74
std::uint32_t rule_index
Index of the winning pattern, in the order passed to the ctor.
Definition dfa.hpp:75
std::size_t length
Byte length of the (non-empty) match.
Definition dfa.hpp:76
REAL's version macros and the C++20 language-standard guard.