use super::phase2::{ascii_fold_regex_src, gate_prefix_literals, MIN_PREFIX_BYTES};
use super::phase2_first_bigram::FirstBigramSet;
use crate::anchored_regex::AnchoredRegex;
use crate::types::*;
use aho_corasick::{AhoCorasick, AhoCorasickBuilder, MatchKind};
use regex_syntax::hir::literal::{ExtractKind, Extractor};
use std::sync::{Arc, Mutex, OnceLock};
const MAX_LITERALS_PER_PATTERN: usize = 32;
pub(crate) const CONFIRMED_MAX_LITERALS_PER_PATTERN: usize = 8;
struct LazyAnchorAc {
literals: Mutex<Option<Box<[Arc<str>]>>>,
ascii_case_insensitive: bool,
failure_message: &'static str,
cell: OnceLock<Option<AhoCorasick>>,
}
impl LazyAnchorAc {
fn new(
literals: Vec<Arc<str>>,
ascii_case_insensitive: bool,
failure_message: &'static str,
) -> Self {
Self {
literals: Mutex::new(Some(literals.into_boxed_slice())),
ascii_case_insensitive,
failure_message,
cell: OnceLock::new(),
}
}
fn get(&self) -> (Option<&AhoCorasick>, bool) {
let already_initialized = self.cell.get().is_some();
let anchor = self.cell.get_or_init(|| {
let literals = self
.literals
.lock()
.unwrap_or_else(|error| error.into_inner())
.take()
.expect("lazy phase-2 anchor literals must exist before initialization");
match AhoCorasickBuilder::new()
.match_kind(MatchKind::Standard)
.ascii_case_insensitive(self.ascii_case_insensitive)
.build(literals.iter().map(|literal| literal.as_bytes()))
{
Ok(anchor) => Some(anchor),
Err(error) => {
tracing::warn!(
literals = literals.len(),
%error,
"{}",
self.failure_message
);
None
}
}
});
(anchor.as_ref(), !already_initialized)
}
fn is_available(&self) -> bool {
!matches!(self.cell.get(), Some(None))
}
}
fn intern_anchor_literal(
ids: &mut std::collections::HashMap<Arc<str>, usize>,
literals: &mut Vec<Arc<str>>,
literal: &str,
) -> usize {
if let Some(&id) = ids.get(literal) {
return id;
}
let literal: Arc<str> = Arc::from(literal);
let id = literals.len();
literals.push(Arc::clone(&literal));
ids.insert(literal, id);
id
}
pub(crate) struct Phase2AnchorIndex {
anchor_ac: Option<LazyAnchorAc>,
anchor_first_bigram: Option<FirstBigramSet>,
literal_patterns: super::CsrU32,
eligible: Vec<bool>,
always_active_eligible: Vec<bool>,
always_anchor_ac: Option<AhoCorasick>,
always_anchor_literals: Vec<String>,
always_anchor_first_bigram: Option<FirstBigramSet>,
always_literal_patterns: super::CsrU32,
anchored: Vec<Option<AnchoredRegex>>,
eligible_count: usize,
plain_anchor_ac: Option<LazyAnchorAc>,
plain_anchor_first_bigram: Option<FirstBigramSet>,
plain_literal_patterns: super::CsrU32,
plain_always_mark: Vec<u32>,
}
pub(crate) fn compile_localization_hint(
pattern: &CompiledPattern,
) -> crate::compiler::compiler_build::Phase2LocalizationHint {
use crate::compiler::compiler_build::Phase2LocalizationHint;
let source = pattern.regex.as_str();
if let Some(literals) = required_prefix_literals(source) {
return Phase2LocalizationHint::Prefix { literals };
}
if pattern.regex.is_case_insensitive() {
return Phase2LocalizationHint::None;
}
let folded_regex = ascii_fold_regex_src(source);
Phase2LocalizationHint::Plain {
literals: leading_literals_of_folded(&folded_regex),
folded_regex,
}
}
impl Phase2AnchorIndex {
pub(crate) fn eligible_count(&self) -> usize {
self.eligible_count
}
pub(crate) fn materialize_for_batch(&self, plain_localizer: bool) -> bool {
let mut materialized = self.anchor_ac.as_ref().is_some_and(|anchor| anchor.get().1);
if plain_localizer {
materialized |= self
.plain_anchor_ac
.as_ref()
.is_some_and(|anchor| anchor.get().1);
}
materialized
}
#[inline]
pub(crate) fn is_eligible(&self, phase2_idx: usize) -> bool {
if !self
.anchor_ac
.as_ref()
.is_some_and(LazyAnchorAc::is_available)
{
return false;
}
matches!(self.eligible.get(phase2_idx), Some(true)) }
#[inline]
pub(crate) fn is_always_active_eligible(&self, phase2_idx: usize) -> bool {
if self.always_anchor_ac.is_none() {
return false;
}
matches!(self.always_active_eligible.get(phase2_idx), Some(true)) }
pub(crate) fn always_anchor_literals(&self) -> &[String] {
&self.always_anchor_literals
}
pub(crate) fn build(
phase2_patterns: &[(CompiledPattern, Vec<String>)],
always_active_indices: &[usize],
) -> Option<Self> {
Self::build_with_hints(phase2_patterns, always_active_indices, None)
}
pub(crate) fn build_with_hints(
phase2_patterns: &[(CompiledPattern, Vec<String>)],
always_active_indices: &[usize],
localization_hints: Option<Vec<crate::compiler::compiler_build::Phase2LocalizationHint>>,
) -> Option<Self> {
if localization_hints.is_none() {
crate::execution_pack::matcher_sections::record_runtime_localization_hint_fallback();
}
let mut localization_hints = localization_hints.map(Vec::into_iter);
Self::build_from_hints(
phase2_patterns,
always_active_indices,
&mut localization_hints,
)
}
fn build_from_hints(
phase2_patterns: &[(CompiledPattern, Vec<String>)],
always_active_indices: &[usize],
localization_hints: &mut Option<
std::vec::IntoIter<crate::compiler::compiler_build::Phase2LocalizationHint>,
>,
) -> Option<Self> {
let mut eligible = vec![false; phase2_patterns.len()];
let mut anchored: Vec<Option<AnchoredRegex>> =
(0..phase2_patterns.len()).map(|_| None).collect();
let mut literal_ids: std::collections::HashMap<Arc<str>, usize> =
std::collections::HashMap::new();
let mut literals: Vec<Arc<str>> = Vec::new();
let mut literal_pattern_pairs = Vec::new();
let mut plain_literal_ids: std::collections::HashMap<Arc<str>, usize> =
std::collections::HashMap::new();
let mut plain_literals: Vec<Arc<str>> = Vec::new();
let mut plain_literal_pattern_pairs = Vec::new();
let mut plain_always_mark: Vec<u32> = Vec::new();
for (idx, (pattern, _keywords)) in phase2_patterns.iter().enumerate() {
use crate::compiler::compiler_build::Phase2LocalizationHint;
let hint = match localization_hints.as_mut() {
Some(hints) => hints.next().unwrap_or_else(|| {
panic!(
"BUILD-INVARIANT VIOLATION: phase-2 localization hint cardinality is shorter than the compiled pattern set"
)
}),
None => compile_localization_hint(pattern),
};
match hint {
Phase2LocalizationHint::Prefix {
literals: pattern_literals,
} => {
for literal in &pattern_literals {
let id = intern_anchor_literal(&mut literal_ids, &mut literals, literal);
literal_pattern_pairs.push((id, idx));
}
eligible[idx] = true;
anchored[idx] = Some(AnchoredRegex::new(
pattern.regex.as_str(),
pattern.regex.is_case_insensitive(),
));
}
Phase2LocalizationHint::Plain {
folded_regex,
literals: Some(pattern_literals),
} => {
for literal in &pattern_literals {
let id = intern_anchor_literal(
&mut plain_literal_ids,
&mut plain_literals,
literal,
);
plain_literal_pattern_pairs.push((id, idx));
}
anchored[idx] = Some(AnchoredRegex::new(&folded_regex, false));
}
Phase2LocalizationHint::Plain { literals: None, .. } => {
plain_always_mark.push(idx as u32);
}
Phase2LocalizationHint::None => {}
}
}
drop(literal_ids);
drop(plain_literal_ids);
let literal_patterns = super::CsrU32::from_pairs(literals.len(), literal_pattern_pairs);
let plain_literal_patterns =
super::CsrU32::from_pairs(plain_literals.len(), plain_literal_pattern_pairs);
let eligible_count = eligible.iter().filter(|&&e| e).count();
if eligible_count == 0 && plain_literals.is_empty() && plain_always_mark.is_empty() {
return None;
}
let mut always_active_eligible = vec![false; phase2_patterns.len()];
for &i in always_active_indices {
if eligible.get(i).copied().is_some_and(|v| v) {
always_active_eligible[i] = true;
}
}
let mut always_literals: Vec<String> = Vec::new();
let mut always_literal_pattern_pairs = Vec::new();
for (literal_id, patterns) in literal_patterns.iter().enumerate() {
let always_literal_id = always_literals.len();
let mut retained_literal = false;
for &pattern in patterns {
if matches!(always_active_eligible.get(pattern as usize), Some(true)) {
if !retained_literal {
if let Some(literal) = literals.get(literal_id) {
always_literals.push(literal.to_string());
}
retained_literal = true;
}
always_literal_pattern_pairs.push((always_literal_id, pattern as usize));
}
}
}
let always_literal_patterns =
super::CsrU32::from_pairs(always_literals.len(), always_literal_pattern_pairs);
let anchor_first_bigram = (!literals.is_empty()).then(|| {
FirstBigramSet::from_literals(literals.iter().map(|literal| literal.as_bytes()), true)
});
let anchor_ac = (!literals.is_empty()).then(|| {
LazyAnchorAc::new(
literals,
true,
"phase-2 shared-anchor Aho-Corasick build failed; keyword-triggered anchored patterns stay on the whole-chunk path (recall preserved)",
)
});
let always_anchor_first_bigram = (!always_literals.is_empty()).then(|| {
FirstBigramSet::from_literals(always_literals.iter().map(String::as_bytes), true)
});
let always_anchor_ac = if always_literals.is_empty() {
None
} else {
match AhoCorasickBuilder::new()
.match_kind(MatchKind::Standard)
.ascii_case_insensitive(true)
.build(&always_literals)
{
Ok(ac) => Some(ac),
Err(error) => {
tracing::warn!(
literals = always_literals.len(),
%error,
"phase-2 always-active shared-anchor Aho-Corasick build failed; always-active anchored patterns stay on the RegexSet path (recall preserved)"
);
None
}
}
};
let plain_anchor_first_bigram = (!plain_literals.is_empty()).then(|| {
FirstBigramSet::from_literals(
plain_literals.iter().map(|literal| literal.as_bytes()),
false,
)
});
let plain_anchor_ac = (!plain_literals.is_empty()).then(|| {
LazyAnchorAc::new(
plain_literals,
false,
"phase-2 plain-anchor Aho-Corasick build failed; plain patterns stay on the folded RegexSet path (recall preserved)",
)
});
Some(Self {
anchor_ac,
anchor_first_bigram,
literal_patterns,
eligible,
always_active_eligible,
always_anchor_ac,
always_anchor_literals: always_literals,
always_anchor_first_bigram,
always_literal_patterns,
anchored,
eligible_count,
plain_anchor_ac,
plain_anchor_first_bigram,
plain_literal_patterns,
plain_always_mark,
})
}
pub(crate) fn collect_candidates(
&self,
text: &str,
is_active: impl Fn(usize) -> bool,
is_allowed: impl Fn(usize) -> bool,
out: &mut Vec<(u32, u32)>,
) {
out.clear();
if self
.anchor_first_bigram
.as_ref()
.is_some_and(|gate| !gate.may_have_match(text))
{
return;
}
let Some(ac) = self.anchor_ac.as_ref().and_then(|anchor| anchor.get().0) else {
return;
};
for m in ac.find_overlapping_iter(text) {
let lit_id = m.pattern().as_usize();
let pos = m.start() as u32;
if let Some(pats) = self.literal_patterns.get(lit_id) {
for &pat in pats {
let p = pat as usize;
if is_allowed(p) && (self.is_always_active_eligible(p) || is_active(p)) {
out.push((pat, pos));
}
}
}
}
out.sort_unstable();
out.dedup();
}
pub(crate) fn collect_always_active_candidates(
&self,
text: &str,
is_allowed: impl Fn(usize) -> bool,
out: &mut Vec<(u32, u32)>,
) {
out.clear();
let Some(ac) = &self.always_anchor_ac else {
return;
};
if self
.always_anchor_first_bigram
.as_ref()
.is_some_and(|gate| !gate.may_have_match(text))
{
return;
}
for m in ac.find_overlapping_iter(text) {
let lit_id = m.pattern().as_usize();
let pos = m.start() as u32;
if let Some(pats) = self.always_literal_patterns.get(lit_id) {
for &pat in pats {
if is_allowed(pat as usize) {
out.push((pat, pos));
}
}
}
}
out.sort_unstable();
out.dedup();
}
pub(crate) fn collect_always_active_candidates_from_literal_matches(
&self,
literal_matches: &[(u32, u32)],
is_allowed: impl Fn(usize) -> bool,
out: &mut Vec<(u32, u32)>,
) {
out.clear();
for &(literal_id, pos) in literal_matches {
let Some(patterns) = self.always_literal_patterns.get(literal_id as usize) else {
continue;
};
for &pattern in patterns {
if is_allowed(pattern as usize) {
out.push((pattern, pos));
}
}
}
out.sort_unstable();
out.dedup();
}
pub(crate) fn anchored_regex(&self, phase2_idx: usize) -> Option<&AnchoredRegex> {
let anchored = self.anchored.get(phase2_idx)?.as_ref()?;
Some(anchored)
}
pub(crate) fn has_plain_localizer(&self, phase2_plain_localizer: bool) -> bool {
if !phase2_plain_localizer {
return false;
}
self.plain_anchor_ac
.as_ref()
.is_some_and(LazyAnchorAc::is_available)
|| (!self.plain_always_mark.is_empty() && self.plain_anchor_ac.is_none())
}
pub(crate) fn plain_always_mark(&self) -> &[u32] {
&self.plain_always_mark
}
pub(crate) fn collect_plain_candidates(
&self,
text: &str,
is_allowed: impl Fn(usize) -> bool,
out: &mut Vec<(u32, u32)>,
) {
out.clear();
if self
.plain_anchor_first_bigram
.as_ref()
.is_some_and(|gate| !gate.may_have_match(text))
{
return;
}
let Some(ac) = self
.plain_anchor_ac
.as_ref()
.and_then(|anchor| anchor.get().0)
else {
return;
};
for m in ac.find_overlapping_iter(text) {
let lit_id = m.pattern().as_usize();
let pos = m.start() as u32;
if let Some(pats) = self.plain_literal_patterns.get(lit_id) {
for &pat in pats {
if is_allowed(pat as usize) {
out.push((pat, pos));
}
}
}
}
out.sort_unstable();
out.dedup();
}
}
fn leading_literals_of_folded(folded: &str) -> Option<Vec<String>> {
const MAX_VARIANTS: usize = 64;
let hir = regex_syntax::ParserBuilder::new()
.build()
.parse(folded)
.ok()?; let mut extractor = Extractor::new();
extractor.kind(ExtractKind::Prefix);
let seq = extractor.extract(&hir);
if !seq.is_finite() {
return None;
}
let literals = seq.literals()?;
if literals.is_empty() || literals.len() > MAX_VARIANTS {
return None;
}
let mut out: Vec<String> = Vec::with_capacity(literals.len());
for lit in literals {
if lit.len() < MIN_PREFIX_BYTES {
return None;
}
out.push(std::str::from_utf8(lit.as_bytes()).ok()?.to_string()); }
out.sort_unstable();
out.dedup();
Some(out)
}
pub(crate) fn required_prefix_literals(src: &str) -> Option<Vec<String>> {
required_prefix_literals_with_cap(src, MAX_LITERALS_PER_PATTERN)
}
pub(crate) fn required_prefix_literals_with_cap(
src: &str,
max_literals_per_pattern: usize,
) -> Option<Vec<String>> {
let literals = gate_prefix_literals(src)?;
let mut out: Vec<String> = Vec::with_capacity(literals.len());
for lit in literals {
debug_assert!(lit.len() >= MIN_PREFIX_BYTES);
debug_assert!(lit.is_ascii());
let s = std::str::from_utf8(&lit)
.ok()? .to_ascii_lowercase();
out.push(s);
}
out.sort_unstable();
out.dedup();
if out.len() > max_literals_per_pattern {
return None;
}
Some(out)
}