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,
}
}
}
#[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 | u32::from(tok_bytes.first().copied().unwrap_or(0)),
_ => base,
}
}
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)> {
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() {
let (s, e) = self.spans[i];
if f.period_strength >= self.template_strength && f.period > 0 {
match run.as_mut() {
Some(r) => r.1 = e,
None => run = Some((s, e, f.period)),
}
} else if let Some(r) = run.take() {
out.push(r);
}
}
if let Some(r) = run.take() {
out.push(r);
}
out
}
#[must_use]
pub fn template_spans(&self) -> Vec<(usize, usize, u16)> {
let mut out: Vec<(usize, usize, u16)> = Vec::new();
let mut run: Option<(usize, usize, u16)> = None;
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 close = |(first, last, period): (usize, usize, u16)| {
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;
}
out.push((self.spans[first].0, self.spans[last].1, period));
};
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() {
close(r);
}
}
if let Some(r) = run.take() {
close(r);
}
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)> {
let templates = analyze_bytes(input).template_spans();
crate::spectral::code_regions(input)
.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 {
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()
}
#[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"
);
}
}
#[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));
}
}