mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-10 12:29:58 -06:00
address review: build wildcard literal masks lazily with a bounded cache
This commit is contained in:
@@ -34,17 +34,24 @@ pub(crate) fn wildcard(text: &str, pat: &str) -> bool {
|
||||
}
|
||||
let words = (p.len() + 1).div_ceil(64);
|
||||
let bit = |set: &mut [u64], i: usize| set[i / 64] |= 1 << (i % 64);
|
||||
// Positions holding a `*`, a `?`, and each literal character.
|
||||
// Positions holding a `*` and a `?`, and where each literal character sits in the pattern.
|
||||
let mut stars = vec![0u64; words];
|
||||
let mut any = vec![0u64; words];
|
||||
let mut lits: std::collections::HashMap<char, Vec<u64>> = std::collections::HashMap::new();
|
||||
let mut at: std::collections::HashMap<char, Vec<usize>> = std::collections::HashMap::new();
|
||||
for (i, item) in p.iter().enumerate() {
|
||||
match item {
|
||||
('*', false) => bit(&mut stars, i),
|
||||
('?', false) => bit(&mut any, i),
|
||||
(c, _) => bit(lits.entry(*c).or_insert_with(|| vec![0u64; words]), i),
|
||||
(c, _) => at.entry(*c).or_default().push(i),
|
||||
}
|
||||
}
|
||||
// A literal's bit mask is built when the text first reads it, and kept for the first
|
||||
// `MAX_CACHED_MASKS` literals: a long pattern of mostly distinct characters would otherwise
|
||||
// allocate a mask of the pattern's width for each one up front. Past the cap the mask is
|
||||
// rebuilt in `scratch` from the character's positions, which costs about one step of the scan.
|
||||
const MAX_CACHED_MASKS: usize = 256;
|
||||
let mut masks: std::collections::HashMap<char, Vec<u64>> = std::collections::HashMap::new();
|
||||
let mut scratch = vec![0u64; words];
|
||||
// A set bit i: the text read so far matches the first i pattern items. A `*` matches nothing
|
||||
// as well, so a state at a `*` is also at the item after it.
|
||||
let close = |set: &mut Vec<u64>| {
|
||||
@@ -61,7 +68,22 @@ pub(crate) fn wildcard(text: &str, pat: &str) -> bool {
|
||||
close(&mut state);
|
||||
let mut next = vec![0u64; words];
|
||||
for ch in text.chars() {
|
||||
let lit = lits.get(&ch);
|
||||
let lit: Option<&[u64]> = match at.get(&ch) {
|
||||
None => None,
|
||||
Some(positions) if masks.contains_key(&ch) || masks.len() < MAX_CACHED_MASKS => {
|
||||
let mask = masks.entry(ch).or_insert_with(|| {
|
||||
let mut m = vec![0u64; words];
|
||||
positions.iter().for_each(|&i| bit(&mut m, i));
|
||||
m
|
||||
});
|
||||
Some(mask)
|
||||
}
|
||||
Some(positions) => {
|
||||
scratch.fill(0);
|
||||
positions.iter().for_each(|&i| bit(&mut scratch, i));
|
||||
Some(&scratch)
|
||||
}
|
||||
};
|
||||
let mut carry = 0u64;
|
||||
let mut alive = 0u64;
|
||||
for w in 0..words {
|
||||
|
||||
@@ -366,6 +366,18 @@ fn wildcard_agrees_with_backtracking() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wildcard_with_many_distinct_literals_agrees_with_backtracking() {
|
||||
// 600 distinct characters in the pattern: more than the masks kept, so the rest are rebuilt.
|
||||
let chars: Vec<char> = (0x4E00u32..0x4E00 + 600).filter_map(char::from_u32).collect();
|
||||
let word: String = chars.iter().collect();
|
||||
let pat = format!("*{word}?{}", chars[3]);
|
||||
for text in [format!("x{word}yq{}", chars[3]), word.clone(), format!("{word}{word}"), chars.iter().rev().collect::<String>()] {
|
||||
assert_eq!(wildcard(&text, &pat), wildcard_backtracking(&text, &pat), "{} chars", text.chars().count());
|
||||
}
|
||||
assert!(wildcard(&format!("{word}zz{}", chars[3]).replace("zz", "z"), &pat));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wildcard_on_long_repetitive_text_is_linear() {
|
||||
// The backtracking matcher compared about text × pattern characters here (10^8).
|
||||
|
||||
Reference in New Issue
Block a user