mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 10:41:11 +08:00
1275 lines
44 KiB
Rust
1275 lines
44 KiB
Rust
use crate::classicalbacktrack;
|
||
use crate::emit;
|
||
use crate::exec;
|
||
use crate::indexing;
|
||
use crate::insn::CompiledRegex;
|
||
use crate::optimizer;
|
||
use crate::parse;
|
||
use crate::types::MAX_CAPTURE_GROUPS;
|
||
use std::iter::FusedIterator;
|
||
|
||
#[cfg(feature = "utf16")]
|
||
use crate::{
|
||
classicalbacktrack::MatchAttempter,
|
||
indexing::{InputIndexer, Ucs2Input, Utf16Input},
|
||
};
|
||
|
||
#[cfg(feature = "backend-pikevm")]
|
||
use crate::pikevm;
|
||
use crate::util::to_char_sat;
|
||
use std::sync::OnceLock;
|
||
|
||
use core::{fmt, str::FromStr};
|
||
#[cfg(feature = "std")]
|
||
#[cfg(not(feature = "std"))]
|
||
use {
|
||
alloc::{string::String, vec::Vec},
|
||
hashbrown::{HashMap, hash_map::Iter},
|
||
};
|
||
|
||
pub use parse::Error;
|
||
|
||
/// Flags used to control regex parsing.
|
||
/// The default flags are case-sensitive, not-multiline, and optimizing.
|
||
#[derive(Debug, Copy, Clone, Default)]
|
||
pub struct Flags {
|
||
/// If set, make the regex case-insensitive.
|
||
/// Equivalent to the 'i' flag in JavaScript.
|
||
pub icase: bool,
|
||
|
||
/// If set, ^ and $ match at line separators, not just the input boundaries.
|
||
/// Equivalent to the 'm' flag in JavaScript.
|
||
pub multiline: bool,
|
||
|
||
/// If set, . matches at line separators as well as any other character.
|
||
/// Equivalent to the 's' flag in JavaScript.
|
||
pub dot_all: bool,
|
||
|
||
/// If set, disable regex IR passes.
|
||
pub no_opt: bool,
|
||
|
||
/// If set, the regex is interpreted as a Unicode regex.
|
||
/// Equivalent to the 'u' flag in JavaScript.
|
||
pub unicode: bool,
|
||
|
||
/// If set, the regex is interpreted as a UnicodeSets regex.
|
||
/// Equivalent to the 'v' flag in JavaScript.
|
||
pub unicode_sets: bool,
|
||
}
|
||
|
||
impl Flags {
|
||
/// Construct a Flags from a Unicode codepoints iterator, using JavaScript field names.
|
||
/// 'i' means to ignore case, 'm' means multiline, 'u' means unicode.
|
||
/// Note the 'g' flag implies a stateful regex and is not supported.
|
||
/// Other flags are not implemented and are ignored.
|
||
#[inline]
|
||
pub fn new<T: Iterator<Item = u32>>(chars: T) -> Self {
|
||
let mut result = Self::default();
|
||
for c in chars {
|
||
match to_char_sat(c) {
|
||
'm' => {
|
||
result.multiline = true;
|
||
}
|
||
'i' => {
|
||
result.icase = true;
|
||
}
|
||
's' => {
|
||
result.dot_all = true;
|
||
}
|
||
'u' => {
|
||
result.unicode = true;
|
||
}
|
||
'v' => {
|
||
result.unicode_sets = true;
|
||
}
|
||
_ => {
|
||
// Silently skip unsupported flags.
|
||
}
|
||
}
|
||
}
|
||
result
|
||
}
|
||
}
|
||
|
||
impl From<&str> for Flags {
|
||
/// Construct a Flags from a string, using JavaScript field names.
|
||
///
|
||
/// See also: [`Flags::new`].
|
||
#[inline]
|
||
fn from(s: &str) -> Self {
|
||
Self::new(s.chars().map(u32::from))
|
||
}
|
||
}
|
||
|
||
impl fmt::Display for Flags {
|
||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||
if self.multiline {
|
||
f.write_str("m")?;
|
||
}
|
||
if self.icase {
|
||
f.write_str("i")?;
|
||
}
|
||
if self.dot_all {
|
||
f.write_str("s")?;
|
||
}
|
||
if self.unicode {
|
||
f.write_str("u")?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Range is used to express the extent of a match, as indexes into the input
|
||
/// string.
|
||
pub type Range = core::ops::Range<usize>;
|
||
|
||
/// An iterator type which yields `Match`es found in a string.
|
||
pub type Matches<'r, 't> = exec::Matches<backends::DefaultExecutor<'r, 't>>;
|
||
|
||
/// An iterator type which yields `Match`es found in a string, supporting ASCII
|
||
/// only.
|
||
pub type AsciiMatches<'r, 't> = exec::Matches<backends::DefaultAsciiExecutor<'r, 't>>;
|
||
|
||
/// A Match represents a portion of a string which was found to match a Regex.
|
||
#[derive(Debug, Clone)]
|
||
pub struct Match {
|
||
/// The total range of the match. Note this may be empty, if the regex
|
||
/// matched an empty string.
|
||
pub range: Range,
|
||
|
||
/// The list of captures. This has length equal to the number of capturing
|
||
/// groups in the regex. For each capture, if the value is None, that group
|
||
/// did not match (for example, it was in a not-taken branch of an
|
||
/// alternation). If the value is Some, the group did match with the
|
||
/// enclosed range.
|
||
pub captures: Vec<Option<Range>>,
|
||
|
||
// A list of capture group names. This is either:
|
||
// - Empty, if there were no named capture groups.
|
||
// - A list of names with length `captures.len()`, corresponding to the
|
||
// capture group names in order. Groups without names have an empty string.
|
||
pub(crate) group_names: Box<[Box<str>]>,
|
||
}
|
||
|
||
impl Match {
|
||
/// Access a group by index, using the convention of Python's group()
|
||
/// function. Index 0 is the total match, index 1 is the first capture
|
||
/// group.
|
||
#[inline]
|
||
pub fn group(&self, idx: usize) -> Option<Range> {
|
||
if idx == 0 {
|
||
Some(self.range.clone())
|
||
} else if idx <= self.captures.len() {
|
||
self.captures[idx - 1].clone()
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
/// Access a named group by name.
|
||
#[inline]
|
||
pub fn named_group(&self, name: &str) -> Option<Range> {
|
||
// Empty strings are used as sentinels to indicate unnamed group.
|
||
if name.is_empty() {
|
||
return None;
|
||
}
|
||
let pos = self.group_names.iter().position(|s| s.as_ref() == name)?;
|
||
self.captures[pos].clone()
|
||
}
|
||
|
||
/// Return an iterator over the named groups of a Match.
|
||
#[inline]
|
||
pub fn named_groups(&self) -> NamedGroups<'_> {
|
||
NamedGroups::new(self)
|
||
}
|
||
|
||
/// Returns the range over the starting and ending byte offsets of the match in the haystack.
|
||
///
|
||
/// This is a convenience function to work around
|
||
/// the fact that Range does not support Copy.
|
||
#[inline]
|
||
pub fn range(&self) -> Range {
|
||
self.range.clone()
|
||
}
|
||
|
||
/// Returns the starting byte offset of the match in the haystack.
|
||
#[inline]
|
||
pub fn start(&self) -> usize {
|
||
self.range.start
|
||
}
|
||
|
||
/// Returns the ending byte offset of the match in the haystack.
|
||
#[inline]
|
||
pub fn end(&self) -> usize {
|
||
self.range.end
|
||
}
|
||
|
||
/// Returns the matched text as a string slice.
|
||
///
|
||
/// # Examples
|
||
///
|
||
/// ```rust
|
||
/// use regress::Regex;
|
||
///
|
||
/// let re = Regex::new(r"\d+").unwrap();
|
||
/// let text = "Price: $123";
|
||
/// let m = re.find(text).unwrap();
|
||
/// assert_eq!(m.as_str(text), "123");
|
||
/// ```
|
||
#[inline]
|
||
pub fn as_str<'t>(&self, text: &'t str) -> &'t str {
|
||
&text[self.range()]
|
||
}
|
||
|
||
/// Return an iterator over a Match. The first returned value is the total
|
||
/// match, and subsequent values represent the capture groups.
|
||
#[inline]
|
||
pub fn groups(&self) -> Groups<'_> {
|
||
Groups::new(self)
|
||
}
|
||
}
|
||
|
||
/// An iterator over the capture groups of a [`Match`]
|
||
///
|
||
/// This struct is created by the [`groups`] method on [`Match`].
|
||
///
|
||
/// [`Match`]: ../struct.Match.html
|
||
/// [`groups`]: ../struct.Match.html#method.groups
|
||
#[derive(Clone)]
|
||
pub struct Groups<'m> {
|
||
mat: &'m Match,
|
||
|
||
// The next group index to return, where 0 references the total match.
|
||
next_group_idx: usize,
|
||
|
||
// The maximum group index to return, with a +1 for the implicit total match.
|
||
// For example, in a regex with 1 capture group, this will be 2.
|
||
max: usize,
|
||
}
|
||
|
||
impl<'m> Groups<'m> {
|
||
#[inline]
|
||
fn new(mat: &'m Match) -> Self {
|
||
Self {
|
||
mat,
|
||
next_group_idx: 0,
|
||
max: mat.captures.len() + 1, // +1 for the total match
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Iterator for Groups<'_> {
|
||
type Item = Option<Range>;
|
||
|
||
#[inline]
|
||
fn next(&mut self) -> Option<Self::Item> {
|
||
let i = self.next_group_idx;
|
||
if i < self.max {
|
||
self.next_group_idx += 1;
|
||
Some(self.mat.group(i))
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||
let size = self.max.saturating_sub(self.next_group_idx);
|
||
(size, Some(size))
|
||
}
|
||
}
|
||
|
||
impl<'m> ExactSizeIterator for Groups<'m> {}
|
||
impl<'m> FusedIterator for Groups<'m> {}
|
||
|
||
/// An iterator over the named capture groups of a [`Match`]
|
||
///
|
||
/// This struct is created by the [`named_groups`] method on [`Match`].
|
||
///
|
||
/// [`Match`]: ../struct.Match.html
|
||
/// [`named_groups`]: ../struct.Match.html#method.named_groups
|
||
#[derive(Clone)]
|
||
pub struct NamedGroups<'m> {
|
||
mat: &'m Match,
|
||
|
||
// The next group name index to return.
|
||
// Note unlike `Groups` this does NOT include the implicit total match.
|
||
// That is, group 0 is the first capture group, NOT the total match.
|
||
next_group_idx: usize,
|
||
}
|
||
|
||
impl<'m> NamedGroups<'m> {
|
||
#[inline]
|
||
fn new(mat: &'m Match) -> Self {
|
||
Self {
|
||
mat,
|
||
next_group_idx: 0,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl<'m> Iterator for NamedGroups<'m> {
|
||
type Item = (&'m str, Option<Range>);
|
||
|
||
#[inline]
|
||
fn next(&mut self) -> Option<Self::Item> {
|
||
// Increment next_group_idx until we find a non-empty name that we haven't seen yet.
|
||
debug_assert!(self.next_group_idx <= self.mat.group_names.len());
|
||
let end = self.mat.group_names.len();
|
||
|
||
loop {
|
||
let mut idx = self.next_group_idx;
|
||
// Skip empty names
|
||
while idx < end && self.mat.group_names[idx].is_empty() {
|
||
idx += 1;
|
||
}
|
||
if idx == end {
|
||
return None;
|
||
}
|
||
|
||
let name = self.mat.group_names[idx].as_ref();
|
||
|
||
// Check if we've already returned this name (by looking backwards)
|
||
let already_seen = self.mat.group_names[..idx]
|
||
.iter()
|
||
.any(|n| n.as_ref() == name);
|
||
|
||
if already_seen {
|
||
// Skip this duplicate and continue to next
|
||
self.next_group_idx = idx + 1;
|
||
continue;
|
||
}
|
||
|
||
// This is the first occurrence of this name. Find the best range value.
|
||
// Prefer a Some value over None when there are duplicate names.
|
||
let mut best_range = self.mat.captures[idx].clone();
|
||
for check_idx in (idx + 1)..end {
|
||
if self.mat.group_names[check_idx].as_ref() == name {
|
||
// Found a duplicate name. Prefer a Some value over None.
|
||
if best_range.is_none() && self.mat.captures[check_idx].is_some() {
|
||
best_range = self.mat.captures[check_idx].clone();
|
||
break; // Stop once we find a Some value
|
||
}
|
||
}
|
||
}
|
||
|
||
self.next_group_idx = idx + 1;
|
||
return Some((name, best_range));
|
||
}
|
||
}
|
||
|
||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||
let size = self.mat.group_names[self.next_group_idx..]
|
||
.iter()
|
||
.filter(|s| !s.is_empty())
|
||
.count();
|
||
|
||
(size, Some(size))
|
||
}
|
||
}
|
||
|
||
impl<'m> ExactSizeIterator for NamedGroups<'m> {}
|
||
impl<'m> FusedIterator for NamedGroups<'m> {}
|
||
|
||
/// A Regex is the compiled version of a pattern.
|
||
#[derive(Debug, Clone)]
|
||
pub struct Regex {
|
||
cr: CompiledRegex,
|
||
|
||
// 模式源码码点序列:str 路径编译一次消费,留存供单元向 IR 惰性重解析。
|
||
// 仅 `From<CompiledRegex>` 构造无源码(None)。
|
||
#[cfg_attr(not(feature = "utf16"), allow(dead_code))]
|
||
pattern: Option<Vec<u32>>,
|
||
|
||
// 单元向编译产物(未跑字节字面量 pass),首次单元输入入口时惰性物化。
|
||
// 两路缓存:码点向入口(`find_from_utf16`)不拆非 BMP 码点字面量;
|
||
// 码元向入口(`find_from_ucs2`)拆为 surrogate 对。两 IR 不得共用。
|
||
#[cfg_attr(not(feature = "utf16"), allow(dead_code))]
|
||
units_cr: OnceLock<CompiledRegex>,
|
||
#[cfg_attr(not(feature = "utf16"), allow(dead_code))]
|
||
units_cr_ucs2: OnceLock<CompiledRegex>,
|
||
}
|
||
|
||
impl From<CompiledRegex> for Regex {
|
||
fn from(cr: CompiledRegex) -> Self {
|
||
Self {
|
||
cr,
|
||
pattern: None,
|
||
units_cr: OnceLock::new(),
|
||
units_cr_ucs2: OnceLock::new(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Regex {
|
||
/// Construct a regex by parsing `pattern` using the default flags.
|
||
/// An Error may be returned if the syntax is invalid.
|
||
/// Note that this is rather expensive; prefer to cache a Regex which is
|
||
/// intended to be used more than once.
|
||
#[inline]
|
||
pub fn new(pattern: &str) -> Result<Regex, Error> {
|
||
Self::with_flags(pattern, Flags::default())
|
||
}
|
||
|
||
/// Construct a regex by parsing `pattern` with `flags`.
|
||
/// An Error may be returned if the syntax is invalid.
|
||
//
|
||
/// Note it is preferable to cache a Regex which is intended to be used more
|
||
/// than once, as the parse may be expensive. For example:
|
||
#[inline]
|
||
pub fn with_flags<F>(pattern: &str, flags: F) -> Result<Regex, Error>
|
||
where
|
||
F: Into<Flags>,
|
||
{
|
||
Self::from_unicode(pattern.chars().map(u32::from), flags)
|
||
}
|
||
|
||
/// Construct a regex by parsing `pattern` with `flags`, where
|
||
/// `pattern` is an iterator of `u32` Unicode codepoints.
|
||
/// An Error may be returned if the syntax is invalid.
|
||
/// This allows parsing regular expressions from exotic strings in
|
||
/// other encodings, such as UTF-16 or UTF-32.
|
||
pub fn from_unicode<I, F>(pattern: I, flags: F) -> Result<Regex, Error>
|
||
where
|
||
I: Iterator<Item = u32> + Clone,
|
||
F: Into<Flags>,
|
||
{
|
||
let flags = flags.into();
|
||
|
||
// 先收集源码:str 路径编译照旧消费一次,留存的码点序列供单元向 IR 惰性重解析。
|
||
let pattern: Vec<u32> = pattern.collect();
|
||
let mut ire = parse::try_parse(pattern.iter().copied(), flags)?;
|
||
if !flags.no_opt {
|
||
optimizer::optimize(&mut ire);
|
||
}
|
||
let cr = emit::emit(&ire);
|
||
Ok(Regex {
|
||
cr,
|
||
pattern: Some(pattern),
|
||
units_cr: OnceLock::new(),
|
||
units_cr_ucs2: OnceLock::new(),
|
||
})
|
||
}
|
||
|
||
/// 编译本正则所用的 flags(u/v 标志决定码点语义,供调用方选择
|
||
/// 码点向或码元向的单元输入入口)。
|
||
pub fn flags(&self) -> &Flags {
|
||
&self.cr.flags
|
||
}
|
||
|
||
/// 单元向编译产物(码点向):首次使用时按同源码同 flags 重解析、关字节
|
||
/// 字面量 pass 重新优化并 emit,得到可在 UTF-16 单元输入上执行的 IR。
|
||
/// 非 BMP 码点字面量保持单码点元素(码点向输入把代理对视作单码点)。
|
||
/// 无源码构造(`From<CompiledRegex>`)无重解析来源,回退复用既有产物。
|
||
#[cfg(feature = "utf16")]
|
||
fn units_cr(&self) -> &CompiledRegex {
|
||
self.units_cr.get_or_init(|| self.compile_units(false))
|
||
}
|
||
|
||
/// 单元向编译产物(码元向):同 `units_cr`,但非 BMP 码点字面量拆为
|
||
/// 高+低 surrogate 码元,使码元向输入(UCS-2,每单元独立元素)可命中。
|
||
#[cfg(feature = "utf16")]
|
||
fn units_cr_ucs2(&self) -> &CompiledRegex {
|
||
self.units_cr_ucs2.get_or_init(|| self.compile_units(true))
|
||
}
|
||
|
||
/// 两路单元向缓存共用的惰性编译体:`decompose` 决定非 BMP 码点字面量
|
||
/// 是否拆为 surrogate 对。
|
||
#[cfg(feature = "utf16")]
|
||
fn compile_units(&self, decompose: bool) -> CompiledRegex {
|
||
let Some(pattern) = &self.pattern else {
|
||
return self.cr.clone();
|
||
};
|
||
|
||
// 同源码同 flags 在构造期已成功解析编译,重解析不可能失败。
|
||
let mut ire =
|
||
parse::try_parse(pattern.iter().copied(), self.cr.flags).expect("构造期已成功解析的模式重解析失败");
|
||
if !self.cr.flags.no_opt {
|
||
optimizer::optimize_with_byte_literals(&mut ire, false, decompose);
|
||
}
|
||
emit::emit(&ire)
|
||
}
|
||
|
||
/// Searches `text` to find the first match.
|
||
#[inline]
|
||
pub fn find(&self, text: &str) -> Option<Match> {
|
||
self.find_iter(text).next()
|
||
}
|
||
|
||
/// Searches `text`, returning an iterator over non-overlapping matches.
|
||
/// Note that the resulting Iterator borrows both the regex `'r` and the
|
||
/// input string as `'t`.
|
||
#[inline]
|
||
pub fn find_iter<'r, 't>(&'r self, text: &'t str) -> Matches<'r, 't> {
|
||
self.find_from(text, 0)
|
||
}
|
||
|
||
/// Returns an iterator for matches found in 'text' starting at byte index
|
||
/// `start`. Note this may be different from passing a sliced `text` in
|
||
/// the case of lookbehind assertions.
|
||
/// Example:
|
||
///
|
||
/// ```rust
|
||
/// use regress::Regex;
|
||
/// let text = "xyxy";
|
||
/// let re = Regex::new(r"(?<=x)y").unwrap();
|
||
/// let t1 = re.find(&text[1..]).unwrap().range();
|
||
/// assert!(t1 == (2..3));
|
||
/// let t2 = re.find_from(text, 1).next().unwrap().range();
|
||
/// assert!(t2 == (1..2));
|
||
/// ```
|
||
#[inline]
|
||
pub fn find_from<'r, 't>(&'r self, text: &'t str, start: usize) -> Matches<'r, 't> {
|
||
backends::find(self, text, start)
|
||
}
|
||
|
||
/// Searches `text` to find the first match.
|
||
/// The input text is expected to be ascii-only: only ASCII case-folding is
|
||
/// supported.
|
||
#[inline]
|
||
pub fn find_ascii(&self, text: &str) -> Option<Match> {
|
||
self.find_iter_ascii(text).next()
|
||
}
|
||
|
||
/// Searches `text`, returning an iterator over non-overlapping matches.
|
||
/// The input text is expected to be ascii-only: only ASCII case-folding is
|
||
/// supported.
|
||
#[inline]
|
||
pub fn find_iter_ascii<'r, 't>(&'r self, text: &'t str) -> AsciiMatches<'r, 't> {
|
||
self.find_from_ascii(text, 0)
|
||
}
|
||
|
||
/// Returns an iterator for matches found in 'text' starting at byte index
|
||
/// `start`.
|
||
#[inline]
|
||
pub fn find_from_ascii<'r, 't>(&'r self, text: &'t str, start: usize) -> AsciiMatches<'r, 't> {
|
||
backends::find(self, text, start)
|
||
}
|
||
|
||
/// Returns an iterator for matches found in 'text' starting at index `start`.
|
||
#[cfg(feature = "utf16")]
|
||
pub fn find_from_utf16<'r, 't>(
|
||
&'r self,
|
||
text: &'t [u16],
|
||
start: usize,
|
||
) -> exec::Matches<super::classicalbacktrack::BacktrackExecutor<'r, indexing::Utf16Input<'t>>>
|
||
{
|
||
// 码点向 IR 无字节指令,UTF-16 单元输入可执行;代理对按单码点匹配。
|
||
let cr = self.units_cr();
|
||
let input = Utf16Input::new(text, cr.flags.unicode);
|
||
exec::Matches::new(
|
||
super::classicalbacktrack::BacktrackExecutor::new(
|
||
input,
|
||
MatchAttempter::new(cr, input.left_end()),
|
||
),
|
||
start,
|
||
)
|
||
}
|
||
|
||
/// Returns an iterator for matches found in 'text' starting at index `start`.
|
||
#[cfg(feature = "utf16")]
|
||
pub fn find_from_ucs2<'r, 't>(
|
||
&'r self,
|
||
text: &'t [u16],
|
||
start: usize,
|
||
) -> exec::Matches<super::classicalbacktrack::BacktrackExecutor<'r, indexing::Ucs2Input<'t>>>
|
||
{
|
||
// 码元向 IR:UCS-2 同为单元输入(str IR 的字节指令在单元索引器上
|
||
// 不可执行),且非 BMP 码点字面量须拆为 surrogate 对以命中码元。
|
||
let cr = self.units_cr_ucs2();
|
||
let input = Ucs2Input::new(text, cr.flags.unicode);
|
||
exec::Matches::new(
|
||
super::classicalbacktrack::BacktrackExecutor::new(
|
||
input,
|
||
MatchAttempter::new(cr, input.left_end()),
|
||
),
|
||
start,
|
||
)
|
||
}
|
||
|
||
/// Replaces the first match of the regex in `text` with the replacement string.
|
||
///
|
||
/// The replacement string may contain capture group references in the form `$1`, `$2`, etc.,
|
||
/// where `$1` refers to the first capture group, `$2` to the second, and so on.
|
||
/// `$0` refers to the entire match. Use `$$` to insert a literal `$`.
|
||
///
|
||
/// If no match is found, the original text is returned unchanged.
|
||
///
|
||
/// # Examples
|
||
///
|
||
/// ```rust
|
||
/// use regress::Regex;
|
||
///
|
||
/// let re = Regex::new(r"(\w+)\s+(\w+)").unwrap();
|
||
/// let result = re.replace("hello world", "$2 $1");
|
||
/// assert_eq!(result, "world hello");
|
||
///
|
||
/// let re = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
|
||
/// let result = re.replace("2023-12-25", "$2/$3/$1");
|
||
/// assert_eq!(result, "12/25/2023");
|
||
/// ```
|
||
pub fn replace(&self, text: &str, replacement: &str) -> String {
|
||
match self.find(text) {
|
||
Some(m) => {
|
||
let mut result = String::with_capacity(text.len());
|
||
result.push_str(&text[..m.start()]);
|
||
self.expand_replacement(&m, text, replacement, &mut result);
|
||
result.push_str(&text[m.end()..]);
|
||
result
|
||
}
|
||
None => text.to_string(),
|
||
}
|
||
}
|
||
|
||
/// Replaces all matches of the regex in `text` with the replacement string.
|
||
///
|
||
/// The replacement string may contain capture group references in the form `$1`, `$2`, etc.,
|
||
/// where `$1` refers to the first capture group, `$2` to the second, and so on.
|
||
/// `$0` refers to the entire match. Use `$$` to insert a literal `$`.
|
||
///
|
||
/// # Examples
|
||
///
|
||
/// ```rust
|
||
/// use regress::Regex;
|
||
///
|
||
/// let re = Regex::new(r"(\w+)\s+(\w+)").unwrap();
|
||
/// let result = re.replace_all("hello world foo bar", "$2-$1");
|
||
/// assert_eq!(result, "world-hello bar-foo");
|
||
///
|
||
/// let re = Regex::new(r"\b(\w)(\w+)").unwrap();
|
||
/// let result = re.replace_all("hello world", "$1.$2");
|
||
/// assert_eq!(result, "h.ello w.orld");
|
||
/// ```
|
||
pub fn replace_all(&self, text: &str, replacement: &str) -> String {
|
||
let mut result = String::with_capacity(text.len());
|
||
let mut last_end = 0;
|
||
|
||
for m in self.find_iter(text) {
|
||
result.push_str(&text[last_end..m.start()]);
|
||
self.expand_replacement(&m, text, replacement, &mut result);
|
||
last_end = m.end();
|
||
}
|
||
|
||
result.push_str(&text[last_end..]);
|
||
result
|
||
}
|
||
|
||
/// Replaces the first match of the regex in `text` using a closure.
|
||
///
|
||
/// The closure receives a `&Match` and should return the replacement string.
|
||
/// This is useful for dynamic replacements that depend on the match details.
|
||
///
|
||
/// If no match is found, the original text is returned unchanged.
|
||
///
|
||
/// # Examples
|
||
///
|
||
/// ```rust
|
||
/// use regress::Regex;
|
||
///
|
||
/// let re = Regex::new(r"\d+").unwrap();
|
||
/// let text = "Price: $123";
|
||
/// let result = re.replace_with(text, |m| {
|
||
/// let num: i32 = m.as_str(text).parse().unwrap();
|
||
/// format!("{}", num * 2)
|
||
/// });
|
||
/// assert_eq!(result, "Price: $246");
|
||
/// ```
|
||
pub fn replace_with<F>(&self, text: &str, replacement: F) -> String
|
||
where
|
||
F: FnOnce(&Match) -> String,
|
||
{
|
||
match self.find(text) {
|
||
Some(m) => {
|
||
let mut result = String::with_capacity(text.len());
|
||
result.push_str(&text[..m.start()]);
|
||
result.push_str(&replacement(&m));
|
||
result.push_str(&text[m.end()..]);
|
||
result
|
||
}
|
||
None => text.to_string(),
|
||
}
|
||
}
|
||
|
||
/// Replaces all matches of the regex in `text` using a closure.
|
||
///
|
||
/// The closure receives a `&Match` and should return the replacement string.
|
||
/// This is useful for dynamic replacements that depend on the match details.
|
||
///
|
||
/// # Examples
|
||
///
|
||
/// ```rust
|
||
/// use regress::Regex;
|
||
///
|
||
/// let re = Regex::new(r"\d+").unwrap();
|
||
/// let text = "Items: 5, 10, 15";
|
||
/// let result = re.replace_all_with(text, |m| {
|
||
/// let num: i32 = m.as_str(text).parse().unwrap();
|
||
/// format!("[{}]", num * 10)
|
||
/// });
|
||
/// assert_eq!(result, "Items: [50], [100], [150]");
|
||
/// ```
|
||
pub fn replace_all_with<F>(&self, text: &str, replacement: F) -> String
|
||
where
|
||
F: Fn(&Match) -> String,
|
||
{
|
||
let mut result = String::with_capacity(text.len());
|
||
let mut last_end = 0;
|
||
|
||
for m in self.find_iter(text) {
|
||
result.push_str(&text[last_end..m.start()]);
|
||
result.push_str(&replacement(&m));
|
||
last_end = m.end();
|
||
}
|
||
|
||
result.push_str(&text[last_end..]);
|
||
result
|
||
}
|
||
|
||
/// Helper method to expand replacement strings with capture group substitutions.
|
||
fn expand_replacement(&self, m: &Match, text: &str, replacement: &str, output: &mut String) {
|
||
let mut chars = replacement.chars().peekable();
|
||
|
||
while let Some(ch) = chars.next() {
|
||
if ch == '$' {
|
||
match chars.peek() {
|
||
Some('$') => {
|
||
// $$ -> literal $
|
||
chars.next();
|
||
output.push('$');
|
||
}
|
||
Some(&digit) if digit.is_ascii_digit() => {
|
||
// Parse the group number
|
||
let mut group_num = 0;
|
||
while let Some(&digit) = chars.peek() {
|
||
if digit.is_ascii_digit() {
|
||
chars.next();
|
||
group_num = group_num * 10 + (digit as u32 - '0' as u32) as usize;
|
||
// Limit to reasonable group numbers to avoid overflow
|
||
if group_num > MAX_CAPTURE_GROUPS {
|
||
break;
|
||
}
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Get the matched text for this group
|
||
if let Some(range) = m.group(group_num) {
|
||
output.push_str(&text[range]);
|
||
}
|
||
// If group doesn't exist or didn't match, add nothing
|
||
}
|
||
Some('{') => {
|
||
// Handle ${name} syntax for named groups
|
||
chars.next(); // consume '{'
|
||
let mut name = String::new();
|
||
let mut found_closing_brace = false;
|
||
|
||
for ch in chars.by_ref() {
|
||
if ch == '}' {
|
||
found_closing_brace = true;
|
||
break;
|
||
}
|
||
name.push(ch);
|
||
}
|
||
|
||
if found_closing_brace {
|
||
if let Some(range) = m.named_group(&name) {
|
||
output.push_str(&text[range]);
|
||
}
|
||
} else {
|
||
// Malformed ${...}, treat as literal
|
||
output.push_str("${");
|
||
output.push_str(&name);
|
||
}
|
||
}
|
||
_ => {
|
||
// Just a $ at end or followed by non-digit
|
||
output.push('$');
|
||
}
|
||
}
|
||
} else {
|
||
output.push(ch);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl FromStr for Regex {
|
||
type Err = Error;
|
||
|
||
/// Attempts to parse a string into a regular expression
|
||
#[inline]
|
||
fn from_str(s: &str) -> Result<Self, Error> {
|
||
Self::new(s)
|
||
}
|
||
}
|
||
|
||
// Pattern trait implementation for str::find, str::contains, etc.
|
||
#[cfg(feature = "pattern")]
|
||
mod pattern_impl {
|
||
use super::*;
|
||
use core::str::pattern::{Pattern, ReverseSearcher, SearchStep, Searcher};
|
||
|
||
/// A searcher for a regex pattern.
|
||
pub struct RegexSearcher<'r, 't> {
|
||
haystack: &'t str,
|
||
regex: &'r Regex,
|
||
current_pos: usize,
|
||
done: bool,
|
||
// For reverse searching
|
||
reverse_pos: usize,
|
||
reverse_done: bool,
|
||
}
|
||
|
||
impl<'r, 't> RegexSearcher<'r, 't> {
|
||
fn new(regex: &'r Regex, haystack: &'t str) -> Self {
|
||
Self {
|
||
haystack,
|
||
regex,
|
||
current_pos: 0,
|
||
done: false,
|
||
reverse_pos: haystack.len(),
|
||
reverse_done: false,
|
||
}
|
||
}
|
||
|
||
fn find_last_match_before(&self, pos: usize) -> Option<super::Match> {
|
||
// Find all matches up to the given position and return the last one
|
||
let mut last_match = None;
|
||
for m in self.regex.find_from(self.haystack, 0) {
|
||
if m.end() <= pos {
|
||
last_match = Some(m);
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
last_match
|
||
}
|
||
}
|
||
|
||
unsafe impl<'r, 't> Searcher<'t> for RegexSearcher<'r, 't> {
|
||
fn haystack(&self) -> &'t str {
|
||
self.haystack
|
||
}
|
||
|
||
fn next(&mut self) -> SearchStep {
|
||
if self.done {
|
||
return SearchStep::Done;
|
||
}
|
||
|
||
// Try to find the next match starting from current position
|
||
if let Some(m) = self.regex.find_from(self.haystack, self.current_pos).next() {
|
||
let match_start = m.start();
|
||
let match_end = m.end();
|
||
|
||
// Handle any gap between current position and match start
|
||
if self.current_pos < match_start {
|
||
let reject_end = match_start;
|
||
let reject_start = self.current_pos;
|
||
self.current_pos = match_start;
|
||
return SearchStep::Reject(reject_start, reject_end);
|
||
}
|
||
|
||
// Return the match
|
||
self.current_pos = match_end;
|
||
|
||
// Handle zero-width matches to avoid infinite loops
|
||
if match_start == match_end {
|
||
// For zero-width matches, we need to advance at least one byte
|
||
// to avoid infinite loops
|
||
if match_end < self.haystack.len() {
|
||
// Find the next character boundary
|
||
let mut next_pos = match_end + 1;
|
||
while next_pos < self.haystack.len()
|
||
&& !self.haystack.is_char_boundary(next_pos)
|
||
{
|
||
next_pos += 1;
|
||
}
|
||
self.current_pos = next_pos;
|
||
} else {
|
||
// We're at the end of the string
|
||
self.done = true;
|
||
}
|
||
}
|
||
|
||
SearchStep::Match(match_start, match_end)
|
||
} else {
|
||
// No more matches, reject remaining text if any
|
||
if self.current_pos < self.haystack.len() {
|
||
let reject_start = self.current_pos;
|
||
let reject_end = self.haystack.len();
|
||
self.current_pos = self.haystack.len();
|
||
self.done = true;
|
||
SearchStep::Reject(reject_start, reject_end)
|
||
} else {
|
||
self.done = true;
|
||
SearchStep::Done
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
unsafe impl<'r, 't> ReverseSearcher<'t> for RegexSearcher<'r, 't> {
|
||
fn next_back(&mut self) -> SearchStep {
|
||
if self.reverse_done {
|
||
return SearchStep::Done;
|
||
}
|
||
|
||
// Try to find the last match before current reverse position
|
||
if let Some(m) = self.find_last_match_before(self.reverse_pos) {
|
||
let match_start = m.start();
|
||
let match_end = m.end();
|
||
|
||
// Handle any gap between match end and current reverse position
|
||
if match_end < self.reverse_pos {
|
||
let reject_start = match_end;
|
||
let reject_end = self.reverse_pos;
|
||
self.reverse_pos = match_end;
|
||
return SearchStep::Reject(reject_start, reject_end);
|
||
}
|
||
|
||
// Return the match
|
||
self.reverse_pos = match_start;
|
||
|
||
// Handle zero-width matches
|
||
if match_start == match_end {
|
||
// For zero-width matches, move back by one character
|
||
if match_start > 0 {
|
||
let mut prev_pos = match_start - 1;
|
||
while prev_pos > 0 && !self.haystack.is_char_boundary(prev_pos) {
|
||
prev_pos -= 1;
|
||
}
|
||
self.reverse_pos = prev_pos;
|
||
} else {
|
||
// We're at the beginning of the string
|
||
self.reverse_done = true;
|
||
}
|
||
}
|
||
|
||
SearchStep::Match(match_start, match_end)
|
||
} else {
|
||
// No more matches, reject remaining text if any
|
||
if self.reverse_pos > 0 {
|
||
let reject_start = 0;
|
||
let reject_end = self.reverse_pos;
|
||
self.reverse_pos = 0;
|
||
self.reverse_done = true;
|
||
SearchStep::Reject(reject_start, reject_end)
|
||
} else {
|
||
self.reverse_done = true;
|
||
SearchStep::Done
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl<'r> Pattern for &'r Regex {
|
||
type Searcher<'a> = RegexSearcher<'r, 'a>;
|
||
|
||
fn into_searcher(self, haystack: &str) -> Self::Searcher<'_> {
|
||
RegexSearcher::new(self, haystack)
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(feature = "pattern")]
|
||
pub use pattern_impl::*;
|
||
|
||
// Support for using regress with different regex backends.
|
||
// Currently there is only the classical backtracking, and PikeVM.
|
||
#[doc(hidden)]
|
||
pub mod backends {
|
||
use super::Regex;
|
||
use super::exec;
|
||
use super::indexing;
|
||
pub use crate::emit::emit;
|
||
pub use crate::optimizer::{optimize, optimize_with_byte_literals};
|
||
pub use crate::parse::try_parse;
|
||
|
||
/// An Executor using the classical backtracking algorithm.
|
||
pub type BacktrackExecutor<'r, 't> =
|
||
super::classicalbacktrack::BacktrackExecutor<'r, indexing::Utf8Input<'t>>;
|
||
|
||
/// A Executor using the PikeVM executor.
|
||
#[cfg(feature = "backend-pikevm")]
|
||
pub type PikeVMExecutor<'r, 't> = super::pikevm::PikeVMExecutor<'r, indexing::Utf8Input<'t>>;
|
||
|
||
/// An alias type to the default Executor.
|
||
pub type DefaultExecutor<'r, 't> = BacktrackExecutor<'r, 't>;
|
||
|
||
/// An alias type to the default executor's ASCII form.
|
||
pub type DefaultAsciiExecutor<'r, 't> =
|
||
<DefaultExecutor<'r, 't> as exec::Executor<'r, 't>>::AsAscii;
|
||
|
||
/// Searches `text`, returning an iterator over non-overlapping matches.
|
||
pub fn find<'r, 't, Executor: exec::Executor<'r, 't>>(
|
||
re: &'r Regex,
|
||
text: &'t str,
|
||
start: usize,
|
||
) -> exec::Matches<Executor> {
|
||
exec::Matches::new(Executor::new(&re.cr, text), start)
|
||
}
|
||
|
||
/// Searches `text`, returning an iterator over non-overlapping matches.
|
||
/// This is a convenience method to avoid E0223.
|
||
pub fn find_ascii<'r, 't, Executor: exec::Executor<'r, 't>>(
|
||
re: &'r Regex,
|
||
text: &'t str,
|
||
start: usize,
|
||
) -> exec::Matches<Executor::AsAscii> {
|
||
find::<Executor::AsAscii>(re, text, start)
|
||
}
|
||
}
|
||
|
||
/// Escapes all special regex characters in a string to make it a literal match.
|
||
///
|
||
/// This function takes a string and returns a new string with all special
|
||
/// regex characters escaped with backslashes, so the resulting string can be
|
||
/// used as a literal pattern in a regular expression.
|
||
///
|
||
/// # Example
|
||
///
|
||
/// ```
|
||
/// use regress::escape;
|
||
///
|
||
/// let escaped = escape("Hello. How are you?");
|
||
/// assert_eq!(escaped, "Hello\\. How are you\\?");
|
||
///
|
||
/// let escaped = escape("$100 + tax (15%)");
|
||
/// assert_eq!(escaped, "\\$100 \\+ tax \\(15%\\)");
|
||
/// ```
|
||
pub fn escape(text: &str) -> String {
|
||
let mut result = String::with_capacity(text.len());
|
||
|
||
for c in text.chars() {
|
||
match c {
|
||
// Characters that have special meaning in regex and need escaping
|
||
'\\' | '^' | '$' | '.' | '|' | '?' | '*' | '+' | '(' | ')' | '[' | ']' | '{' | '}' => {
|
||
result.push('\\');
|
||
result.push(c);
|
||
}
|
||
// All other characters are literal
|
||
_ => result.push(c),
|
||
}
|
||
}
|
||
|
||
result
|
||
}
|
||
|
||
#[cfg(all(test, feature = "utf16"))]
|
||
mod utf16_tests {
|
||
use super::*;
|
||
use crate::insn::{Insn, StartPredicate};
|
||
|
||
/// 单元序列解码为码点序列:代理对合并为单码点,孤立 surrogate 保留单元值。
|
||
fn decode_units(units: &[u16]) -> Vec<u32> {
|
||
let mut out = Vec::with_capacity(units.len());
|
||
let mut i = 0;
|
||
while i < units.len() {
|
||
let u = units[i];
|
||
|
||
// 高 surrogate 后随低 surrogate 时合并为代理对码点。
|
||
if (0xD800..0xDC00).contains(&u) && i + 1 < units.len() {
|
||
let lo = units[i + 1];
|
||
if (0xDC00..0xE000).contains(&lo) {
|
||
out.push(0x10000 + ((u as u32 - 0xD800) << 10) + (lo as u32 - 0xDC00));
|
||
i += 2;
|
||
continue;
|
||
}
|
||
}
|
||
out.push(u as u32);
|
||
i += 1;
|
||
}
|
||
out
|
||
}
|
||
|
||
fn to_units(text: &str) -> Vec<u16> {
|
||
let mut out = Vec::with_capacity(text.len());
|
||
for c in text.chars() {
|
||
let mut buf = [0u16; 2];
|
||
out.extend_from_slice(c.encode_utf16(&mut buf));
|
||
}
|
||
out
|
||
}
|
||
|
||
/// 单元下标 → 码点下标映射:代理对的两个单元同指一个码点。
|
||
fn unit_to_cp_index(units: &[u16]) -> Vec<usize> {
|
||
let mut map = Vec::with_capacity(units.len());
|
||
let mut cp = 0usize;
|
||
let mut i = 0;
|
||
while i < units.len() {
|
||
let u = units[i];
|
||
if (0xD800..0xDC00).contains(&u)
|
||
&& i + 1 < units.len()
|
||
&& (0xDC00..0xE000).contains(&units[i + 1])
|
||
{
|
||
map.push(cp);
|
||
map.push(cp);
|
||
cp += 1;
|
||
i += 2;
|
||
} else {
|
||
map.push(cp);
|
||
cp += 1;
|
||
i += 1;
|
||
}
|
||
}
|
||
map
|
||
}
|
||
|
||
/// 收集全部匹配(主匹配 + 捕获组,均码点序列)。
|
||
/// 两种输入形态的偏移口径(字节 / 单元)经码点归一后直接可比。
|
||
type MatchCps = (Vec<u32>, Vec<Option<Vec<u32>>>);
|
||
|
||
fn str_matches(re: &Regex, text: &str) -> Vec<MatchCps> {
|
||
let decode = |s: &str| s.chars().map(u32::from).collect::<Vec<_>>();
|
||
re.find_iter(text)
|
||
.map(|m| {
|
||
let caps = m
|
||
.captures
|
||
.iter()
|
||
.map(|c| c.as_ref().map(|r| decode(&text[r.clone()])))
|
||
.collect();
|
||
(decode(m.as_str(text)), caps)
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn utf16_matches(re: &Regex, units: &[u16]) -> Vec<MatchCps> {
|
||
let cps = decode_units(units);
|
||
let idx = unit_to_cp_index(units);
|
||
|
||
// 空匹配不占码点(含尾部位);非空匹配经单元→码点下标映射切片。
|
||
let cp_slice = |r: &core::ops::Range<usize>| -> Vec<u32> {
|
||
if r.start == r.end {
|
||
return Vec::new();
|
||
}
|
||
cps[idx[r.start]..(idx[r.end - 1] + 1)].to_vec()
|
||
};
|
||
re.find_from_utf16(units, 0)
|
||
.map(|m| {
|
||
let caps = m
|
||
.captures
|
||
.iter()
|
||
.map(|c| c.as_ref().map(|r| cp_slice(&r)))
|
||
.collect();
|
||
(cp_slice(&m.range()), caps)
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn is_byte_insn(i: &Insn) -> bool {
|
||
matches!(
|
||
i,
|
||
Insn::ByteSet2(_) | Insn::ByteSet3(_) | Insn::ByteSet4(_)
|
||
| Insn::ByteSeq1(_)
|
||
| Insn::ByteSeq2(_)
|
||
| Insn::ByteSeq3(_)
|
||
| Insn::ByteSeq4(_)
|
||
| Insn::ByteSeq5(_)
|
||
| Insn::ByteSeq6(_)
|
||
| Insn::ByteSeq7(_)
|
||
| Insn::ByteSeq8(_)
|
||
| Insn::ByteSeq9(_)
|
||
| Insn::ByteSeq10(_)
|
||
| Insn::ByteSeq11(_)
|
||
| Insn::ByteSeq12(_)
|
||
| Insn::ByteSeq13(_)
|
||
| Insn::ByteSeq14(_)
|
||
| Insn::ByteSeq15(_)
|
||
| Insn::ByteSeq16(_)
|
||
)
|
||
}
|
||
|
||
/// well-formed 语料:units IR 与 str IR 匹配结果逐一相等。
|
||
#[test]
|
||
fn units_ir_matches_equal_str_path_on_well_formed() {
|
||
let cases = [
|
||
(r"\d+", "abc123x456"),
|
||
(r"a[bc]d", "xabcdyabd"),
|
||
(r"(f+)(o+)(r*)", "foo of foo"),
|
||
(r"\w+\s+\w+", "héllo wörld"),
|
||
(r"x*", "abab"),
|
||
(r"(a)|(b)", "abab"),
|
||
(r"nope", "nothing here"),
|
||
];
|
||
for (pattern, text) in cases {
|
||
let re = Regex::new(pattern).unwrap();
|
||
let units = to_units(text);
|
||
assert_eq!(str_matches(&re, text), utf16_matches(&re, &units), "pattern {pattern}");
|
||
}
|
||
}
|
||
|
||
/// 孤立 surrogate 单元在 \p{General_Category=Surrogate} 下命中,
|
||
/// 普通字符不命中,代理对(单码点)不命中。
|
||
#[test]
|
||
fn units_ir_matches_lone_surrogate_property() {
|
||
let re = Regex::with_flags(r"\p{General_Category=Surrogate}", "u").unwrap();
|
||
let m = re.find_from_utf16(&[0xD800], 0).next();
|
||
assert!(m.is_some(), "孤立 surrogate 单元应命中 GC=Surrogate");
|
||
assert_eq!(m.unwrap().range(), 0..1);
|
||
assert!(re.find_from_utf16(&[0x41], 0).next().is_none(), "A 不应命中");
|
||
|
||
// 代理对是单码点,不是两个孤立 surrogate。
|
||
assert!(re.find_from_utf16(&[0xD834, 0xDE00], 0).next().is_none());
|
||
}
|
||
|
||
/// 代理对单元序列按单码点匹配,与 str 路径逐位等价。
|
||
#[test]
|
||
fn units_ir_surrogate_pair_equals_str_path() {
|
||
let cp: u32 = 0x1D11E;
|
||
let re = Regex::from_unicode([cp].iter().copied(), "").unwrap();
|
||
let ch = char::from_u32(cp).unwrap();
|
||
let text: String = [ch, 'a', 'b', 'c'].iter().copied().collect();
|
||
let m_str = re.find(&text).expect("str 路径应命中");
|
||
assert_eq!(m_str.range(), 0..4, "str 路径命中 1 个码点(4 字节)");
|
||
|
||
let units = to_units(&text);
|
||
let m_u16 = re.find_from_utf16(&units, 0).next().expect("单元路径应命中");
|
||
assert_eq!(m_u16.range(), 0..2, "单元路径命中 1 个码点(2 个单元)");
|
||
assert_eq!(decode_units(&units[m_u16.range()]), vec![cp]);
|
||
assert_eq!(str_matches(&re, &text), utf16_matches(&re, &units));
|
||
}
|
||
|
||
/// 字节字面量 pass 的运行时门控:str IR 含字节指令与字节形起始谓词,
|
||
/// units IR 两者皆无;well-formed 输入上两 IR 匹配结果一致。
|
||
#[test]
|
||
fn byte_literal_pass_gated_per_ir() {
|
||
let re = Regex::new("abcdef").unwrap();
|
||
assert!(
|
||
re.cr.insns.iter().any(is_byte_insn),
|
||
"str IR 应含字节指令(字节字面量 pass 生效)"
|
||
);
|
||
assert!(
|
||
!matches!(re.cr.start_pred, StartPredicate::Arbitrary),
|
||
"str 起始谓词应为字节形"
|
||
);
|
||
|
||
let units = re.units_cr();
|
||
assert!(!units.insns.iter().any(is_byte_insn), "units IR 不得含字节指令");
|
||
assert!(
|
||
matches!(units.start_pred, StartPredicate::Arbitrary),
|
||
"units 起始谓词应为 Arbitrary"
|
||
);
|
||
|
||
let text = "xxabcdefyy";
|
||
assert_eq!(str_matches(&re, text), utf16_matches(&re, &to_units(text)));
|
||
}
|
||
|
||
/// no_opt 标志贯穿单元向惰性编译:两 IR 均不跑优化 pass,指令流同长且匹配一致。
|
||
#[test]
|
||
fn no_opt_flag_respected_by_units_ir() {
|
||
let flags = Flags {
|
||
no_opt: true,
|
||
..Default::default()
|
||
};
|
||
let re = Regex::with_flags("abcdef", flags).unwrap();
|
||
assert!(!re.cr.insns.iter().any(is_byte_insn), "no_opt 下 str IR 不含字节指令");
|
||
let units = re.units_cr();
|
||
assert!(!units.insns.iter().any(is_byte_insn));
|
||
assert_eq!(re.cr.insns.len(), units.insns.len(), "no_opt 下两 IR 指令流同长");
|
||
|
||
let text = "xxabcdefyy";
|
||
assert_eq!(str_matches(&re, text), utf16_matches(&re, &to_units(text)));
|
||
}
|
||
}
|