use rustc_hash::FxHashMap;
pub trait Unit: Copy + PartialEq + Eq {
type Map: UnitMap<Self>;
fn new_map() -> Self::Map;
}
pub trait UnitMap<T> {
fn get(&self, key: T) -> Option<usize>;
fn set(&mut self, key: T, row: usize);
fn clear(&mut self);
}
#[derive(Debug)]
pub struct ByteMap {
rows: [usize; 256],
}
impl UnitMap<u8> for ByteMap {
#[inline]
fn get(&self, key: u8) -> Option<usize> {
let v = self.rows[key as usize];
(v != usize::MAX).then_some(v)
}
#[inline]
fn set(&mut self, key: u8, row: usize) {
self.rows[key as usize] = row;
}
#[inline]
fn clear(&mut self) {
self.rows = [usize::MAX; 256];
}
}
impl Unit for u8 {
type Map = ByteMap;
fn new_map() -> Self::Map {
ByteMap {
rows: [usize::MAX; 256],
}
}
}
impl UnitMap<u16> for FxHashMap<u16, usize> {
#[inline]
fn get(&self, key: u16) -> Option<usize> {
FxHashMap::get(self, &key).copied()
}
#[inline]
fn set(&mut self, key: u16, row: usize) {
self.insert(key, row);
}
#[inline]
fn clear(&mut self) {
FxHashMap::clear(self);
}
}
impl Unit for u16 {
type Map = FxHashMap<u16, usize>;
fn new_map() -> Self::Map {
FxHashMap::default()
}
}
#[inline]
pub fn dispatch<R>(a: &str, b: &str, f: impl for<'x> FnOnce(Operands<'x>) -> R) -> R {
if a.is_ascii() && b.is_ascii() {
f(Operands::Bytes(a.as_bytes(), b.as_bytes()))
} else {
let ua: Vec<u16> = a.encode_utf16().collect();
let ub: Vec<u16> = b.encode_utf16().collect();
f(Operands::Units(&ua, &ub))
}
}
#[derive(Debug, Clone, Copy)]
pub enum Operands<'a> {
Bytes(&'a [u8], &'a [u8]),
Units(&'a [u16], &'a [u16]),
}
#[inline]
pub fn utf16_len(s: &str) -> usize {
if s.is_ascii() {
return s.len();
}
s.chars().map(|c| c.len_utf16()).sum()
}
pub(crate) trait BitPeq: Unit + std::hash::Hash {
type Table1;
fn peq1(pattern: &[Self]) -> Self::Table1;
fn peq1_get(table: &Self::Table1, unit: Self) -> u64;
type TableN;
fn peqn(pattern: &[Self], blocks: usize) -> Self::TableN;
fn peqn_row(table: &Self::TableN, unit: Self) -> Option<&[u64]>;
}
impl BitPeq for u8 {
type Table1 = [u64; 256];
fn peq1(pattern: &[Self]) -> Self::Table1 {
let mut table = [0u64; 256];
for (i, &c) in pattern.iter().enumerate() {
table[c as usize] |= 1u64 << i;
}
table
}
#[inline]
fn peq1_get(table: &Self::Table1, unit: Self) -> u64 {
table[unit as usize]
}
type TableN = ([u32; 256], Vec<u64>, usize);
fn peqn(pattern: &[Self], blocks: usize) -> Self::TableN {
let mut index = [u32::MAX; 256];
let mut rows: Vec<u64> = Vec::new();
for (i, &c) in pattern.iter().enumerate() {
let slot = &mut index[c as usize];
if *slot == u32::MAX {
*slot = rows.len() as u32;
rows.resize(rows.len() + blocks, 0);
}
rows[*slot as usize + i / 64] |= 1u64 << (i % 64);
}
(index, rows, blocks)
}
#[inline]
fn peqn_row(table: &Self::TableN, unit: Self) -> Option<&[u64]> {
let start = table.0[unit as usize];
(start != u32::MAX).then(|| {
let s = start as usize;
&table.1[s..s + table.2]
})
}
}
impl BitPeq for u16 {
type Table1 = FxHashMap<u16, u64>;
fn peq1(pattern: &[Self]) -> Self::Table1 {
let mut table = FxHashMap::default();
for (i, &c) in pattern.iter().enumerate() {
*table.entry(c).or_insert(0u64) |= 1u64 << i;
}
table
}
#[inline]
fn peq1_get(table: &Self::Table1, unit: Self) -> u64 {
table.get(&unit).copied().unwrap_or(0)
}
type TableN = (FxHashMap<u16, u32>, Vec<u64>, usize);
fn peqn(pattern: &[Self], blocks: usize) -> Self::TableN {
let mut index: FxHashMap<u16, u32> = FxHashMap::default();
let mut rows: Vec<u64> = Vec::new();
for (i, &c) in pattern.iter().enumerate() {
let slot = index.entry(c).or_insert_with(|| {
let start = rows.len() as u32;
rows.resize(rows.len() + blocks, 0);
start
});
rows[*slot as usize + i / 64] |= 1u64 << (i % 64);
}
(index, rows, blocks)
}
#[inline]
fn peqn_row(table: &Self::TableN, unit: Self) -> Option<&[u64]> {
table.0.get(&unit).map(|&start| {
let s = start as usize;
&table.1[s..s + table.2]
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn utf16_len_matches_utf16_count() {
for s in ["", "abc", "café", "Москва", "😀", "a😀b", "𝕳𝖊𝖑𝖑𝖔"] {
assert_eq!(utf16_len(s), s.encode_utf16().count(), "for {s:?}");
}
}
#[test]
fn ascii_takes_the_byte_path() {
let took_bytes = dispatch("abc", "abd", |ops| matches!(ops, Operands::Bytes(..)));
assert!(took_bytes);
}
#[test]
fn non_ascii_promotes_both_operands() {
let took_units = dispatch("abc", "café", |ops| matches!(ops, Operands::Units(..)));
assert!(took_units, "one non-ASCII operand must promote the pair");
}
#[test]
fn byte_map_roundtrips() {
let mut m = u8::new_map();
assert_eq!(m.get(b'a'), None);
m.set(b'a', 7);
assert_eq!(m.get(b'a'), Some(7));
m.clear();
assert_eq!(m.get(b'a'), None);
}
#[test]
fn unit_map_roundtrips() {
let mut m = u16::new_map();
assert_eq!(UnitMap::get(&m, 0x1234), None);
m.set(0x1234, 9);
assert_eq!(UnitMap::get(&m, 0x1234), Some(9));
m.clear();
assert_eq!(UnitMap::get(&m, 0x1234), None);
}
fn naive_mask<T: Copy + PartialEq>(pattern: &[T], unit: T, block: usize) -> u64 {
let mut mask = 0u64;
for (i, &c) in pattern.iter().enumerate() {
if i / 64 == block && c == unit {
mask |= 1u64 << (i % 64);
}
}
mask
}
#[test]
fn bitpeq_u8_tables_match_naive_masks() {
let mut pattern: Vec<u8> = Vec::new();
for i in 0..150usize {
pattern.push([0u8, b'a', 255, b'b', b'a'][i % 5]);
}
pattern.extend_from_slice(&[255, 0, b'z']);
let blocks = pattern.len().div_ceil(64);
assert_eq!(blocks, 3);
let t1 = <u8 as BitPeq>::peq1(&pattern[..60]);
for unit in [0u8, b'a', 255, b'b', b'z', b'q', 7] {
assert_eq!(
<u8 as BitPeq>::peq1_get(&t1, unit),
naive_mask(&pattern[..60], unit, 0),
"peq1 mismatch for byte {unit}"
);
}
let tn = <u8 as BitPeq>::peqn(&pattern, blocks);
for unit in [0u8, b'a', 255, b'b', b'z'] {
let row = <u8 as BitPeq>::peqn_row(&tn, unit).expect("present unit must have a row");
assert_eq!(row.len(), blocks);
for (b, &word) in row.iter().enumerate() {
assert_eq!(
word,
naive_mask(&pattern, unit, b),
"peqn mismatch for byte {unit}, block {b}"
);
}
}
for absent in [b'q', 1u8, 254] {
assert!(
<u8 as BitPeq>::peqn_row(&tn, absent).is_none(),
"absent byte {absent} must resolve to None"
);
}
}
#[test]
fn bitpeq_u16_tables_match_naive_masks() {
let mut pattern: Vec<u16> = Vec::new();
for i in 0..320usize {
pattern.push(i as u16); }
for i in 0..70usize {
pattern.push([0u16, 5, 0xFFFF, 0xD83D, 0xDE00, 17][i % 6]);
}
let blocks = pattern.len().div_ceil(64);
let head = &pattern[..48];
let t1 = <u16 as BitPeq>::peq1(head);
for unit in [0u16, 5, 17, 47, 48, 0xFFFF] {
assert_eq!(
<u16 as BitPeq>::peq1_get(&t1, unit),
naive_mask(head, unit, 0),
"peq1 mismatch for unit {unit}"
);
}
let tn = <u16 as BitPeq>::peqn(&pattern, blocks);
for unit in [0u16, 5, 17, 100, 319, 0xFFFF, 0xD83D, 0xDE00] {
let row = <u16 as BitPeq>::peqn_row(&tn, unit).expect("present unit must have a row");
assert_eq!(row.len(), blocks);
for (b, &word) in row.iter().enumerate() {
assert_eq!(
word,
naive_mask(&pattern, unit, b),
"peqn mismatch for unit {unit}, block {b}"
);
}
}
assert!(<u16 as BitPeq>::peqn_row(&tn, 999).is_none());
assert!(<u16 as BitPeq>::peqn_row(&tn, 0xFFFE).is_none());
}
#[test]
fn bitpeq_packed_rows_are_disjoint_per_unit() {
let pattern: Vec<u8> = (0..200).map(|i| b"xy"[i % 2]).collect();
let blocks = pattern.len().div_ceil(64);
let tn = <u8 as BitPeq>::peqn(&pattern, blocks);
let rx = <u8 as BitPeq>::peqn_row(&tn, b'x').unwrap().to_vec();
let ry = <u8 as BitPeq>::peqn_row(&tn, b'y').unwrap().to_vec();
for b in 0..blocks {
assert_eq!(rx[b] & ry[b], 0, "rows alias in block {b}");
let expected_union = naive_mask(&pattern, b'x', b) | naive_mask(&pattern, b'y', b);
assert_eq!(rx[b] | ry[b], expected_union, "union wrong in block {b}");
}
}
}