use crate::lattice::{LANES, Quotient};
use crate::prior::{CLASSES, Chain, Class, members};
const JOINT: usize = LANES * CLASSES;
const ITERATIONS: usize = 512;
const fn slot(block: u8, class: usize) -> usize {
block as usize * CLASSES + class
}
const CLASS_OF: [usize; 256] = {
let mut t = [0usize; 256];
let mut b = 0u8;
loop {
t[b as usize] = Class::of(b) as usize;
if b == u8::MAX {
break t;
}
b += 1;
}
};
#[derive(Clone, Copy)]
struct Edge {
to: u8,
share: f64,
}
pub struct Spread {
blocks: usize,
start: u8,
threshold: u8,
edges: Vec<Edge>,
spans: [[(u16, u16); CLASSES]; LANES],
}
impl Spread {
#[must_use]
pub fn of(q: &Quotient) -> Self {
let blocks = usize::from(q.blocks);
let mut edges = Vec::with_capacity(blocks * CLASSES * 2);
let mut spans = [[(0u16, 0u16); CLASSES]; LANES];
for (block, spans_of) in spans[..blocks].iter_mut().enumerate() {
let mut tally = [[0u32; LANES]; CLASSES];
for (row, &class) in q.rows.iter().zip(&CLASS_OF) {
tally[class][usize::from(row[block])] += 1;
}
for ((span, seen), class) in spans_of.iter_mut().zip(&tally).zip(Class::ALL) {
let from = edges.len() as u16;
for (to, &n) in seen[..blocks].iter().enumerate() {
if n > 0 {
edges.push(Edge {
to: to as u8,
share: f64::from(n) / members(class),
});
}
}
let until = edges.len() as u16;
*span = (from, until);
}
}
Self {
blocks,
start: q.start,
threshold: q.threshold,
edges,
spans,
}
}
#[must_use]
pub fn rate(&self, chain: &Chain) -> f64 {
let mut held = [0.0f64; JOINT];
let start = slot(self.start, 0);
held[start..start + CLASSES].copy_from_slice(&chain.start);
let mut next = [0.0f64; JOINT];
let mut cesaro = [0.0f64; JOINT];
let live = self.blocks * CLASSES;
for _ in 0..ITERATIONS {
next[..live].fill(0.0);
for (block, spans_of) in self.spans[..self.blocks].iter().enumerate() {
let was = &held[block * CLASSES..][..CLASSES];
let mut draw = [0.0f64; CLASSES];
for (row, &mass) in chain.next.iter().zip(was) {
if mass == 0.0 {
continue;
}
for (d, &p) in draw.iter_mut().zip(row) {
*d += mass * p;
}
}
for (class, (&(from, until), mass)) in spans_of.iter().zip(draw).enumerate() {
if mass == 0.0 {
continue;
}
for edge in &self.edges[usize::from(from)..usize::from(until)] {
next[usize::from(edge.to) * CLASSES + class] += mass * edge.share;
}
}
}
std::mem::swap(&mut held, &mut next);
for (sum, &x) in cesaro[..live].iter_mut().zip(&held[..live]) {
*sum += x;
}
}
let iterations = ITERATIONS as f64;
cesaro[slot(self.threshold, 0)..live].iter().sum::<f64>() / iterations
}
}
#[must_use]
pub fn worst_case(quotients: &[Quotient], chains: &[Chain]) -> f64 {
quotients
.iter()
.map(|q| {
let spread = Spread::of(q);
chains
.iter()
.map(|chain| spread.rate(chain))
.fold(0.0f64, f64::max)
})
.fold(1.0f64, f64::min)
}
#[cfg(test)]
mod tests {
use regex_automata::dfa::dense;
use regex_automata::nfa::thompson;
use regex_automata::util::syntax;
use super::*;
use crate::{lattice, prior, projection};
fn quotients(pattern: &str) -> Vec<Quotient> {
let dfa = dense::Builder::new()
.syntax(syntax::Config::new().utf8(false))
.thompson(thompson::Config::new().utf8(false))
.build(pattern)
.expect("pattern builds");
let core = projection::Projection::of(&dfa).expect("projects");
lattice::harvest(&core)
}
fn by_byte(q: &Quotient, chain: &Chain) -> f64 {
let weights: Vec<[f64; 256]> = Class::ALL.iter().map(|&c| chain.bytes_after(c)).collect();
let mut p = [0.0f64; JOINT];
let start = slot(q.start, 0);
p[start..start + CLASSES].copy_from_slice(&chain.start);
let mut cesaro = [0.0f64; JOINT];
for _ in 0..ITERATIONS {
let mut next = [0.0f64; JOINT];
for block in 0..q.blocks {
for (from, row) in weights.iter().enumerate() {
let mass = p[slot(block, from)];
if mass == 0.0 {
continue;
}
for (byte, &weight) in row.iter().enumerate() {
if weight != 0.0 {
let to = q.rows[byte][usize::from(block)];
next[slot(to, CLASS_OF[byte])] += mass * weight;
}
}
}
}
p = next;
for (c, &x) in cesaro.iter_mut().zip(&p) {
*c += x;
}
}
let iterations = ITERATIONS as f64;
cesaro[slot(q.threshold, 0)..slot(q.blocks, 0)]
.iter()
.sum::<f64>()
/ iterations
}
#[test]
fn grouping_the_bytes_by_class_computes_the_same_chain() {
const SLATE: &[&str] = &[
r"(?-u)WalletService",
r"(?-u)[0-9]{3}-[0-9]{4}",
r"(?-u)(alpha|beta|gamma)",
r"(?-u)<[^>]*>",
r"(?-u)#[0-9a-fA-F]{6}",
r"(?-u)a[^\n]*b",
];
let mut checked = 0usize;
for pattern in SLATE {
for q in quotients(pattern) {
let spread = Spread::of(&q);
for chain in &prior::DEFAULT_CHAINS {
let (fast, slow) = (spread.rate(chain), by_byte(&q, chain));
let scale = slow.abs().max(fast.abs()).max(f64::MIN_POSITIVE);
assert!(
(fast - slow).abs() / scale < 1e-9,
"{pattern:?}: grouped {fast:e} against by-byte {slow:e}"
);
checked += 1;
}
}
}
assert!(
checked >= 12,
"only {checked} comparisons — the slate harvested nothing"
);
}
#[test]
fn every_span_is_a_distribution_over_the_blocks_a_class_can_reach() {
for q in quotients(r"(?-u)[A-Z][a-z]+Service") {
let spread = Spread::of(&q);
for block in 0..spread.blocks {
for (class, &(from, until)) in spread.spans[block].iter().enumerate() {
let sum: f64 = spread.edges[usize::from(from)..usize::from(until)]
.iter()
.map(|e| e.share)
.sum();
assert!(
(sum - 1.0).abs() < 1e-12,
"block {block} class {class} spans {sum} of its bytes"
);
}
}
}
}
}