Skip to main content

resharp/
lib.rs

1//! regex engine with intersection, complement, and lookarounds
2//!
3//! # quick start
4//!
5//! ```
6//! let re = resharp::Regex::new(r"\d{3}-\d{4}").unwrap();
7//! let matches = re.find_all(b"call 555-1234 or 555-5678").unwrap();
8//! assert_eq!(matches.len(), 2);
9//! ```
10//!
11//! # options
12//!
13//! use [`RegexOptions`] with [`Regex::with_options`] for non-default settings:
14//!
15//! ```
16//! use resharp::{Regex, RegexOptions};
17//!
18//! let re = Regex::with_options(
19//!     r"hello world",
20//!     RegexOptions::default()
21//!         .case_insensitive(true)
22//!         .dot_matches_new_line(true),
23//! ).unwrap();
24//! assert!(re.is_match(b"Hello World").unwrap());
25//! ```
26//!
27//! # escaping user input
28//!
29//! use [`escape`] to safely embed literal strings in patterns:
30//!
31//! ```
32//! let user_input = "file (1).txt";
33//! let pattern = format!(r"^{}$", resharp::escape(user_input));
34//! let re = resharp::Regex::new(&pattern).unwrap();
35//! assert!(re.is_match(b"file (1).txt").unwrap());
36//! ```
37
38#![deny(missing_docs)]
39
40#[cfg(not(any(
41    target_arch = "x86_64",
42    all(target_arch = "aarch64", target_feature = "neon"),
43    all(target_arch = "wasm32", target_feature = "simd128")
44)))]
45compile_error!(
46    "resharp requires a SIMD-capable target: x86_64, aarch64 with target_feature=neon, or wasm32 with target_feature=simd128"
47);
48
49pub(crate) mod accel;
50pub(crate) mod bdfa;
51#[cfg_attr(not(feature = "experimental_capture_groups"), allow(dead_code))]
52pub(crate) mod captures;
53pub(crate) mod ldfa;
54#[cfg_attr(not(feature = "experimental_capture_groups"), allow(dead_code))]
55pub(crate) mod pparse;
56pub(crate) mod fas;
57pub(crate) mod minterms;
58pub(crate) mod fwd;
59pub(crate) mod ismatch;
60pub(crate) mod prefix;
61pub(crate) mod scan;
62
63#[cfg(feature = "stream")]
64pub(crate) mod stream;
65#[cfg(feature = "stream")]
66pub use stream::StreamState;
67
68#[cfg(feature = "serialize")]
69pub mod dump;
70#[cfg(feature = "serialize")]
71#[allow(missing_docs)]
72pub use dump::RegexDump;
73#[cfg(feature = "serialize")]
74#[allow(missing_docs)]
75pub use bdfa::BDFA;
76#[cfg(feature = "serialize")]
77#[allow(missing_docs)]
78pub use ldfa::LDFA;
79#[cfg(feature = "serialize")]
80#[allow(missing_docs)]
81pub use prefix::{NegLb, NegLbTerm, PrefixKind};
82
83pub(crate) mod simd;
84
85#[cfg(feature = "diag")]
86pub use prefix::calc_potential_start;
87#[cfg(feature = "diag")]
88pub use prefix::calc_potential_start_prune;
89#[cfg(feature = "diag")]
90pub use prefix::calc_prefix_sets;
91#[cfg(feature = "diag")]
92pub use prefix::PrefixSets;
93#[cfg(feature = "diag")]
94pub use simd::{force_scalar_scope, ForceScalarGuard};
95pub(crate) use resharp_algebra::nulls::{Nullability, StartPositions};
96pub(crate) use resharp_algebra::solver::TSetId;
97use resharp_algebra::Kind;
98#[doc(hidden)]
99pub use resharp_algebra::NodeId;
100#[doc(hidden)]
101pub use resharp_algebra::RegexBuilder;
102
103/// escape all resharp meta characters in `text`, returning a pattern
104/// that matches the literal string.
105///
106/// ```
107/// assert_eq!(resharp::escape("a+b"), r"a\+b");
108/// ```
109pub use resharp_parser::escape;
110/// like [`escape`] but appends to an existing buffer.
111pub use resharp_parser::escape_into;
112
113use std::sync::Mutex;
114
115/// error from compiling or matching a regex.
116#[derive(Debug)]
117#[non_exhaustive]
118pub enum Error {
119    /// parse failure.
120    Parse(Box<resharp_parser::ParseError>),
121    /// algebra error (unsupported pattern, anchor limit).
122    Algebra(resharp_algebra::ResharpError),
123    /// DFA state cache exceeded `max_dfa_capacity`.
124    CapacityExceeded,
125    /// pattern produced more algebra nodes than the engine supports.
126    PatternTooLarge,
127    /// serialization or deserialization failure.
128    Serialize(String),
129    /// internal invariant violated; indicates a bug in the engine.
130    InternalError(&'static str),
131}
132
133impl std::fmt::Display for Error {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        match self {
136            Error::Parse(e) => write!(f, "parse error: {}", e),
137            Error::Algebra(e) => write!(f, "{}", e),
138            Error::CapacityExceeded => write!(f, "DFA state capacity exceeded"),
139            Error::PatternTooLarge => write!(f, "pattern too large"),
140            Error::Serialize(ref s) => write!(f, "serialization error: {}", s),
141            Error::InternalError(msg) => write!(f, "internal error: {}", msg),
142        }
143    }
144}
145
146impl std::error::Error for Error {
147    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
148        match self {
149            Error::Parse(e) => Some(e),
150            Error::Algebra(e) => Some(e),
151            Error::CapacityExceeded => None,
152            Error::PatternTooLarge => None,
153            Error::Serialize(_) => None,
154            Error::InternalError(_) => None,
155        }
156    }
157}
158
159impl From<resharp_parser::ParseError> for Error {
160    fn from(e: resharp_parser::ParseError) -> Self {
161        Error::Parse(Box::new(e))
162    }
163}
164
165impl From<resharp_algebra::ResharpError> for Error {
166    fn from(e: resharp_algebra::ResharpError) -> Self {
167        Error::Algebra(e)
168    }
169}
170
171/// Coverage of `\w`/`\d`/`\s` and the width of `.` / negated classes.
172#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
173pub enum UnicodeMode {
174    /// ASCII `\w`/`\d`/`\s`; `.` and negated classes step byte-by-byte. Fastest.
175    Ascii,
176    /// `\w` covers scripts up to 2-byte UTF-8 (Latin, Greek, Cyrillic, Hebrew, Arabic, ...);
177    /// `\d` and `\s` are ASCII.
178    #[default]
179    Default,
180    /// Full Unicode `\w`/`\d`/`\s` (incl. CJK and historic scripts, up to 4-byte UTF-8);
181    /// `.` and negated classes match one full codepoint.
182    Full,
183    /// ASCII `\w`/`\d`/`\s`, but `.`, `[^...]`, `\W`/`\D`/`\S` match one full codepoint.
184    /// Matches default JS `RegExp` behavior (no `u` flag).
185    Javascript,
186}
187
188/// Regex configuration, passed to [`Regex::with_options`].
189///
190/// all options have sensible defaults via [`Default`]. use the builder
191/// methods to override:
192///
193/// ```
194/// use resharp::{RegexOptions, UnicodeMode};
195///
196/// let opts = RegexOptions::default()
197///     .unicode(UnicodeMode::Ascii)
198///     .case_insensitive(true)
199///     .dot_matches_new_line(true);
200/// ```
201pub struct RegexOptions {
202    /// max cached DFA states, clamped to `u16::MAX` (default: `u16::MAX`).
203    pub max_dfa_capacity: usize,
204    /// max lookahead context distance (default: 800).
205    pub lookahead_context_max: u32,
206    /// Unicode coverage for `\w`/`\d`/`\s` and width of `.` / negated classes
207    /// (default: `UnicodeMode::Default`).
208    pub unicode: UnicodeMode,
209    /// global case-insensitive matching (default: false).
210    pub case_insensitive: bool,
211    /// `.` matches `\n` (default: false). `_` always matches any byte.
212    pub dot_matches_new_line: bool,
213    /// `^`/`$` match start/end of line as well as of input (default: true).
214    /// Disable with `(?-m)` inline or this flag.
215    pub multiline: bool,
216    /// allow whitespace and `#` comments in the pattern (default: false).
217    pub ignore_whitespace: bool,
218    /// when true, every bare `(...)` group captures unnamed, same as writing
219    /// `(??...)` (default: false). Unnamed groups report `None` from
220    /// `capture_names()`; use `(?<name>...)` for a name you can look up.
221    #[cfg(feature = "experimental_capture_groups")]
222    pub implicit_captures: bool,
223    /// use hardened forward scan (default: false).
224    /// slower, but prevents O(n^2) all-matches blowup on adversarial combinations.
225    pub hardened: bool,
226    /// remove the default pattern size limit for very large regexes (default: false).
227    pub unbounded_size: bool,
228    #[doc(hidden)]
229    pub force_convergence: bool,
230    /// disable all prefix-acceleration. much slower,
231    /// but can be useful for sanity checks and serialization
232    #[doc(hidden)]
233    pub disable_prefixes: bool,
234}
235
236impl Default for RegexOptions {
237    fn default() -> Self {
238        Self {
239            max_dfa_capacity: u16::MAX as usize,
240            lookahead_context_max: 800,
241            unicode: UnicodeMode::Default,
242            case_insensitive: false,
243            dot_matches_new_line: false,
244            multiline: true,
245            ignore_whitespace: false,
246            #[cfg(feature = "experimental_capture_groups")]
247            implicit_captures: false,
248            hardened: false,
249            unbounded_size: false,
250            force_convergence: false,
251            disable_prefixes: false,
252        }
253    }
254}
255
256impl RegexOptions {
257    /// set Unicode coverage for `\w`/`\d`/`\s` and width of `.` / negated classes.
258    pub fn unicode(mut self, mode: UnicodeMode) -> Self {
259        self.unicode = mode;
260        self
261    }
262    /// set case-insensitive mode.
263    pub fn case_insensitive(mut self, yes: bool) -> Self {
264        self.case_insensitive = yes;
265        self
266    }
267    /// set dot-matches-newline mode.
268    pub fn dot_matches_new_line(mut self, yes: bool) -> Self {
269        self.dot_matches_new_line = yes;
270        self
271    }
272    /// `^`/`$` match start/end of line (default: true), false makes them `\A`/`\z`.
273    pub fn multiline(mut self, yes: bool) -> Self {
274        self.multiline = yes;
275        self
276    }
277    /// set ignore-whitespace (verbose) mode.
278    pub fn ignore_whitespace(mut self, yes: bool) -> Self {
279        self.ignore_whitespace = yes;
280        self
281    }
282    /// make every bare `(...)` group capture (auto-indexed, unnamed).
283    #[cfg(feature = "experimental_capture_groups")]
284    pub fn implicit_captures(mut self, yes: bool) -> Self {
285        self.implicit_captures = yes;
286        self
287    }
288    /// enable hardened mode for untrusted patterns: uses only O(N·S) forward scan, slower.
289    pub fn hardened(mut self, yes: bool) -> Self {
290        self.hardened = yes;
291        self
292    }
293    #[doc(hidden)]
294    pub fn force_convergence(mut self, yes: bool) -> Self {
295        self.force_convergence = yes;
296        self
297    }
298    /// disable parser and algebra size caps.
299    /// the defaults are generous; if you hit them, splitting the pattern into
300    /// several smaller regexes is almost always the better fix than raising the limit.
301    pub fn unbounded_size(mut self, yes: bool) -> Self {
302        self.unbounded_size = yes;
303        self
304    }
305}
306
307/// byte-offset range `[start, end)`.
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309#[repr(C)]
310pub struct Match {
311    /// inclusive start.
312    pub start: usize,
313    /// exclusive end.
314    pub end: usize,
315}
316
317/// Capture group spans of one match, produced by [`Regex::captures_all`].
318/// Index 0 is the whole match, then one slot per capturing group in source
319/// order. A slot is `None` when that group did not participate in the match.
320#[cfg(feature = "experimental_capture_groups")]
321#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct Captures<'r> {
323    names: &'r [Option<String>],
324    spans: Vec<Option<(usize, usize)>>,
325}
326
327#[cfg(feature = "experimental_capture_groups")]
328impl Captures<'_> {
329    /// Span of group `i`, where 0 is the whole match.
330    pub fn get(&self, i: usize) -> Option<Match> {
331        self.spans
332            .get(i)
333            .copied()
334            .flatten()
335            .map(|(start, end)| Match { start, end })
336    }
337
338    /// Span of the group named `name`.
339    pub fn name(&self, name: &str) -> Option<Match> {
340        let i = self.names.iter().position(|n| n.as_deref() == Some(name))?;
341        self.get(i)
342    }
343
344    /// All slots, whole match first.
345    pub fn spans(&self) -> &[Option<(usize, usize)>] {
346        &self.spans
347    }
348}
349
350pub(crate) struct RegexInner {
351    pub(crate) b: RegexBuilder,
352    pub(crate) fwd: ldfa::LDFA,
353    pub(crate) fwd_ts: ldfa::LDFA,
354    #[cfg_attr(not(feature = "stream"), allow(dead_code))]
355    pub(crate) rev: Option<ldfa::LDFA>,
356    pub(crate) rev_ts: ldfa::LDFA,
357    #[cfg(feature = "convergence_prefix")]
358    pub(crate) conv_b: Option<ldfa::LDFA>,
359    #[cfg(feature = "stream")]
360    pub(crate) stream: stream::StreamInit,
361    pub(crate) nulls: StartPositions,
362    pub(crate) matches: Vec<Match>,
363    pub(crate) bounded: Option<bdfa::BDFA>,
364    pub(crate) fas: Option<fas::FwdDFA>,
365    pub(crate) lb_verify: Option<ldfa::LDFA>,
366    #[cfg_attr(not(feature = "experimental_capture_groups"), allow(dead_code))]
367    pub(crate) capture_root: NodeId,
368    #[cfg_attr(not(feature = "experimental_capture_groups"), allow(dead_code))]
369    pub(crate) capture_dfa: pparse::PosixParser,
370    #[cfg_attr(not(feature = "experimental_capture_groups"), allow(dead_code))]
371    pub(crate) skeleton: Option<resharp_parser::Skeleton>,
372}
373
374#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
375#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
376pub(crate) struct InitialNodeFlags(u8);
377
378impl InitialNodeFlags {
379    const HAS_ANCHORS: u8 = 1 << 0;
380    const HAS_LB: u8 = 1 << 1;
381    const HAS_LA: u8 = 1 << 2;
382
383    pub(crate) fn new(has_anchors: bool, has_lb: bool, has_la: bool) -> Self {
384        let mut bits = 0;
385        if has_anchors {
386            bits |= Self::HAS_ANCHORS;
387        }
388        if has_lb {
389            bits |= Self::HAS_LB;
390        }
391        if has_la {
392            bits |= Self::HAS_LA;
393        }
394        InitialNodeFlags(bits)
395    }
396
397    #[inline]
398    pub(crate) fn has_anchors(self) -> bool {
399        self.0 & Self::HAS_ANCHORS != 0
400    }
401    
402    #[inline]
403    #[allow(dead_code)]
404    pub(crate) fn has_lb(self) -> bool {
405        self.0 & Self::HAS_LB != 0
406    }
407    #[inline]
408    pub(crate) fn has_la(self) -> bool {
409        self.0 & Self::HAS_LA != 0
410    }
411}
412
413/// Lazily compiled regex instance.
414/// Uses Mutex for interior mutability.
415pub struct Regex {
416    pub(crate) inner: Mutex<RegexInner>,
417    pub(crate) prefix: Option<prefix::PrefixKind>,
418    pub(crate) fixed_length: Option<u32>,
419    pub(crate) empty_nullable: bool,
420    pub(crate) always_nullable: bool,
421    pub(crate) star_loop: bool,
422    /// node = ⊥
423    /// found to be trivially unmatchable, not guaranteed before full expansion
424    pub(crate) is_empty_lang: bool,
425    #[allow(dead_code)]
426    pub(crate) fwd_begin_anchored: bool,
427    pub(crate) fwd_lb_stripped: bool,
428    #[allow(dead_code)]
429    pub(crate) rev_end_anchored: bool,
430    /// rev = _*, skip rev pass entirely
431    pub(crate) initial_nullability: Nullability,
432    #[allow(dead_code)]
433    pub(crate) fwd_end_nullable: bool,
434    pub(crate) rev_end_nullable: bool,
435    pub(crate) hardened: bool,
436    #[allow(dead_code)]
437    pub(crate) has_bounded: bool,
438    pub(crate) bounded_safe_find_all: bool,
439    pub(crate) lb_check_bytes: u8,
440    pub(crate) fwd_lb_begin_nullable: bool,
441    pub(crate) fwd_lb_begin_len: u8,
442    pub(crate) fwd_lb_begin_classes: Vec<crate::accel::TSet>,
443    pub(crate) fwd_lb_body_nullable: bool,
444    pub(crate) init_flags: InitialNodeFlags,
445    #[cfg(feature = "convergence_prefix")]
446    pub(crate) conv_prefix: bool,
447    /// fixed-length negative lookbehind reject test paired with an `AnchoredFwd`
448    /// body-literal prefix; verifies the lookbehind the SIMD prefix cannot.
449    pub(crate) neg_lb: Option<prefix::NegLb>,
450    #[cfg_attr(not(feature = "experimental_capture_groups"), allow(dead_code))]
451    pub(crate) group_names: Vec<Option<String>>,
452    #[cfg_attr(not(any(feature = "experimental_capture_groups", feature = "diag")), allow(dead_code))]
453    pub(crate) captures_dispatch: captures::CaptureDispatch,
454    pub(crate) find_all: FindAll,
455    pub(crate) class_plus: Option<[u64; 4]>,
456    #[cfg(feature = "stream")]
457    pub(crate) stream_cache: stream::StreamCache,
458}
459
460/// cached dispatch decision for `find_all`. computed once at construction.
461#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
462#[derive(Clone, Copy, PartialEq, Eq, Debug)]
463pub enum FindAll {
464    /// language is ⊥; never matches.
465    EmptyLang,
466    /// `\A`-anchored; single forward scan from byte 0.
467    Anchored,
468    /// `\z`-anchored; single reverse scan from the string end.
469    EndAnchored,
470    /// O(N·S) bulk scan for untrusted patterns.
471    Hardened,
472    /// SIMD forward-prefix anchored scan.
473    FwdPrefix,
474    /// SIMD forward-prefix with leading lookbehind.
475    FwdLbPrefix,
476    /// bounded-DFA fwd-only scan.
477    Bounded,
478    /// generic rev-collect + fwd-verify.
479    Dfa,
480    /// language is `CLASS+` for one frequent byte class
481    ClassPlus,
482}
483
484// not a security measure. only flags obvious cases where hardening results in better performance
485#[derive(Clone, Copy, Default)]
486struct Hardening {
487    full: bool,
488    no_fwd_prefix: bool,
489}
490
491/// auto-hardening heuristic, flags some patterns for hardened forward scan.
492fn auto_harden(b: &mut RegexBuilder, start: NodeId, has_anchors: bool) -> Hardening {
493    const NODE_BUDGET: usize = 128;
494    const LARGE_COVER: u32 = 128;
495    let opener = opener_class(b, start);
496    if opener == TSetId::EMPTY {
497        return Hardening::default();
498    }
499    let opener_full = b.solver().is_full_id(opener);
500    let Some(graph) = build_partial_graph(b, start, NODE_BUDGET) else {
501        return Hardening::default();
502    };
503    if graph
504        .nodes
505        .iter()
506        .any(|&n| n.is_compl(b))
507    {
508        return Hardening::default();
509    }
510    let mut pure_star: Vec<bool> = vec![false; graph.nodes.len()];
511    for (i, &n) in graph.nodes.iter().enumerate() {
512        if i == 0 {
513            continue;
514        }
515        if n.nullability(b) != resharp_algebra::nulls::Nullability::ALWAYS {
516            continue;
517        }
518        if graph.edges[i].len() == 1 {
519            let e = &graph.edges[i][0];
520            if e.dst == i && b.solver().is_full_id(e.set) {
521                pure_star[i] = true;
522            }
523        }
524    }
525    if !has_anchors
526        && graph.edges[0].len() == 1
527        && graph.edges[0][0].dst == 0
528        && b.solver().is_full_id(graph.edges[0][0].set)
529    {
530        return Hardening::default();
531    }
532
533    let reach = transitive_closure(&graph);
534    let sccs = sccs_from_reach(&reach);
535    let mut node_scc: Vec<usize> = vec![0; graph.nodes.len()];
536    for (sid, scc) in sccs.iter().enumerate() {
537        for &n in scc {
538            node_scc[n] = sid;
539        }
540    }
541    let start_in_cycle = sccs[node_scc[0]].len() > 1 || graph.edges[0].iter().any(|e| e.dst == 0);
542    let total_wide_self_loops = graph
543        .nodes
544        .iter()
545        .enumerate()
546        .filter(|(i, _)| !pure_star[*i])
547        .filter(|(i, _)| {
548            let self_cov = graph.edges[*i]
549                .iter()
550                .filter(|e| e.dst == *i)
551                .fold(TSetId::EMPTY, |acc, e| b.solver().or_id(acc, e.set));
552            b.solver().byte_count(self_cov) >= 2
553        })
554        .count();
555    let (min_len, _) = b.get_min_max_length(start);
556    const SHORT_PREFIX: u32 = 3;
557    const ENTRY_BYTES: u32 = 2;
558    for (i, &n) in graph.nodes.iter().enumerate() {
559        if n.nullability(b) == resharp_algebra::nulls::Nullability::NEVER {
560            continue;
561        }
562        let scc = &sccs[node_scc[i]];
563        let scc_non_trivial = scc.len() > 1 || graph.edges[i].iter().any(|e| e.dst == i);
564        if !scc_non_trivial {
565            continue;
566        }
567        let scc_set: std::collections::HashSet<usize> = scc.iter().copied().collect();
568        let in_scc_cov = graph.edges[i]
569            .iter()
570            .filter(|e| scc_set.contains(&e.dst))
571            .fold(TSetId::EMPTY, |acc, e| b.solver().or_id(acc, e.set));
572        if b.solver().byte_count(in_scc_cov) < LARGE_COVER {
573            continue;
574        }
575        if i == 0 {
576            return Hardening {
577                full: true,
578                no_fwd_prefix: true,
579            };
580        }
581        let start_to_i = graph.edges[0]
582            .iter()
583            .filter(|e| e.dst == i)
584            .fold(TSetId::EMPTY, |acc, e| b.solver().or_id(acc, e.set));
585        let entry_wide = b.solver().byte_count(start_to_i) >= ENTRY_BYTES;
586        let start_is_union = start.is_union(b);
587        if !has_anchors && min_len <= SHORT_PREFIX && entry_wide && start_is_union {
588            return Hardening {
589                full: true,
590                no_fwd_prefix: true,
591            };
592        }
593    }
594    let mut no_fwd_prefix = false;
595    let opener_wide = opener_full || b.solver().byte_count(opener) >= LARGE_COVER;
596    for scc in sccs {
597        let non_trivial = scc.len() > 1 || graph.edges[scc[0]].iter().any(|e| e.dst == scc[0]);
598        if !non_trivial {
599            continue;
600        }
601        if scc.iter().all(|&n| pure_star[n]) {
602            continue;
603        }
604        let scc_set: std::collections::HashSet<usize> = scc.iter().copied().collect();
605        if scc_set.contains(&0) {
606            continue; // (3b) start in SCC
607        }
608        let sticky = scc.iter().all(|&n| {
609            let cover = graph.edges[n]
610                .iter()
611                .fold(TSetId::EMPTY, |acc, e| b.solver().or_id(acc, e.set));
612            b.solver().is_full_id(cover)
613        });
614        const SPIN_FREQ_THRESHOLD: u64 = crate::prefix::TOTAL_BYTE_FREQ / 2;
615        let scc_set_local: std::collections::HashSet<usize> = scc.iter().copied().collect();
616        let has_wide_spin = scc.iter().any(|&n| {
617            let in_scc_cover = graph.edges[n]
618                .iter()
619                .filter(|e| scc_set_local.contains(&e.dst))
620                .fold(TSetId::EMPTY, |acc, e| b.solver().or_id(acc, e.set));
621            let freq: u64 = b
622                .solver()
623                .collect_bytes(in_scc_cover)
624                .iter()
625                .map(|&byte| crate::simd::BYTE_FREQ[byte as usize] as u64)
626                .sum();
627            freq >= SPIN_FREQ_THRESHOLD
628        });
629        if !has_wide_spin {
630            continue;
631        }
632        let restartable = scc.iter().any(|&n| {
633            graph.edges[n]
634                .iter()
635                .any(|e| scc_set.contains(&e.dst) && b.solver().is_sat_id(e.set, opener))
636        });
637        if !restartable {
638            continue;
639        }
640        if !has_anchors {
641            no_fwd_prefix = true;
642        }
643        let start_branches = graph.edges[0].len() >= 2;
644        let scc_branches = scc.iter().any(|&n| graph.edges[n].len() >= 3);
645        if !start_branches && total_wide_self_loops <= 1 {
646            continue;
647        }
648        let start_escapes_scc = if has_anchors {
649            let start_into_scc = graph.edges[0]
650                .iter()
651                .filter(|e| scc_set.contains(&e.dst))
652                .count();
653            graph.edges[0].len() > start_into_scc
654        } else {
655            let cover = graph.edges[0]
656                .iter()
657                .filter(|e| scc_set.contains(&e.dst) || scc.iter().any(|&s| reach[e.dst][s]))
658                .fold(TSetId::EMPTY, |acc, e| b.solver().or_id(acc, e.set));
659            !b.solver().is_full_id(cover)
660        };
661        if start_escapes_scc && !start_in_cycle {
662            continue;
663        }
664        if min_len <= SHORT_PREFIX && sticky && opener_wide && (start_branches || scc_branches) {
665            return Hardening {
666                full: true,
667                no_fwd_prefix: true,
668            };
669        }
670    }
671    if no_fwd_prefix {
672        return Hardening {
673            full: false,
674            no_fwd_prefix: true,
675        };
676    }
677    Hardening::default()
678}
679
680struct Edge {
681    dst: usize,
682    set: TSetId,
683}
684
685struct Graph {
686    edges: Vec<Vec<Edge>>,
687    nodes: Vec<NodeId>,
688}
689
690const BUILD_PARTIAL_GRAPH_CREATION_BUDGET: u32 = 100_000;
691
692fn build_partial_graph(b: &mut RegexBuilder, start: NodeId, budget: usize) -> Option<Graph> {
693    use std::collections::HashMap;
694    let mut idx: HashMap<NodeId, usize> = HashMap::from([(start, 0)]);
695    let mut edges: Vec<Vec<Edge>> = vec![Vec::new()];
696    let mut nodes: Vec<NodeId> = vec![start];
697    let mut queue: Vec<(usize, NodeId)> = vec![(0, start)];
698    let mut overflow = false;
699    let node_budget_start = b.num_nodes();
700    while let Some((u, node)) = queue.pop() {
701        if b.num_nodes().wrapping_sub(node_budget_start) > BUILD_PARTIAL_GRAPH_CREATION_BUDGET {
702            return None;
703        }
704        let sder = b.der(node, Nullability::CENTER).ok()?;
705        let mut stack = vec![(sder, TSetId::FULL)];
706        b.iter_sat(&mut stack, &mut |_, next, set| {
707            let dst = *idx.entry(next).or_insert_with(|| {
708                if edges.len() >= budget {
709                    overflow = true;
710                    return usize::MAX;
711                }
712                let i = edges.len();
713                edges.push(Vec::new());
714                nodes.push(next);
715                queue.push((i, next));
716                i
717            });
718            if dst != usize::MAX {
719                edges[u].push(Edge { dst, set });
720            }
721        });
722        if overflow {
723            return None;
724        }
725    }
726    Some(Graph { edges, nodes })
727}
728
729fn transitive_closure(graph: &Graph) -> Vec<Vec<bool>> {
730    let n = graph.edges.len();
731    let mut r = vec![vec![false; n]; n];
732    for i in 0..n {
733        for e in &graph.edges[i] {
734            r[i][e.dst] = true;
735        }
736    }
737    for k in 0..n {
738        for i in 0..n {
739            if !r[i][k] {
740                continue;
741            }
742            for j in 0..n {
743                if r[k][j] {
744                    r[i][j] = true;
745                }
746            }
747        }
748    }
749    r
750}
751
752// extract SCCs from a reach matrix: i,j share an SCC iff each reaches the other.
753fn sccs_from_reach(reach: &[Vec<bool>]) -> Vec<Vec<usize>> {
754    let n = reach.len();
755    let mut visited = vec![false; n];
756    let mut sccs: Vec<Vec<usize>> = Vec::new();
757    for i in 0..n {
758        if visited[i] {
759            continue;
760        }
761        visited[i] = true;
762        let mut scc = vec![i];
763        for j in (i + 1)..n {
764            if !visited[j] && reach[i][j] && reach[j][i] {
765                visited[j] = true;
766                scc.push(j);
767            }
768        }
769        sccs.push(scc);
770    }
771    sccs
772}
773
774fn opener_class(b: &mut RegexBuilder, start: NodeId) -> TSetId {
775    let sder = match b.der(start, Nullability::CENTER) {
776        Ok(d) => d,
777        Err(_) => return TSetId::EMPTY,
778    };
779    let mut stack = vec![(sder, TSetId::FULL)];
780    let mut acc = TSetId::EMPTY;
781    b.iter_sat(
782        &mut stack,
783        &mut (|bb, next, set| {
784            if next.0 > NodeId::BOT.0 {
785                acc = bb.solver().or_id(acc, set);
786            }
787        }),
788    );
789    acc
790}
791
792fn collect_union_branches(b: &RegexBuilder, node: NodeId, out: &mut Vec<NodeId>) {
793    if node.is_union(b) {
794        collect_union_branches(b, node.left(b), out);
795        collect_union_branches(b, node.right(b), out);
796    } else {
797        out.push(node);
798    }
799}
800
801fn first_lb_in_branch(b: &RegexBuilder, node: NodeId) -> Option<NodeId> {
802    if node.is_lookbehind(b) {
803        return Some(node);
804    }
805    if node.is_concat(b) {
806        return first_lb_in_branch(b, node.left(b));
807    }
808    None
809}
810
811// mk_neg_lookbehind's shape for (?<!body) is Concat(BEGIN, Compl(Concat(TS, body))); recognize it so lookback bound is body's max length, not the unbounded-looking Compl wrapper
812fn neg_lookbehind_body(b: &RegexBuilder, inner: NodeId) -> Option<NodeId> {
813    if !inner.is_concat(b) || inner.left(b) != NodeId::BEGIN {
814        return None;
815    }
816    let compl = inner.right(b);
817    if !compl.is_compl(b) {
818        return None;
819    }
820    let body_ts = compl.left(b);
821    if !body_ts.is_concat(b) || body_ts.left(b) != NodeId::TS {
822        return None;
823    }
824    Some(body_ts.right(b))
825}
826
827fn lb_is_unbounded(b: &RegexBuilder, lb_node: NodeId) -> bool {
828    let inner = b.get_lookbehind_inner(lb_node);
829    if let Some(body) = neg_lookbehind_body(b, inner) {
830        return b.get_min_max_length(body).1 == u32::MAX;
831    }
832    let rest = if inner.is_concat(b) && inner.left(b).is_star(b) {
833        inner.right(b)
834    } else {
835        inner
836    };
837    b.get_min_max_length(rest).1 == u32::MAX
838}
839
840fn any_unbounded_lookback(b: &RegexBuilder, node: NodeId) -> bool {
841    if !node.contains_lookbehind(b) {
842        return false;
843    }
844    if node.is_lookbehind(b) && lb_is_unbounded(b, node) {
845        return true;
846    }
847    [node.left(b), node.right(b)]
848        .into_iter()
849        .any(|c| c != NodeId::MISSING && any_unbounded_lookback(b, c))
850}
851
852/// heuristic checks if we can support an union with lookbehinds,
853/// we wont determine which one matched so we require them to be disjoint
854fn union_branches_distinguishable(b: &mut RegexBuilder, union_node: NodeId) -> bool {
855    let mut branches = Vec::new();
856    collect_union_branches(b, union_node, &mut branches);
857    union_branches_distinguishable_list(b, &branches)
858}
859
860fn union_branches_distinguishable_list(b: &mut RegexBuilder, branches: &[NodeId]) -> bool {
861    // a BOT branch never matches anything; drop it before the geometric disambiguation below or its dead length still gets folded in
862    let branches: Vec<NodeId> = branches.iter().copied().filter(|&n| n != NodeId::BOT).collect();
863    if branches.len() <= 1 {
864        return true;
865    }
866    let branches = branches.as_slice();
867    let any_lb = branches.iter().any(|n| n.contains_lookbehind(b));
868    if !any_lb {
869        return true;
870    }
871    let union_node_len = branches
872        .iter()
873        .fold((0u32, 0u32), |(mn, mx), &n| {
874            let (bn, bx) = b.get_min_max_length(n);
875            (mn.min(bn), mx.max(bx))
876        });
877    // lookbehind defers its reverse-pass null emission by its lookback length.
878    if union_node_len.1 > 0 && branches.iter().any(|&br| any_unbounded_lookback(b, br)) {
879        return false;
880    }
881    let any_anchors = branches.iter().any(|&br| b.contains_anchors(br));
882    if any_anchors
883        && branches
884            .iter()
885            .any(|&br| br.contains_lookbehind(b) && first_lb_in_branch(b, br).is_none())
886    {
887        return false;
888    }
889    // this is outside of formally verified territory, careful with changes
890    let fixed_lens: Option<Vec<u32>> = branches.iter().map(|&br| b.get_fixed_length(br)).collect();
891    if let Some(lens) = fixed_lens {
892        if lens.iter().all(|&l| l == lens[0]) {
893            return true;
894        }
895    }
896    let mut firsts: Vec<(bool, TSetId, Option<NodeId>, Option<u32>)> =
897        Vec::with_capacity(branches.len());
898    for &br in branches {
899        let has_lb = br.contains_lookbehind(b);
900        let lb_node = if has_lb {
901            first_lb_in_branch(b, br)
902        } else {
903            None
904        };
905        let stripped = match b.strip_lb(br) {
906            Ok(s) => s,
907            Err(_) => return false,
908        };
909        let sets = match prefix::calc_potential_start_prune(b, stripped, 1, 64, false) {
910            Ok(s) => s,
911            Err(_) => return false,
912        };
913        let (bmin, bmax) = b.get_min_max_length(br);
914        let first = match sets.first() {
915            Some(&s) => s,
916            None => {
917                if bmin == 0 {
918                    return false;
919                }
920                continue;
921            }
922        };
923        let fixed_len = if bmin == bmax { Some(bmin) } else { None };
924        firsts.push((has_lb, first, lb_node, fixed_len));
925    }
926    for i in 0..firsts.len() {
927        if !firsts[i].0 {
928            continue;
929        }
930        for j in 0..firsts.len() {
931            if i == j {
932                continue;
933            }
934            let inter = b.solver().and_id(firsts[i].1, firsts[j].1);
935            if inter == TSetId::EMPTY {
936                continue;
937            }
938            let lb_same = match (firsts[i].2, firsts[j].2) {
939                (Some(ni), Some(nj)) => {
940                    b.get_lookbehind_inner(ni) == b.get_lookbehind_inner(nj)
941                }
942                (None, None) => true,
943                _ => false,
944            };
945            if lb_same {
946                continue;
947            }
948            let same_fixed = matches!(
949                (firsts[i].3, firsts[j].3),
950                (Some(a), Some(c)) if a == c
951            );
952            if !same_fixed {
953                return false;
954            }
955        }
956    }
957    true
958}
959
960#[derive(Clone, Copy, Debug, PartialEq, Eq)]
961enum Compatibility {
962    LookaroundUnion,
963}
964
965fn combine_compatibility(
966    left: Option<Compatibility>,
967    right: Option<Compatibility>,
968) -> Option<Compatibility> {
969    left.or(right)
970}
971
972fn ensure_supported_rec(
973    b: &mut RegexBuilder,
974    node: NodeId,
975    at_start: bool,
976    strict_lb_start: bool,
977    memo: &mut std::collections::HashSet<(NodeId, bool, bool)>,
978) -> Result<Option<Compatibility>, resharp_algebra::ResharpError> {
979    if !node.contains_lookaround(b) {
980        return Ok(None);
981    }
982    if !memo.insert((node, at_start, strict_lb_start)) {
983        return Ok(None);
984    }
985    match b.get_kind(node) {
986        Kind::Union => {
987            let (l, r) = (node.left(b), node.right(b));
988            let has_lb = l.contains_lookbehind(b) || r.contains_lookbehind(b);
989            if has_lb && !union_branches_distinguishable(b, node) {
990                return Err(resharp_algebra::ResharpError::UnsupportedPattern);
991            }
992            let left = ensure_supported_rec(b, l, at_start, strict_lb_start, memo)?;
993            let right = ensure_supported_rec(b, r, at_start, strict_lb_start, memo)?;
994            let union = if has_lb {
995                Some(Compatibility::LookaroundUnion)
996            } else {
997                None
998            };
999            Ok(combine_compatibility(
1000                union,
1001                combine_compatibility(left, right),
1002            ))
1003        }
1004        Kind::Inter => {
1005            let (l, r) = (node.left(b), node.right(b));
1006            // distributing (A|B) & C eagerly to (A&C)|(B&C)
1007            // to unlock some patterns outside of RE# fragment like `(^abc|def)&.*`
1008            for (u, other) in [(l, r), (r, l)] {
1009                if u.is_union(b) && u.contains_lookbehind(b) {
1010                    if strict_lb_start && !at_start {
1011                        return Err(resharp_algebra::ResharpError::UnsupportedPattern);
1012                    }
1013                    let mut branches = Vec::new();
1014                    collect_union_branches(b, u, &mut branches);
1015                    let distributed_branches: Vec<NodeId> =
1016                        branches.iter().map(|&br| b.mk_inter(br, other)).collect();
1017                    if !union_branches_distinguishable_list(b, &distributed_branches) {
1018                        return Err(resharp_algebra::ResharpError::UnsupportedPattern);
1019                    }
1020                    let other_compatibility =
1021                        ensure_supported_rec(b, other, at_start, strict_lb_start, memo)?;
1022                    return Ok(combine_compatibility(
1023                        Some(Compatibility::LookaroundUnion),
1024                        other_compatibility,
1025                    ));
1026                }
1027            }
1028            let left = ensure_supported_rec(b, l, at_start, strict_lb_start, memo)?;
1029            let right = ensure_supported_rec(b, r, at_start, strict_lb_start, memo)?;
1030            Ok(combine_compatibility(left, right))
1031        }
1032        Kind::Concat => {
1033            let left = node.left(b);
1034            let right = node.right(b);
1035            let (_, left_max) = b.get_min_max_length(left);
1036            if left_max > 0 && right.is_union(b) && right.contains_lookbehind(b) {
1037                return Err(resharp_algebra::ResharpError::UnsupportedPattern);
1038            }
1039            if left.is_union(b) && left.contains_lookbehind(b) {
1040                if strict_lb_start && !at_start {
1041                    return Err(resharp_algebra::ResharpError::UnsupportedPattern);
1042                }
1043                let mut branches = Vec::new();
1044                collect_union_branches(b, left, &mut branches);
1045                let distributed_branches: Vec<NodeId> =
1046                    branches.iter().map(|&br| b.mk_concat(br, right)).collect();
1047                if union_branches_distinguishable_list(b, &distributed_branches) {
1048                    let right_compatibility =
1049                        ensure_supported_rec(b, right, at_start, strict_lb_start, memo)?;
1050                    return Ok(combine_compatibility(
1051                        Some(Compatibility::LookaroundUnion),
1052                        right_compatibility,
1053                    ));
1054                } else {
1055                    return Err(resharp_algebra::ResharpError::UnsupportedPattern);
1056                }
1057            }
1058            let left_compatibility = ensure_supported_rec(b, left, at_start, strict_lb_start, memo)?;
1059            let (_, left_max) = b.get_min_max_length(left);
1060            let right_compatibility =
1061                ensure_supported_rec(b, right, at_start && left_max == 0, strict_lb_start, memo)?;
1062            Ok(combine_compatibility(
1063                left_compatibility,
1064                right_compatibility,
1065            ))
1066        }
1067        Kind::Star => {
1068            if node.left(b).contains_lookaround(b) {
1069                return Err(resharp_algebra::ResharpError::UnsupportedPattern);
1070            }
1071            ensure_supported_rec(b, node.left(b), at_start, strict_lb_start, memo)
1072        }
1073        Kind::Ordered => ensure_supported_rec(b, node.left(b), at_start, strict_lb_start, memo),
1074        Kind::Compl => ensure_supported_rec(b, node.left(b), at_start, strict_lb_start, memo),
1075        Kind::Lookbehind => {
1076            let prev = node.right(b);
1077            let (_, prev_max) = if prev == NodeId::MISSING {
1078                (0, 0)
1079            } else {
1080                b.get_min_max_length(prev)
1081            };
1082            if !at_start || prev_max > 0 {
1083                return Err(resharp_algebra::ResharpError::UnsupportedPattern);
1084            }
1085            let left = ensure_supported_rec(b, node.left(b), at_start, strict_lb_start, memo)?;
1086            let right = ensure_supported_rec(b, prev, at_start, strict_lb_start, memo)?;
1087            Ok(combine_compatibility(left, right))
1088        }
1089        Kind::Lookahead => {
1090            let left = ensure_supported_rec(b, node.left(b), at_start, strict_lb_start, memo)?;
1091            let right = ensure_supported_rec(b, node.right(b), at_start, strict_lb_start, memo)?;
1092            Ok(combine_compatibility(left, right))
1093        }
1094        Kind::Pred => Ok(None),
1095        Kind::Begin => Ok(None),
1096        Kind::End => Ok(None),
1097        Kind::Tag => Ok(None),
1098    }
1099}
1100
1101fn peel_bare_begin_chain(b: &RegexBuilder, node: NodeId) -> NodeId {
1102    let mut cur = node;
1103    while cur.is_concat(b) && b.is_begin_only_shape(cur.left(b)) {
1104        cur = cur.right(b);
1105    }
1106    cur
1107}
1108
1109fn peel_nullable_lookbehind_prefix_chain(b: &mut RegexBuilder, node: NodeId) -> NodeId {
1110    if node.is_concat(b) {
1111        let left = node.left(b);
1112        if b.contains_lookbehind(left) && b.nullability(left).has(Nullability::BEGIN) {
1113            if left.is_lookbehind(b) {
1114                let prev = b.get_lookbehind_prev(left);
1115                let cont = if prev == NodeId::MISSING { NodeId::EPS } else { prev };
1116                let peeled_prev = peel_nullable_lookbehind_prefix_chain(b, cont);
1117                let right = peel_nullable_lookbehind_prefix_chain(b, node.right(b));
1118                return b.mk_concat(peeled_prev, right);
1119            }
1120            return peel_nullable_lookbehind_prefix_chain(b, node.right(b));
1121        }
1122        return node;
1123    }
1124    if node.is_lookbehind(b) && b.nullability(node).has(Nullability::BEGIN) {
1125        let prev = b.get_lookbehind_prev(node);
1126        let cont = if prev == NodeId::MISSING { NodeId::EPS } else { prev };
1127        return peel_nullable_lookbehind_prefix_chain(b, cont);
1128    }
1129    node
1130}
1131
1132fn neg_lookbehind_marker_prev_end_nullable(b: &mut RegexBuilder, node: NodeId) -> bool {
1133    if !node.is_lookbehind(b) {
1134        return false;
1135    }
1136    let prev = b.get_lookbehind_prev(node);
1137    if prev == NodeId::MISSING {
1138        return false;
1139    }
1140    let inner = b.get_lookbehind_inner(node);
1141    let is_neg_marker =
1142        b.get_kind(inner) == Kind::Begin || (inner.is_concat(b) && inner.left(b) == NodeId::BEGIN);
1143    if !is_neg_marker {
1144        return false;
1145    }
1146    b.nullability(prev).has(Nullability::END)
1147}
1148
1149fn body_after_begin_of(b: &mut RegexBuilder, node: NodeId) -> NodeId {
1150    let after_bare_begin = peel_bare_begin_chain(b, node);
1151    peel_nullable_lookbehind_prefix_chain(b, after_bare_begin)
1152}
1153
1154fn ensure_begin_leading(
1155    b: &RegexBuilder,
1156    node: NodeId,
1157    at_start: bool,
1158    memo: &mut std::collections::HashSet<(NodeId, bool)>,
1159) -> Result<(), resharp_algebra::ResharpError> {
1160    if !b.contains_anchors(node) {
1161        return Ok(());
1162    }
1163    if !memo.insert((node, at_start)) {
1164        return Ok(());
1165    }
1166    match b.get_kind(node) {
1167        Kind::Begin => {
1168            if at_start {
1169                Ok(())
1170            } else {
1171                Err(resharp_algebra::ResharpError::UnsupportedPattern)
1172            }
1173        }
1174        Kind::End | Kind::Pred | Kind::Tag => Ok(()),
1175        Kind::Concat => {
1176            let l = node.left(b);
1177            ensure_begin_leading(b, l, at_start, memo)?;
1178            let (lmin, _) = b.get_min_max_length(l);
1179            ensure_begin_leading(b, node.right(b), at_start && lmin == 0, memo)
1180        }
1181        Kind::Union | Kind::Inter => {
1182            ensure_begin_leading(b, node.left(b), at_start, memo)?;
1183            ensure_begin_leading(b, node.right(b), at_start, memo)
1184        }
1185        Kind::Star | Kind::Ordered => ensure_begin_leading(b, node.left(b), false, memo),
1186        Kind::Compl => Ok(()),
1187        Kind::Lookbehind | Kind::Lookahead => Ok(()),
1188    }
1189}
1190
1191fn ensure_supported(
1192    b: &mut RegexBuilder,
1193    node: NodeId,
1194    group_names: &[Option<String>],
1195) -> Result<Option<Compatibility>, resharp_algebra::ResharpError> {
1196    ensure_begin_leading(b, node, true, &mut std::collections::HashSet::new())?;
1197    captures::ensure_captures_supported(b, node, group_names)?;
1198    ensure_supported_rec(b, node, true, true, &mut std::collections::HashSet::new())
1199}
1200
1201impl Regex {
1202    /// compile a pattern with default options.
1203    ///
1204    /// ```
1205    /// let re = resharp::Regex::new(r"\b\w+\b").unwrap();
1206    /// ```
1207    pub fn new(pattern: &str) -> Result<Regex, Error> {
1208        Self::with_options(pattern, RegexOptions::default())
1209    }
1210
1211    /// compile a pattern with custom [`RegexOptions`].
1212    ///
1213    /// ```
1214    /// use resharp::{Regex, RegexOptions};
1215    ///
1216    /// let re = Regex::with_options(
1217    ///     r"hello",
1218    ///     RegexOptions::default().case_insensitive(true),
1219    /// ).unwrap();
1220    /// assert!(re.is_match(b"HELLO").unwrap());
1221    /// ```
1222    pub fn with_options(pattern: &str, opts: RegexOptions) -> Result<Regex, Error> {
1223        let mut b = RegexBuilder::new();
1224        b.lookahead_context_max = opts.lookahead_context_max;
1225        let pflags = resharp_parser::PatternFlags {
1226            unicode: opts.unicode != UnicodeMode::Ascii,
1227            full_unicode: opts.unicode == UnicodeMode::Full,
1228            ascii_perl_classes: opts.unicode == UnicodeMode::Javascript,
1229            case_insensitive: opts.case_insensitive,
1230            dot_matches_new_line: opts.dot_matches_new_line,
1231            multiline: opts.multiline,
1232            ignore_whitespace: opts.ignore_whitespace,
1233            #[cfg(feature = "experimental_capture_groups")]
1234            implicit_captures: opts.implicit_captures,
1235            #[cfg(not(feature = "experimental_capture_groups"))]
1236            implicit_captures: false,
1237            expanded_ast_limit: if opts.unbounded_size {
1238                u64::MAX
1239            } else {
1240                resharp_parser::DEFAULT_EXPANDED_AST_LIMIT
1241            },
1242            max_list_len: if opts.unbounded_size {
1243                usize::MAX
1244            } else {
1245                resharp_parser::DEFAULT_MAX_LIST_LEN
1246            },
1247            max_repeat: if opts.unbounded_size {
1248                u32::MAX
1249            } else {
1250                resharp_parser::DEFAULT_MAX_REPEAT
1251            },
1252            max_depth: if opts.unbounded_size {
1253                usize::MAX
1254            } else {
1255                resharp_parser::DEFAULT_MAX_DEPTH
1256            },
1257        };
1258        let (node, group_names, skeleton) =
1259            resharp_parser::parse_ast_with_names_and_skeleton(&mut b, pattern, &pflags)?;
1260        Self::from_node_inner(b, node, opts, pattern.len(), group_names, skeleton)
1261    }
1262
1263    /// build from a pre-constructed AST node.
1264    #[doc(hidden)]
1265    pub fn from_node(b: RegexBuilder, node: NodeId, opts: RegexOptions) -> Result<Regex, Error> {
1266        Self::from_node_inner(b, node, opts, 0, Vec::new(), None)
1267    }
1268
1269    fn from_node_inner(
1270        mut b: RegexBuilder,
1271        node: NodeId,
1272        opts: RegexOptions,
1273        pattern_len: usize,
1274        group_names: Vec<Option<String>>,
1275        skeleton: Option<resharp_parser::Skeleton>,
1276    ) -> Result<Regex, Error> {
1277        // sanity check
1278        let node_limit = if opts.unbounded_size {
1279            usize::MAX
1280        } else {
1281            200_000
1282        };
1283        if b.tree_size(node, node_limit) >= node_limit {
1284            return Err(Error::PatternTooLarge);
1285        }
1286        let _compatibility = ensure_supported(&mut b, node, &group_names)?;
1287
1288        let empty_nullable = b
1289            .nullability_emptystring(node)
1290            .has(Nullability::EMPTYSTRING);
1291        let initial_nullability = b.nullability(node);
1292
1293        let node_fwd_simpl = b.simplify_fwd_initial(node);
1294        let fwd_start = b.strip_lb(node_fwd_simpl)?;
1295        let fwd_end_nullable = b.nullability(fwd_start).has(Nullability::END);
1296        let rev_basis = b
1297            .strip_trailing_redundant_lookahead(node_fwd_simpl)
1298            .unwrap_or(node_fwd_simpl);
1299        let ts_rev_start = b.ts_rev_start(rev_basis)?;
1300        // ensure_supported_rec(&mut b, ts_rev_start, true, false)?;
1301        #[cfg(feature = "debug")]
1302        {
1303            eprintln!("[fwd]: {:.70}", b.pp(node));
1304            eprintln!("[ts_rev]: {:.70}", b.pp(ts_rev_start));
1305        }
1306
1307        let is_empty_lang = node_fwd_simpl == NodeId::BOT;
1308        // TODO: make it configurable to actually check and reject empty lang entriely
1309        let body_after_begin = body_after_begin_of(&mut b, node_fwd_simpl);
1310        let lb_stripped = fwd_start != body_after_begin;
1311        let fwd_begin_anchored = b.is_begin_anchored(node_fwd_simpl) && !lb_stripped;
1312        let has_look = b.contains_look(node_fwd_simpl);
1313        let rev_node = b.reverse(node_fwd_simpl)?;
1314        let rev_end_nullable = initial_nullability.has(Nullability::END)
1315            || b.nullability(ts_rev_start).has(Nullability::BEGIN)
1316            || neg_lookbehind_marker_prev_end_nullable(&mut b, node_fwd_simpl);
1317        let rev_end_anchored = b.is_begin_anchored(rev_node) && !fwd_end_nullable;
1318        let fixed_length = b.get_fixed_length(node_fwd_simpl);
1319        let (min_len, max_len) = b.get_min_max_length(node_fwd_simpl);
1320        let max_length = if max_len != u32::MAX {
1321            Some(max_len)
1322        } else {
1323            None
1324        };
1325        let max_cap = opts.max_dfa_capacity.min(u16::MAX as usize);
1326        let mut opts = opts;
1327        let has_anchors_pre = b.contains_anchors(node_fwd_simpl);
1328        let ah = auto_harden(&mut b, fwd_start, has_anchors_pre);
1329        if ah.full {
1330            opts.hardened = true;
1331        }
1332        let (selected, rev_skip, _fwd_prefix_wins) = if opts.disable_prefixes {
1333            (None, None, false)
1334        } else {
1335            prefix::select_prefix(
1336                &mut b,
1337                node_fwd_simpl,
1338                ts_rev_start,
1339                has_look,
1340                min_len,
1341                max_cap,
1342                ah.no_fwd_prefix,
1343                opts.hardened,
1344                opts.force_convergence,
1345            )?
1346        };
1347        #[cfg(feature = "debug")]
1348        {
1349            let kind = match (&selected, &rev_skip) {
1350                (Some(prefix::PrefixKind::AnchoredFwd(_)), _) => "AnchoredFwd",
1351                (Some(prefix::PrefixKind::AnchoredFwdLb(_)), _) => "AnchoredFwdLb",
1352                (Some(prefix::PrefixKind::AnchoredRev), _) => "AnchoredRev",
1353                (Some(prefix::PrefixKind::PotentialStart), _) => "PotentialStart",
1354                #[cfg(feature = "convergence_prefix")]
1355                (Some(prefix::PrefixKind::Convergence), _) => "Convergence",
1356                (None, Some(_)) => "<none> (rev prefix_skip)",
1357                (None, None) => "<none>",
1358            };
1359            eprintln!("[prefix] selected={kind} rev_skip={}", rev_skip.is_some());
1360        }
1361        let has_fwd_prefix = matches!(
1362            selected,
1363            Some(prefix::PrefixKind::AnchoredFwd(_) | prefix::PrefixKind::AnchoredFwdLb(_))
1364        );
1365        let fwd = ldfa::LDFA::new_fwd(&mut b, fwd_start, max_cap)?;
1366
1367        let ts_fwd_start = {
1368            let with_ts = b.mk_concat(NodeId::TS, node);
1369            // b.simplify_fwd_initial(with_ts)
1370            with_ts
1371        };
1372        #[allow(unused_mut)]
1373        let mut ts_fwd = ldfa::LDFA::new_fwd(&mut b, ts_fwd_start, max_cap)?;
1374
1375        let mut rev_ts = ldfa::LDFA::new_rev(&mut b, ts_rev_start, max_cap)?;
1376        #[cfg(feature = "convergence_prefix")]
1377        let mut conv_b: Option<ldfa::LDFA> = None;
1378        #[cfg(feature = "convergence_prefix")]
1379        let mut conv_prefix = false;
1380        if let Some((search, resume_node, b_node)) = rev_skip {
1381            #[cfg(not(feature = "convergence_prefix"))]
1382            let _ = b_node;
1383            #[cfg(feature = "debug")]
1384            eprintln!("[conv split] resume_node={:?} b_node={:?}", resume_node.map(|n| b.pp(n)), b_node.map(|n| b.pp(n)));
1385            let resume = match resume_node {
1386                Some(node) => {
1387                    let pruned_node = rev_ts.state_nodes[rev_ts.pruned as usize];
1388                    let union = b.mk_union(node, pruned_node);
1389                    #[cfg(feature = "debug")]
1390                    eprintln!("[conv resume build] node={} pruned_node={} union={}", b.pp(node), b.pp(pruned_node), b.pp(union));
1391                    rev_ts.get_or_register(&mut b, union)
1392                }
1393                None => 0,
1394            };
1395            #[allow(unused_mut)]
1396            let mut window = 0u32;
1397            #[cfg(feature = "convergence_prefix")]
1398            if resume != 0 {
1399                let b_node = b_node.expect(
1400                    "convergence prefix (resume != 0) must carry its right-side `b` node",
1401                );
1402                let b_max = b.get_min_max_length(b_node).1;
1403                let fwd_window = if b_max == u32::MAX { 0 } else { b_max };
1404                let rev_b_node = b.reverse(b_node)?;
1405                let rev_b_node = b.normalize_rev(rev_b_node, 0)?;
1406                let rev_window = b.get_min_max_length(rev_b_node).0;
1407                window = fwd_window.max(rev_window);
1408                conv_b = Some(ldfa::LDFA::new_fwd(&mut b, b_node, max_cap)?);
1409                conv_prefix = true;
1410            }
1411            rev_ts.install_prefix(&mut b, search, resume as u32, window)?;
1412        }
1413        if !b.starts_with_ts(ts_rev_start) {
1414            rev_ts.ensure_dead_skip();
1415        }
1416
1417        let rev_anchored = if rev_end_anchored {
1418            let rev_no_ts = b.normalize_rev(rev_node, 0)?;
1419            Some(ldfa::LDFA::new_rev(&mut b, rev_no_ts, max_cap)?)
1420        } else {
1421            None
1422        };
1423
1424        #[cfg(feature = "stream")]
1425        let stream_init = {
1426            let fwd_pruned = b.prune_begin_eps(ts_fwd_start);
1427            let rev_pruned = b.prune_begin_eps(ts_rev_start);
1428            stream::StreamInit {
1429                start_node: node_fwd_simpl,
1430                seek_fwd: ts_fwd.get_or_register(&mut b, fwd_pruned).into(),
1431                seek_rev: rev_ts.get_or_register(&mut b, rev_pruned).into(),
1432            }
1433        };
1434
1435        let mut lb_verify: Option<ldfa::LDFA> = None;
1436        let (
1437            fwd_lb_begin_nullable,
1438            fwd_lb_begin_len,
1439            fwd_lb_begin_classes,
1440            fwd_lb_body_nullable,
1441            lb_check_bytes,
1442        ) =
1443            if matches!(selected, Some(prefix::PrefixKind::AnchoredFwdLb(_))) {
1444                let lb_node = node_fwd_simpl.left(&b);
1445                let lb_inner = b.get_lookbehind_inner(lb_node);
1446                let (lb_stripped_node, lb_fixed) = prefix::fwd_lb_class(&mut b, lb_node)
1447                    .ok_or(Error::InternalError("AnchoredFwdLb requires fixed-length lb"))?;
1448                lb_verify = Some(ldfa::LDFA::new_fwd(&mut b, lb_stripped_node, max_cap)?);
1449                let (begin_nullable, begin_len, begin_classes) =
1450                    match prefix::fwd_lb_begin_info(&mut b, lb_inner) {
1451                        prefix::BeginInfo::None => (false, 0, Vec::new()),
1452                        prefix::BeginInfo::Node(_, 0) => (true, 0, Vec::new()),
1453                        prefix::BeginInfo::Node(began, len) => {
1454                            let classes = prefix::fwd_lb_begin_classes(&mut b, began, len).ok_or(
1455                                Error::InternalError("AnchoredFwdLb begin path has no byte classes"),
1456                            )?;
1457                            (
1458                                true,
1459                                u8::try_from(len).map_err(|_| {
1460                                    Error::InternalError("AnchoredFwdLb begin_len exceeds u8")
1461                                })?,
1462                                classes,
1463                            )
1464                        }
1465                        prefix::BeginInfo::Unrepresentable => {
1466                            return Err(Error::InternalError(
1467                                "AnchoredFwdLb selected with unrepresentable begin path",
1468                            ));
1469                        }
1470                    };
1471                let body_nullable = b.nullability(fwd_start) != Nullability::NEVER;
1472                (
1473                    begin_nullable,
1474                    begin_len,
1475                    begin_classes,
1476                    body_nullable,
1477                    u8::try_from(lb_fixed)
1478                        .map_err(|_| Error::InternalError("AnchoredFwdLb lb_fixed exceeds u8"))?,
1479                )
1480            } else {
1481                (false, 0, Vec::new(), false, 0)
1482            };
1483
1484        // lots of conditions when something else is better.. possibly removing it entirely
1485        let always_nullable = initial_nullability == Nullability::ALWAYS;
1486        let max_len_limit = if always_nullable { 512 } else { 100 };
1487        let use_bounded = !opts.disable_prefixes
1488            && !has_fwd_prefix
1489            && max_length.is_some()
1490            && max_len <= max_len_limit
1491            && !b.contains_lookbehind(node_fwd_simpl)
1492            && !node_fwd_simpl.contains_lookahead(&b)
1493            && !b.contains_anchors(node_fwd_simpl)
1494            && pattern_len <= 150 // a guess..
1495            && (!empty_nullable || always_nullable);
1496
1497        let bounded = if use_bounded {
1498            Some(bdfa::BDFA::new(&mut b, fwd_start)?)
1499        } else {
1500            None
1501        };
1502
1503        let has_bounded = bounded.is_some();
1504        let bounded_safe_find_all = if has_bounded {
1505            if always_nullable {
1506                true
1507            } else if max_len < min_len.saturating_add(2) {
1508                true
1509            } else {
1510                let inner_match = b.mk_concat(node_fwd_simpl, resharp_algebra::NodeId::TOPPLUS);
1511                let interior = b.mk_concat(resharp_algebra::NodeId::TOPPLUS, inner_match);
1512                let overlap = b.mk_inter(node_fwd_simpl, interior);
1513                b.is_empty_lang(overlap) == Some(true)
1514            }
1515        } else {
1516            false
1517        };
1518        let has_anchors = b.contains_anchors(node_fwd_simpl);
1519        let has_lb = b.contains_lookbehind(node_fwd_simpl);
1520        let has_la = node_fwd_simpl.contains_lookahead(&b);
1521
1522        const CLASS_PLUS_FREQ: u64 = 65_535;
1523        let class_plus = if !has_lb && !has_la && !has_anchors && !has_look && fixed_length.is_none() {
1524            detect_class_plus(&mut b, fwd_start)
1525                .filter(|&c| {
1526                    let class = class_freq_sum(&mut b, c);
1527                    let compl = total_byte_freq().saturating_sub(class);
1528                    class >= CLASS_PLUS_FREQ && compl >= CLASS_PLUS_FREQ
1529                })
1530                .map(|c| class_membership(&mut b, c))
1531        } else {
1532            None
1533        };
1534
1535        let neg_lb = if has_lb && matches!(selected, Some(prefix::PrefixKind::AnchoredFwd(_))) {
1536            prefix::neg_lb_classes(&mut b, node_fwd_simpl)
1537        } else {
1538            None
1539        };
1540
1541        let need_nn_cycle = (opts.hardened && !has_bounded && fixed_length.is_none() && max_cap >= 64)
1542            || initial_nullability == Nullability::ALWAYS;
1543        let nn_cycle = need_nn_cycle && fwd.has_nonnullable_cycle(&mut b, 256);
1544        let hardened = opts.hardened && !has_bounded && fixed_length.is_none() && max_cap >= 64 && nn_cycle;
1545        let star_loop =
1546            initial_nullability == Nullability::ALWAYS && !has_lb && !nn_cycle;
1547
1548        let fas = if hardened || initial_nullability == Nullability::ALWAYS {
1549            let ksm = if hardened {
1550                fwd_start.contains_lookahead(&b) || initial_nullability != Nullability::ALWAYS
1551            } else {
1552                true
1553            };
1554            let ksm = ksm || initial_nullability == Nullability::ALWAYS;
1555            Some(fas::FwdDFA::new(&fwd, ksm))
1556        } else {
1557            None
1558        };
1559
1560        if b.take_unsupported_lb_fusion() {
1561            return Err(Error::Algebra(resharp_algebra::ResharpError::UnsupportedPattern));
1562        }
1563
1564        let captures_dispatch =
1565            captures::compute_capture_dispatch(&b, node_fwd_simpl, group_names.len())?;
1566
1567        let group_names = {
1568            let mut named = Vec::with_capacity(group_names.len() + 1);
1569            named.push(None);
1570            named.extend(group_names);
1571            named
1572        };
1573
1574        Ok(Regex {
1575            inner: Mutex::new(RegexInner {
1576                b,
1577                fwd,
1578                fwd_ts: ts_fwd,
1579                rev: rev_anchored,
1580                rev_ts,
1581                #[cfg(feature = "stream")]
1582                stream: stream_init,
1583                nulls: StartPositions::new(),
1584                matches: Vec::new(),
1585                bounded,
1586                fas,
1587                lb_verify,
1588                #[cfg(feature = "convergence_prefix")]
1589                conv_b,
1590                capture_root: node_fwd_simpl,
1591                capture_dfa: pparse::PosixParser::new(max_cap),
1592                skeleton,
1593            }),
1594            find_all: compute_find_all(
1595                is_empty_lang,
1596                fwd_begin_anchored,
1597                rev_end_anchored,
1598                hardened,
1599                has_bounded,
1600                class_plus.is_some(),
1601                &selected,
1602            ),
1603            fwd_lb_stripped: lb_stripped,
1604            class_plus,
1605            prefix: selected,
1606            fixed_length,
1607            empty_nullable,
1608            always_nullable: initial_nullability == Nullability::ALWAYS,
1609            star_loop,
1610            is_empty_lang,
1611            fwd_begin_anchored,
1612            rev_end_anchored,
1613            initial_nullability,
1614            fwd_end_nullable,
1615            rev_end_nullable,
1616            hardened,
1617            has_bounded,
1618            bounded_safe_find_all,
1619            lb_check_bytes,
1620            fwd_lb_begin_nullable,
1621            fwd_lb_begin_len,
1622            fwd_lb_begin_classes,
1623            fwd_lb_body_nullable,
1624            init_flags: InitialNodeFlags::new(has_anchors, has_lb, has_la),
1625            #[cfg(feature = "convergence_prefix")]
1626            conv_prefix,
1627            neg_lb,
1628            group_names,
1629            captures_dispatch,
1630            #[cfg(feature = "stream")]
1631            stream_cache: Default::default(),
1632        })
1633    }
1634
1635    #[cfg(feature = "diag")]
1636    #[allow(missing_docs)]
1637    pub fn node_count(&self) -> u32 {
1638        self.inner.lock().unwrap_or_else(|e| e.into_inner()).b.num_nodes()
1639    }
1640
1641    #[cfg(feature = "diag")]
1642    #[allow(missing_docs)]
1643    pub fn dfa_stats(&self) -> (usize, usize) {
1644        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
1645        (inner.fwd.state_nodes.len(), inner.rev_ts.state_nodes.len())
1646    }
1647
1648    #[cfg(feature = "diag")]
1649    #[allow(missing_docs)]
1650    pub fn is_hardened(&self) -> bool {
1651        self.hardened
1652    }
1653
1654    #[cfg(feature = "convergence_prefix")]
1655    #[allow(missing_docs)]
1656    pub fn uses_convergence_prefix(&self) -> bool {
1657        self.conv_prefix
1658    }
1659
1660    #[cfg(feature = "diag")]
1661    #[allow(missing_docs)]
1662    pub fn has_fwd_prefix(&self) -> bool {
1663        matches!(
1664            self.prefix,
1665            Some(prefix::PrefixKind::AnchoredFwd(_) | prefix::PrefixKind::AnchoredFwdLb(_))
1666        )
1667    }
1668
1669    #[cfg(feature = "diag")]
1670    #[allow(missing_docs)]
1671    pub fn has_prefix(&self) -> bool {
1672        self.prefix.is_some()
1673    }
1674
1675    #[cfg(feature = "diag")]
1676    #[allow(missing_docs)]
1677    pub fn is_fwd_begin_anchored(&self) -> bool {
1678        self.fwd_begin_anchored
1679    }
1680
1681    #[cfg(feature = "diag")]
1682    #[allow(missing_docs)]
1683    pub fn bdfa_stats(&self) -> Option<(usize, usize, usize)> {
1684        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
1685        inner
1686            .bounded
1687            .as_ref()
1688            .map(|b| (b.states.len(), 1usize << b.mt_log, b.prefix_len))
1689    }
1690
1691    #[cfg(feature = "diag")]
1692    #[allow(missing_docs)]
1693    pub fn find_all_kind_name(&self) -> &'static str {
1694        match self.find_all {
1695            FindAll::EmptyLang => "EmptyLang",
1696            FindAll::Anchored => "Anchored",
1697            FindAll::EndAnchored => "EndAnchored",
1698            FindAll::Hardened => "Hardened",
1699            FindAll::Dfa => "Dfa",
1700            FindAll::ClassPlus => "ClassPlus",
1701            FindAll::Bounded => "Bounded",
1702            FindAll::FwdPrefix => "FwdPrefix",
1703            FindAll::FwdLbPrefix => "FwdLbPrefix",
1704        }
1705    }
1706
1707    #[cfg(feature = "diag")]
1708    #[allow(missing_docs)]
1709    pub fn captures_kind_name(&self) -> &'static str {
1710        match &self.captures_dispatch {
1711            captures::CaptureDispatch::Empty => "Empty",
1712            captures::CaptureDispatch::FixedOffsets(_) => "FixedOffsets",
1713            captures::CaptureDispatch::Dfa => "Dfa",
1714        }
1715    }
1716
1717    #[cfg(feature = "diag")]
1718    #[allow(missing_docs)]
1719    pub fn prefix_kind_name(&self) -> Option<&'static str> {
1720        match &self.prefix {
1721            None => None,
1722            Some(prefix::PrefixKind::AnchoredFwd(_)) => Some("AnchoredFwd"),
1723            Some(prefix::PrefixKind::AnchoredFwdLb(_)) => Some("AnchoredFwdLb"),
1724            Some(prefix::PrefixKind::AnchoredRev) => Some("AnchoredRev"),
1725            Some(prefix::PrefixKind::PotentialStart) => Some("PotentialStart"),
1726            #[cfg(feature = "convergence_prefix")]
1727            Some(prefix::PrefixKind::Convergence) => Some("Convergence"),
1728        }
1729    }
1730
1731    #[cfg(feature = "diag")]
1732    #[allow(missing_docs)]
1733    pub fn fwd_prefix_kind(&self) -> Option<(&'static str, usize)> {
1734        match &self.prefix {
1735            Some(prefix::PrefixKind::AnchoredFwd(fp))
1736            | Some(prefix::PrefixKind::AnchoredFwdLb(fp)) => Some((fp.variant_name(), fp.len())),
1737            _ => None,
1738        }
1739    }
1740
1741    #[cfg(feature = "diag")]
1742    #[allow(missing_docs)]
1743    pub fn has_accel(&self) -> (bool, bool) {
1744        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
1745        let fwd = self.prefix.as_ref().is_some_and(|p| p.is_fwd());
1746        let rev = self.prefix.as_ref().is_some_and(|p| p.is_rev())
1747            || inner.rev_ts.can_skip();
1748        (fwd, rev)
1749    }
1750
1751    // sole verdict for empty input: a zero-width match at 0 iff the empty
1752    // string is in the language.
1753    pub(crate) fn empty_input_match(&self) -> Option<Match> {
1754        (self.empty_nullable && !self.is_empty_lang).then_some(Match { start: 0, end: 0 })
1755    }
1756
1757    /// all leftmost-longest, non-overlapping matches as `[start, end)` byte ranges.
1758    ///
1759    /// ```
1760    /// let re = resharp::Regex::new(r"\d+").unwrap();
1761    /// let m = re.find_all(b"abc 123 def 456").unwrap();
1762    /// assert_eq!(m.len(), 2);
1763    /// assert_eq!((m[0].start, m[0].end), (4, 7));
1764    /// ```
1765    pub fn find_all(&self, input: &[u8]) -> Result<Vec<Match>, Error> {
1766        if input.is_empty() {
1767            return Ok(self.empty_input_match().into_iter().collect());
1768        }
1769
1770        #[cfg(all(feature = "debug", debug_assertions))]
1771        eprintln!("[algorithm] {:?} input={:?}", self.find_all, input);
1772
1773        match self.find_all {
1774            FindAll::EmptyLang => Ok(vec![]),
1775            FindAll::ClassPlus => Ok(self.find_all_class_plus(input)),
1776            FindAll::Anchored => Ok(self.find_anchored(input)?.into_iter().collect()),
1777            FindAll::EndAnchored => Ok(self.find_end_anchored(input)?.into_iter().collect()),
1778            FindAll::Hardened | FindAll::Dfa => self.find_all_dfa(input),
1779            FindAll::Bounded => {
1780                if self.bounded_safe_find_all {
1781                    self.find_all_fwd_bounded(input)
1782                } else {
1783                    self.find_all_dfa(input)
1784                }
1785            }
1786            FindAll::FwdPrefix => match &self.prefix {
1787                Some(prefix::PrefixKind::AnchoredFwd(fp)) => {
1788                    self.find_all_fwd_prefix(fp, self.neg_lb.as_ref(), input)
1789                }
1790                _ => Err(Error::InternalError("FwdPrefix without AnchoredFwd prefix")),
1791            },
1792            FindAll::FwdLbPrefix => match &self.prefix {
1793                Some(prefix::PrefixKind::AnchoredFwdLb(fp)) => {
1794                    self.find_all_fwd_lb_prefix(fp, input)
1795                }
1796                _ => Err(Error::InternalError("FwdLbPrefix without AnchoredFwdLb prefix")),
1797            },
1798        }
1799    }
1800
1801    /// Capture groups of every [`Regex::find_all`] match, in the same order.
1802    ///
1803    /// ```
1804    /// let re = resharp::Regex::new(r"(?<user>[a-z]+)@(?<host>[a-z.]+)").unwrap();
1805    /// let caps = re.captures_all(b"joe@ex.com").unwrap();
1806    /// assert_eq!(caps[0].get(0), Some(resharp::Match { start: 0, end: 10 }));
1807    /// assert_eq!(caps[0].name("host"), Some(resharp::Match { start: 4, end: 10 }));
1808    /// ```
1809    #[cfg(feature = "experimental_capture_groups")]
1810    pub fn captures_all(&self, input: &[u8]) -> Result<Vec<Captures<'_>>, Error> {
1811        let matches = self.find_all(input)?;
1812        {
1813            let inner = &mut *self.inner.lock().unwrap_or_else(|e| e.into_inner());
1814            inner.capture_dfa.begin_input(input);
1815        }
1816        matches.into_iter().map(|m| self.captures_of(input, m)).collect()
1817    }
1818
1819    #[cfg(feature = "experimental_capture_groups")]
1820    fn captures_of(&self, input: &[u8], m: Match) -> Result<Captures<'_>, Error> {
1821        let mut spans = Vec::with_capacity(self.group_names.len());
1822        spans.push(Some((m.start, m.end)));
1823        spans.extend(self.captures_at(input, m.start, m.end)?);
1824        if spans.len() != self.group_names.len() {
1825            return Err(Error::InternalError("capture slot count does not match group count"));
1826        }
1827        Ok(Captures { names: &self.group_names, spans })
1828    }
1829
1830    #[cfg(feature = "experimental_capture_groups")]
1831    fn captures_at(
1832        &self,
1833        input: &[u8],
1834        begin: usize,
1835        end: usize,
1836    ) -> Result<Vec<Option<(usize, usize)>>, Error> {
1837        match &self.captures_dispatch {
1838            captures::CaptureDispatch::Empty => return Ok(Vec::new()),
1839            captures::CaptureDispatch::FixedOffsets(offsets) => {
1840                return Ok(offsets
1841                    .iter()
1842                    .map(|g| match *g {
1843                        captures::GroupOffset::FromBegin { open, close } => {
1844                            Some((begin + open as usize, begin + close as usize))
1845                        }
1846                        captures::GroupOffset::FromEnd { open, close } => {
1847                            Some((end - open as usize, end - close as usize))
1848                        }
1849                    })
1850                    .collect());
1851            }
1852            captures::CaptureDispatch::Dfa => {}
1853        }
1854        let inner = &mut *self.inner.lock().unwrap_or_else(|e| e.into_inner());
1855        let RegexInner {
1856            ref mut b,
1857            ref mut capture_dfa,
1858            ref skeleton,
1859            capture_root,
1860            ..
1861        } = *inner;
1862        pparse::extract_captures(b, capture_dfa, capture_root, skeleton.as_ref(), input, begin, end)
1863    }
1864
1865    /// Group names, indexed like [`Captures`]. `None` for entry 0 and for
1866    /// unnamed captures.
1867    ///
1868    /// ```
1869    /// let re = resharp::Regex::new(r"(?<year>\d{4})-(?<month>\d{2})").unwrap();
1870    /// assert_eq!(
1871    ///     re.capture_names(),
1872    ///     &[None, Some("year".to_string()), Some("month".to_string())]
1873    /// );
1874    /// ```
1875    #[cfg(feature = "experimental_capture_groups")]
1876    pub fn capture_names(&self) -> &[Option<String>] {
1877        &self.group_names
1878    }
1879
1880    /// Returns the index (as used by [`Captures::get`]) of the given group
1881    /// name, or `None` if there is no such named group in this pattern.
1882    #[cfg(feature = "experimental_capture_groups")]
1883    pub fn capture_index_for_name(&self, name: &str) -> Option<usize> {
1884        self.group_names
1885            .iter()
1886            .position(|n| n.as_deref() == Some(name))
1887    }
1888}
1889
1890fn push_end_zero_width(matches: &mut Vec<Match>, len: usize) {
1891    if matches.last().map(|m| m.start) != Some(len) {
1892        matches.push(Match { start: len, end: len });
1893    }
1894}
1895
1896fn single_sat_target(b: &mut RegexBuilder, node: NodeId) -> Option<(NodeId, TSetId)> {
1897    let der = b.der(node, Nullability::CENTER).ok()?;
1898    let mut targets: Vec<(NodeId, TSetId)> = Vec::new();
1899    b.collect_der_targets(der, TSetId::FULL, &mut targets);
1900    let mut live = targets.into_iter().filter(|(t, _)| *t != NodeId::BOT);
1901    let first = live.next()?;
1902    if live.next().is_some() {
1903        return None;
1904    }
1905    Some(first)
1906}
1907
1908fn detect_class_plus(b: &mut RegexBuilder, node: NodeId) -> Option<TSetId> {
1909    if b.nullability(node).has(Nullability::CENTER) {
1910        return None;
1911    }
1912    let (t1, c1) = single_sat_target(b, node)?;
1913    if !b.nullability(t1).has(Nullability::CENTER) {
1914        return None;
1915    }
1916    let (t2, c2) = single_sat_target(b, t1)?;
1917    if t2 != t1 || c2 != c1 {
1918        return None;
1919    }
1920    Some(c1)
1921}
1922
1923fn class_freq_sum(b: &mut RegexBuilder, set: TSetId) -> u64 {
1924    b.solver()
1925        .collect_bytes(set)
1926        .iter()
1927        .map(|&c| crate::simd::BYTE_FREQ[c as usize] as u64)
1928        .sum()
1929}
1930
1931fn total_byte_freq() -> u64 {
1932    crate::simd::BYTE_FREQ.iter().map(|&f| f as u64).sum()
1933}
1934
1935fn class_membership(b: &mut RegexBuilder, set: TSetId) -> [u64; 4] {
1936    let mut table = [0u64; 4];
1937    for &c in b.solver().collect_bytes(set).iter() {
1938        table[(c >> 6) as usize] |= 1u64 << (c & 63);
1939    }
1940    table
1941}
1942
1943fn compute_find_all(
1944    is_empty_lang: bool,
1945    fwd_begin_anchored: bool,
1946    rev_end_anchored: bool,
1947    hardened: bool,
1948    has_bounded: bool,
1949    class_plus: bool,
1950    prefix: &Option<prefix::PrefixKind>,
1951) -> FindAll {
1952    if is_empty_lang {
1953        return FindAll::EmptyLang;
1954    }
1955    if fwd_begin_anchored {
1956        return FindAll::Anchored;
1957    }
1958    if hardened {
1959        return FindAll::Hardened;
1960    }
1961    if class_plus {
1962        return FindAll::ClassPlus;
1963    }
1964    if rev_end_anchored {
1965        return FindAll::EndAnchored;
1966    }
1967    match prefix {
1968        Some(prefix::PrefixKind::AnchoredFwd(_)) => FindAll::FwdPrefix,
1969        Some(prefix::PrefixKind::AnchoredFwdLb(_)) => FindAll::FwdLbPrefix,
1970        Some(prefix::PrefixKind::AnchoredRev | prefix::PrefixKind::PotentialStart) => {
1971            FindAll::Dfa
1972        }
1973        _ => {
1974            if has_bounded {
1975                FindAll::Bounded
1976            } else {
1977                FindAll::Dfa
1978            }
1979        }
1980    }
1981}
1982
1983#[cfg(feature = "convergence_prefix")]
1984pub(crate) fn find_inner_literal(
1985    b: &mut resharp_algebra::RegexBuilder,
1986    search_start: resharp_algebra::NodeId,
1987) -> Option<(
1988    resharp_algebra::NodeId,
1989    Vec<resharp_algebra::solver::TSetId>,
1990    resharp_algebra::solver::TSetId,
1991)> {
1992    use resharp_algebra::NodeId;
1993    use resharp_algebra::solver::TSetId;
1994
1995    if search_start == NodeId::BOT || b.get_min_max_length(search_start).0 == 0 {
1996        return None;
1997    }
1998
1999    const MAX_SINK_FREQ: u64 = 25_000;
2000    fn set_freq(b: &mut resharp_algebra::RegexBuilder, set: TSetId) -> u64 {
2001        b.solver()
2002            .collect_bytes(set)
2003            .iter()
2004            .map(|&c| crate::simd::BYTE_FREQ[c as usize] as u64)
2005            .sum()
2006    }
2007
2008    let mut spine: Vec<(NodeId, NodeId)> = Vec::new();
2009    let mut curr = search_start;
2010    loop {
2011        let is_concat = curr.is_concat(b);
2012        let head = if is_concat { curr.left(b) } else { curr };
2013        spine.push((curr, head));
2014        if is_concat {
2015            curr = curr.right(b);
2016        } else {
2017            break;
2018        }
2019    }
2020
2021    let mut best: Option<(usize, TSetId)> = None;
2022    let mut best_score = u64::MAX;
2023    let mut needle_union = TSetId::EMPTY;
2024    let mut quad = false;
2025    for (i, &(_, head)) in spine.iter().enumerate() {
2026        if !quad && head.is_pred(b) {
2027            let l = head.pred_tset(b);
2028            let freq = set_freq(b, l);
2029            if freq < MAX_SINK_FREQ && freq < best_score {
2030                best = Some((i, l));
2031                best_score = freq;
2032            }
2033        }
2034        if b.get_min_max_length(head).1 == u32::MAX {
2035            let lead = match b.der(head, Nullability::CENTER) {
2036                Ok(d) => {
2037                    let mut stack = vec![(d, TSetId::FULL)];
2038                    let mut acc = TSetId::EMPTY;
2039                    b.iter_sat(&mut stack, &mut |bb, _n, set| {
2040                        acc = bb.solver().or_id(acc, set);
2041                    });
2042                    acc
2043                }
2044                Err(_) => b.solver().not_id(TSetId::EMPTY),
2045            };
2046            if b.solver().is_sat_id(lead, needle_union) {
2047                quad = true;
2048            }
2049        } else if head.is_pred(b) {
2050            let s = head.pred_tset(b);
2051            if set_freq(b, s) < MAX_SINK_FREQ {
2052                needle_union = b.solver().or_id(needle_union, s);
2053            }
2054        }
2055    }
2056    let (i, _l) = best?;
2057
2058    let is_byte_lit = |b: &mut RegexBuilder, head: NodeId| {
2059        if !head.is_pred(b) {
2060            return false;
2061        }
2062        let ts = head.pred_tset(b);
2063        b.solver().collect_bytes(ts).len() == 1
2064    };
2065    let mut i_lo = i;
2066    let mut i_hi = i;
2067    if is_byte_lit(b, spine[i].1) {
2068        while i_lo > 0 && is_byte_lit(b, spine[i_lo - 1].1) {
2069            i_lo -= 1;
2070        }
2071        while i_hi + 1 < spine.len() && is_byte_lit(b, spine[i_hi + 1].1) {
2072            i_hi += 1;
2073        }
2074    }
2075    let run: Vec<TSetId> = (i_lo..=i_hi).map(|k| spine[k].1.pred_tset(b)).collect();
2076    let anchor_set = run[0];
2077    let l_rep = run
2078        .iter()
2079        .copied()
2080        .min_by_key(|&s| set_freq(b, s))
2081        .unwrap();
2082
2083    let mut j = i_lo;
2084    while j > 0 {
2085        let prev_head = spine[j - 1].1;
2086        if !prev_head.is_star(b) {
2087            break;
2088        }
2089        let body = prev_head.left(b);
2090        if !body.is_pred(b) {
2091            break;
2092        }
2093        let body_set = body.pred_tset(b);
2094        if !b.solver().is_sat_id(body_set, anchor_set) {
2095            break;
2096        }
2097        j -= 1;
2098    }
2099    let prefix_result = spine[j].0;
2100    Some((prefix_result, run, l_rep))
2101}
2102
2103#[cfg(feature = "convergence_prefix")]
2104#[doc(hidden)]
2105pub fn detect_inner_literal_bytes(pattern: &str) -> Option<Vec<u8>> {
2106    let mut b = resharp_algebra::RegexBuilder::new();
2107    let pflags = resharp_parser::PatternFlags::default();
2108    let node = resharp_parser::parse_ast_with(&mut b, pattern, &pflags).ok()?;
2109    let node_fwd_simpl = b.simplify_fwd_initial(node);
2110    let rev_basis = b
2111        .strip_trailing_redundant_lookahead(node_fwd_simpl)
2112        .unwrap_or(node_fwd_simpl);
2113    let ts_rev_start = b.ts_rev_start(rev_basis).ok()?;
2114    let rev_stripped = crate::prefix::PrefixSets::compute(&mut b, node_fwd_simpl, ts_rev_start)
2115        .ok()?
2116        .rev_stripped;
2117    let (_c, run, _l) = find_inner_literal(&mut b, rev_stripped)?;
2118    let mut bytes: Vec<u8> = Vec::new();
2119    for &s in run.iter().rev() {
2120        bytes.extend(b.solver().collect_bytes(s));
2121    }
2122    Some(bytes)
2123}
2124
2125impl Regex {
2126    #[cfg(feature = "diag")]
2127    #[allow(missing_docs)]
2128    pub fn rev_state_dump(&self) -> String {
2129        let inner = &mut *self.inner.lock().unwrap_or_else(|e| e.into_inner());
2130        let rev = &inner.rev_ts;
2131        let mut out = String::new();
2132        for (i, &node) in rev.state_nodes.iter().enumerate() {
2133            let eid = rev.effects_id.get(i).copied().unwrap_or(0);
2134            let alg_nid = inner.b.get_nulls_id(node);
2135            let pretty = inner.b.pp(node);
2136            let pretty = if pretty.len() > 200 {
2137                format!("{}...", &pretty[..200])
2138            } else {
2139                pretty
2140            };
2141            out += &format!(
2142                "  s[{}] eid={} alg_nid={:?} pp={}\n",
2143                i, eid, alg_nid, pretty
2144            );
2145        }
2146        out
2147    }
2148
2149    #[cfg(feature = "diag")]
2150    #[allow(missing_docs)]
2151    pub fn diag_flags(&self) -> String {
2152        format!("fixed_length={:?} always_nullable={} hardened={} find_all={:?} initial_nullability={:?}", self.fixed_length, self.always_nullable, self.hardened, self.find_all, self.initial_nullability)
2153    }
2154
2155    #[cfg(feature = "diag")]
2156    #[allow(missing_docs)]
2157    pub fn fwd_effects_debug(&self) -> String {
2158        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
2159        let RegexInner { ref mut b, ref fwd, .. } = *inner;
2160        let mut out = String::new();
2161        for (i, &eid) in fwd.effects_id.iter().enumerate() {
2162            if eid != 0 {
2163                let nulls: Vec<String> = fwd.effects[eid as usize]
2164                    .iter()
2165                    .map(|n| format!("(mask={},rel={})", n.mask.0, n.rel))
2166                    .collect();
2167                let node = fwd.state_nodes.get(i).copied();
2168                let pp = node.map(|n| b.pp(n)).unwrap_or_default();
2169                out += &format!("  state[{}] node={:?}({}) eid={} nulls=[{}]\n", i, node, pp, eid, nulls.join(", "));
2170            }
2171        }
2172        out
2173    }
2174
2175    #[cfg(feature = "diag")]
2176    #[allow(missing_docs)]
2177    pub fn effects_debug(&self) -> String {
2178        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
2179        let rev = &inner.rev_ts;
2180        let mut out = String::new();
2181        for (i, &eid) in rev.effects_id.iter().enumerate() {
2182            if eid != 0 {
2183                let nulls: Vec<String> = rev.effects[eid as usize]
2184                    .iter()
2185                    .map(|n| format!("(mask={},rel={})", n.mask.0, n.rel))
2186                    .collect();
2187                out += &format!("  state[{}] eid={} nulls=[{}]\n", i, eid, nulls.join(", "));
2188            }
2189        }
2190        out
2191    }
2192
2193    #[cfg(feature = "diag")]
2194    #[allow(missing_docs)]
2195    pub fn collect_rev_nulls_debug(&self, input: &[u8]) -> Vec<usize> {
2196        let inner = &mut *self.inner.lock().unwrap_or_else(|e| e.into_inner());
2197        inner.nulls.clear();
2198        #[cfg(feature = "convergence_prefix")]
2199        {
2200            let RegexInner { rev_ts, b, nulls, conv_b, .. } = &mut *inner;
2201            rev_ts
2202                .collect_rev(b, input.len() - 1, input, nulls, conv_b.as_mut())
2203                .unwrap();
2204        }
2205        #[cfg(not(feature = "convergence_prefix"))]
2206        inner
2207            .rev_ts
2208            .collect_rev(&mut inner.b, input.len() - 1, input, &mut inner.nulls, None)
2209            .unwrap();
2210        inner.nulls.positions_desc().collect()
2211    }
2212
2213    #[cfg(feature = "diag")]
2214    #[allow(missing_docs)]
2215    pub fn scan_fwd_debug(&self, input: &[u8], pos: usize) -> Option<usize> {
2216        let inner = &mut *self.inner.lock().unwrap_or_else(|e| e.into_inner());
2217        inner.fwd.scan_fwd_optional(&mut inner.b, pos, input).unwrap()
2218    }
2219
2220    #[cfg(feature = "diag")]
2221    #[allow(missing_docs)]
2222    pub fn scan_fwd_all_nulls_debug(&self, input: &[u8], pos: usize) -> Vec<usize> {
2223        let inner = &mut *self.inner.lock().unwrap_or_else(|e| e.into_inner());
2224        let mut nulls = Vec::new();
2225        inner
2226            .fwd
2227            .scan_fwd_all_nulls_to(&mut inner.b, pos, input.len(), input, &mut nulls)
2228            .unwrap();
2229        nulls.sort_unstable();
2230        nulls.dedup();
2231        nulls
2232    }
2233
2234    /// Walk RTL step by step, printing the rev-DFA state and its nulls
2235    /// metadata at each position. Returns the trace as a string.
2236    #[cfg(feature = "diag")]
2237    #[allow(missing_docs)]
2238    pub fn rev_walk_trace(&self, input: &[u8]) -> String {
2239        use std::fmt::Write;
2240        let inner = &mut *self.inner.lock().unwrap_or_else(|e| e.into_inner());
2241        let rev = &mut inner.rev_ts;
2242        let b = &mut inner.b;
2243        let mut out = String::new();
2244        if input.is_empty() {
2245            return out;
2246        }
2247        let last = input.len() - 1;
2248        let mt = rev.mt_lookup[input[last] as usize] as u32;
2249        let mut sid = rev.begin_table[mt as usize];
2250        writeln!(
2251            out,
2252            "pos={} byte={:?} (BEGIN ctx) -> s[{}]",
2253            last, input[last] as char, sid
2254        )
2255        .unwrap();
2256        Self::dump_state(&mut out, b, rev, sid);
2257        for i in (0..last).rev() {
2258            let mt = rev.mt_lookup[input[i] as usize] as u32;
2259            sid = rev.lazy_transition(b, sid, mt).unwrap();
2260            writeln!(
2261                out,
2262                "pos={} byte={:?} (CENTER ctx) -> s[{}]",
2263                i, input[i] as char, sid
2264            )
2265            .unwrap();
2266            Self::dump_state(&mut out, b, rev, sid);
2267            if sid as u32 <= ldfa::DFA_DEAD as u32 {
2268                break;
2269            }
2270        }
2271        out
2272    }
2273
2274    #[cfg(feature = "diag")]
2275    #[allow(missing_docs)]
2276    pub fn fwd_state_dump(&self) -> String {
2277        let inner = &mut *self.inner.lock().unwrap_or_else(|e| e.into_inner());
2278        let fwd = &inner.fwd;
2279        let mut out = String::new();
2280        for (i, &node) in fwd.state_nodes.iter().enumerate() {
2281            let eid = fwd.effects_id.get(i).copied().unwrap_or(0);
2282            let ceid = fwd.center_effect_id.get(i).copied().unwrap_or(0);
2283            let pretty = inner.b.pp(node);
2284            let pretty = if pretty.len() > 400 {
2285                format!("{}...", &pretty[..400])
2286            } else {
2287                pretty
2288            };
2289            out += &format!("  s[{}] eid={} ceid={} pp={}\n", i, eid, ceid, pretty);
2290        }
2291        out
2292    }
2293
2294    #[cfg(feature = "diag")]
2295    #[allow(missing_docs)]
2296    pub fn fwd_walk_trace(&self, input: &[u8]) -> String {
2297        use std::fmt::Write;
2298        let inner = &mut *self.inner.lock().unwrap_or_else(|e| e.into_inner());
2299        let fwd = &mut inner.fwd;
2300        let b = &mut inner.b;
2301        let mut out = String::new();
2302        if input.is_empty() {
2303            return out;
2304        }
2305        let mt = fwd.mt_lookup[input[0] as usize] as u32;
2306        let mut sid = fwd.begin_table[mt as usize];
2307        writeln!(
2308            out,
2309            "pos=0 byte={:?} (BEGIN) -> s[{}]",
2310            input[0] as char, sid
2311        )
2312        .unwrap();
2313        Self::dump_fwd_state(&mut out, b, fwd, sid);
2314        for i in 1..input.len() {
2315            let mt = fwd.mt_lookup[input[i] as usize] as u32;
2316            sid = fwd.lazy_transition(b, sid, mt).unwrap();
2317            writeln!(out, "pos={} byte={:?} -> s[{}]", i, input[i] as char, sid).unwrap();
2318            Self::dump_fwd_state(&mut out, b, fwd, sid);
2319            if sid as u32 <= ldfa::DFA_DEAD as u32 {
2320                break;
2321            }
2322        }
2323        out
2324    }
2325
2326    #[cfg(feature = "diag")]
2327    fn dump_fwd_state(
2328        out: &mut String,
2329        b: &mut resharp_algebra::RegexBuilder,
2330        fwd: &ldfa::LDFA,
2331        sid: u16,
2332    ) {
2333        use std::fmt::Write;
2334        if (sid as usize) >= fwd.state_nodes.len() {
2335            writeln!(out, "  (uninitialized state)").unwrap();
2336            return;
2337        }
2338        let node = fwd.state_nodes[sid as usize];
2339        let eid = fwd.effects_id.get(sid as usize).copied().unwrap_or(0);
2340        let ceid = fwd.center_effect_id.get(sid as usize).copied().unwrap_or(0);
2341        let pp = b.pp(node);
2342        let pp = if pp.len() > 240 {
2343            format!("{}...", &pp[..240])
2344        } else {
2345            pp
2346        };
2347        writeln!(out, "  pp = {}", pp).unwrap();
2348        writeln!(out, "  eid={} (end), center_eid={}", eid, ceid).unwrap();
2349        for (label, e) in [("end", eid), ("center", ceid)] {
2350            if e != 0 && (e as usize) < fwd.effects.len() {
2351                let entries: Vec<String> = fwd.effects[e as usize]
2352                    .iter()
2353                    .map(|n| format!("(mask={:#b},rel={})", n.mask.0, n.rel))
2354                    .collect();
2355                writeln!(
2356                    out,
2357                    "  effects[{}][{}] = [{}]",
2358                    label,
2359                    e,
2360                    entries.join(", ")
2361                )
2362                .unwrap();
2363            }
2364        }
2365    }
2366
2367    #[cfg(feature = "diag")]
2368    fn dump_state(
2369        out: &mut String,
2370        b: &mut resharp_algebra::RegexBuilder,
2371        rev: &ldfa::LDFA,
2372        sid: u16,
2373    ) {
2374        use std::fmt::Write;
2375        if (sid as usize) >= rev.state_nodes.len() {
2376            writeln!(out, "  (uninitialized state)").unwrap();
2377            return;
2378        }
2379        let node = rev.state_nodes[sid as usize];
2380        let eid = rev.effects_id.get(sid as usize).copied().unwrap_or(0);
2381        let alg_nid = b.get_nulls_id(node);
2382        let pp = b.pp(node);
2383        let pp = if pp.len() > 240 {
2384            format!("{}...", &pp[..240])
2385        } else {
2386            pp
2387        };
2388        writeln!(out, "  pp = {}", pp).unwrap();
2389        writeln!(out, "  alg_nulls = {:?}", alg_nid).unwrap();
2390        if eid != 0 {
2391            let entries: Vec<String> = rev.effects[eid as usize]
2392                .iter()
2393                .map(|n| format!("(mask={:#b},rel={})", n.mask.0, n.rel))
2394                .collect();
2395            writeln!(
2396                out,
2397                "  dfa_effects[eid={}] = [{}] -> EMIT NULL",
2398                eid,
2399                entries.join(", ")
2400            )
2401            .unwrap();
2402        } else {
2403            writeln!(out, "  dfa_effects = (none, eid=0)").unwrap();
2404        }
2405    }
2406
2407    /// streamlined [X]+ for very common regexes like [a-z]+ etc
2408    fn find_all_class_plus(&self, input: &[u8]) -> Vec<Match> {
2409        let table = self
2410            .class_plus
2411            .expect("FindAll::ClassPlus requires a materialized class membership table");
2412        let member = |byte: u8| table[(byte >> 6) as usize] & (1u64 << (byte & 63)) != 0;
2413        let mut matches = Vec::new();
2414        let n = input.len();
2415        let mut i = 0usize;
2416        while i < n {
2417            if member(input[i]) {
2418                let start = i;
2419                i += 1;
2420                while i < n && member(input[i]) {
2421                    i += 1;
2422                }
2423                matches.push(Match { start, end: i });
2424            } else {
2425                i += 1;
2426            }
2427        }
2428        matches
2429    }
2430
2431    /// `Y·_*` shape: emit single match at leftmost Y start.
2432    #[allow(dead_code)]
2433    fn find_all_trailing_star(&self, input: &[u8]) -> Result<Vec<Match>, Error> {
2434        let inner = &mut *self.inner.lock().unwrap_or_else(|e| e.into_inner());
2435        let mut pos = 0;
2436        while pos < input.len() {
2437            if let Some(max_end) = inner.fwd.scan_fwd_optional(&mut inner.b, pos, input)? {
2438                if max_end > pos {
2439                    return Ok(vec![Match {
2440                        start: pos,
2441                        end: max_end,
2442                    }]);
2443                }
2444            }
2445            pos += 1;
2446        }
2447        Ok(vec![])
2448    }
2449
2450    fn find_all_dfa(&self, input: &[u8]) -> Result<Vec<Match>, Error> {
2451        self.find_all_dfa_inner(input)
2452    }
2453
2454    fn find_all_dfa_inner(&self, input: &[u8]) -> Result<Vec<Match>, Error> {
2455        debug_assert!(!input.is_empty());
2456        let inner = &mut *self.inner.lock().unwrap_or_else(|e| e.into_inner());
2457        inner.nulls.clear();
2458        inner.matches.clear();
2459
2460        if self.always_nullable && !self.hardened {
2461            if self.star_loop {
2462                let RegexInner {
2463                    ref mut b,
2464                    ref mut fwd,
2465                    ref mut matches,
2466                    ..
2467                } = *inner;
2468                let len = input.len();
2469                let mut start = 0usize;
2470                while start < len {
2471                    let end = fwd.scan_fwd_optional(b, start, input)?.expect(
2472                        "always-nullable pattern matches (at least empty) at every position",
2473                    );
2474                    if end > start {
2475                        matches.push(Match { start, end });
2476                        start = end;
2477                    } else {
2478                        matches.push(Match { start, end: start });
2479                        start += 1;
2480                    }
2481                }
2482                push_end_zero_width(matches, len);
2483                return Ok(matches.clone());
2484            }
2485            let RegexInner {
2486                ref mut b,
2487                ref mut fwd,
2488                ref mut matches,
2489                ref mut fas,
2490                ..
2491            } = *inner;
2492            let fas = fas.as_mut().expect("fas initialized for always_nullable");
2493            fwd.scan_fwd_active_set::<true>(b, fas, input, &StartPositions::new(), matches)?;
2494            push_end_zero_width(matches, input.len());
2495            return Ok(matches.clone());
2496        }
2497
2498        if self.rev_end_nullable {
2499            inner.nulls.add(input.len());
2500        }
2501        {
2502            #[cfg(feature = "convergence_prefix")]
2503            {
2504                let RegexInner { rev_ts, b, nulls, conv_b, .. } = &mut *inner;
2505                rev_ts.collect_rev(b, input.len() - 1, input, nulls, conv_b.as_mut())?;
2506            }
2507            #[cfg(not(feature = "convergence_prefix"))]
2508            {
2509                let RegexInner { rev_ts, b, nulls, .. } = &mut *inner;
2510                rev_ts.collect_rev(b, input.len() - 1, input, nulls, None)?;
2511            }
2512        }
2513
2514        if self.initial_nullability.has(Nullability::BEGIN) {
2515            inner.nulls.add(0);
2516        }
2517
2518        #[cfg(all(feature = "debug", debug_assertions))]
2519        eprintln!("[nulls] {:?}", inner.nulls);
2520
2521        if self.hardened {
2522            let RegexInner {
2523                ref mut b,
2524                ref mut fwd,
2525                ref mut matches,
2526                ref mut fas,
2527                ref nulls,
2528                ..
2529            } = *inner;
2530            let fas = fas.as_mut().unwrap();
2531            if self.always_nullable {
2532                fwd.scan_fwd_active_set::<true>(b, fas, input, nulls, matches)?;
2533                push_end_zero_width(matches, input.len());
2534            } else {
2535                fwd.scan_fwd_active_set::<false>(b, fas, input, nulls, matches)?;
2536            }
2537            return Ok(matches.clone());
2538        }
2539
2540        if let Some(fl) = self.fixed_length {
2541            let fl = fl as usize;
2542            let mut last_end = 0;
2543            for start in inner.nulls.positions_asc() {
2544                if start >= last_end  {
2545                    inner.matches.push(Match {
2546                        start,
2547                        end: start + fl,
2548                    });
2549                    last_end = start + fl;
2550                }
2551            }
2552        } else {
2553            inner
2554                .fwd
2555                .scan_fwd_all(&mut inner.b, &inner.nulls, input, &mut inner.matches)?;
2556        }
2557
2558        if self.always_nullable {
2559            inner.matches.push(Match {
2560                start: input.len(),
2561                end: input.len(),
2562            });
2563        }
2564
2565        Ok(inner.matches.clone())
2566    }
2567
2568    /// longest match anchored at position 0.
2569    ///
2570    /// returns `None` if the pattern does not match at position 0.
2571    pub fn find_anchored(&self, input: &[u8]) -> Result<Option<Match>, Error> {
2572        if input.is_empty() {
2573            return Ok(self.empty_input_match());
2574        }
2575        let inner = &mut *self.inner.lock().unwrap_or_else(|e| e.into_inner());
2576        if self.fwd_lb_stripped {
2577            // ugly scenario for find_anchored, easier to reject it than to special case it
2578            return Err(Error::Algebra(resharp_algebra::ResharpError::UnsupportedPattern))
2579        }
2580        Ok(inner.fwd.scan_fwd_optional(&mut inner.b, 0, input)?.map(|end| Match { start: 0, end }))
2581    }
2582
2583    /// whether the whole input matches the pattern, i.e. a match spanning
2584    /// `0..input.len()`.
2585    ///
2586    /// equivalent to checking `find_anchored(input)?.map_or(false, |m| m.end == input.len())`.
2587    pub fn is_full_match(&self, input: &[u8]) -> Result<bool, Error> {
2588        Ok(self
2589            .find_anchored(input)?
2590            .is_some_and(|m| m.end == input.len()))
2591    }
2592
2593    /// longest match anchored at the string end (`\z`).
2594    ///
2595    /// returns `None` if the pattern does not match ending at the string end.
2596    pub(crate) fn find_end_anchored(&self, input: &[u8]) -> Result<Option<Match>, Error> {
2597        debug_assert!(!input.is_empty());
2598        let len = input.len();
2599        let inner = &mut *self.inner.lock().unwrap_or_else(|e| e.into_inner());
2600        let RegexInner { b, rev, .. } = &mut *inner;
2601        let rev_dfa = rev.as_mut().expect(
2602            "find_end_anchored requires the _*-free reverse DFA, built whenever rev_end_anchored holds (the same condition that selects FindAll::EndAnchored)",
2603        );
2604        Ok(rev_dfa
2605            .scan_rev_from(b, len, 0, input)?
2606            .map(|start| Match { start, end: len }))
2607    }
2608
2609}
2610