use crate::token::{Token, TokenKind};
#[derive(Clone, Copy, Debug)]
pub struct ShapeConfig {
pub max_lag: usize,
pub period_window: usize,
pub period_hop: usize,
pub novelty_k: usize,
pub novelty_window: usize,
pub template_strength: f32,
pub cp_min_gap: usize,
}
impl Default for ShapeConfig {
fn default() -> Self {
Self {
max_lag: 64,
period_window: 256,
period_hop: 8,
novelty_k: 3,
novelty_window: 4096,
template_strength: 0.6,
cp_min_gap: 4,
}
}
}
const ROW_SPREAD: f32 = 0.5;
#[must_use]
pub fn shape_class(kind: TokenKind, tok_bytes: &[u8]) -> u32 {
let base = kind.code() << 16;
match kind {
TokenKind::Word => {
let s = crate::tokutil::shape(&String::from_utf8_lossy(tok_bytes));
base | word_shape_code(s)
}
TokenKind::Punct => base | glyph(tok_bytes),
_ => base,
}
}
fn glyph(tok: &[u8]) -> u32 {
let Some(chunk) = tok.utf8_chunks().next() else { return 0 };
match chunk.valid().chars().next() {
Some(c) => u32::from(c) & 0xFFFF,
None => chunk.invalid().first().map_or(0, |&b| u32::from(b)),
}
}
fn word_shape_code(s: &str) -> u32 {
match s {
"Pascal" => 1,
"snake" => 2,
"camel" => 3,
"SCREAM" => 4,
"short" => 5,
_ => 0, }
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ShapeFrame {
pub class: u32,
pub period: u16,
pub period_strength: f32,
pub novelty: f32,
}
#[derive(Clone, Debug, Default)]
pub struct ShapeField {
pub n_tokens: usize,
pub spans: Vec<(usize, usize)>,
pub frames: Vec<ShapeFrame>,
pub boundaries: Vec<usize>,
template_strength: f32,
}
impl ShapeField {
fn token_at(&self, byte: usize) -> Option<usize> {
if self.spans.is_empty() {
return None;
}
let i = self.spans.partition_point(|&(s, _)| s <= byte);
Some(i.saturating_sub(1))
}
#[must_use]
pub fn class_at(&self, byte: usize) -> u32 {
self.token_at(byte)
.and_then(|i| self.frames.get(i))
.map_or(0, |f| f.class)
}
#[must_use]
pub fn period_at(&self, byte: usize) -> (u16, f32) {
self.token_at(byte)
.and_then(|i| self.frames.get(i))
.map_or((0, 0.0), |f| (f.period, f.period_strength))
}
#[must_use]
pub fn in_template(&self, byte: usize) -> bool {
self.period_at(byte).1 >= self.template_strength
}
#[must_use]
pub fn shape_regions(&self) -> Vec<(usize, usize, u16)> {
self.template_runs()
.into_iter()
.map(|(first, last, period)| (self.spans[first].0, self.spans[last].1, period))
.collect()
}
fn template_runs(&self) -> Vec<(usize, usize, u16)> {
let kind = |i: usize| self.frames[i].class >> 16;
let trim = |(first, last, period): (usize, usize, u16)| {
let lag = usize::from(period);
let mut last = last;
while last > first && !(last >= lag && kind(last) == kind(last - lag)) {
last -= 1;
}
(first, last, period)
};
let mut out: Vec<(usize, usize, u16)> = Vec::new();
let mut run: Option<(usize, usize, u16)> = None;
for (i, f) in self.frames.iter().enumerate() {
if f.period_strength >= self.template_strength && f.period > 0 {
match run.as_mut() {
Some(r) => r.1 = i,
None => run = Some((i, i, f.period)),
}
} else if let Some(r) = run.take() {
out.push(trim(r));
}
}
if let Some(r) = run.take() {
out.push(trim(r));
}
out
}
#[must_use]
pub fn template_spans(&self) -> Vec<(usize, usize, u16)> {
self.extended_runs(|_, _, _| true)
}
#[must_use]
pub fn table_spans(&self, input: &[u8]) -> Vec<(usize, usize, u16)> {
let word = TokenKind::Word.code();
self.extended_runs(|first, last, period| {
(first..=last).any(|i| self.frames[i].class >> 16 != word)
&& (period > 2 || self.rows_are_regular(input, first, last))
})
}
fn rows_are_regular(&self, input: &[u8], first: usize, last: usize) -> bool {
let mut rows: Vec<f32> = Vec::new();
let mut tokens = 1u32;
for j in first + 1..=last {
if input[self.spans[j - 1].1..self.spans[j].0].iter().any(|&b| b == b'\n' || b == b'\r') {
rows.push(tokens as f32);
tokens = 1;
} else {
tokens += 1;
}
}
rows.push(tokens as f32);
if rows.len() < 2 {
return false;
}
let n = rows.len() as f32;
let mean = rows.iter().sum::<f32>() / n;
let variance = rows.iter().map(|r| (r - mean).powi(2)).sum::<f32>() / n;
variance.sqrt() <= ROW_SPREAD * mean
}
fn extended_runs(&self, keep: impl Fn(usize, usize, u16) -> bool) -> Vec<(usize, usize, u16)> {
let n = self.frames.len();
let kind = |i: usize| self.frames[i].class >> 16;
let repeats = |j: usize, lag: usize| {
if j + lag < n {
kind(j) == kind(j + lag)
} else {
j >= lag && kind(j) == kind(j - lag)
}
};
let word = TokenKind::Word.code();
let mut out: Vec<(usize, usize, u16)> = self
.template_runs()
.into_iter()
.filter(|&(first, last, period)| keep(first, last, period))
.map(|(first, last, period)| {
let lag = usize::from(period);
let mut first = first;
while first > 0 {
let j = first - 1;
if !repeats(j, lag) || (j..(j + lag).min(n)).all(|i| kind(i) == word) {
break;
}
first = j;
}
(self.spans[first].0, self.spans[last].1, period)
})
.collect();
out.sort_unstable_by_key(|&(s, _, _)| s);
out
}
}
#[must_use]
pub fn analyze(tokens: &[Token], bytes: &[u8]) -> ShapeField {
analyze_with(tokens, bytes, &ShapeConfig::default())
}
#[must_use]
pub fn analyze_bytes(bytes: &[u8]) -> ShapeField {
let toks = crate::tokutil::lex_sig(bytes);
analyze(&toks, bytes)
}
#[must_use]
pub fn shape_class_over(kind: TokenKind, tok_bytes: &[u8], group: crate::orbit::OrbitGroup) -> u32 {
let base = kind.code() << 16;
let canon = crate::orbit::canonical(tok_bytes, group);
let mut h: u32 = 2166136261;
for &b in canon.as_bytes() {
h = (h ^ u32::from(b)).wrapping_mul(16777619);
}
base | ((h ^ (h >> 16)) & 0xFFFF)
}
fn build_field(tokens: &[Token], classes: &[u32], cfg: &ShapeConfig) -> ShapeField {
let n = tokens.len();
let mut field = ShapeField {
n_tokens: n,
spans: Vec::with_capacity(n),
frames: Vec::with_capacity(n),
boundaries: Vec::new(),
template_strength: cfg.template_strength,
};
if n == 0 {
return field;
}
for t in tokens {
field.spans.push((t.start(), t.end()));
}
let k = cfg.novelty_k.max(1);
let mut counts: std::collections::HashMap<u64, u32> =
std::collections::HashMap::with_capacity(cfg.novelty_window.min(n) + 1);
let mut ring: std::collections::VecDeque<u64> = std::collections::VecDeque::with_capacity(cfg.novelty_window + 1);
let mut last_cut: isize = -(cfg.cp_min_gap as isize);
let mut cur_period: u16 = 0;
let mut cur_strength: f32 = 0.0;
let mut frames: Vec<ShapeFrame> = Vec::with_capacity(n);
for i in 0..n {
let novelty = if i + 1 >= k {
let mut h: u64 = 1469598103934665603;
for &c in &classes[i + 1 - k..=i] {
h = (h ^ u64::from(c)).wrapping_mul(1099511628211);
}
let prev = *counts.get(&h).unwrap_or(&0);
let entry = counts.entry(h).or_insert(0);
*entry += 1;
ring.push_back(h);
if ring.len() > cfg.novelty_window
&& let Some(old) = ring.pop_front()
&& let Some(c) = counts.get_mut(&old)
{
*c = c.saturating_sub(1);
}
1.0 / (1.0 + prev as f32)
} else {
1.0
};
if novelty > 0.5 && (i as isize - last_cut) >= cfg.cp_min_gap as isize && i > 0 {
field.boundaries.push(tokens[i].start());
last_cut = i as isize;
}
if i % cfg.period_hop == 0 || i + 1 == n {
let lo = i.saturating_sub(cfg.period_window);
let (p, s) = dominant_shape_period(&classes[lo..=i], cfg.max_lag);
cur_period = p;
cur_strength = s;
}
frames.push(ShapeFrame {
class: classes[i],
period: cur_period,
period_strength: cur_strength,
novelty,
});
}
field.frames = frames;
field
}
#[must_use]
pub fn analyze_with(tokens: &[Token], bytes: &[u8], cfg: &ShapeConfig) -> ShapeField {
let classes: Vec<u32> = tokens
.iter()
.map(|t| shape_class(t.kind, &bytes[t.span()]))
.collect();
build_field(tokens, &classes, cfg)
}
#[must_use]
pub fn analyze_over_with(
tokens: &[Token],
bytes: &[u8],
group: crate::orbit::OrbitGroup,
cfg: &ShapeConfig,
) -> ShapeField {
let classes: Vec<u32> = tokens
.iter()
.map(|t| shape_class_over(t.kind, &bytes[t.span()], group))
.collect();
build_field(tokens, &classes, cfg)
}
#[must_use]
pub fn analyze_over(tokens: &[Token], bytes: &[u8], group: crate::orbit::OrbitGroup) -> ShapeField {
analyze_over_with(tokens, bytes, group, &ShapeConfig::default())
}
#[must_use]
pub fn analyze_bytes_over(bytes: &[u8], group: crate::orbit::OrbitGroup) -> ShapeField {
let toks = crate::tokutil::lex_sig(bytes);
analyze_over(&toks, bytes, group)
}
fn dominant_shape_period(win: &[u32], max_lag: usize) -> (u16, f32) {
let n = win.len();
if n < 4 {
return (0, 0.0);
}
let hi = max_lag.min(n / 2);
let mut best_lag = 0usize;
let mut best = 0.0f32;
for lag in 1..=hi {
let mut matches = 0u32;
for (a, b) in win[lag..].iter().zip(&win[..n - lag]) {
matches += u32::from(a == b);
}
let matches = matches as usize;
let frac = matches as f32 / (n - lag) as f32;
if frac > best {
best = frac;
best_lag = lag;
}
}
if best < 0.5 {
(0, best)
} else {
(best_lag as u16, best)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RegionKind {
Table(u16),
Blob,
Prose,
Numeric,
Code,
Mixed,
}
impl RegionKind {
#[must_use]
pub fn label(self) -> &'static str {
match self {
RegionKind::Table(_) => "table",
RegionKind::Blob => "blob",
RegionKind::Prose => "prose",
RegionKind::Numeric => "numeric",
RegionKind::Code => "code",
RegionKind::Mixed => "mixed",
}
}
pub const NAMES: [&'static str; 6] = ["table", "blob", "prose", "numeric", "code", "mixed"];
#[must_use]
pub fn named(self, name: &str) -> bool {
self.label() == name
}
}
#[must_use]
pub fn keeps_texture(asked: &[(String, bool)], kind: Option<RegionKind>) -> bool {
let named = |name: &str| kind.is_some_and(|k| k.named(name));
if asked.iter().any(|(name, keeps)| !keeps && named(name)) {
return false;
}
match asked.iter().any(|(_, keeps)| *keeps) {
true => asked.iter().any(|(name, keeps)| *keeps && named(name)),
false => true,
}
}
#[must_use]
pub fn classified_regions(input: &[u8]) -> Vec<(usize, usize, RegionKind)> {
classified_regions_with(input, &crate::spectral::SpectralConfig::default())
}
#[must_use]
pub fn classified_regions_with(input: &[u8], spectral: &crate::spectral::SpectralConfig) -> Vec<(usize, usize, RegionKind)> {
let toks = crate::tokutil::lex_sig(input);
let templates = analyze(&toks, input).table_spans(input);
let encoded = encoded_spans(input, &toks);
crate::spectral::code_regions_with(input, spectral)
.into_iter()
.map(|(s, e, tex)| {
let mut covered = 0usize;
let mut reach = s;
let mut widest: Option<(usize, u16)> = None;
for &(ts, te, p) in &templates {
let (lo, hi) = (ts.max(s), te.min(e));
if lo >= hi {
continue;
}
covered += hi.saturating_sub(lo.max(reach));
reach = reach.max(hi);
if widest.is_none_or(|(w, _)| hi - lo > w) {
widest = Some((hi - lo, p));
}
}
let period = widest.filter(|_| 2 * covered > e - s).map(|(_, p)| p);
let kind = match period {
_ if 2 * covered_by(&encoded, s, e) > e - s => RegionKind::Blob,
Some(p) => RegionKind::Table(p),
None => match tex {
crate::spectral::CodeTexture::Blob => RegionKind::Blob,
crate::spectral::CodeTexture::Prose => RegionKind::Prose,
crate::spectral::CodeTexture::Numeric => RegionKind::Numeric,
crate::spectral::CodeTexture::Mixed => RegionKind::Mixed,
crate::spectral::CodeTexture::Code => RegionKind::Code,
},
};
(s, e, kind)
})
.collect()
}
fn encoded_spans(input: &[u8], toks: &[Token]) -> Vec<(usize, usize)> {
let mut spans: Vec<(usize, usize)> = toks
.iter()
.filter(|t| matches!(t.kind, TokenKind::Base64 | TokenKind::HashDigest | TokenKind::Hex))
.map(|t| (t.start(), t.end()))
.chain(crate::lexer::blob_runs(input))
.collect();
spans.sort_unstable();
let mut merged: Vec<(usize, usize)> = Vec::with_capacity(spans.len());
for (s, e) in spans {
match merged.last_mut() {
Some(last) if s <= last.1 => last.1 = last.1.max(e),
_ => merged.push((s, e)),
}
}
merged
}
fn covered_by(spans: &[(usize, usize)], s: usize, e: usize) -> usize {
let first = spans.partition_point(|&(_, end)| end <= s);
spans[first..].iter().take_while(|&&(start, _)| start < e).map(|&(start, end)| end.min(e) - start.max(s)).sum()
}
#[must_use]
pub fn dominant_kind(input: &[u8]) -> Option<RegionKind> {
let regions = classified_regions(input);
let mut totals: Vec<(&'static str, usize, RegionKind, usize)> = Vec::new();
for (s, e, kind) in regions {
let span = e.saturating_sub(s);
match totals.iter_mut().find(|(name, _, _, _)| *name == kind.label()) {
Some((_, bytes, widest, widest_span)) => {
*bytes += span;
if span > *widest_span {
*widest = kind;
*widest_span = span;
}
}
None => totals.push((kind.label(), span, kind, span)),
}
}
totals.into_iter().max_by_key(|&(_, bytes, _, _)| bytes).map(|(_, _, kind, _)| kind)
}
#[cfg(test)]
mod tests {
use super::*;
fn field(s: &str) -> ShapeField {
analyze_bytes(s.as_bytes())
}
#[test]
fn ragged_csv_has_strong_shape_period() {
let f = field("1,22,3\n444,5,66\n7,888,9\n12,3,456\n");
let strong = f.frames.iter().any(|fr| fr.period > 0 && fr.period_strength >= 0.6);
assert!(strong, "ragged CSV should show a strong shape period");
assert!(!f.shape_regions().is_empty(), "should report a template region");
}
#[test]
fn prose_has_no_shape_period() {
let f = field("the quick brown fox jumps over the lazy dog and then rests");
let any_template = f.frames.iter().any(|fr| fr.period_strength >= 0.8 && fr.period > 1);
assert!(!any_template, "free prose should not read as a strong template");
}
#[test]
fn repeated_idiom_is_low_novelty() {
let f = field("self.a = a; self.b = b; self.c = c; self.d = d;");
let tail: f32 = f.frames.iter().rev().take(4).map(|fr| fr.novelty).sum::<f32>() / 4.0;
assert!(tail < 0.6, "a repeated template should have low tail novelty, got {tail}");
}
#[test]
fn a_texture_filter_keeps_by_name_and_drops_first() {
let asked = |list: &[(&str, bool)]| list.iter().map(|(n, k)| ((*n).to_string(), *k)).collect::<Vec<_>>();
let table = Some(RegionKind::Table(7));
assert!(keeps_texture(&[], None));
assert!(keeps_texture(&asked(&[("table", true)]), table));
assert!(!keeps_texture(&asked(&[("prose", true)]), table));
assert!(!keeps_texture(&asked(&[("prose", true)]), None));
assert!(!keeps_texture(&asked(&[("table", false)]), table));
assert!(keeps_texture(&asked(&[("blob", false)]), table));
assert!(keeps_texture(&asked(&[("blob", false)]), None));
assert!(!keeps_texture(&asked(&[("table", true), ("table", false)]), table));
}
#[test]
fn empty_is_safe() {
let f = field("");
assert_eq!(f.n_tokens, 0);
assert!(f.frames.is_empty());
assert!(f.shape_regions().is_empty());
assert_eq!(f.class_at(0), 0);
assert!(!f.in_template(0));
}
#[test]
fn class_at_maps_byte_to_silhouette() {
let f = field("foo(a, b)");
assert_ne!(f.class_at(0), 0);
let g = field("bar(x, y)");
assert_eq!(f.class_at(0), g.class_at(0), "same silhouette -> same class");
}
#[test]
fn classified_regions_names_a_table() {
let csv = "name,age,score\nalice,30,95\nbob,25,88\ncarol,41,73\ndan,38,91\n";
let regions = classified_regions(csv.as_bytes());
assert!(
regions.iter().any(|&(_, _, k)| matches!(k, RegionKind::Table(_))),
"ragged CSV should classify as a Table region, got {regions:?}"
);
}
#[test]
fn a_short_template_run_in_prose_is_no_table() {
let prose = "# Reading a directory\n\nThe walk reports what it found rather than what it was asked for. A filter that\nsilently drops a file reads exactly the same as a directory that never held one, and\nthe reader cannot tell the two apart afterwards.\n";
assert!(
!field(prose).shape_regions().is_empty(),
"the prose holds a template run, which is what the rule must not take for a table"
);
assert!(
!classified_regions(prose.as_bytes()).iter().any(|&(_, _, k)| matches!(k, RegionKind::Table(_))),
"prose with a short template run reads by its texture"
);
let words = "queue drained\nnothing to report\n";
assert!(!classified_regions(words.as_bytes()).iter().any(|&(_, _, k)| matches!(k, RegionKind::Table(_))));
}
#[test]
fn a_table_whose_period_is_found_at_its_last_row_is_a_table() {
for table in ["alpha 10\nbeta 20\ngamma 300\ndelta 4000\nepsilon 5\n", "alpha 1000B 80ms\nalpha 2000B 80ms\nalpha 4000B 80ms\nbeta 8000B 300ms\n"] {
assert!(
classified_regions(table.as_bytes()).iter().all(|&(_, _, k)| matches!(k, RegionKind::Table(_))),
"{table:?} reads as one table"
);
}
}
fn all_tables(input: &str) -> bool {
classified_regions(input.as_bytes()).iter().all(|&(_, _, k)| matches!(k, RegionKind::Table(_)))
}
#[test]
fn a_column_of_numbers_one_to_a_line_is_a_table() {
let column: String = (1..=40).map(|i| format!("{}\n", i * 37 % 1000)).collect();
assert!(all_tables(&column), "{column:?}");
let cr = column.replace('\n', "\r");
assert!(all_tables(&cr), "{cr:?}");
}
#[test]
fn a_list_run_along_one_line_is_no_table() {
let list: String = (1..=60).map(|i| format!("{}, ", i * 37 % 1000)).collect();
let f = field(&list);
assert!(!f.template_spans().is_empty(), "the list repeats, which is what the row rule weighs");
assert!(f.table_spans(list.as_bytes()).is_empty(), "{list:?}");
let rows: String = (1..=60).map(|i| format!("{},{}", i * 37 % 1000, if i % 3 == 0 { "\n" } else { " " })).collect();
assert!(all_tables(&rows), "{rows:?}");
}
#[test]
fn digests_one_to_a_line_read_as_a_blob_though_they_repeat_as_a_table() {
let digests = "e5dd0024dd43d92434808c8e1df3c09870a2832744283232aa1dc16c737237b1\n\
756d56532a8ba4f64281743a9aa5ce68a6a1d25962e722c80c334cab62232fca\n\
730e4c677c1913d48d81535890070c6ca04b9768fc5edccb7600d1d251ea15ea\n\
ac4fb8cbb647821b1ffddfa312804f941ca08de57d3cdbcc92f890498cee64f5\n\
72d31464e6ef9df69c740c364e18099be00dbdca22a12bd04ed919f7e664e6c3\n\
c6a7202fa3e271d49d0d3a45f75b2b01b687f67698765d04e7e6abbdc93cf0c1\n\
0c28e709ef02d738012348236e52c0f2ce0b9dad70f567dd73ac7769c08af03d\n\
f21e2b588e7fb5766c405084872a1a31c79677d5cf49ebfc3b62b3b5863e59ee\n\
959ad168d33c61cab403f00f22e8faf36a2138d567a5d513fae827d4c9565d12\n\
4eba41e47c256a732446527a2889eaf7e88bb959ce250905eb5d98fa48fb6b20\n\
74e6d06c9d5daceafb13535c47e9f28ed95bc9cf08a611bba485499b11e85ab2\n\
2bb471318d836aa874fecbcf2dd3da00c6bedee70ec20c141f9ca02bc25abe07\n\
6aab6defb79d1c856cb8fdfb98b9dc93f6144f1d4bf25faad0647c5282ee28ff\n\
1e0700dba02c4e8baeafc5ab588b7b38c10caa19dbced125fa7472e1651a95e0\n\
d109b20a1ef2dcc601d3fc3dca2b7d06f423b72d3a0fa650afb4cef5fa8e9a18\n\
31f54ed24f1733a2029ebbbe08ed8343dd18c06de0356e5e3fc611fe700bd345\n";
assert!(!field(digests).table_spans(digests.as_bytes()).is_empty(), "the digests are a table by shape alone");
let regions = classified_regions(digests.as_bytes());
assert!(regions.iter().all(|&(_, _, k)| k == RegionKind::Blob), "{regions:?}");
}
#[test]
fn a_punctuation_class_is_its_character() {
let class = |s: &str| shape_class(TokenKind::Punct, s.as_bytes());
assert_ne!(class("、"), class("。"), "two characters sharing a first byte");
assert_ne!(class("─"), class("│"), "two box-drawing characters");
assert_eq!(class(","), (TokenKind::Punct.code() << 16) | u32::from(b','));
}
#[test]
fn orbit_case_fold_finds_the_true_period() {
let s: &[u8] = b"the cat sat THE CAT SAT the cat sat THE CAT SAT";
let toks = crate::tokutil::lex_sig(s);
let id = analyze_over(&toks, s, crate::orbit::OrbitGroup::Identity);
let ca = analyze_over(&toks, s, crate::orbit::OrbitGroup::Case);
let id_p = id.frames.last().map_or(0, |f| f.period);
let ca_p = ca.frames.last().map_or(0, |f| f.period);
assert!(
ca_p > 0 && ca_p < id_p,
"case orbit should find a tighter period ({ca_p}) than identity ({id_p})"
);
}
#[test]
fn orbit_identity_matches_default_silhouette_periodicity() {
let s: &[u8] = b"a,1,b,2,a,1,b,2,a,1,b,2";
let toks = crate::tokutil::lex_sig(s);
let f = analyze_over(&toks, s, crate::orbit::OrbitGroup::Identity);
assert_eq!(f.n_tokens, toks.len());
assert!(f.frames.iter().any(|fr| fr.period > 0));
}
}