deps: vendor regress 字节字面量优化 gate 改逐编译运行时标志 + 惰性物化单元向 IR——utf16 feature 编译期全局 gate 原会关掉全部 regex 的字节 pass 与执行器起始谓词快路径,启用即劣化全部 str 路径;现字节 pass 由 byte_literals 编译参数控制(str IR 传 true 与旧行为逐位一致、单元 IR 传 false),执行器起始谓词分派按输入编码切换(字节输入保留锚定/字节定位快路径、单元输入逐位尝试),单元向 IR 首次 find_from_utf16/find_from_ucs2 时重解析编译并缓存(正则留存源码码点序列),no_opt 标志同路径贯穿;fork 增量边界 = optimizer.rs 两处 cfg 改运行时标志(optimize 降为包装)+ api.rs Regex 两字段与 utf16 两入口改走单元产物 + classicalbacktrack.rs 两处 cfg 去除分派运行时化 + manifest 独立 [workspace] 表与 feature 注释刷新,其余与上游 0.11.1 逐位一致;新增回归钉五则(well-formed 匹配逐一相等/孤立 surrogate 属性命中/代理对 ≡ str 路径/字节指令与起始谓词按 IR 门控/no_opt 贯穿)

This commit is contained in:
2026-09-13 17:56:09 +00:00
parent f088858cf4
commit 2eea4f1fbc
4 changed files with 320 additions and 39 deletions
+8 -1
View File
@@ -1,3 +1,9 @@
# Standalone workspace root: this vendored crate is built and tested on its own
# (standalone `cargo build` / `cargo test` in this directory), not as a member
# of the host workspace that vendors it. The empty table keeps it out of the
# parent workspace's member resolution.
[workspace]
[package] [package]
name = "regress" name = "regress"
version = "0.11.1" version = "0.11.1"
@@ -23,7 +29,8 @@ index-positions = []
# Prohibits all uses of unsafe code, for the paranoid. # Prohibits all uses of unsafe code, for the paranoid.
prohibit-unsafe = [] prohibit-unsafe = []
# Enables UTF-16 support. This disables some optimizations, so it should only be used when necessary. # Enables UTF-16 support. Byte-literal optimization is decided per compilation
# (runtime gate), so enabling this feature no longer degrades the str path.
utf16 = [] utf16 = []
# Enables Pattern trait implementation for str::find, str::contains, etc. # Enables Pattern trait implementation for str::find, str::contains, etc.
+271 -8
View File
@@ -17,6 +17,7 @@ use crate::{
#[cfg(feature = "backend-pikevm")] #[cfg(feature = "backend-pikevm")]
use crate::pikevm; use crate::pikevm;
use crate::util::to_char_sat; use crate::util::to_char_sat;
use std::sync::OnceLock;
use core::{fmt, str::FromStr}; use core::{fmt, str::FromStr};
#[cfg(feature = "std")] #[cfg(feature = "std")]
@@ -373,11 +374,24 @@ impl<'m> FusedIterator for NamedGroups<'m> {}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Regex { pub struct Regex {
cr: CompiledRegex, cr: CompiledRegex,
// 模式源码码点序列:str 路径编译一次消费,留存供单元向 IR 惰性重解析。
// 仅 `From<CompiledRegex>` 构造无源码(None)。
#[cfg_attr(not(feature = "utf16"), allow(dead_code))]
pattern: Option<Vec<u32>>,
// 单元向编译产物(未跑字节字面量 pass),首次单元输入入口时惰性物化。
#[cfg_attr(not(feature = "utf16"), allow(dead_code))]
units_cr: OnceLock<CompiledRegex>,
} }
impl From<CompiledRegex> for Regex { impl From<CompiledRegex> for Regex {
fn from(cr: CompiledRegex) -> Self { fn from(cr: CompiledRegex) -> Self {
Self { cr } Self {
cr,
pattern: None,
units_cr: OnceLock::new(),
}
} }
} }
@@ -415,12 +429,39 @@ impl Regex {
F: Into<Flags>, F: Into<Flags>,
{ {
let flags = flags.into(); let flags = flags.into();
let mut ire = parse::try_parse(pattern, flags)?;
// 先收集源码:str 路径编译照旧消费一次,留存的码点序列供单元向 IR 惰性重解析。
let pattern: Vec<u32> = pattern.collect();
let mut ire = parse::try_parse(pattern.iter().copied(), flags)?;
if !flags.no_opt { if !flags.no_opt {
optimizer::optimize(&mut ire); optimizer::optimize(&mut ire);
} }
let cr = emit::emit(&ire); let cr = emit::emit(&ire);
Ok(Regex { cr }) Ok(Regex {
cr,
pattern: Some(pattern),
units_cr: OnceLock::new(),
})
}
/// 单元向编译产物:首次使用时按同源码同 flags 重解析、关字节字面量 pass
/// 重新优化并 emit,得到可在 UTF-16/UCS-2 单元输入上执行的 IR。
/// 无源码构造(`From<CompiledRegex>`)无重解析来源,回退复用既有产物。
#[cfg(feature = "utf16")]
fn units_cr(&self) -> &CompiledRegex {
self.units_cr.get_or_init(|| {
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);
}
emit::emit(&ire)
})
} }
/// Searches `text` to find the first match. /// Searches `text` to find the first match.
@@ -487,11 +528,13 @@ impl Regex {
start: usize, start: usize,
) -> exec::Matches<super::classicalbacktrack::BacktrackExecutor<'r, indexing::Utf16Input<'t>>> ) -> exec::Matches<super::classicalbacktrack::BacktrackExecutor<'r, indexing::Utf16Input<'t>>>
{ {
let input = Utf16Input::new(text, self.cr.flags.unicode); // 单元向 IR 无字节指令,UTF-16 单元输入可执行。
let cr = self.units_cr();
let input = Utf16Input::new(text, cr.flags.unicode);
exec::Matches::new( exec::Matches::new(
super::classicalbacktrack::BacktrackExecutor::new( super::classicalbacktrack::BacktrackExecutor::new(
input, input,
MatchAttempter::new(&self.cr, input.left_end()), MatchAttempter::new(cr, input.left_end()),
), ),
start, start,
) )
@@ -505,11 +548,14 @@ impl Regex {
start: usize, start: usize,
) -> exec::Matches<super::classicalbacktrack::BacktrackExecutor<'r, indexing::Ucs2Input<'t>>> ) -> exec::Matches<super::classicalbacktrack::BacktrackExecutor<'r, indexing::Ucs2Input<'t>>>
{ {
let input = Ucs2Input::new(text, self.cr.flags.unicode); // 与 `find_from_utf16` 共用单元向 IR:UCS-2 同为单元输入,
// str IR 的字节指令在单元索引器上不可执行。
let cr = self.units_cr();
let input = Ucs2Input::new(text, cr.flags.unicode);
exec::Matches::new( exec::Matches::new(
super::classicalbacktrack::BacktrackExecutor::new( super::classicalbacktrack::BacktrackExecutor::new(
input, input,
MatchAttempter::new(&self.cr, input.left_end()), MatchAttempter::new(cr, input.left_end()),
), ),
start, start,
) )
@@ -912,7 +958,7 @@ pub mod backends {
use super::exec; use super::exec;
use super::indexing; use super::indexing;
pub use crate::emit::emit; pub use crate::emit::emit;
pub use crate::optimizer::optimize; pub use crate::optimizer::{optimize, optimize_with_byte_literals};
pub use crate::parse::try_parse; pub use crate::parse::try_parse;
/// An Executor using the classical backtracking algorithm. /// An Executor using the classical backtracking algorithm.
@@ -984,3 +1030,220 @@ pub fn escape(text: &str) -> String {
result 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)));
}
}
+6 -7
View File
@@ -7,7 +7,6 @@ use crate::cursor::{Backward, Direction, Forward};
use crate::exec; use crate::exec;
use crate::indexing; use crate::indexing;
use crate::indexing::{AsciiInput, ElementType, InputIndexer, Utf8Input}; use crate::indexing::{AsciiInput, ElementType, InputIndexer, Utf8Input};
#[cfg(not(feature = "utf16"))]
use crate::insn::StartPredicate; use crate::insn::StartPredicate;
use crate::insn::{CompiledRegex, Insn, LoopFields}; use crate::insn::{CompiledRegex, Insn, LoopFields};
use crate::matchers; use crate::matchers;
@@ -1053,7 +1052,6 @@ impl<Input: InputIndexer> BacktrackExecutor<'_, Input> {
/// \return the next match for an anchored regex that only matches at the start. /// \return the next match for an anchored regex that only matches at the start.
/// This avoids any string searching and only tries matching at the given position. /// This avoids any string searching and only tries matching at the given position.
#[cfg(not(feature = "utf16"))]
fn next_match_anchored( fn next_match_anchored(
&mut self, &mut self,
pos: Input::Position, pos: Input::Position,
@@ -1118,11 +1116,9 @@ impl<Input: InputIndexer> exec::MatchProducer for BacktrackExecutor<'_, Input> {
pos: Input::Position, pos: Input::Position,
next_start: &mut Option<Input::Position>, next_start: &mut Option<Input::Position>,
) -> Option<Match> { ) -> Option<Match> {
// When UTF-16 support is active prefix search is not used due to the different encoding. // 字节输入(UTF-8/ASCII)按起始谓词走定位快路径;
#[cfg(feature = "utf16")] // 单元输入(UTF-16/UCS-2)无法字节级前缀搜索,逐位尝试匹配。
return self.next_match_with_prefix_search(pos, next_start, &bytesearch::EmptyString {}); if Input::CODE_UNITS_ARE_BYTES {
#[cfg(not(feature = "utf16"))]
match &self.matcher.re.start_pred { match &self.matcher.re.start_pred {
StartPredicate::Arbitrary => { StartPredicate::Arbitrary => {
self.next_match_with_prefix_search(pos, next_start, &bytesearch::EmptyString {}) self.next_match_with_prefix_search(pos, next_start, &bytesearch::EmptyString {})
@@ -1144,6 +1140,9 @@ impl<Input: InputIndexer> exec::MatchProducer for BacktrackExecutor<'_, Input> {
self.next_match_with_prefix_search(pos, next_start, bitmap) self.next_match_with_prefix_search(pos, next_start, bitmap)
} }
} }
} else {
self.next_match_with_prefix_search(pos, next_start, &bytesearch::EmptyString {})
}
} }
} }
+16 -4
View File
@@ -405,9 +405,9 @@ fn promote_1char_loops(n: &mut Node, _w: &Walk) -> PassAction {
/// Replace Cat(Char) with ByteSeq. /// Replace Cat(Char) with ByteSeq.
/// Also replace chars with literal bytes. /// Also replace chars with literal bytes.
/// Don't do this in utf16 mode because UTF-16 should never match against bytes. /// 字节指令无法在单元输入(UTF-16/UCS-2)上执行,面向单元的编译由调用方
/// 关闭本 pass(见 `optimize_with_byte_literals`)。
/// TODO: this seems to do too much; consider breaking this up. /// TODO: this seems to do too much; consider breaking this up.
#[cfg(not(feature = "utf16"))]
fn form_literal_bytes(n: &mut Node, walk: &Walk) -> PassAction { fn form_literal_bytes(n: &mut Node, walk: &Walk) -> PassAction {
// Helper to return a mutable reference to the nodes of a literal bytes. // Helper to return a mutable reference to the nodes of a literal bytes.
fn get_literal_bytes(n: &mut Node) -> Option<&mut Vec<u8>> { fn get_literal_bytes(n: &mut Node) -> Option<&mut Vec<u8>> {
@@ -521,7 +521,17 @@ fn simplify_brackets(n: &mut Node, _walk: &Walk) -> PassAction {
} }
} }
/// 编译主入口:字节字面量 pass 恒开,编译产物与旧的全局 feature 门控形态逐位一致。
pub fn optimize(r: &mut Regex) { pub fn optimize(r: &mut Regex) {
optimize_with_byte_literals(r, true)
}
/// 与 `optimize` 同一条定点 pass 链,仅字节字面量 pass
/// (Char → ByteSequence、ASCII 集合 → ByteSet)由 `byte_literals` 控制。
///
/// 字节指令无法在单元输入(UTF-16/UCS-2)上执行:面向单元输入的 IR 必须以
/// `false` 编译,字节输入(UTF-8)IR 以 `true` 编译保持编译与执行策略不变。
pub fn optimize_with_byte_literals(r: &mut Regex, byte_literals: bool) {
run_pass(r, &mut simplify_brackets); run_pass(r, &mut simplify_brackets);
loop { loop {
let mut changed = false; let mut changed = false;
@@ -531,10 +541,12 @@ pub fn optimize(r: &mut Regex) {
} }
changed |= run_pass(r, &mut unroll_loops); changed |= run_pass(r, &mut unroll_loops);
changed |= run_pass(r, &mut promote_1char_loops); changed |= run_pass(r, &mut promote_1char_loops);
#[cfg(not(feature = "utf16"))]
{ // 字节字面量 pass:仅字节输入编译允许,单元输入 IR 不得含字节指令。
if byte_literals {
changed |= run_pass(r, &mut form_literal_bytes); changed |= run_pass(r, &mut form_literal_bytes);
} }
changed |= run_pass(r, &mut remove_empties); changed |= run_pass(r, &mut remove_empties);
changed |= run_pass(r, &mut propagate_early_fails); changed |= run_pass(r, &mut propagate_early_fails);
if !changed { if !changed {