Skip to main content

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}