trex/echo.rs
1//! The echo axis - trex's recurrence substrate, the connection on the bundle.
2//!
3//! Every other axis is a one-point function: `magnitude` reads a value's
4//! scale, `stress` its structural load, `spectral` its temporal texture,
5//! `flow` its dynamics, `shape` its form, `orbit` its symmetry class, `seam`
6//! its predictability boundary, `observation` its vantage-dependence - each a
7//! local functional of a window around one position. Echo is the two-point
8//! function: for each token, does this content occur ELSEWHERE, how often, how
9//! far away, and how regularly? It is content-addressed and unbounded-range,
10//! where `spectral` autocorrelates a bounded window; a log template recurring
11//! every 2 KB, an identifier bound 40 times across a file, and a phrase that
12//! returns 400 KB later are all echo and nothing else.
13//!
14//! Per token the axis reads four fields:
15//!
16//! - **count** - how many times this token's key occurs in the document;
17//! - **back / forward lag** - the byte distance to the previous / next
18//! occurrence (no previous = **novel**, the first appearance);
19//! - **period** - when a key recurs at least three times with regular
20//! spacing, the mean lag: the document-scale pitch of a repeating template;
21//! - **strength** - the recurrence mass, `count - 1` (0 = unique).
22//!
23//! The key is the token's text quotiented by an [`OrbitGroup`], so recurrence
24//! composes with the symmetry axis: under `Case`, `Foo` and `foo` are one
25//! echo; under `Shape`, `1,22,3` and `4,55,6` are. Recurrence also lifts to
26//! the supertoken tower ([`analyze_super`]): the same structural unit (role
27//! plus silhouette) returning across a document is structural rhyme - a
28//! repeated config block, a log template, a stanza.
29
30use std::collections::HashMap;
31use std::num::NonZeroU32;
32
33use crate::orbit::{OrbitGroup, canonical};
34use crate::token::{Token, TokenKind};
35
36/// Tuning for the echo analysis.
37#[derive(Clone, Copy, Debug)]
38pub struct EchoConfig {
39 /// The symmetry group the key is quotiented by: recurrence up to this
40 /// equivalence. `Identity` (the default) is exact-text recurrence.
41 pub orbit: OrbitGroup,
42 /// Minimum byte length for a token to be keyed; shorter tokens get an
43 /// unkeyed (zero-echo) frame.
44 pub min_len: usize,
45 /// Maximum coefficient of variation of a key's successive lags for the
46 /// recurrence to count as periodic (the mean lag is then its period).
47 pub max_period_cv: f32,
48}
49
50impl Default for EchoConfig {
51 fn default() -> Self {
52 EchoConfig { orbit: OrbitGroup::Identity, min_len: 1, max_period_cv: 0.3 }
53 }
54}
55
56/// One token's echo reading.
57#[derive(Clone, Copy, Debug, Default, PartialEq)]
58pub struct EchoFrame {
59 /// Byte offset where the token begins, at the width [`Token`] stores it.
60 pub start: u32,
61 /// Byte offset just past the token, at the same width.
62 pub end: u32,
63 /// Whether the token participates in the recurrence field (word / number /
64 /// quoted / typed-literal kinds at or above the length floor). Unkeyed
65 /// tokens (whitespace, punctuation, brackets) carry a zero frame.
66 pub keyed: bool,
67 /// Occurrences of this token's key in the document (1 = unique).
68 pub count: u32,
69 /// Byte distance back to the previous occurrence of the key, or `None`
70 /// when this is the first (a novel token).
71 ///
72 /// Non-zero because two occurrences of one key begin at different bytes,
73 /// which is the niche that keeps the `Option` free: a plain `Option<u32>`
74 /// is eight bytes where this is four, over one frame a token.
75 pub back_lag: Option<NonZeroU32>,
76 /// Byte distance forward to the next occurrence, or `None` at the last,
77 /// non-zero for the same reason as [`EchoFrame::back_lag`].
78 pub fwd_lag: Option<NonZeroU32>,
79 /// The key's recurrence period in bytes when its lags are regular, and zero
80 /// where it has none.
81 ///
82 /// Zero rather than `None`, which saves the four bytes an `Option<f32>`
83 /// spends on a discriminant over one frame a token. A period is only ever
84 /// set from a mean lag the writer requires to be above zero, so zero was
85 /// never a reading this could carry and reads as the absence it is.
86 ///
87 /// Zero rather than a NaN, which would be the other way to mark it and is
88 /// wrong twice here: `Default` gives an unkeyed token a zero frame and
89 /// `f32::default()` is zero, not NaN, so absence would have two spellings;
90 /// and NaN is unequal to itself, so the derived `PartialEq` would report two
91 /// frames with no period as different.
92 pub period: f32,
93 /// This occurrence's place among the key's, counting from one; zero for
94 /// an unkeyed token.
95 pub nth: u32,
96}
97
98impl EchoFrame {
99 /// The first appearance of a keyed token: no prior occurrence.
100 #[must_use]
101 pub fn novel(&self) -> bool {
102 self.keyed && self.back_lag.is_none()
103 }
104
105 /// A keyed token whose key occurs more than once in the document.
106 #[must_use]
107 pub fn echoed(&self) -> bool {
108 self.keyed && self.count >= 2
109 }
110
111 /// The recurrence mass: occurrences beyond this one (0 = unique).
112 #[must_use]
113 pub fn strength(&self) -> f32 {
114 self.count.saturating_sub(1) as f32
115 }
116}
117
118/// The echo field over a token stream: one frame per token, aligned with the
119/// input token slice, plus document-level summary readings.
120#[derive(Clone, Debug)]
121pub struct EchoField {
122 /// One frame per input token (index-aligned with the lexed stream).
123 pub frames: Vec<EchoFrame>,
124 /// Keyed tokens in the stream.
125 pub keyed: usize,
126 /// Distinct keys among them.
127 pub distinct: usize,
128 /// Keyed tokens that are first appearances.
129 pub novel: usize,
130 /// Keyed tokens whose key recurs.
131 pub echoed: usize,
132}
133
134impl EchoField {
135 /// The fraction of keyed tokens that are first appearances - the
136 /// document's novelty rate (1.0 = nothing ever repeats).
137 #[must_use]
138 pub fn novelty(&self) -> f32 {
139 if self.keyed == 0 { 0.0 } else { self.novel as f32 / self.keyed as f32 }
140 }
141
142 /// The fraction of keyed tokens that recur - the document's echo rate.
143 #[must_use]
144 pub fn echo_rate(&self) -> f32 {
145 if self.keyed == 0 { 0.0 } else { self.echoed as f32 / self.keyed as f32 }
146 }
147}
148
149/// Whether a token kind participates in the recurrence field. Structure
150/// (whitespace, punctuation, brackets) and unclassified spans recur by
151/// grammar, not by content, so they are not keyed.
152pub(crate) fn keyed_kind(kind: TokenKind) -> bool {
153 !matches!(
154 kind,
155 TokenKind::Whitespace
156 | TokenKind::Punct
157 | TokenKind::Open(_)
158 | TokenKind::Close(_)
159 | TokenKind::Other
160 )
161}
162
163/// The group number standing for a token that belongs to no group, so the
164/// layout skips it. A stream long enough to reach it could not be indexed by
165/// the `u32` that layout uses either.
166const UNKEYED: u32 = u32::MAX;
167
168/// Number the keyed tokens' keys in first-occurrence order, answering each
169/// token's group and how many tokens each group holds. Marks the keyed
170/// tokens on their frames on the way.
171///
172/// The key type is the caller's, so a rung whose representative is the token's
173/// own bytes hands back a borrow of the input and one whose representative is
174/// rewritten text hands back the text it made. The map holds a group number
175/// rather than that group's occurrences, so a value is four bytes here instead
176/// of a `Vec` whose buffer is a second allocation per distinct key - and text
177/// where nearly every token is unique, identifiers carrying a serial number
178/// being the usual shape of real source, reaches one distinct key per token.
179fn group_tokens<K: Eq + std::hash::Hash>(
180 tokens: &[Token],
181 cfg: &EchoConfig,
182 frames: &mut [EchoFrame],
183 mut key_of: impl FnMut(usize) -> K,
184) -> (Vec<u32>, Vec<u32>) {
185 let n = tokens.len();
186 let keyed = |t: &Token| keyed_kind(t.kind) && t.len() >= cfg.min_len;
187
188 // Which keys can possibly occur twice, read before any of them is put in a
189 // map. A key that occurs once wants a group holding only itself, and a map
190 // large enough to hold one per distinct key misses cache on every probe -
191 // which on text where nearly every token is unique is nearly every token.
192 //
193 // The counters saturate at two, and the reading is one-sided: a key
194 // occurring twice increments the same counter twice and cannot read one, so
195 // a counter reading one PROVES its key unique. Two different keys sharing a
196 // counter both read two and both go on to the map, which costs work and
197 // answers the same.
198 // The hash a token's key carries, kept so the pass below need not build a
199 // key it will not use. Under a rung that rewrites the text a key is a fresh
200 // String, and a key proved unique is never looked up, so building it twice
201 // would cost the rung the very work the skip saves.
202 let counting = crate::trace::phase("echo: keying, counting the keys");
203 let mut hashes: Vec<u32> = vec![0; n];
204 // How many tokens this pass builds a key for. The grouping pass below
205 // builds one only for the tokens its skip does not answer, so the two
206 // counts divide the key's own cost out of the difference between them.
207 let mut keyed_count = 0u64;
208 let seen = Repeats::over(n, |table| {
209 for (i, t) in tokens.iter().enumerate() {
210 if keyed(t) {
211 keyed_count += 1;
212 let h = Repeats::index_of(&key_of(i));
213 hashes[i] = h;
214 table.saw(h);
215 }
216 }
217 });
218 crate::trace::counted("echo: tokens keyed", keyed_count);
219 drop(counting);
220
221 let _grouping = crate::trace::phase("echo: keying, grouping what repeats");
222 // The crate's own hash with an avalanche over what it finishes with. Plain
223 // fxhash measured 19% worse than SipHash here, on keys differing only in a
224 // trailing serial number: hashbrown reads a bucket from one end of the hash
225 // and a control byte from the other, and fxhash distributes one end and not
226 // the other. The repeat table above already carries its index through the
227 // same five steps for the same reason.
228 let mut ids: HashMap<K, u32, crate::fxhash::FxFinalBuild> = HashMap::default();
229 let mut group_of: Vec<u32> = vec![UNKEYED; n];
230 let mut counts: Vec<u32> = Vec::new();
231 // How far down this loop a token gets, carried locally and handed over
232 // once. The loop walks every token whether or not its key can echo, so
233 // what the map costs and what the walk costs are different questions and
234 // the phase around them answers neither on its own.
235 //
236 // `inserted` divides the probes again: an entry that is written is a
237 // different cost from one that is found, and a probe count alone reads them
238 // as the same operation.
239 let (mut walked, mut probed, mut inserted) = (0u64, 0u64, 0u64);
240 for (i, t) in tokens.iter().enumerate() {
241 walked += 1;
242 if !keyed(t) {
243 continue;
244 }
245 frames[i].keyed = true;
246 if seen.once(hashes[i]) {
247 // Its own group, and nothing to look up: the map never learns of a
248 // key that cannot echo.
249 group_of[i] = u32::try_from(counts.len()).expect("a group index within the stored width");
250 counts.push(1);
251 continue;
252 }
253 probed += 1;
254 let fresh = u32::try_from(counts.len()).expect("a group index within the stored width");
255 let g = *ids.entry(key_of(i)).or_insert(fresh);
256 if g == fresh {
257 inserted += 1;
258 counts.push(0);
259 }
260 counts[g as usize] += 1;
261 group_of[i] = g;
262 }
263 crate::trace::counted("echo: tokens the grouping loop walks", walked);
264 crate::trace::counted("echo: tokens that reach the map", probed);
265 // What the probe does, and over which key. Writing an entry and finding one
266 // are different costs, and a probe count alone reads them as one operation.
267 // The key's type is the other half: this is generic over `K`, and a rung
268 // that rewrites the text hands it an owned key where an identity rung hands
269 // it a borrow, so the row is named by the type each instantiation carries.
270 crate::trace::counted("echo: map probes that write an entry", inserted);
271 crate::trace::counted("echo: map probes that find one", probed - inserted);
272 crate::trace::counted(std::any::type_name::<K>(), probed);
273 (group_of, counts)
274}
275
276/// Saturating two-bit counters over the hashes of the keys, answering whether a
277/// key was seen once or more than once.
278///
279/// Sized from the token count rather than to a figure of its own, and two bits
280/// wide because "once, or more than once" is the whole question.
281struct Repeats {
282 /// Four counters a byte.
283 slots: Vec<u8>,
284 /// One less than the counter count, which is a power of two.
285 mask: u64,
286}
287
288impl Repeats {
289 /// Count every key `fill` names, over a table sized for `tokens` of them.
290 fn over(tokens: usize, fill: impl FnOnce(&mut Self)) -> Self {
291 let counters = tokens.saturating_mul(2).next_power_of_two().max(64);
292 let mut table = Repeats {
293 slots: vec![0u8; counters / 4],
294 mask: (counters - 1) as u64,
295 };
296 fill(&mut table);
297 table
298 }
299
300 /// The number this table counts a key under, which a caller keeps rather
301 /// than rebuilding the key to ask twice.
302 ///
303 /// Carried through [`crate::fxhash::avalanche`], because the crate's hash is
304 /// built for a map that takes its bucket from the high bits and leaves the
305 /// low ones poorly mixed - and a counter is chosen by the low ones. It is
306 /// kept to four bytes: a counter is chosen by twenty-two bits on the largest
307 /// input this crate's token indices admit, so the other half of a word is
308 /// memory traffic spent and never read.
309 ///
310 /// The named function is the one place those five steps live, so this table
311 /// and the key map in `group_tokens` mix alike rather than by two copies
312 /// that can drift apart.
313 fn index_of<K: std::hash::Hash>(key: &K) -> u32 {
314 use std::hash::BuildHasher;
315 let mixed = crate::fxhash::avalanche(crate::fxhash::FxBuild::process().hash_one(key));
316 (mixed & 0xffff_ffff) as u32
317 }
318
319 /// Where an index's counter is: the byte holding it, and its shift in it.
320 fn at(&self, index: u32) -> (usize, u32) {
321 let at = (u64::from(index) & self.mask) as usize;
322 (at / 4, ((at % 4) * 2) as u32)
323 }
324
325 /// Record one appearance of a key counted under `index`, saturating at two.
326 fn saw(&mut self, index: u32) {
327 let (byte, shift) = self.at(index);
328 let held = (self.slots[byte] >> shift) & 0b11;
329 if held < 2 {
330 self.slots[byte] += 1 << shift;
331 }
332 }
333
334 /// Whether a key counted under `index` was seen exactly once, which is
335 /// proof that it occurs once.
336 fn once(&self, index: u32) -> bool {
337 let (byte, shift) = self.at(index);
338 (self.slots[byte] >> shift) & 0b11 == 1
339 }
340}
341
342/// Analyze the echo field with the default configuration.
343#[must_use]
344pub fn analyze(tokens: &[Token], bytes: &[u8]) -> EchoField {
345 analyze_with(tokens, bytes, &EchoConfig::default())
346}
347
348/// Lex `bytes` and analyze its echo field with the default configuration.
349#[must_use]
350pub fn analyze_bytes(bytes: &[u8]) -> EchoField {
351 analyze(&crate::lexer::lex(bytes), bytes)
352}
353
354/// Analyze the echo field: key each participating token by its orbit-canonical
355/// text, collect per-key occurrence lists, and read count / lags / period back
356/// onto every token's frame.
357#[must_use]
358pub fn analyze_with(tokens: &[Token], bytes: &[u8], cfg: &EchoConfig) -> EchoField {
359 // The parts this divides into, named so a share of the field is read rather
360 // than reasoned about. It is the largest of the axis fields the set engine
361 // builds, so which part carries the time decides where work on it goes.
362 let framing = crate::trace::phase("echo: a frame a token");
363 let mut frames: Vec<EchoFrame> =
364 tokens
365 .iter()
366 .map(|t| EchoFrame { start: t.start, end: t.end, ..Default::default() })
367 .collect();
368 drop(framing);
369 // The group each token's key belongs to, and how many tokens each group
370 // holds. The identity rung's representative is the token's own literal
371 // bytes, so its key is borrowed from the input: a rung that genuinely
372 // rewrites the text owns its key, and only that rung allocates one.
373 let keying = crate::trace::phase("echo: keying the tokens");
374 let (group_of, counts) = match cfg.orbit {
375 OrbitGroup::Identity => {
376 group_tokens(tokens, cfg, &mut frames, |i| &bytes[tokens[i].span()])
377 }
378 g => group_tokens(tokens, cfg, &mut frames, |i| {
379 canonical(&bytes[tokens[i].span()], g).into_bytes()
380 }),
381 };
382
383 drop(keying);
384
385 // Every group's occurrences, contiguous and in stream order: the counts
386 // prefix-summed give each group its run, and one pass over the tokens
387 // scatters each index into the run its group owns. Walking the tokens in
388 // order is what leaves each run ascending.
389 let laying = crate::trace::phase("echo: laying out the occurrences");
390 let distinct = counts.len();
391 let mut starts: Vec<u32> = Vec::with_capacity(distinct + 1);
392 let mut acc = 0u32;
393 for &c in &counts {
394 starts.push(acc);
395 acc += c;
396 }
397 starts.push(acc);
398 let mut cursor: Vec<u32> = starts[..distinct].to_vec();
399 let mut flat: Vec<u32> = vec![0; acc as usize];
400 for (i, &g) in group_of.iter().enumerate() {
401 if g == UNKEYED {
402 continue;
403 }
404 let slot = &mut cursor[g as usize];
405 flat[*slot as usize] = i as u32;
406 *slot += 1;
407 }
408
409 drop(laying);
410
411 let _reading = crate::trace::phase("echo: reading the runs back onto the frames");
412 let mut keyed = 0usize;
413 let mut novel = 0usize;
414 let mut echoed = 0usize;
415 for g in 0..distinct {
416 let list = &flat[starts[g] as usize..starts[g + 1] as usize];
417 let count = list.len() as u32;
418 // Successive byte lags between occurrences, for the period test. The
419 // run is contiguous, so a lag is read off it where it was collected.
420 let lags = list.len().saturating_sub(1);
421 let lag = |w: usize| {
422 (tokens[list[w + 1] as usize].start - tokens[list[w] as usize].start) as f32
423 };
424 let period = (lags >= 2)
425 .then(|| {
426 let mut sum = 0.0f32;
427 for w in 0..lags {
428 sum += lag(w);
429 }
430 let mean = sum / lags as f32;
431 let mut spread = 0.0f32;
432 for w in 0..lags {
433 let d = lag(w) - mean;
434 spread += d * d;
435 }
436 let var = spread / lags as f32;
437 (mean > 0.0 && var.sqrt() / mean <= cfg.max_period_cv).then_some(mean)
438 })
439 .flatten();
440 for (j, &slot) in list.iter().enumerate() {
441 let i = slot as usize;
442 let f = &mut frames[i];
443 f.count = count;
444 f.back_lag = (j > 0).then(|| {
445 let lag = tokens[i].start - tokens[list[j - 1] as usize].start;
446 NonZeroU32::new(lag).expect("two occurrences of a key begin at different bytes")
447 });
448 f.fwd_lag = (j + 1 < list.len()).then(|| {
449 let lag = tokens[list[j + 1] as usize].start - tokens[i].start;
450 NonZeroU32::new(lag).expect("two occurrences of a key begin at different bytes")
451 });
452 f.period = period.unwrap_or(0.0);
453 f.nth = j as u32 + 1;
454 keyed += 1;
455 if j == 0 {
456 novel += 1;
457 }
458 if count >= 2 {
459 echoed += 1;
460 }
461 }
462 }
463 EchoField { frames, keyed, distinct, novel, echoed }
464}
465
466/// One recurring structural unit in the supertoken tower: echo lifted to the
467/// layer above tokens. The key is the unit's role plus the shape-class
468/// silhouette of its span, so two units that differ only in their identifiers
469/// and values are the same structure - structural rhyme.
470#[derive(Clone, Debug)]
471pub struct SuperEcho {
472 /// The unit's role label plus silhouette (the structural key).
473 pub key: String,
474 /// How many units in the document share it.
475 pub count: u32,
476 /// The recurrence period in bytes, when the spacing is regular.
477 pub period: Option<f32>,
478 /// Byte offset of the first occurrence.
479 pub first: usize,
480}
481
482/// The letter one silhouette code spells in a structural-rhyme key.
483///
484/// The kind half of [`crate::shape::shape_class`] read back: its high sixteen
485/// bits are a [`TokenKind`] code, and for punctuation its low sixteen bits are
486/// the glyph's code point. A word is `W`, a number `N`, a quoted run `Q`,
487/// punctuation and a bracket the glyph itself, and every other kind `T`; a
488/// glyph whose low sixteen bits name no character is spelled U+FFFD, the
489/// replacement character.
490///
491/// The key is read by a person - `kv:W:W` is a key beside a value, twice, the
492/// middle colon being the punctuation's own glyph - so it spells the kinds
493/// rather than printing the codes it compares on.
494fn silhouette_letter(code: u32) -> char {
495 let kind = code >> 16;
496 if kind == TokenKind::Word.code() {
497 return 'W';
498 }
499 if kind == TokenKind::Number.code() {
500 return 'N';
501 }
502 if kind == TokenKind::Quoted.code() {
503 return 'Q';
504 }
505 if kind == TokenKind::Punct.code() {
506 return match char::from_u32(code & 0xFFFF) {
507 Some(glyph) => glyph,
508 None => char::REPLACEMENT_CHARACTER,
509 };
510 }
511 // A bracket carries no glyph in its code, so it is spelled from the pair
512 // the code names rather than read out of the low bits.
513 match TokenKind::bracket_of_code(kind) {
514 Some((true, crate::token::BracketKind::Paren)) => '(',
515 Some((true, crate::token::BracketKind::Square)) => '[',
516 Some((true, crate::token::BracketKind::Brace)) => '{',
517 Some((false, crate::token::BracketKind::Paren)) => ')',
518 Some((false, crate::token::BracketKind::Square)) => ']',
519 Some((false, crate::token::BracketKind::Brace)) => '}',
520 None => 'T',
521 }
522}
523
524/// Recurring supertoken structures, most frequent first. Only structures that
525/// actually recur are reported (a unique unit is not rhyme).
526///
527/// The structural key is the unit's role plus its token-kind silhouette (words
528/// as `W`, numbers as `N`, quoted as `Q`, typed literals as `T`, punctuation
529/// and brackets as themselves) - coarse enough that `alpha: one` and
530/// `bravo: two` are the same structure, which byte-level shape classes are
531/// not.
532#[must_use]
533pub fn analyze_super(bytes: &[u8]) -> Vec<SuperEcho> {
534 analyze_super_with(bytes, &EchoConfig::default())
535}
536
537/// [`analyze_super`] with a period read under `cfg.max_period_cv`.
538#[must_use]
539pub fn analyze_super_with(bytes: &[u8], cfg: &EchoConfig) -> Vec<SuperEcho> {
540 analyze_super_tokens(&crate::lexer::lex(bytes), bytes, cfg)
541}
542
543/// [`analyze_super_with`] over every token of `bytes` already lexed, as a
544/// lex under declarations reads them.
545#[must_use]
546pub fn analyze_super_tokens(toks: &[Token], bytes: &[u8], cfg: &EchoConfig) -> Vec<SuperEcho> {
547 let units = crate::supertoken::supertokens_from(toks, bytes);
548 // The unit's silhouette comes from the shape axis, which is what computes
549 // silhouettes. Folding it through the profile monoid also means the key
550 // tracks that axis rather than a second, private idea of token shape.
551 let ctx = crate::profile::AxisCtx::new(bytes);
552 let mut occ: HashMap<String, Vec<usize>> = HashMap::new();
553 // The units are in stream order and so are the tokens, so one cursor walks
554 // both instead of rescanning the token stream per unit.
555 let mut cursor = 0usize;
556 for u in &units {
557 while cursor < toks.len() && toks[cursor].start() < u.start {
558 cursor += 1;
559 }
560 let lo = cursor;
561 let mut hi = cursor;
562 while hi < toks.len() && toks[hi].end() <= u.end {
563 hi += 1;
564 }
565 let shape: crate::profile::ShapeProfile = crate::profile::fold_tokens(
566 lo,
567 toks[lo..hi].iter().filter(|t| t.is_significant()).copied().collect::<Vec<_>>().as_slice(),
568 &ctx,
569 );
570 let mut key = String::from(u.role.label());
571 key.push(':');
572 for &code in &shape.silhouette {
573 key.push(silhouette_letter(code));
574 }
575 occ.entry(key).or_default().push(u.start);
576 }
577 let mut out: Vec<SuperEcho> = occ
578 .into_iter()
579 .filter(|(_, starts)| starts.len() >= 2)
580 .map(|(key, starts)| {
581 let lags: Vec<f32> = starts.windows(2).map(|w| (w[1] - w[0]) as f32).collect();
582 let period = (lags.len() >= 2)
583 .then(|| {
584 let mean = lags.iter().sum::<f32>() / lags.len() as f32;
585 let var = lags.iter().map(|l| (l - mean) * (l - mean)).sum::<f32>()
586 / lags.len() as f32;
587 (mean > 0.0 && var.sqrt() / mean <= cfg.max_period_cv).then_some(mean)
588 })
589 .flatten();
590 SuperEcho { key, count: starts.len() as u32, period, first: starts[0] }
591 })
592 .collect();
593 out.sort_by(|a, b| b.count.cmp(&a.count).then(a.first.cmp(&b.first)));
594 out
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600
601 /// One frame a token of the input, so this width multiplies by the token
602 /// count: 2,650,000 of them on the comparison corpus. Pinned the way the
603 /// lexer pins a token's and the engine pins a match's, because a field
604 /// added here is written that many times and the phase that writes the table
605 /// runs at memory bandwidth.
606 #[test]
607 fn a_frame_is_the_width_the_table_is_counted_at() {
608 assert_eq!(size_of::<EchoFrame>(), 32, "a frame is {} bytes", size_of::<EchoFrame>());
609 }
610
611 #[test]
612 fn novel_then_echoed() {
613 // First "whale" is novel; the second echoes it with the right lag.
614 let bytes = b"the whale swam and the whale sang";
615 let field = analyze_bytes(bytes);
616 let toks = crate::lexer::lex(bytes);
617 let whales: Vec<usize> = (0..toks.len())
618 .filter(|&i| &bytes[toks[i].span()] == b"whale")
619 .collect();
620 assert_eq!(whales.len(), 2);
621 let (a, b) = (field.frames[whales[0]], field.frames[whales[1]]);
622 assert!(a.novel() && a.echoed(), "first whale is novel and echoed: {a:?}");
623 assert!(!b.novel() && b.echoed(), "second whale echoes: {b:?}");
624 assert_eq!(a.count, 2);
625 assert_eq!(b.back_lag, NonZeroU32::new(toks[whales[1]].start - toks[whales[0]].start));
626 assert_eq!(a.fwd_lag, b.back_lag);
627 }
628
629 #[test]
630 fn unique_token_is_novel_never_echoed() {
631 let field = analyze_bytes(b"one two three");
632 for f in field.frames.iter().filter(|f| f.keyed) {
633 assert!(f.novel() && !f.echoed(), "{f:?}");
634 assert_eq!(f.strength(), 0.0);
635 }
636 assert_eq!(field.novelty(), 1.0);
637 assert_eq!(field.echo_rate(), 0.0);
638 }
639
640 #[test]
641 fn orbit_quotient_folds_case() {
642 // Exact keying sees two distinct keys; the case quotient sees one echo.
643 let bytes = b"Whale and whale";
644 let exact = analyze_bytes(bytes);
645 assert_eq!(exact.echoed, 0);
646 let folded = analyze_with(
647 &crate::lexer::lex(bytes),
648 bytes,
649 &EchoConfig { orbit: OrbitGroup::Case, ..Default::default() },
650 );
651 assert_eq!(folded.echoed, 2, "Whale/whale are one key under Case");
652 }
653
654 #[test]
655 fn regular_recurrence_has_a_period() {
656 // "tick" every 20 bytes: a periodic echo; the filler words are not.
657 let line = "tick aa bb cc dd ee ".repeat(6);
658 let field = analyze_bytes(line.as_bytes());
659 let toks = crate::lexer::lex(line.as_bytes());
660 let tick = (0..toks.len())
661 .find(|&i| &line.as_bytes()[toks[i].span()] == b"tick")
662 .expect("tick present");
663 let p = field.frames[tick].period;
664 assert!(p > 0.0, "tick recurs regularly, so it carries a period");
665 assert!((p - 20.0).abs() < 1.0, "period ~20 bytes, got {p}");
666 }
667
668 #[test]
669 fn punctuation_is_not_keyed() {
670 let field = analyze_bytes(b"a , b , c , d");
671 let toks = crate::lexer::lex(b"a , b , c , d");
672 for (i, t) in toks.iter().enumerate() {
673 if t.kind == TokenKind::Punct {
674 assert!(!field.frames[i].keyed);
675 assert!(!field.frames[i].echoed());
676 }
677 }
678 }
679
680 #[test]
681 fn super_echo_finds_structural_rhyme() {
682 // Three key: value lines with different words: one recurring structure.
683 // The key is asserted whole rather than by its prefix, because the
684 // silhouette is the half that carries the structure and a prefix test
685 // passes whatever the silhouette is spelled as.
686 let bytes = b"alpha: one\nbravo: two\ndelta: six\n";
687 let rhymes = analyze_super(bytes);
688 assert!(
689 rhymes.iter().any(|r| r.count == 3 && r.key == "kv:W:W"),
690 "three kv units rhyme structurally as kv:W:W: {rhymes:?}"
691 );
692 }
693
694 /// Every branch of the spelling, since the key is what a reader reads.
695 #[test]
696 fn a_silhouette_spells_its_kinds() {
697 let letter = |kind: TokenKind, text: &[u8]| {
698 silhouette_letter(crate::shape::shape_class(kind, text))
699 };
700 assert_eq!(letter(TokenKind::Word, b"alpha"), 'W');
701 assert_eq!(letter(TokenKind::Number, b"42"), 'N');
702 assert_eq!(letter(TokenKind::Quoted, b"\"bob\""), 'Q');
703 // Punctuation keeps its own glyph, which is what puts the colon in
704 // the middle of `kv:W:W` rather than a separator doing it.
705 assert_eq!(letter(TokenKind::Punct, b":"), ':');
706 assert_eq!(letter(TokenKind::Punct, b","), ',');
707 // Outside ASCII too: the glyph is the character, not its first byte.
708 assert_eq!(letter(TokenKind::Punct, "、".as_bytes()), '、');
709 assert_eq!(letter(TokenKind::Punct, "│".as_bytes()), '│');
710 // A bracket carries no glyph in its code and is spelled from the pair.
711 assert_eq!(letter(TokenKind::Open(crate::token::BracketKind::Brace), b"{"), '{');
712 assert_eq!(letter(TokenKind::Close(crate::token::BracketKind::Square), b"]"), ']');
713 // Everything else is one letter, so two typed kinds share it.
714 assert_eq!(letter(TokenKind::Ip, b"10.0.0.1"), 'T');
715 assert_eq!(letter(TokenKind::Email, b"bob@x.com"), 'T');
716 }
717
718 #[test]
719 fn empty_is_safe() {
720 let field = analyze_bytes(b"");
721 assert!(field.frames.is_empty());
722 assert_eq!(field.novelty(), 0.0);
723 assert!(analyze_super(b"").is_empty());
724 }
725
726 /// A key seen more than once never reads as seen once.
727 ///
728 /// The whole of the unique skip rests on this one direction: a key that
729 /// reads as seen once is given its own group and never reaches the map, so
730 /// a key that echoed and read as unique would be reported novel. The other
731 /// direction is allowed to be wrong - two keys sharing a counter both read
732 /// twice and both go to the map, which answers the same and only costs the
733 /// lookup.
734 #[test]
735 fn a_key_seen_twice_never_reads_as_seen_once() {
736 // Serial-numbered identifiers, the shape that fills this table in real
737 // source and the shape whose hashes are closest together.
738 let keys: Vec<String> = (0..20_000).map(|i| format!("value_{i}")).collect();
739 let table = Repeats::over(keys.len(), |t| {
740 for k in &keys {
741 t.saw(Repeats::index_of(k));
742 t.saw(Repeats::index_of(k));
743 }
744 });
745 for k in &keys {
746 assert!(
747 !table.once(Repeats::index_of(k)),
748 "{k} was seen twice and must not read as once"
749 );
750 }
751 }
752
753 /// Counted once reads as once, counted twice does not, and counted not at
754 /// all does not either.
755 ///
756 /// Read over hashes chosen here rather than over keys, so it says what the
757 /// counters do and nothing about how well any hash spreads. How much of a
758 /// real text reaches the skip is a property of that text and is measured
759 /// rather than asserted: on the engine-surface corpus it took the keying of
760 /// the echo field from 89.2 ms to 63.8.
761 #[test]
762 fn a_counter_tells_once_from_more_than_once() {
763 let table = Repeats::over(64, |t| {
764 t.saw(11);
765 t.saw(11);
766 t.saw(22);
767 });
768 assert!(!table.once(11), "counted twice");
769 assert!(table.once(22), "counted once");
770 assert!(!table.once(33), "never counted");
771 }
772
773 /// A counter saturates rather than carrying into the counter beside it.
774 ///
775 /// Nine appearances must read the same as two, and a neighbor must be
776 /// untouched - a carry out of one counter would put a key that echoed into
777 /// a slot reading one, which is the reading that skips the map.
778 #[test]
779 fn a_counter_saturates_and_leaves_its_neighbors_alone() {
780 let table = Repeats::over(64, |t| {
781 for _ in 0..9 {
782 t.saw(7);
783 }
784 t.saw(8);
785 });
786 assert!(!table.once(7), "nine appearances read as more than once");
787 assert!(table.once(8), "its neighbor is untouched");
788 }
789}