Skip to main content

trex/
spectral.rs

1//! The spectral axis: trex's temporal substrate.
2//!
3//! The byte grain (`crate::lexer`) and the token grain (`crate::engine`)
4//! are both *segmentation* scales - they answer "where are the
5//! boundaries?" and differ only in how finely the stream is cut. This
6//! module adds a third, perpendicular scale that answers a different
7//! question: "what is the temporal character of the stream *at this
8//! position*, independent of any cut?"
9//!
10//! A spectral frame is one vector per position, read four ways at once:
11//!
12//! - **texture / time** - a bank of leaky integrators
13//!   `x[t] = a*x[t-1] + (1-a)*u[t]` over byte-class indicators. Each
14//!   decay `a` is one pole of a low-pass filter (a frequency band) and,
15//!   equivalently, a clock with time-constant `tau = -1/ln a` (recency).
16//!   Texture and time are the same numbers read two ways.
17//! - **information density** - a rolling Shannon entropy band that
18//!   separates compressed / packed / random regions from plain text.
19//! - **periodicity** - a bounded autocorrelation that finds the dominant
20//!   byte-period of the neighbourhood (CSV row width, fixed-width
21//!   records, base64 phase) with no delimiter knowledge.
22//! - **novelty** - a rolling k-gram surprise band that flags repeated
23//!   template / boilerplate against genuinely new content.
24//!
25//! On top of the per-position frame sits an online **change-point**
26//! detector: it records the byte offsets where the temporal signature
27//! jumps. Those boundaries are "BPE with time" - a dictionary-free,
28//! one-pass, scale-free segmentation that cuts where the stream's own
29//! dynamics change rather than where a learned merge table says.
30//!
31//! Everything is a single causal forward pass, so the reader composes
32//! with streaming and carries an honest arrow of time (the state at
33//! position `t` is exactly the decayed past available at `t`).
34//!
35//! Documented in `wiki/content/docs/reference/axes/spectral.md`.
36
37use std::collections::{HashMap, VecDeque};
38
39/// Number of byte classes in the filterbank one-hot input.
40pub const N_CLASSES: usize = 5;
41/// Number of decay clocks in the filterbank.
42pub const N_DECAYS: usize = 5;
43/// Filterbank dimension: one EMA per (decay, class).
44pub const N_BANDS: usize = N_CLASSES * N_DECAYS;
45
46/// Time-constants of the filterbank clocks, in bytes, pow2-spaced from
47/// byte-local to block scale. `a_d = exp(-1/tau_d)`.
48pub const TAUS: [f32; N_DECAYS] = [2.0, 8.0, 32.0, 128.0, 512.0];
49
50/// The byte class an input byte contributes to (the filterbank's `u[t]`).
51///
52/// Exactly one class per byte, so the one-hot input keeps each band a
53/// bounded leaky average in `[0, 1]`.
54#[must_use]
55pub fn classify(b: u8) -> usize {
56    if b.is_ascii_digit() {
57        0
58    } else if b == b'_' || b.is_ascii_alphabetic() {
59        1
60    } else if b.is_ascii_whitespace() {
61        2
62    } else if b.is_ascii_graphic() {
63        3
64    } else {
65        4
66    }
67}
68
69/// A coarse texture class read from a frame's band mixture and entropy.
70#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
71pub enum Texture {
72    /// Alphabetic-dominated, low punctuation - natural-language prose.
73    Prose,
74    /// High punctuation / symbol energy alongside identifiers - code.
75    Code,
76    /// Mixed single-character symbols and digits - mathematics.
77    Math,
78    /// High entropy or high non-ASCII energy - compressed / packed / binary.
79    Data,
80    /// No class dominates.
81    Mixed,
82}
83
84impl Texture {
85    /// A short, stable label.
86    #[must_use]
87    pub fn label(self) -> &'static str {
88        match self {
89            Texture::Prose => "prose",
90            Texture::Code => "code",
91            Texture::Math => "math",
92            Texture::Data => "data",
93            Texture::Mixed => "mixed",
94        }
95    }
96}
97
98/// One position's spectral reading.
99#[derive(Clone, Copy, Debug, Default, PartialEq)]
100pub struct SpectralFrame {
101    /// Filterbank EMA: `bands[d * N_CLASSES + c]` is decay `d` x class `c`,
102    /// each in `[0, 1]` (fraction of recent bytes in class `c` at
103    /// timescale `tau_d`).
104    pub bands: [f32; N_BANDS],
105    /// Rolling Shannon entropy of the local window, normalized to `[0, 1]`.
106    pub entropy: f32,
107    /// Dominant byte-period of the neighbourhood (`0` = none detected).
108    pub period: u16,
109    /// Normalized autocorrelation peak height `[0, 1]`.
110    pub period_strength: f32,
111    /// k-gram surprise `[0, 1]` (`1` = first sighting in window).
112    pub novelty: f32,
113}
114
115impl SpectralFrame {
116    /// The class mixture at the medium clock (decay index 2), the band
117    /// most representative of word / line-scale texture.
118    #[must_use]
119    pub fn texture_mix(&self) -> [f32; N_CLASSES] {
120        let base = 2 * N_CLASSES;
121        let mut out = [0.0f32; N_CLASSES];
122        out.copy_from_slice(&self.bands[base..base + N_CLASSES]);
123        out
124    }
125}
126
127/// Knobs for the reader. `Default` is tuned for general text + binary.
128#[derive(Clone, Copy, Debug)]
129pub struct SpectralConfig {
130    /// Frame sampling stride: `frames[j]` summarises bytes up to
131    /// `(j + 1) * hop - 1`.
132    pub hop: usize,
133    /// Rolling entropy window, in bytes.
134    pub entropy_window: usize,
135    /// Autocorrelation window, in bytes.
136    pub period_window: usize,
137    /// Largest lag (period) the autocorrelation searches.
138    pub max_lag: usize,
139    /// Baseline stride between autocorrelation evaluations (raised
140    /// adaptively for large inputs to bound total periodicity work).
141    pub period_hop: usize,
142    /// k-gram size for the novelty band.
143    pub ngram: usize,
144    /// Sliding window for novelty, in k-grams.
145    pub novelty_window: usize,
146    /// Change-point sensitivity: a boundary needs `dist > mean + k*mad`.
147    pub cp_threshold: f32,
148    /// How close to the mean the change-point threshold may sit, as a fraction
149    /// of the mean. The spread is the reader's own estimate of how noisy the
150    /// divergence is, and a divergence that is constant rather than noisy
151    /// drives it to zero, which puts the threshold on the signal. A floor is a
152    /// share of the mean, so it holds at any scale and carries no constant
153    /// tied to the data. At zero the threshold is `mean + k*mad` whatever the
154    /// spread.
155    pub cp_floor: f32,
156    /// Minimum bytes between two recorded change-points.
157    pub cp_min_gap: usize,
158}
159
160impl Default for SpectralConfig {
161    fn default() -> Self {
162        Self {
163            hop: 16,
164            entropy_window: 64,
165            period_window: 512,
166            max_lag: 128,
167            period_hop: 64,
168            ngram: 4,
169            novelty_window: 4096,
170            cp_threshold: 4.0,
171            cp_floor: 0.05,
172            cp_min_gap: 4,
173        }
174    }
175}
176
177/// The spectral side table: a byte-offset-keyed reading of the whole
178/// input. Orthogonal to the token stream - bytes, subtokens, and tokens
179/// all query it the same way via [`SpectralField::signature`].
180#[derive(Clone, Debug, Default)]
181pub struct SpectralField {
182    /// Input byte length.
183    pub len: usize,
184    /// Frame sampling stride (see [`SpectralConfig::hop`]).
185    pub hop: usize,
186    /// Downsampled frames; `frames[j]` summarises bytes up to
187    /// `min((j + 1) * hop - 1, len - 1)`.
188    pub frames: Vec<SpectralFrame>,
189    /// Change-point byte offsets, ascending (the spectral subtoken cuts).
190    pub boundaries: Vec<usize>,
191    /// Which readings this field carries. One it does not carry holds the zero
192    /// its frame started at, which is indistinguishable from a genuine zero,
193    /// so a reader asserts against this rather than trusting that the field it
194    /// was handed is a whole one.
195    pub needs: Needs,
196}
197
198impl Default for Needs {
199    /// Every reading, so a field built from a default carries all of them.
200    fn default() -> Needs {
201        Needs::all()
202    }
203}
204
205impl SpectralField {
206    /// Panic in a debug build where this field lacks a reading the caller is
207    /// about to take.
208    ///
209    /// A field answers a reading it does not carry with zero, and zero is a
210    /// value every reading can genuinely have, so nothing downstream can tell
211    /// the two apart. This is what makes that a test failure rather than a
212    /// wrong answer.
213    pub fn assert_carries(&self, want: Needs, who: &str) {
214        debug_assert!(
215            !(want.entropy && !self.needs.entropy)
216                && !(want.period && !self.needs.period)
217                && !(want.bands && !self.needs.bands)
218                && !(want.onset && !self.needs.onset)
219                && !(want.novelty && !self.needs.novelty),
220            "{who} reads {want:?} from a field built for {:?}",
221            self.needs
222        );
223    }
224
225    /// The frame index covering byte offset `byte`.
226    fn idx_of(&self, byte: usize) -> usize {
227        if self.frames.is_empty() {
228            return 0;
229        }
230        (byte / self.hop.max(1)).min(self.frames.len() - 1)
231    }
232
233    /// The frame whose window covers `byte` (nearest sampled frame).
234    #[must_use]
235    pub fn frame_at(&self, byte: usize) -> SpectralFrame {
236        if self.frames.is_empty() {
237            return SpectralFrame::default();
238        }
239        self.frames[self.idx_of(byte)]
240    }
241
242    /// The pooled spectral signature of the span `[start, end)`: the mean
243    /// of the covered frames' bands / entropy / novelty, with the period
244    /// taken from the strongest-periodicity frame in the span.
245    #[must_use]
246    pub fn signature(&self, start: usize, end: usize) -> SpectralFrame {
247        if self.frames.is_empty() || end <= start {
248            return SpectralFrame::default();
249        }
250        let lo = self.idx_of(start);
251        let hi = self.idx_of(end.saturating_sub(1));
252        let mut acc = SpectralFrame::default();
253        let mut n = 0.0f32;
254        let mut best_strength = -1.0f32;
255        for f in &self.frames[lo..=hi] {
256            for (a, b) in acc.bands.iter_mut().zip(f.bands.iter()) {
257                *a += *b;
258            }
259            acc.entropy += f.entropy;
260            acc.novelty += f.novelty;
261            if f.period_strength > best_strength {
262                best_strength = f.period_strength;
263                acc.period = f.period;
264                acc.period_strength = f.period_strength;
265            }
266            n += 1.0;
267        }
268        if n > 0.0 {
269            for a in &mut acc.bands {
270                *a /= n;
271            }
272            acc.entropy /= n;
273            acc.novelty /= n;
274        }
275        acc
276    }
277
278    /// Whether a change-point lies within `tol` bytes of `byte`: the first
279    /// change-point at or past `byte - tol`, found by binary search over the
280    /// ascending offsets, is the only one that can.
281    #[must_use]
282    pub fn boundary_near(&self, byte: usize, tol: usize) -> bool {
283        let i = self.boundaries.partition_point(|&b| b.saturating_add(tol) < byte);
284        self.boundaries.get(i).is_some_and(|&b| b <= byte.saturating_add(tol))
285    }
286
287    /// Whether any change-point lies inside the half-open span `[start, end)`:
288    /// the first change-point at or past `start`, found by binary search over
289    /// the ascending offsets, is the only one that can.
290    #[must_use]
291    pub fn boundary_in(&self, start: usize, end: usize) -> bool {
292        let i = self.boundaries.partition_point(|&b| b < start);
293        self.boundaries.get(i).is_some_and(|&b| b < end)
294    }
295}
296
297/// The texture class of a frame, from its band mixture and entropy.
298#[must_use]
299pub fn texture_of(f: &SpectralFrame) -> Texture {
300    let m = f.texture_mix();
301    let (digit, alpha, _space, punct, high) = (m[0], m[1], m[2], m[3], m[4]);
302    if f.entropy > 0.85 || high > 0.30 {
303        return Texture::Data;
304    }
305    let total = digit + alpha + punct + high + m[2] + 1e-6;
306    let punct_frac = punct / total;
307    let alpha_frac = alpha / total;
308    let digit_frac = digit / total;
309    if alpha_frac > 0.55 && punct_frac < 0.12 {
310        Texture::Prose
311    } else if punct_frac > 0.20 && alpha_frac > 0.12 {
312        Texture::Code
313    } else if punct_frac > 0.12 && digit_frac > 0.10 {
314        Texture::Math
315    } else {
316        Texture::Mixed
317    }
318}
319
320/// Code-tuned texture - a fork of [`texture_of`] for the construct/POS layer.
321/// The generic classifier is built for the {prose, code, math, data} split and
322/// reads alpha-heavy base64 as `Prose`; the code layer needs {code, blob,
323/// prose, numeric}, where a packed / base64 / hex run is `Blob` (a lower 0.78
324/// entropy gate, no high-byte requirement) and a digit-dense span is `Numeric`.
325/// This is the byte-region signal the grammar-free construct tagger folds into
326/// its context-key so the same surface shape resolves differently inside a
327/// comment / string / blob than it does in live code.
328#[derive(Clone, Copy, PartialEq, Eq, Debug)]
329pub enum CodeTexture {
330    /// Punctuation-bearing live code.
331    Code,
332    /// Packed / base64 / hex / binary run (an opaque literal body).
333    Blob,
334    /// Alpha-dominant, low-punctuation prose - a comment or a natural-language
335    /// string literal.
336    Prose,
337    /// Digit-dominant - a numeric literal run.
338    Numeric,
339    /// None of the above decisively.
340    Mixed,
341}
342
343impl CodeTexture {
344    /// A one-char context-key suffix for the decisively non-code textures, or
345    /// `None` for `Code` / `Mixed` so the bulk of code keys stay byte-identical
346    /// and the corpus does not re-key (only comment / string / blob / numeric
347    /// regions earn a distinct key). `b` blob, `p` prose, `n` numeric.
348    #[must_use]
349    pub fn key_suffix(self) -> Option<char> {
350        match self {
351            CodeTexture::Code | CodeTexture::Mixed => None,
352            CodeTexture::Blob => Some('b'),
353            CodeTexture::Prose => Some('p'),
354            CodeTexture::Numeric => Some('n'),
355        }
356    }
357}
358
359/// Classify a frame into a code-relevant texture (see [`CodeTexture`]).
360#[must_use]
361pub fn code_texture(f: &SpectralFrame) -> CodeTexture {
362    let m = f.texture_mix();
363    let (digit, alpha, _space, punct, high) = (m[0], m[1], m[2], m[3], m[4]);
364    // Blob first: a packed / base64 / hex / binary run. The generic 0.85 gate
365    // misses base64 (~0.83, alpha-heavy); the code gate is 0.78 plus the
366    // high-byte path for true binary.
367    if f.entropy > 0.78 || high > 0.30 {
368        return CodeTexture::Blob;
369    }
370    let total = digit + alpha + punct + high + m[2] + 1e-6;
371    let punct_frac = punct / total;
372    let alpha_frac = alpha / total;
373    let digit_frac = digit / total;
374    if digit_frac > 0.45 && alpha_frac < 0.30 {
375        CodeTexture::Numeric
376    } else if alpha_frac > 0.60 && punct_frac < 0.08 {
377        CodeTexture::Prose
378    } else if punct_frac > 0.15 {
379        CodeTexture::Code
380    } else {
381        CodeTexture::Mixed
382    }
383}
384
385/// The `c * log2(c)` table, resolved once: the per-bin term of a windowed
386/// entropy sum for every count up to the table's length, each entry computed
387/// by the same formula [`log_term`] falls back to past it. Counts are bounded
388/// by the entropy window (64 by default; the period windows are hundreds),
389/// so the table covers the whole working domain. A caller inside a per-byte
390/// loop takes this before the loop instead of paying [`log_term`]'s lock
391/// check on every lookup.
392pub(crate) fn log_table() -> &'static [f64] {
393    const N: usize = 4096;
394    static LUT: std::sync::OnceLock<Vec<f64>> = std::sync::OnceLock::new();
395    LUT.get_or_init(|| {
396        (0..N as u32).map(|c| if c > 1 { f64::from(c) * f64::from(c).log2() } else { 0.0 }).collect()
397    })
398}
399
400/// The scale of the fixed-point `c * log2(c)` table: one unit is `2^-32`.
401///
402/// A sliding entropy sum is four of these terms in and out per position, and
403/// as floating-point adds they are one dependency chain - each add waits on
404/// the last, so the position's cost is four add latencies whatever else the
405/// core could overlap. As integers the two in-terms and the two out-terms
406/// fold to one delta off the chain, the chain is a single integer add, and
407/// the sum is exact at every position rather than drifting with the order the
408/// terms happened to arrive in. The scale keeps sixteen bits of headroom
409/// above the largest window sum the readers use (`4096 * 12 * 2^32 < 2^48`).
410pub(crate) const FIXED_LOG_SCALE: f64 = 4_294_967_296.0;
411
412/// `c * log2(c)` in units of [`FIXED_LOG_SCALE`], one table for every sliding
413/// entropy reader.
414pub(crate) fn fixed_log_table() -> &'static [i64] {
415    const N: usize = 4096;
416    static LUT: std::sync::OnceLock<Vec<i64>> = std::sync::OnceLock::new();
417    LUT.get_or_init(|| (0..N as u32).map(fixed_term_computed).collect())
418}
419
420/// `term(c + 1) - term(c)` in units of [`FIXED_LOG_SCALE`]: what a count
421/// going from `c` to `c + 1` adds to a window's sum, one lookup where the
422/// difference of two terms is two.
423pub(crate) fn fixed_log_delta_table() -> &'static [i64] {
424    const N: usize = 4096;
425    static LUT: std::sync::OnceLock<Vec<i64>> = std::sync::OnceLock::new();
426    LUT.get_or_init(|| (0..N as u32).map(|c| fixed_term_computed(c + 1) - fixed_term_computed(c)).collect())
427}
428
429/// One fixed-point term from an already-resolved table, computed past its
430/// end.
431///
432/// The table holds a few thousand entries, so it serves a count that a window
433/// bounds by sliding, and not one that only ever rises. `observation`'s
434/// vantages add and remove over a window of 32 and never leave it; `seam`'s
435/// follower counts accumulate over the whole input, and over 7.34 MB of
436/// source they reach about 48000 - 95.1% of the calls fell past the end and
437/// computed a `log2`, nineteen million of them, until the sum that asked for
438/// them moved to where it is read. A caller whose counts are unbounded is
439/// asking this for a logarithm, whatever the table suggests.
440#[inline]
441pub(crate) fn fixed_term(lut: &[i64], c: u32) -> i64 {
442    if let Some(&v) = lut.get(c as usize) {
443        return v;
444    }
445    fixed_term_computed(c)
446}
447
448fn fixed_term_computed(c: u32) -> i64 {
449    if c > 1 {
450        let cf = f64::from(c);
451        (cf * cf.log2() * FIXED_LOG_SCALE).round() as i64
452    } else {
453        0
454    }
455}
456
457#[inline]
458pub fn log_term(c: u32) -> f64 {
459    let lut = log_table();
460    if let Some(&v) = lut.get(c as usize) {
461        return v;
462    }
463    if c > 1 {
464        let cf = f64::from(c);
465        cf * cf.log2()
466    } else {
467        0.0
468    }
469}
470
471/// A sliding centered autocorrelation over a fixed-width window.
472///
473/// The frame loop advances the window by a fixed hop, so consecutive
474/// evaluations share all but the hop: at the production settings a 512-byte
475/// window moving 64 bytes repeats seven eighths of its work. This keeps the
476/// raw cross-sums between calls and slides them, paying the hop rather than
477/// the window.
478///
479/// The cross-sums are exact. A byte times a byte summed over a bounded window
480/// is at most `255 * 255 * 512`, under `2^25`, so an `i32` holds it with
481/// room to spare and adding the entering terms and subtracting the leaving
482/// ones is exact arithmetic: no drift accumulates however long the stream
483/// runs. A floating accumulator slid the same way would drift, which is what
484/// makes the integer form the one worth having rather than merely the fast
485/// one. The narrow lane is also what makes the slide vector work: sixteen
486/// lags per 512-bit register.
487///
488/// The slide runs with the lag as the inner index. For one byte leaving or
489/// entering, its products with every lag's partner are a contiguous run of
490/// the input against a contiguous run of the sums, which is a vector
491/// multiply-add; the other nesting made each lag a serial chain of scalar
492/// products.
493///
494/// Centering does not block the slide once the sum is expanded. Writing
495/// `sum (x[t+L] - m)(x[t] - m)` as `cross - m*(A + B) + (n - L)*m^2` leaves
496/// only `cross` position-dependent; `A`, `B` and the mean come from prefix
497/// sums in constant time, and `m` may move freely between windows.
498struct PeriodScanner {
499    win: usize,
500    max_lag: usize,
501    /// `cross[L - 2]` is `sum x[t] * x[t + L]` over the current window.
502    cross: Vec<i32>,
503    /// `prefix[i]` is the sum of the first `i` bytes; `prefix_sq` their squares.
504    prefix: Vec<i64>,
505    prefix_sq: Vec<i64>,
506    /// Exclusive end of the window `cross` currently describes.
507    at: Option<usize>,
508}
509
510impl PeriodScanner {
511    fn new(input: &[u8], win: usize, max_lag: usize) -> Self {
512        let mut prefix = Vec::with_capacity(input.len() + 1);
513        let mut prefix_sq = Vec::with_capacity(input.len() + 1);
514        let (mut s, mut q) = (0i64, 0i64);
515        prefix.push(0);
516        prefix_sq.push(0);
517        for &b in input {
518            let v = i64::from(b);
519            s += v;
520            q += v * v;
521            prefix.push(s);
522            prefix_sq.push(q);
523        }
524        let hi = max_lag.min(win / 2);
525        PeriodScanner {
526            win,
527            max_lag,
528            cross: vec![0; hi.saturating_sub(1)],
529            prefix,
530            prefix_sq,
531            at: None,
532        }
533    }
534
535    /// The lag range this scans, matching [`dominant_period`].
536    fn hi(&self) -> usize {
537        self.max_lag.min(self.win / 2)
538    }
539
540    /// Rebuild the cross-sums for the window ending at `end` from scratch.
541    fn seed(&mut self, input: &[u8], end: usize) {
542        let start = end - self.win;
543        let hi = self.hi();
544        self.cross.fill(0);
545        // Every left index of the window, each against its partners at every
546        // lag that stays inside the window.
547        for t in start..end.saturating_sub(2) {
548            let x = i32::from(input[t]);
549            let last = hi.min(end - 1 - t);
550            let partners = &input[t + 2..=t + last];
551            for (c, &y) in self.cross.iter_mut().zip(partners) {
552                *c += x * i32::from(y);
553            }
554        }
555        self.at = Some(end);
556    }
557
558    /// Slide the cross-sums forward to the window ending at `end`.
559    fn advance(&mut self, input: &[u8], end: usize) {
560        let Some(prev) = self.at else {
561            self.seed(input, end);
562            return;
563        };
564        let step = end - prev;
565        // Past a whole window there is nothing left to reuse, and the two
566        // correction ranges would overlap; seeding is then both cheaper and
567        // simpler than a special case.
568        if step >= self.win {
569            self.seed(input, end);
570            return;
571        }
572        let old_start = prev - self.win;
573        let new_start = end - self.win;
574        let hi = self.hi();
575        // Terms whose left index leaves the window: the leaving byte against
576        // its partner at every lag, a contiguous run to its right.
577        for t in old_start..new_start {
578            let x = i32::from(input[t]);
579            let partners = &input[t + 2..=t + hi];
580            for (c, &y) in self.cross.iter_mut().zip(partners) {
581                *c -= x * i32::from(y);
582            }
583        }
584        // Terms whose right index enters at the far end: the entering byte
585        // against its partner at every lag, a contiguous run to its left
586        // read backwards.
587        for u in prev..end {
588            let x = i32::from(input[u]);
589            let partners = &input[u - hi..=u - 2];
590            for (c, &y) in self.cross.iter_mut().zip(partners.iter().rev()) {
591                *c += x * i32::from(y);
592            }
593        }
594        self.at = Some(end);
595    }
596
597    /// The dominant period of the window ending at `end`, and its strength.
598    fn eval(&mut self, input: &[u8], end: usize) -> (u16, f32) {
599        if self.win < 4 || end < self.win {
600            return (0, 0.0);
601        }
602        self.advance(input, end);
603        let start = end - self.win;
604        let n = self.win as f64;
605        let total = (self.prefix[end] - self.prefix[start]) as f64;
606        let m = total / n;
607        let sumsq = (self.prefix_sq[end] - self.prefix_sq[start]) as f64;
608        let denom = sumsq - n * m * m;
609        if denom <= f64::EPSILON {
610            return (0, 0.0);
611        }
612        let (mut best_lag, mut best) = (0usize, 0.0f64);
613        for lag in 2..=self.hi() {
614            // sum over t of (x[t+lag] - m)(x[t] - m), expanded so only the
615            // cross term depends on where the window sits.
616            let a = (self.prefix[end - lag] - self.prefix[start]) as f64;
617            let b = (self.prefix[end] - self.prefix[start + lag]) as f64;
618            let len = (self.win - lag) as f64;
619            let corr = f64::from(self.cross[lag - 2]) - m * (a + b) + len * m * m;
620            let r = corr / denom;
621            if r > best {
622                best = r;
623                best_lag = lag;
624            }
625        }
626        let best = best as f32;
627        if best < PERIOD_FLOOR { (0, best.max(0.0)) } else { (best_lag as u16, best) }
628    }
629}
630
631/// Correlation a lag must reach to count as a period at all.
632pub(crate) const PERIOD_FLOOR: f32 = 0.20;
633
634/// Independent lane accumulators in the autocorrelation's inner loop.
635///
636/// Sixteen `f32` is one AVX-512 register and two AVX2 ones, so the wider
637/// setting costs nothing where only AVX2 is available and uses the whole
638/// register where 512 is. The count is a lane count rather than a tuning
639/// constant: it is what the register holds.
640const LANES: usize = 16;
641
642/// The dominant period of a window by centered autocorrelation, and its
643/// normalized strength. Returns `(0, _)` when no lag clears the floor.
644pub fn dominant_period(win: &[u8], max_lag: usize) -> (u16, f32) {
645    let n = win.len();
646    if n < 4 {
647        return (0, 0.0);
648    }
649    let mean = win.iter().map(|&b| f32::from(b)).sum::<f32>() / n as f32;
650    // The window is centered once, so the autocorrelation below multiplies
651    // precomputed deviations rather than converting and subtracting the mean
652    // twice per (lag, t). Bit-exact: c[t] == f32(win[t]) - mean.
653    let c: Vec<f32> = win.iter().map(|&b| f32::from(b) - mean).collect();
654    let denom: f32 = c.iter().map(|&x| x * x).sum();
655    if denom <= f32::EPSILON {
656        return (0, 0.0);
657    }
658    let hi = max_lag.min(n / 2);
659    let mut best_lag = 0usize;
660    let mut best = 0.0f32;
661    for lag in 2..=hi {
662        // Autocorrelation at `lag` = dot(c[lag..], c[..n-lag]). Eight independent lane accumulators
663        // break the serial add-chain so LLVM vectorizes the multiply-add (one AVX2 YMM step per
664        // 8-wide chunk); a horizontal sum + scalar tail finish it. This reorders the f32 sum vs a
665        // strictly serial accumulation - a ~1 ULP shift in the autocorrelation, immaterial to a
666        // heuristic period detector whose output is the argmax over lags.
667        let a = &c[lag..];
668        let b = &c[..n - lag];
669        let (a_ch, a_rest) = a.as_chunks::<LANES>();
670        let (b_ch, b_rest) = b.as_chunks::<LANES>();
671        let mut acc = [0.0f32; LANES];
672        for (ca, cb) in a_ch.iter().zip(b_ch) {
673            for ((acck, &x), &y) in acc.iter_mut().zip(ca).zip(cb) {
674                *acck += x * y;
675            }
676        }
677        let mut sum: f32 = acc.iter().sum();
678        for (&x, &y) in a_rest.iter().zip(b_rest) {
679            sum += x * y;
680        }
681        let r = sum / denom;
682        if r > best {
683            best = r;
684            best_lag = lag;
685        }
686    }
687    if best < PERIOD_FLOOR {
688        (0, best.max(0.0))
689    } else {
690        (best_lag as u16, best)
691    }
692}
693
694/// Which readings of the field a caller will take.
695///
696/// The pass is a step a byte, so a reading nobody asks for is paid at every
697/// byte of the input. A `\F{entropy>0.7}` scan reads the entropy and nothing
698/// else, and without this it would also decay a filterbank, hash and count a
699/// k-gram through a map, and take a square root for a change-point, at each of
700/// the input's bytes.
701#[derive(Clone, Copy, Debug, PartialEq, Eq)]
702pub struct Needs {
703    /// The rolling-entropy reading.
704    pub entropy: bool,
705    /// The dominant byte-period and its strength.
706    pub period: bool,
707    /// The class mix, which the texture is read off and the change-point
708    /// measures its divergence over.
709    pub bands: bool,
710    /// The change-point boundaries.
711    pub onset: bool,
712    /// The k-gram surprise. No pattern atom reads it: [`SpectralPred`] names
713    /// entropy, period, texture and onset, and nothing else reaches a frame's
714    /// novelty. The `spectral` command reports it, and that is the whole of
715    /// what asks for it.
716    ///
717    /// [`SpectralPred`]: crate::ast::SpectralPred
718    pub novelty: bool,
719}
720
721impl Needs {
722    /// Every reading, which is what the `spectral` command reports.
723    #[must_use]
724    pub fn all() -> Needs {
725        Needs { entropy: true, period: true, bands: true, onset: true, novelty: true }
726    }
727
728    /// No reading at all.
729    #[must_use]
730    pub fn none() -> Needs {
731        Needs { entropy: false, period: false, bands: false, onset: false, novelty: false }
732    }
733
734    /// The readings one atom takes, with what each rests on: a texture is read
735    /// off the class mix and the entropy together, and a change-point measures
736    /// how far the two have diverged, so naming either takes both.
737    #[must_use]
738    pub fn of(pred: &crate::ast::SpectralPred) -> Needs {
739        use crate::ast::SpectralPred as P;
740        let mut needs = Needs::none();
741        match pred {
742            P::EntropyGe(_) | P::EntropyLe(_) => needs.entropy = true,
743            P::PeriodEq(_) | P::PeriodAny => needs.period = true,
744            P::Texture(_) => {
745                needs.entropy = true;
746                needs.bands = true;
747            }
748            P::Onset => {
749                needs.entropy = true;
750                needs.bands = true;
751                needs.onset = true;
752            }
753        }
754        needs
755    }
756
757    /// Everything either takes, for a pattern naming several atoms.
758    #[must_use]
759    pub fn and(self, other: Needs) -> Needs {
760        Needs {
761            entropy: self.entropy || other.entropy,
762            period: self.period || other.period,
763            bands: self.bands || other.bands,
764            onset: self.onset || other.onset,
765            novelty: self.novelty || other.novelty,
766        }
767    }
768
769    /// Whether any reading is asked for.
770    #[must_use]
771    pub fn any(self) -> bool {
772        self != Needs::none()
773    }
774}
775
776/// Analyze `input` with the default configuration, computing every reading.
777#[must_use]
778pub fn analyze(input: &[u8]) -> SpectralField {
779    analyze_with(input, &SpectralConfig::default())
780}
781
782/// Analyze `input` into a [`SpectralField`] in one causal forward pass,
783/// computing every reading.
784#[must_use]
785pub fn analyze_with(input: &[u8], cfg: &SpectralConfig) -> SpectralField {
786    analyze_needing(input, cfg, Needs::all())
787}
788
789/// [`analyze_with`] computing only the readings `needs` names.
790///
791/// A reading left out is absent from every frame rather than wrong: it holds
792/// the zero its frame was built with, so a caller that did not ask for one
793/// must not read it. The guards are loop-invariant, so the branch a byte costs
794/// what a predicted branch costs and saves the block it skips.
795#[must_use]
796pub fn analyze_needing(input: &[u8], cfg: &SpectralConfig, needs: Needs) -> SpectralField {
797    let n = input.len();
798    let hop = cfg.hop.max(1);
799    if n == 0 {
800        return SpectralField { len: 0, hop, frames: Vec::new(), boundaries: Vec::new(), needs };
801    }
802
803    // Filterbank decays: a_d = exp(-1/tau_d), and 1 - a_d (the input gain
804    // that keeps each band a bounded leaky average).
805    let mut a = [0.0f32; N_DECAYS];
806    let mut oma = [0.0f32; N_DECAYS];
807    for ((ad, od), &tau) in a.iter_mut().zip(oma.iter_mut()).zip(TAUS.iter()) {
808        *ad = (-1.0 / tau).exp();
809        *od = 1.0 - *ad;
810    }
811    let mut bands = [0.0f32; N_BANDS];
812    // Each band's decay laid out as the bands are, so decaying the bank is
813    // one multiply across the array - a lane per band - rather than a short
814    // loop per clock the vectorizer cannot fill.
815    let mut decay = [0.0f32; N_BANDS];
816    for (d, &ad) in a.iter().enumerate() {
817        decay[d * N_CLASSES..(d + 1) * N_CLASSES].fill(ad);
818    }
819
820    // Entropy: 256-bin sliding-window histogram with an incremental
821    // `sum c*log2(c)`.
822    let w = cfg.entropy_window.max(1);
823    let mut hist = [0u32; 256];
824    // The window's `sum c*log2(c)`, in fixed point (see `FIXED_LOG_SCALE`).
825    let mut s = 0i64;
826    let entropy_norm = (w.min(256) as f32).max(2.0).log2();
827
828    // Novelty: counts of recent k-grams over a sliding window. The key is
829    // already a mixed hash of the gram, so the table takes it as is instead
830    // of hashing it again.
831    let k = cfg.ngram.max(1);
832    let nov_win = cfg.novelty_window.max(1);
833    let mut ngram_counts: HashMap<u64, u32, std::hash::BuildHasherDefault<crate::tokutil::IdHash>> =
834        HashMap::default();
835    let mut ngram_ring: VecDeque<u64> = VecDeque::with_capacity(nov_win + 1);
836    let mut cur_novelty = 0.0f32;
837
838    // Periodicity: adaptive hop keeps total autocorrelation work bounded.
839    let pwin = cfg.period_window.max(8);
840    let max_lag = cfg.max_lag.clamp(2, pwin / 2);
841    let per_eval = pwin.saturating_mul(max_lag).max(1);
842    const PERIOD_OP_BUDGET: usize = 1_000_000_000;
843    let max_evals = (PERIOD_OP_BUDGET / per_eval).max(1);
844    let period_hop = (n / max_evals).max(cfg.period_hop).max(1);
845    let mut period_evals = 0u64;
846    let mut cur_period = 0u16;
847    let mut cur_strength = 0.0f32;
848    let mut period_scanner = PeriodScanner::new(input, pwin, max_lag);
849    // Constants of the rolling-entropy window, hoisted out of the per-byte
850    // loop; the reciprocal carries the fixed-point scale of the sum.
851    let ent_lut = fixed_log_table();
852    let ent_full_log2 = (w as f64).log2();
853    let ent_full_recip = 1.0 / (w as f64 * FIXED_LOG_SCALE);
854
855    // Change-point: how far the recent texture (fast clock) has diverged
856    // from the established baseline (slow clock), with an adaptive EWMA
857    // threshold and rising-edge hysteresis (one boundary per regime onset,
858    // not a cluster while the slow clock catches up).
859    const CP_ALPHA: f32 = 0.02;
860    const CP_WARMUP: usize = 32;
861    const ENT_SLOW_A: f32 = 0.992;
862    const ENT_WEIGHT: f32 = 2.0;
863    let mut ent_slow = 0.0f32;
864    let mut pow_f = 1.0f32;
865    let mut pow_s = 1.0f32;
866    let mut pow_e = 1.0f32;
867    // Whether every bias correction has reached exactly one, past which the
868    // powers above are no longer read and the divisions they feed are skipped.
869    let mut settled = false;
870    let mut cp_mean = 0.0f32;
871    let mut cp_mad = 0.0f32;
872    let mut cp_armed = true;
873    let mut last_boundary: isize = -(cfg.cp_min_gap as isize);
874    let mut boundaries: Vec<usize> = Vec::new();
875
876    let mut frames: Vec<SpectralFrame> = Vec::with_capacity(n / hop + 1);
877
878    for i in 0..n {
879        let b = input[i];
880        let c = classify(b);
881
882        // Filterbank: decay every band, then inject the current class.
883        if needs.bands {
884            for (v, &dk) in bands.iter_mut().zip(decay.iter()) {
885                *v *= dk;
886            }
887            for (d, &od) in oma.iter().enumerate() {
888                bands[d * N_CLASSES + c] += od;
889            }
890        }
891
892        // Entropy: add the new byte, evict the one leaving the window. The
893        // window is full after `w` bytes and stays full, so its logarithm and
894        // reciprocal are constants rather than a libm call per byte, and the
895        // count table is resolved once above rather than per lookup.
896        let cur_entropy = if needs.entropy {
897            let nb = hist[b as usize];
898            hist[b as usize] = nb + 1;
899            let mut delta = fixed_term(ent_lut, nb + 1) - fixed_term(ent_lut, nb);
900            if i >= w {
901                let ob = input[i - w] as usize;
902                let oc = hist[ob];
903                hist[ob] = oc - 1;
904                delta -= fixed_term(ent_lut, oc) - fixed_term(ent_lut, oc - 1);
905            }
906            s += delta;
907            let (log_nwin, recip) = if i + 1 >= w {
908                (ent_full_log2, ent_full_recip)
909            } else {
910                let nwin = f64::from((i + 1) as u32);
911                (nwin.log2(), 1.0 / (nwin * FIXED_LOG_SCALE))
912            };
913            let hbits = (log_nwin - s as f64 * recip) as f32;
914            (hbits / entropy_norm).clamp(0.0, 1.0)
915        } else {
916            0.0
917        };
918
919        // Novelty: surprise of the k-gram ending at this byte.
920        if needs.novelty && i + 1 >= k {
921            let mut h = 1_469_598_103_934_665_603u64;
922            for &x in &input[i + 1 - k..=i] {
923                h ^= u64::from(x);
924                h = h.wrapping_mul(1_099_511_628_211);
925            }
926            // One lookup reads the count and bumps it.
927            let count = ngram_counts.entry(h).or_insert(0);
928            cur_novelty = 1.0 / (1.0 + *count as f32);
929            *count += 1;
930            ngram_ring.push_back(h);
931            if ngram_ring.len() > nov_win
932                && let Some(old) = ngram_ring.pop_front()
933                && let Some(cc) = ngram_counts.get_mut(&old)
934            {
935                *cc -= 1;
936                if *cc == 0 {
937                    ngram_counts.remove(&old);
938                }
939            }
940        }
941
942        // Periodicity: recompute on the adaptive hop, hold between. The
943        // scanner slides its cross-sums from the previous evaluation instead
944        // of rebuilding them, so each one pays the hop rather than the window.
945        if needs.period && i + 1 >= pwin && i % period_hop == 0 {
946            // What the pass actually spends, carried in a register and handed
947            // over once at the end. The cost of this block is the count of
948            // these times what one costs, and a count is the same on a busy box
949            // as on a quiet one where a clock is not: two identical
950            // configurations of this scan timed 161.338 ms and 83.775 ms in one
951            // run while another crate was building beside it.
952            period_evals += 1;
953            let (p, st) = period_scanner.eval(input, i + 1);
954            cur_period = p;
955            cur_strength = st;
956        }
957
958        // Change-point: divergence of the fast clock (tau=8, recent) from
959        // the slow clock (tau=128, baseline) over the class mix, plus the
960        // gap between the windowed entropy and its slow average. A regime
961        // switch (prose -> packed data, code -> blob) drives both terms;
962        // uniform text leaves them near zero.
963        // Bias-correct each clock (x / (1 - a^(t+1))) so it reads its true
964        // class fraction from byte 0 - no warmup transient inflating the
965        // divergence while the slow clock is still filling.
966        if needs.onset {
967            // Each correction is `1 / (1 - a^(t+1))` over a decay below one, so
968            // the power it subtracts shrinks geometrically until it falls under
969            // the last bit of an f32 beside one. From that byte on the
970            // subtraction is exactly one, the correction is exactly one, and
971            // multiplying or dividing by it returns its operand unchanged - so
972            // the settled reading is the same bits as the divided one, and the
973            // longer the input the larger the share of it taken this way.
974            let (corr_f, corr_s, corr_e);
975            if settled {
976                corr_f = 1.0;
977                corr_s = 1.0;
978                corr_e = 1.0;
979            } else {
980                pow_f *= a[1];
981                pow_s *= a[3];
982                pow_e *= ENT_SLOW_A;
983                // The corrections divide every class, so they are inverted once
984                // per byte and applied as multiplies rather than divided out per
985                // class.
986                corr_f = 1.0 / (1.0 - pow_f).max(1e-4);
987                corr_s = 1.0 / (1.0 - pow_s).max(1e-4);
988                corr_e = (1.0 - pow_e).max(1e-4);
989                settled = corr_f == 1.0 && corr_s == 1.0 && corr_e == 1.0;
990            }
991            ent_slow += (1.0 - ENT_SLOW_A) * (cur_entropy - ent_slow);
992            let ent_slow_c = if corr_e == 1.0 { ent_slow } else { ent_slow / corr_e };
993            let fast_mix = &bands[N_CLASSES..2 * N_CLASSES];
994            let slow_mix = &bands[3 * N_CLASSES..4 * N_CLASSES];
995            let pairs = fast_mix.iter().zip(slow_mix.iter());
996            let class_div: f32 = if settled {
997                pairs.map(|(x, y)| (x - y).powi(2)).sum()
998            } else {
999                pairs.map(|(x, y)| (x * corr_f - y * corr_s).powi(2)).sum()
1000            };
1001            let dist = class_div.sqrt() + ENT_WEIGHT * (cur_entropy - ent_slow_c).abs();
1002            if i == 0 {
1003                cp_mean = dist;
1004                cp_mad = dist * 0.5 + 0.01;
1005            } else {
1006                let dev = (dist - cp_mean).abs();
1007                cp_mean += CP_ALPHA * (dist - cp_mean);
1008                cp_mad += CP_ALPHA * (dev - cp_mad);
1009            }
1010            // `cp_mad` is the reader's estimate of how far `dist` usually
1011            // moves, so `k*mad` is a spread that goes to zero where `dist`
1012            // stops moving at all - and a threshold at the mean is one an ULP
1013            // of noise clears. The floor is a share of the mean rather than a
1014            // quantity in the signal's units, so it holds wherever the mean
1015            // sits; at zero the branch is not taken and the threshold is the
1016            // spread's alone.
1017            let spread = cfg.cp_threshold * cp_mad;
1018            let thresh = cp_mean
1019                + if cfg.cp_floor > 0.0 { spread.max(cfg.cp_floor * cp_mean) } else { spread };
1020            if i >= CP_WARMUP {
1021                if cp_armed && dist > thresh && i as isize - last_boundary >= cfg.cp_min_gap as isize
1022                {
1023                    boundaries.push(i);
1024                    last_boundary = i as isize;
1025                    cp_armed = false;
1026                } else if !cp_armed && dist < thresh * 0.5 {
1027                    cp_armed = true;
1028                }
1029            }
1030        }
1031
1032        // Sample a frame at the end of each hop window and at the input end.
1033        if (i + 1) % hop == 0 || i + 1 == n {
1034            frames.push(SpectralFrame {
1035                bands,
1036                entropy: cur_entropy,
1037                period: cur_period,
1038                period_strength: cur_strength,
1039                novelty: cur_novelty,
1040            });
1041        }
1042    }
1043
1044    // Handed over once rather than once an evaluation: `counted` takes a lock,
1045    // and a lock inside the byte loop would price the watch above the work it
1046    // watches.
1047    crate::trace::counted("the spectral field: period evaluations", period_evals);
1048    SpectralField { len: n, hop, frames, boundaries, needs }
1049}
1050
1051/// Default entropy threshold (x100) above which the lexer treats a
1052/// sustained run as an opaque blob rather than shredding it into tokens.
1053pub const BLOB_ENTROPY_PCT: u8 = 85;
1054/// Minimum length, in bytes, of a high-entropy run before it collapses.
1055pub const BLOB_MIN_LEN: usize = 48;
1056
1057/// Whitespace-bounded byte ranges of sustained high-entropy content - the
1058/// blobs (base64 / hex / packed data) the lexer collapses to one opaque
1059/// token instead of shredding into garbage word and number tokens.
1060///
1061/// A run qualifies when the rolling entropy stays at or above
1062/// `threshold_pct/100` for at least `min_len` bytes; each qualifying run is
1063/// then expanded outward to the enclosing whitespace-free span (so the whole
1064/// blob is one token, not just its high-entropy core), and overlapping spans
1065/// are merged. Returns ascending, non-overlapping ranges.
1066///
1067/// A whitespace byte is never in a run, so a run lies inside one
1068/// whitespace-free span, and a span shorter than `min_len` holds no run that
1069/// qualifies: the flag pass reads the spans at least `min_len` long and
1070/// nothing else, and the runs are the ones a pass over every byte finds.
1071#[must_use]
1072pub fn high_entropy_runs(input: &[u8], threshold_pct: u8, min_len: usize) -> Vec<(usize, usize)> {
1073    let n = input.len();
1074    if n < min_len {
1075        return Vec::new();
1076    }
1077    runs_over_spans(input, &long_spans(input, 0, n, min_len), threshold_pct, min_len)
1078}
1079
1080/// [`high_entropy_runs`] inside one whitespace-free span `a..b` of `input`:
1081/// the runs the whole-input pass finds there, since the flag pass seeds its
1082/// window from the bytes before `a` and a run never crosses whitespace. A
1083/// span shorter than `min_len` holds none.
1084#[must_use]
1085pub fn high_entropy_runs_in_span(
1086    input: &[u8],
1087    a: usize,
1088    b: usize,
1089    threshold_pct: u8,
1090    min_len: usize,
1091) -> Vec<(usize, usize)> {
1092    if b - a < min_len {
1093        return Vec::new();
1094    }
1095    runs_over_spans(input, &[(a, b)], threshold_pct, min_len)
1096}
1097
1098/// [`high_entropy_runs`] inside `input[from..to]`, a stretch that may hold
1099/// whitespace: the runs the whole-input pass finds there, read over the
1100/// whitespace-free spans inside the stretch one by one as that pass reads
1101/// them, each seeded from the bytes before it. A stretch shorter than
1102/// `min_len` holds none.
1103#[must_use]
1104pub fn high_entropy_runs_within(
1105    input: &[u8],
1106    from: usize,
1107    to: usize,
1108    threshold_pct: u8,
1109    min_len: usize,
1110) -> Vec<(usize, usize)> {
1111    if to - from < min_len {
1112        return Vec::new();
1113    }
1114    runs_over_spans(input, &long_spans(input, from, to, min_len), threshold_pct, min_len)
1115}
1116
1117/// The qualifying runs inside `spans`, the long spans of `input` in order,
1118/// from the flag pass over each: maximal high-entropy runs of at least
1119/// `min_len`, closed as the scan leaves them rather than marked per byte and
1120/// collected in a second pass. A span's end closes the run reaching it, as
1121/// the whitespace byte there would.
1122fn runs_over_spans(
1123    input: &[u8],
1124    spans: &[(usize, usize)],
1125    threshold_pct: u8,
1126    min_len: usize,
1127) -> Vec<(usize, usize)> {
1128    let mut raw: Vec<(usize, usize)> = Vec::new();
1129    for &(a, b) in spans {
1130        // Every run inside this span expands to the same enclosing
1131        // whitespace-free span, because the span holds no whitespace to stop
1132        // the expansion anywhere inside it. Reading it once a span rather than
1133        // once a run is what keeps this linear: the walk is a byte at a time
1134        // to the nearest whitespace either side, so on an input that has none
1135        // it reaches the input's own ends, and paying that per run makes the
1136        // pass quadratic in the number of runs.
1137        if holds_a_run(input, a, b, threshold_pct, min_len) {
1138            raw.push(whitespace_free_span(input, a, b));
1139        }
1140    }
1141    merge_spans(raw)
1142}
1143
1144/// Whether the flag pass over `input[from..to]`, seeded from the bytes
1145/// before `from`, meets a high-entropy run at least `min_len` long; a run
1146/// still open at `to` counts by the length it has reached there.
1147fn holds_a_run(input: &[u8], from: usize, to: usize, threshold_pct: u8, min_len: usize) -> bool {
1148    let mut qualifies = false;
1149    let mut run_start: Option<usize> = None;
1150    entropy_flags(input, from, to, threshold_pct, |i, hi| match (hi, run_start) {
1151        (true, None) => run_start = Some(i),
1152        (false, Some(rs)) => {
1153            qualifies |= i - rs >= min_len;
1154            run_start = None;
1155        }
1156        _ => {}
1157    });
1158    if let Some(rs) = run_start {
1159        qualifies |= to - rs >= min_len;
1160    }
1161    qualifies
1162}
1163
1164/// [`runs_over_spans`] across the cores. The spans are cut into pieces of at
1165/// most `leaf_bytes` and consecutive pieces gathered into leaves of about that
1166/// many bytes between them; a piece reads its bytes and `min_len` past its
1167/// cut, so a run crossing a cut is seen at least `min_len` long by the piece
1168/// it starts in, and a span qualifies where any piece of it does. Every run
1169/// starts in some piece, so the spans found are the spans the pass over
1170/// whole spans finds. A single leaf runs that pass itself.
1171fn runs_over_spans_across(
1172    input: &[u8],
1173    spans: &[(usize, usize)],
1174    threshold_pct: u8,
1175    min_len: usize,
1176    least_a_leaf: usize,
1177) -> Vec<(usize, usize)> {
1178    use flynnel::JobPlan;
1179    use flynnel::sched::par_iter::for_each_chunk_indexed_min_leaf;
1180
1181    let span_bytes: usize = spans.iter().map(|&(a, b)| b - a).sum();
1182    let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
1183    let leaf_bytes = span_bytes.div_ceil(cores * 4).max(least_a_leaf).max(1);
1184    // A piece is a span's index and the part of it the piece reads before
1185    // its reach past the cut.
1186    let mut pieces: Vec<(usize, usize, usize)> = Vec::new();
1187    for (k, &(a, b)) in spans.iter().enumerate() {
1188        let mut p = a;
1189        while p < b {
1190            let q = (p + leaf_bytes).min(b);
1191            pieces.push((k, p, q));
1192            p = q;
1193        }
1194    }
1195    // A leaf is a range of consecutive pieces closed once it holds
1196    // `leaf_bytes`.
1197    let mut leaves: Vec<(usize, usize)> = Vec::new();
1198    let mut held = 0usize;
1199    let mut first = 0usize;
1200    for (i, &(_, p, q)) in pieces.iter().enumerate() {
1201        held += q - p;
1202        if held >= leaf_bytes {
1203            leaves.push((first, i + 1));
1204            first = i + 1;
1205            held = 0;
1206        }
1207    }
1208    if first < pieces.len() {
1209        leaves.push((first, pieces.len()));
1210    }
1211    if leaves.len() <= 1 {
1212        return runs_over_spans(input, spans, threshold_pct, min_len);
1213    }
1214    let mut found: Vec<Vec<usize>> = vec![Vec::new(); leaves.len()];
1215    // What a leaf costs, for the pool's own choice between the cores and
1216    // inline: the pass over whole spans read 0.315 ms over the 72,972 span
1217    // bytes of this crate's own src, 4.32 ns a byte.
1218    let per_leaf_ns = (leaf_bytes as u64 * 4320 / 1000).min(u64::from(u32::MAX)) as u32;
1219    let plan = JobPlan::new(0, leaves.len() as u32)
1220        .with_leaf_shape(flynnel::LeafShape::Streaming)
1221        .with_estimated_per_item_ns(per_leaf_ns);
1222    for_each_chunk_indexed_min_leaf(&plan, &mut found, 1, |start, slots| {
1223        for (i, slot) in slots.iter_mut().enumerate() {
1224            let (lo, hi) = leaves[start + i];
1225            for &(k, p, q) in &pieces[lo..hi] {
1226                let reach = (q + min_len).min(spans[k].1);
1227                if slot.last() != Some(&k) && holds_a_run(input, p, reach, threshold_pct, min_len) {
1228                    slot.push(k);
1229                }
1230            }
1231        }
1232    });
1233    let mut qualifies = vec![false; spans.len()];
1234    for k in found.into_iter().flatten() {
1235        qualifies[k] = true;
1236    }
1237    let raw = spans
1238        .iter()
1239        .zip(qualifies)
1240        .filter(|&(_, q)| q)
1241        .map(|(&(a, b), _)| whitespace_free_span(input, a, b))
1242        .collect();
1243    merge_spans(raw)
1244}
1245
1246/// Bytes a leaf of the span finder across the cores holds, at the least. The
1247/// per-byte work is a fraction of a nanosecond, so a leaf below this is
1248/// dispatch rather than work.
1249const ENTROPY_FLAGS_MIN_LEAF: usize = 64 * 1024;
1250
1251/// Bytes a leaf of the runs over the spans across the cores holds, at the
1252/// least. Measured by `where_the_time_goes` on a 24-thread box, 64 kB
1253/// against 16 kB and 8 kB: over the 72,972 span bytes of this crate's own
1254/// src the runs over the spans read 0.400, 0.146 and 0.089 ms a scan, over
1255/// the 107,637 of real_code.txt 0.288, 0.120 and 0.103, and over the 400,754
1256/// of train_corpus.txt 0.488 and 0.226 at 64 kB and 16 kB; the walls of the
1257/// scans around them did not tell 16 kB from 8 kB.
1258const ENTROPY_RUNS_MIN_LEAF: usize = 8 * 1024;
1259
1260/// [`high_entropy_runs`] across cores: the long spans found across the
1261/// cores, then the flag pass over them across the cores, reading only the
1262/// spans' bytes. A byte's flag depends on the sixty-four bytes before it and
1263/// nothing else, so a piece seeded from the bytes before its start reads
1264/// exactly what the whole-stream pass reads there. Byte-identical to the
1265/// serial form.
1266#[must_use]
1267pub fn high_entropy_runs_parallel(input: &[u8], threshold_pct: u8, min_len: usize) -> Vec<(usize, usize)> {
1268    let n = input.len();
1269    if n < min_len {
1270        return Vec::new();
1271    }
1272    // Each part is a phase entered once a call, so the table's cost reads by
1273    // part.
1274    let spanning = crate::trace::phase("the blob table: the long spans");
1275    let spans = long_spans_across(input, min_len, ENTROPY_FLAGS_MIN_LEAF);
1276    let span_bytes: usize = spans.iter().map(|&(a, b)| b - a).sum();
1277    drop(spanning);
1278    crate::trace::counted("the blob table: bytes in the long spans", span_bytes as u64);
1279    let _running = crate::trace::phase("the blob table: the runs over the spans");
1280    runs_over_spans_across(input, &spans, threshold_pct, min_len, ENTROPY_RUNS_MIN_LEAF)
1281}
1282
1283/// The high-entropy flag of each byte of `input[from..to]`, in order, to
1284/// `emit`.
1285///
1286/// The rolling window is the sixty-four bytes up to and including the byte,
1287/// so the pass seeds it from the bytes before `from` and a range starting
1288/// mid-stream reads exactly what a whole-stream pass reads there.
1289/// The flag pass, compiled for whatever instruction set this CPU reports.
1290///
1291/// The body is a serial recurrence - a 256-bin histogram updated in place
1292/// and a running sum carried across bytes - so there is no vector loop here
1293/// to write by hand. What a wider target buys it is scalar: the newer
1294/// addressing and bit-manipulation forms, and a better lowering of the
1295/// float work per byte. `#[target_feature]` grants the whole instruction
1296/// set to the function it marks, so the same source compiled behind each
1297/// gate is enough to collect that, with no intrinsics.
1298///
1299/// Every rung runs the identical body and must agree flag for flag; the
1300/// gate is the only thing that differs between them, which is what
1301/// `entropy_flags_ladder_agrees` checks.
1302///
1303/// The ladder descends AVX-512, AVX2, SSE2, scalar, and every rung is
1304/// present in every binary: the build stays portable and the CPU decides.
1305/// SSE2 is architectural on x86-64 so that rung always applies there, and
1306/// the scalar body is what runs on every other architecture.
1307///
1308/// Calling a `#[target_feature]` function on a CPU without the feature is
1309/// undefined behavior. The `is_x86_feature_detected!` probe discharges
1310/// that precondition immediately before each call, as `crate::byte_simd`
1311/// does for the literal search.
1312#[allow(unsafe_code)]
1313fn entropy_flags(
1314    input: &[u8],
1315    from: usize,
1316    to: usize,
1317    threshold_pct: u8,
1318    emit: impl FnMut(usize, bool),
1319) {
1320    #[cfg(target_arch = "x86_64")]
1321    {
1322        // SAFETY on each arm: `crate::isa::tier` reports the rungs this CPU
1323        // can run, and a rung implies every rung below it, so reaching an
1324        // arm is the guarantee that its features are present.
1325        match crate::isa::tier() {
1326            crate::isa::Tier::Avx512 => {
1327                return unsafe { entropy_flags_avx512(input, from, to, threshold_pct, emit) };
1328            }
1329            crate::isa::Tier::Avx2 => {
1330                return unsafe { entropy_flags_avx2(input, from, to, threshold_pct, emit) };
1331            }
1332            crate::isa::Tier::Sse2 => {
1333                return unsafe { entropy_flags_sse2(input, from, to, threshold_pct, emit) };
1334            }
1335            crate::isa::Tier::Scalar => {}
1336        }
1337    }
1338    entropy_flags_impl(input, from, to, threshold_pct, emit);
1339}
1340
1341#[cfg(target_arch = "x86_64")]
1342#[allow(unsafe_code)]
1343#[target_feature(enable = "avx512f", enable = "avx512bw")]
1344unsafe fn entropy_flags_avx512(
1345    input: &[u8],
1346    from: usize,
1347    to: usize,
1348    threshold_pct: u8,
1349    emit: impl FnMut(usize, bool),
1350) {
1351    entropy_flags_impl(input, from, to, threshold_pct, emit);
1352}
1353
1354#[cfg(target_arch = "x86_64")]
1355#[allow(unsafe_code)]
1356#[target_feature(enable = "avx2", enable = "bmi2")]
1357unsafe fn entropy_flags_avx2(
1358    input: &[u8],
1359    from: usize,
1360    to: usize,
1361    threshold_pct: u8,
1362    emit: impl FnMut(usize, bool),
1363) {
1364    entropy_flags_impl(input, from, to, threshold_pct, emit);
1365}
1366
1367#[cfg(target_arch = "x86_64")]
1368#[allow(unsafe_code)]
1369#[target_feature(enable = "sse2")]
1370unsafe fn entropy_flags_sse2(
1371    input: &[u8],
1372    from: usize,
1373    to: usize,
1374    threshold_pct: u8,
1375    emit: impl FnMut(usize, bool),
1376) {
1377    entropy_flags_impl(input, from, to, threshold_pct, emit);
1378}
1379
1380// Inlined into each gated wrapper so the body is compiled once per target,
1381// rather than called across a boundary that would keep it at baseline.
1382#[inline(always)]
1383fn entropy_flags_impl(input: &[u8], from: usize, to: usize, threshold_pct: u8, mut emit: impl FnMut(usize, bool)) {
1384    let w = 64usize;
1385    let mut hist = [0u32; 256];
1386    // The window's `sum c*log2(c)`, in fixed point (see `FIXED_LOG_SCALE`).
1387    let mut s = 0i64;
1388    let entropy_norm = (w.min(256) as f32).max(2.0).log2();
1389    let thr = f32::from(threshold_pct) / 100.0;
1390    // The window is full after `w` bytes and stays full, so `log2(nwin)` is
1391    // one constant for all but the first `w` positions - it was a libm call
1392    // per byte computing the same number. The reciprocal goes with it, since
1393    // dividing by the window is the other per-byte constant; it carries the
1394    // fixed-point scale so the sum converts in the same multiply.
1395    let full_log2 = (w as f64).log2();
1396    let full_recip = 1.0 / (w as f64 * FIXED_LOG_SCALE);
1397    // The entropy of a full window is a function of `s` alone, and every
1398    // step of it - the product, the subtraction, the narrowing to f32, the
1399    // division - is monotone in `s`, so the positions where it clears the
1400    // threshold are exactly the `s` at or below one boundary. That boundary
1401    // is found once from the same expression the partial window evaluates,
1402    // and a full-window byte then compares `s` to it: no float work per byte,
1403    // and the same flag the expression would give.
1404    let full_high = |s: i64| (full_log2 - s as f64 * full_recip) as f32 / entropy_norm >= thr;
1405    let s_high_max: i64 = {
1406        // Sixty-four bytes give at most `64 * log2(64)` in sum, and no
1407        // count table sum is negative, so the boundary lies in this range or
1408        // below it entirely.
1409        let (mut lo, mut hi) = (-1i64, (w as i64) * 6 * FIXED_LOG_SCALE as i64 + 1);
1410        while hi - lo > 1 {
1411            let mid = lo + (hi - lo) / 2;
1412            if full_high(mid) { lo = mid } else { hi = mid }
1413        }
1414        lo
1415    };
1416    // The tables resolve their locks once here rather than on each lookup
1417    // every byte makes.
1418    let lut = fixed_log_table();
1419    let dlut = fixed_log_delta_table();
1420    for &b in &input[from.saturating_sub(w)..from] {
1421        let nb = hist[b as usize];
1422        hist[b as usize] = nb + 1;
1423        s += fixed_term(lut, nb + 1) - fixed_term(lut, nb);
1424    }
1425    for i in from..to {
1426        let b = input[i] as usize;
1427        let nb = hist[b];
1428        hist[b] = nb + 1;
1429        // Counts stay within the window, so both deltas are in the table.
1430        let mut delta = dlut[nb as usize];
1431        if i >= w {
1432            let ob = input[i - w] as usize;
1433            let oc = hist[ob];
1434            hist[ob] = oc - 1;
1435            delta -= dlut[(oc - 1) as usize];
1436        }
1437        s += delta;
1438        let high = if i + 1 >= w {
1439            s <= s_high_max
1440        } else {
1441            let nwin = f64::from((i + 1) as u32);
1442            (nwin.log2() - s as f64 * (1.0 / (nwin * FIXED_LOG_SCALE))) as f32 / entropy_norm >= thr
1443        };
1444        // A whitespace byte is never part of a run. The window trails the
1445        // cursor, so it stays blob-dominated for up to `w` bytes past a blob's
1446        // end; without this the run continues over the separator into the text
1447        // after it, and the returned span is neither whitespace-free nor
1448        // confined to one line.
1449        emit(i, high && !input[i].is_ascii_whitespace());
1450    }
1451}
1452
1453/// The run `[rs, re)` expanded outward to the enclosing whitespace-free
1454/// span, so the whole blob is one token rather than just its high-entropy
1455/// core.
1456fn whitespace_free_span(input: &[u8], rs: usize, re: usize) -> (usize, usize) {
1457    let mut a = rs;
1458    while a > 0 && !input[a - 1].is_ascii_whitespace() {
1459        a -= 1;
1460    }
1461    let mut b = re;
1462    while b < input.len() && !input[b].is_ascii_whitespace() {
1463        b += 1;
1464    }
1465    (a, b)
1466}
1467
1468/// The parts inside `[from, to)` of the whitespace-free spans of `input` at
1469/// least `min_len` bytes long, ascending. The span scan runs over the range
1470/// widened by `min_len` on each side, so a span crossing the range's edge is
1471/// measured to at least `min_len` before it is cut to the range, which
1472/// decides "at least `min_len`" exactly while reading a bounded distance
1473/// outside it.
1474fn long_spans(input: &[u8], from: usize, to: usize, min_len: usize) -> Vec<(usize, usize)> {
1475    let lo = from.saturating_sub(min_len);
1476    let hi = (to + min_len).min(input.len());
1477    crate::byte_simd::nonspace_spans_at_least(&input[lo..hi], min_len)
1478        .into_iter()
1479        .filter_map(|(a, b)| {
1480            let (a, b) = ((lo + a).max(from), (lo + b).min(to));
1481            (a < b).then_some((a, b))
1482        })
1483        .collect()
1484}
1485
1486/// [`long_spans`] over the whole input across the cores: each leaf reads its
1487/// range as `long_spans` reads one - widened by `min_len` each side and cut
1488/// to the range - so a span crossing a cut arrives from both sides as two
1489/// pieces that touch, and the merge joins them back into the one span the
1490/// pass over the whole input reports. Every piece is a part of a span that
1491/// pass reports and every such span is covered, so the two agree exactly.
1492///
1493/// Each leaf takes at least `least_a_leaf` bytes; the input is read once
1494/// plus `min_len` twice a cut.
1495fn long_spans_across(input: &[u8], min_len: usize, least_a_leaf: usize) -> Vec<(usize, usize)> {
1496    use flynnel::JobPlan;
1497    use flynnel::sched::par_iter::for_each_chunk_indexed_min_leaf;
1498
1499    let n = input.len();
1500    let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
1501    let leaf_bytes = n.div_ceil(cores * 4).max(least_a_leaf).max(1);
1502    let leaves = n.div_ceil(leaf_bytes);
1503    if leaves <= 1 {
1504        return long_spans(input, 0, n, min_len);
1505    }
1506    let mut found: Vec<Vec<(usize, usize)>> = vec![Vec::new(); leaves];
1507    // What a leaf costs, for the pool's own choice between running the
1508    // leaves across the cores and inline: the pass over the whole input read
1509    // 539.74 us over 3,688,563 bytes of this crate's own src, 0.146 ns a byte.
1510    let per_leaf_ns = (leaf_bytes as u64 * 146 / 1000).min(u64::from(u32::MAX)) as u32;
1511    let plan = JobPlan::new(0, leaves as u32)
1512        .with_leaf_shape(flynnel::LeafShape::Streaming)
1513        .with_estimated_per_item_ns(per_leaf_ns);
1514    for_each_chunk_indexed_min_leaf(&plan, &mut found, 1, |start, slots| {
1515        for (i, slot) in slots.iter_mut().enumerate() {
1516            let from = (start + i) * leaf_bytes;
1517            let to = (from + leaf_bytes).min(n);
1518            *slot = long_spans(input, from, to, min_len);
1519        }
1520    });
1521    merge_spans(found.into_iter().flatten().collect())
1522}
1523
1524/// Merge ascending spans that touch or overlap.
1525fn merge_spans(raw: Vec<(usize, usize)>) -> Vec<(usize, usize)> {
1526    let mut runs: Vec<(usize, usize)> = Vec::new();
1527    for (a, b) in raw {
1528        if let Some(last) = runs.last_mut()
1529            && a <= last.1
1530        {
1531            last.1 = last.1.max(b);
1532        } else {
1533            runs.push((a, b));
1534        }
1535    }
1536    runs
1537}
1538
1539/// The intrinsic region split of an analyzed field: the input segmented at
1540/// change-point boundaries, each segment classified by its pooled texture,
1541/// with adjacent same-texture segments merged. This is the dictionary-free,
1542/// fence-free alternative to marker-based region detection - the
1543/// prose/code/math/data split a literate-document consumer (`trimodal`,
1544/// `bundle`, `binpos`) needs, derived from the bytes alone.
1545#[must_use]
1546pub fn regions(field: &SpectralField) -> Vec<(usize, usize, Texture)> {
1547    if field.len == 0 {
1548        return Vec::new();
1549    }
1550    let mut cuts: Vec<usize> = Vec::with_capacity(field.boundaries.len() + 2);
1551    cuts.push(0);
1552    cuts.extend(field.boundaries.iter().copied());
1553    cuts.push(field.len);
1554    cuts.dedup();
1555    let mut out: Vec<(usize, usize, Texture)> = Vec::new();
1556    for w in cuts.windows(2) {
1557        let (s, e) = (w[0], w[1]);
1558        if e <= s {
1559            continue;
1560        }
1561        let tex = texture_of(&field.signature(s, e));
1562        if let Some(last) = out.last_mut()
1563            && last.2 == tex
1564        {
1565            last.1 = e;
1566        } else {
1567            out.push((s, e, tex));
1568        }
1569    }
1570    out
1571}
1572
1573/// Code-tuned region classification - the code analog of [`regions`]. Splits
1574/// `input` at its spectral change-points and labels each span by its
1575/// [`CodeTexture`] (code / blob / prose / numeric), computed from a FRESH
1576/// spectral pass over only that span so a region's label cannot bleed across
1577/// its boundary the way the causal per-byte reader does. For consumers that
1578/// need the code {code, blob, prose, numeric} split rather than the generic
1579/// {prose, code, math, data} one - disassembly code-vs-data, packed-region
1580/// detection, and the construct layer's region texture. Adjacent same-texture
1581/// spans are merged.
1582#[must_use]
1583pub fn code_regions(input: &[u8]) -> Vec<(usize, usize, CodeTexture)> {
1584    let field = analyze(input);
1585    if field.len == 0 {
1586        return Vec::new();
1587    }
1588    let mut cuts: Vec<usize> = Vec::with_capacity(field.boundaries.len() + 4);
1589    cuts.push(0);
1590    cuts.extend(field.boundaries.iter().copied());
1591    // Isolate packed / base64 / hex runs as their own regions: the change-point
1592    // detector alone gives a coarse span that absorbs the surrounding code into
1593    // a blob-dominated region, so add the sharp `high_entropy_runs` bounds.
1594    for (s, e) in high_entropy_runs(input, BLOB_ENTROPY_PCT, BLOB_MIN_LEN) {
1595        cuts.push(s);
1596        cuts.push(e);
1597    }
1598    cuts.push(field.len);
1599    cuts.sort_unstable();
1600    cuts.dedup();
1601    let mut out: Vec<(usize, usize, CodeTexture)> = Vec::new();
1602    for w in cuts.windows(2) {
1603        let (s, e) = (w[0], w[1]);
1604        if e <= s {
1605            continue;
1606        }
1607        let tex = code_texture(&analyze(&input[s..e]).signature(0, e - s));
1608        if let Some(last) = out.last_mut()
1609            && last.2 == tex
1610        {
1611            last.1 = e;
1612        } else {
1613            out.push((s, e, tex));
1614        }
1615    }
1616    out
1617}
1618
1619#[cfg(test)]
1620mod tests {
1621    use super::*;
1622
1623    #[test]
1624    fn the_sliding_scanner_agrees_with_the_one_shot_reading() {
1625        // Two implementations of one reading, so they are held against each
1626        // other rather than each against itself. The scanner slides exact
1627        // integer cross-sums; dominant_period rebuilds a centered f32 dot
1628        // product. They must pick the same lag.
1629        let csv: Vec<u8> = (0..300)
1630            .flat_map(|i| format!("{:03},{:03},{:03}\n", i % 1000, (i * 7) % 1000, (i * 13) % 1000).into_bytes())
1631            .collect();
1632        let prose: Vec<u8> = "it was the best of times it was the worst of times it was the age of wisdom "
1633            .bytes().cycle().take(9000).collect();
1634        let mut x = 0x1234_5678u32;
1635        let noise: Vec<u8> = (0..9000)
1636            .map(|_| {
1637                x ^= x << 13;
1638                x ^= x >> 17;
1639                x ^= x << 5;
1640                (x & 0xff) as u8
1641            })
1642            .collect();
1643
1644        for (tag, data) in [("csv", csv), ("prose", prose), ("noise", noise)] {
1645            let (win, lag, hop) = (512usize, 128usize, 64usize);
1646            let mut scan = PeriodScanner::new(&data, win, lag);
1647            let mut end = win;
1648            let mut checked = 0u32;
1649            while end <= data.len() {
1650                let (want, want_st) = dominant_period(&data[end - win..end], lag);
1651                let (got, got_st) = scan.eval(&data, end);
1652                assert_eq!(got, want, "{tag} at {end}: lag disagrees");
1653                assert!(
1654                    (got_st - want_st).abs() < 1e-3,
1655                    "{tag} at {end}: strength {got_st} vs {want_st}"
1656                );
1657                checked += 1;
1658                end += hop;
1659            }
1660            assert!(checked > 10, "{tag}: the sweep must actually compare windows");
1661        }
1662    }
1663
1664    #[test]
1665    fn the_scanner_reseeds_rather_than_sliding_past_a_whole_window() {
1666        // A jump wider than the window leaves nothing to reuse, and the two
1667        // correction ranges would overlap; it must rebuild instead.
1668        let data: Vec<u8> =
1669            (0..4000).map(|i: usize| b"abcd12"[i % 6]).collect();
1670        let win = 256usize;
1671        let mut scan = PeriodScanner::new(&data, win, 64);
1672        // Evaluate, then jump far past the window, then evaluate again.
1673        let a = scan.eval(&data, win);
1674        let far = scan.eval(&data, 3000);
1675        let fresh = dominant_period(&data[3000 - win..3000], 64);
1676        assert_eq!(far.0, fresh.0, "a reseeded window reads as a fresh one");
1677        assert_eq!(a.0, 6, "the six-byte cycle is found either way");
1678    }
1679
1680    #[test]
1681    fn csv_rows_have_a_dominant_period() {
1682        // "a,b,c\n" is a 6-byte period; autocorrelation must recover it.
1683        let mut input = Vec::new();
1684        for _ in 0..200 {
1685            input.extend_from_slice(b"a,b,c\n");
1686        }
1687        let field = analyze(&input);
1688        assert!(
1689            field.frames.iter().any(|f| f.period == 6 && f.period_strength > 0.3),
1690            "expected a frame with period 6; got periods {:?}",
1691            field.frames.iter().map(|f| f.period).collect::<Vec<_>>()
1692        );
1693    }
1694
1695    #[test]
1696    fn base64_blob_reads_high_entropy() {
1697        let alpha = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1698        let blob: Vec<u8> = (0..800).map(|i| alpha[(i * 7 + i * i * 13) % 64]).collect();
1699        let field = analyze(&blob);
1700        let max_e = field.frames.iter().map(|f| f.entropy).fold(0.0f32, f32::max);
1701        assert!(max_e > 0.7, "base64 blob should read high entropy; max was {max_e}");
1702    }
1703
1704    #[test]
1705    fn prose_and_code_classify_distinctly() {
1706        let prose = b"the quick brown fox jumps over the lazy dog and then the dog sleeps well into the warm afternoon while the fox wanders off across the wide green field again";
1707        let code = b"fn f(x){let y=x+1;return y*2;} fn g(a,b){if(a>b){a-=b;}else{b-=a;} return a;} struct P{x:i32,y:i32}";
1708        let pf = analyze(prose);
1709        let cf = analyze(code);
1710        let pt = texture_of(&pf.signature(0, prose.len()));
1711        let ct = texture_of(&cf.signature(0, code.len()));
1712        assert_eq!(pt, Texture::Prose, "prose misclassified as {}", pt.label());
1713        assert_eq!(ct, Texture::Code, "code misclassified as {}", ct.label());
1714    }
1715
1716    #[test]
1717    fn repeated_template_drops_in_novelty() {
1718        let mut input = Vec::new();
1719        for _ in 0..200 {
1720            input.extend_from_slice(b"INFO: request handled ok\n");
1721        }
1722        let field = analyze(&input);
1723        let n = field.frames.len();
1724        assert!(n >= 4, "need several frames");
1725        let first: f32 = field.frames[..n / 4].iter().map(|f| f.novelty).sum::<f32>()
1726            / (n / 4).max(1) as f32;
1727        let last: f32 = field.frames[3 * n / 4..].iter().map(|f| f.novelty).sum::<f32>()
1728            / (n - 3 * n / 4).max(1) as f32;
1729        assert!(last < first, "novelty should fall on repetition: first {first}, last {last}");
1730    }
1731
1732    #[test]
1733    fn regime_switch_records_a_change_point() {
1734        // Three 300-byte regimes: letters, digits, punctuation. The reader
1735        // should mark a boundary near each switch (300 and 600).
1736        let mut input = Vec::new();
1737        input.extend(std::iter::repeat_n(b'a', 300));
1738        input.extend(std::iter::repeat_n(b'7', 300));
1739        input.extend(std::iter::repeat_n(b'#', 300));
1740        let field = analyze(&input);
1741        assert!(field.boundary_near(300, 24), "no change-point near 300: {:?}", field.boundaries);
1742        assert!(field.boundary_near(600, 24), "no change-point near 600: {:?}", field.boundaries);
1743    }
1744
1745    #[test]
1746    fn a_regime_switch_is_found_past_the_settling_of_the_bias_corrections() {
1747        // Each clock's bias correction is `1 / (1 - a^(t+1))`, and the power it
1748        // subtracts falls under the last bit of an f32 beside one a couple of
1749        // thousand bytes in; from there the correction is exactly one and the
1750        // reading takes the settled path. Five 4000-byte regimes put every
1751        // switch but the first past that point, which is where the longer part
1752        // of any real input is read.
1753        let mut input = Vec::new();
1754        for b in *b"a7#a7" {
1755            input.extend(std::iter::repeat_n(b, 4000));
1756        }
1757        let field = analyze(&input);
1758        for at in [4000usize, 8000, 12000, 16000] {
1759            assert!(
1760                field.boundary_near(at, 64),
1761                "no change-point near {at}: {:?}",
1762                field.boundaries
1763            );
1764        }
1765        // Every boundary is a switch, and the first switch is what this
1766        // covers. A regime is uniform, so the divergence inside it is
1767        // constant and the spread `cp_mad` estimates goes to zero;
1768        // `cp_floor` is what keeps the threshold off the signal there, and a
1769        // boundary inside a run would also hold the hysteresis closed
1770        // through the switch after it. Over six real corpora read in both
1771        // line-ending conventions, 69,118 boundaries as CRLF and 74,841 as
1772        // LF, the floor at its default moves none of them.
1773        for &b in &field.boundaries {
1774            assert!(
1775                [4000usize, 8000, 12000, 16000].iter().any(|&s| b.abs_diff(s) <= 64),
1776                "a change-point at {b} is inside a regime: {:?}",
1777                field.boundaries
1778            );
1779        }
1780    }
1781
1782    #[test]
1783    fn boundary_lookups_answer_as_a_scan_over_every_change_point_does() {
1784        // The binary searches are held against the definitions they stand
1785        // in for, over every span and every byte of a field whose
1786        // change-points sit at both ends and unevenly in between.
1787        let boundaries = vec![0usize, 3, 4, 10, 27, 28, 64, 100];
1788        let field = SpectralField {
1789            len: 101,
1790            hop: 1,
1791            frames: Vec::new(),
1792            boundaries: boundaries.clone(),
1793            needs: Needs::all(),
1794        };
1795        for start in 0..=104 {
1796            for end in 0..=104 {
1797                let scan = boundaries.iter().any(|&b| b >= start && b < end);
1798                assert_eq!(field.boundary_in(start, end), scan, "boundary_in({start}, {end})");
1799            }
1800        }
1801        for byte in 0..=104 {
1802            for tol in 0..8 {
1803                let scan = boundaries.iter().any(|&b| b.abs_diff(byte) <= tol);
1804                assert_eq!(field.boundary_near(byte, tol), scan, "boundary_near({byte}, {tol})");
1805            }
1806        }
1807        let none = SpectralField {
1808            len: 0,
1809            hop: 1,
1810            frames: Vec::new(),
1811            boundaries: Vec::new(),
1812            needs: Needs::all(),
1813        };
1814        assert!(!none.boundary_in(0, 10));
1815        assert!(!none.boundary_near(5, 5));
1816    }
1817
1818    #[test]
1819    fn empty_input_is_safe() {
1820        let field = analyze(b"");
1821        assert_eq!(field.len, 0);
1822        assert!(field.frames.is_empty());
1823        assert_eq!(field.signature(0, 0), SpectralFrame::default());
1824    }
1825
1826    /// A deterministic high-entropy base64-alphabet blob of `n` bytes (a
1827    /// fixed-seed PCG-style LCG, so no RNG and fully reproducible).
1828    fn hi_entropy_blob(n: usize) -> Vec<u8> {
1829        const ALPHA: &[u8; 64] =
1830            b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1831        let mut x = 0x2545_f491_4f6c_dd1du64;
1832        (0..n)
1833            .map(|_| {
1834                x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
1835                ALPHA[((x >> 58) % 64) as usize]
1836            })
1837            .collect()
1838    }
1839
1840    #[test]
1841    fn blob_runs_never_hold_whitespace() {
1842        // The entropy window trails the cursor, so it stays blob-dominated for
1843        // up to 64 bytes past a blob's end. A run must still stop at the
1844        // separator rather than continuing into the text that follows.
1845        //
1846        // The blob is 300 bytes: the window is 64 wide and a run must reach
1847        // BLOB_MIN_LEN, so a run only forms once the window holds mostly blob.
1848        // A blob shorter than roughly 112 bytes never sustains one, and is
1849        // classified by the base64 recognizer instead.
1850        let mut input = Vec::new();
1851        for i in 0..40 {
1852            input.extend_from_slice(format!("line {i} the quick brown fox jumps over\n").as_bytes());
1853            input.extend(hi_entropy_blob(300));
1854            input.push(b'\n');
1855        }
1856        let runs = high_entropy_runs(&input, BLOB_ENTROPY_PCT, BLOB_MIN_LEN);
1857        assert!(!runs.is_empty(), "blobs should be detected at all");
1858        for &(a, b) in &runs {
1859            assert!(
1860                !input[a..b].iter().any(u8::is_ascii_whitespace),
1861                "run [{a}..{b}] holds whitespace: {:?}",
1862                String::from_utf8_lossy(&input[a..b])
1863            );
1864        }
1865        // The prose after a blob still lexes into its own word tokens.
1866        let toks = crate::lexer::lex(&input);
1867        let words = toks
1868            .iter()
1869            .filter(|t| t.kind == crate::token::TokenKind::Word)
1870            .filter(|t| &input[t.span()] == b"quick")
1871            .count();
1872        assert_eq!(words, 40, "every line's prose survives the blob gate");
1873    }
1874
1875    #[test]
1876    fn high_entropy_runs_find_a_base64_blob_not_prose() {
1877        let mut input = Vec::new();
1878        input.extend_from_slice(b"the quick brown fox jumps over the lazy dog near the old bridge today ");
1879        let blob_start = input.len();
1880        input.extend(hi_entropy_blob(300));
1881        let runs = high_entropy_runs(&input, BLOB_ENTROPY_PCT, BLOB_MIN_LEN);
1882        assert!(!runs.is_empty(), "a 300-byte base64 blob should be detected");
1883        let (a, b) = runs[0];
1884        assert!(b - a >= BLOB_MIN_LEN, "blob run too short: {:?}", runs[0]);
1885        assert!(a >= blob_start.saturating_sub(2), "blob should start at/after the prose: {a} vs {blob_start}");
1886    }
1887
1888    /// The runs read from every byte's flag: what the span-gated pass must
1889    /// return.
1890    fn runs_from_every_flag(input: &[u8], threshold_pct: u8, min_len: usize) -> Vec<(usize, usize)> {
1891        let n = input.len();
1892        if n < min_len {
1893            return Vec::new();
1894        }
1895        let mut raw: Vec<(usize, usize)> = Vec::new();
1896        let mut run_start: Option<usize> = None;
1897        entropy_flags(input, 0, n, threshold_pct, |i, hi| match (hi, run_start) {
1898            (true, None) => run_start = Some(i),
1899            (false, Some(rs)) => {
1900                if i - rs >= min_len {
1901                    raw.push(whitespace_free_span(input, rs, i));
1902                }
1903                run_start = None;
1904            }
1905            _ => {}
1906        });
1907        if let Some(rs) = run_start
1908            && n - rs >= min_len
1909        {
1910            raw.push(whitespace_free_span(input, rs, n));
1911        }
1912        merge_spans(raw)
1913    }
1914
1915    /// The spans found across leaves are the spans the pass over the whole
1916    /// input finds, with leaves small enough that spans cross the cuts many
1917    /// times over and at every length that decides a span.
1918    #[test]
1919    fn long_spans_across_leaves_are_the_one_pass_spans() {
1920        // This file's own bytes: real code, whitespace every few bytes, a few
1921        // runs long enough to be spans at the blob length.
1922        let text: &[u8] = include_bytes!("spectral.rs");
1923        let n = text.len();
1924        for min_len in [1usize, 3, 8, BLOB_MIN_LEN, 200] {
1925            let want = long_spans(text, 0, n, min_len);
1926            assert!(!want.is_empty() || min_len == 200, "min_len {min_len}: the file holds spans that long");
1927            for least in [1usize, 5, 64, 1000, 4096, ENTROPY_FLAGS_MIN_LEAF] {
1928                assert_eq!(long_spans_across(text, min_len, least), want, "min_len {min_len}, {least} bytes a leaf");
1929            }
1930        }
1931    }
1932
1933    /// The runs found across leaves are the runs the pass over whole spans
1934    /// finds, with leaves small enough that a span is cut many times and a
1935    /// piece's reach ends inside runs, over this file's bytes and over blobs
1936    /// up to far longer than a leaf.
1937    #[test]
1938    fn runs_over_spans_across_leaves_are_the_whole_span_runs() {
1939        let text: &[u8] = include_bytes!("spectral.rs");
1940        let mut blobbed = Vec::new();
1941        for i in 0..12 {
1942            blobbed.extend_from_slice(b"the quick brown fox jumps over the lazy dog ");
1943            blobbed.extend(hi_entropy_blob(3000 + 700 * i));
1944            blobbed.push(b' ');
1945        }
1946        blobbed.extend(hi_entropy_blob(200_000));
1947        blobbed.push(b'\n');
1948        blobbed.extend_from_slice(text);
1949        for (name, input) in [("this file", text), ("blobs then this file", blobbed.as_slice())] {
1950            for min_len in [16usize, BLOB_MIN_LEN, 200] {
1951                let spans = long_spans(input, 0, input.len(), min_len);
1952                let want = runs_over_spans(input, &spans, BLOB_ENTROPY_PCT, min_len);
1953                assert!(
1954                    !want.is_empty() || name == "this file",
1955                    "{name}, min_len {min_len}: the blobs are runs to find"
1956                );
1957                for least in [1usize, 7, 100, 4096, ENTROPY_RUNS_MIN_LEAF] {
1958                    assert_eq!(
1959                        runs_over_spans_across(input, &spans, BLOB_ENTROPY_PCT, min_len, least),
1960                        want,
1961                        "{name}, min_len {min_len}, {least} bytes a leaf"
1962                    );
1963                }
1964            }
1965        }
1966    }
1967
1968    #[test]
1969    fn long_spans_are_the_bytes_whose_span_reaches_min_len() {
1970        // Every byte of the range is in a reported part exactly when the
1971        // whitespace-free span holding it is at least min_len long, whether
1972        // that span begins before the range or ends past it.
1973        let mut input = Vec::new();
1974        for len in [1usize, 7, 47, 48, 49, 60, 100, 47, 48, 3] {
1975            input.extend(std::iter::repeat_n(b'x', len));
1976            input.push(if len % 2 == 0 { b' ' } else { b'\n' });
1977        }
1978        input.extend(std::iter::repeat_n(b'y', 130));
1979        let n = input.len();
1980        let in_long = |p: usize| {
1981            let mut a = p;
1982            while a > 0 && !input[a - 1].is_ascii_whitespace() {
1983                a -= 1;
1984            }
1985            let mut b = p;
1986            while b < n && !input[b].is_ascii_whitespace() {
1987                b += 1;
1988            }
1989            !input[p].is_ascii_whitespace() && b - a >= 48
1990        };
1991        for (from, to) in
1992            [(0, n), (0, 10), (5, 70), (60, 61), (100, 150), (120, n), (n - 20, n), (n - 1, n), (200, 200)]
1993        {
1994            let parts = long_spans(&input, from, to, 48);
1995            let mut covered = vec![false; n];
1996            for &(a, b) in &parts {
1997                assert!(from <= a && a < b && b <= to, "part [{a}, {b}) outside [{from}, {to})");
1998                for c in &mut covered[a..b] {
1999                    *c = true;
2000                }
2001            }
2002            for (p, &c) in covered.iter().enumerate().take(to).skip(from) {
2003                assert_eq!(c, in_long(p), "byte {p} of [{from}, {to})");
2004            }
2005        }
2006    }
2007
2008    #[test]
2009    fn runs_over_long_spans_alone_equal_runs_over_every_byte() {
2010        // Blobs at both sides of min_len, at the input's ends, split by one
2011        // space, and buried in prose: the gated pass and a pass over every
2012        // byte must return the same runs, serially and across cores.
2013        let mut input = Vec::new();
2014        input.extend(hi_entropy_blob(300));
2015        for (i, len) in [40usize, 47, 48, 49, 60, 111, 112, 113, 200, 300].into_iter().enumerate() {
2016            input.extend_from_slice(b" the quick brown fox jumps over the lazy dog ");
2017            input.extend(hi_entropy_blob(len));
2018            if i % 3 == 1 {
2019                input.push(b' ');
2020                input.extend(hi_entropy_blob(len));
2021            }
2022            input.push(if i % 2 == 0 { b'\n' } else { b'\t' });
2023        }
2024        input.extend_from_slice(b"tail text");
2025        input.extend(hi_entropy_blob(300));
2026        for min_len in [16usize, 48, 64, 200] {
2027            assert_eq!(
2028                high_entropy_runs(&input, BLOB_ENTROPY_PCT, min_len),
2029                runs_from_every_flag(&input, BLOB_ENTROPY_PCT, min_len),
2030                "min_len {min_len}"
2031            );
2032        }
2033
2034        // An input with no whitespace anywhere is one span, and every run
2035        // inside it expands to the same bounds. The expansion walks a byte at
2036        // a time to the nearest whitespace either side, so here it reaches the
2037        // input's own ends: reading it once a run rather than once a span made
2038        // the pass quadratic in the number of runs, and 19 MB of this shape
2039        // took minutes where the same bytes carrying newlines took
2040        // milliseconds.
2041        let mut unbroken = Vec::new();
2042        while unbroken.len() < 64 * 1024 {
2043            unbroken.extend(hi_entropy_blob(64));
2044            unbroken.extend_from_slice(b"plain.text.between.the.blobs");
2045        }
2046        assert!(
2047            !unbroken.iter().any(u8::is_ascii_whitespace),
2048            "the corpus must hold no whitespace, or it does not read this case"
2049        );
2050        for min_len in [16usize, 48, 200] {
2051            assert_eq!(
2052                high_entropy_runs(&unbroken, BLOB_ENTROPY_PCT, min_len),
2053                runs_from_every_flag(&unbroken, BLOB_ENTROPY_PCT, min_len),
2054                "no whitespace anywhere, min_len {min_len}"
2055            );
2056        }
2057        let mut big = Vec::new();
2058        while big.len() < 4 * ENTROPY_FLAGS_MIN_LEAF {
2059            big.extend_from_slice(&input);
2060        }
2061        assert_eq!(
2062            high_entropy_runs_parallel(&big, BLOB_ENTROPY_PCT, BLOB_MIN_LEN),
2063            runs_from_every_flag(&big, BLOB_ENTROPY_PCT, BLOB_MIN_LEN)
2064        );
2065    }
2066
2067    #[cfg(target_arch = "x86_64")]
2068    #[test]
2069    #[allow(unsafe_code)]
2070    fn entropy_flags_ladder_agrees() {
2071        // Four rungs compiled from one body, so a divergence would mean the
2072        // instruction set changed the answer rather than the speed. Each is
2073        // run only where this CPU supports it; a rung the host cannot run
2074        // is reported as skipped rather than counted as agreeing, because a
2075        // ladder that silently tests one rung is a ladder nobody checked.
2076        let mut input = Vec::new();
2077        for i in 0..60 {
2078            input.extend_from_slice(b"the quick brown fox jumps over the lazy dog ");
2079            if i % 5 == 2 {
2080                input.extend(hi_entropy_blob(48 + i));
2081                input.push(b' ');
2082            }
2083        }
2084        /// One entropy-flag implementation: the input, the span, the
2085        /// threshold, and the sink each flagged position goes to.
2086        type FlagPass = dyn Fn(&[u8], usize, usize, u8, &mut dyn FnMut(usize, bool));
2087        let flags = |f: &FlagPass| {
2088            let mut v = Vec::new();
2089            f(&input, 0, input.len(), BLOB_ENTROPY_PCT, &mut |i, h| v.push((i, h)));
2090            v
2091        };
2092        let scalar = flags(&|inp, a, b, t, e| entropy_flags_impl(inp, a, b, t, e));
2093        assert!(!scalar.is_empty(), "the corpus must produce flags, or this asserts nothing");
2094        assert!(scalar.iter().any(|&(_, h)| h), "some byte must flag high, or this is vacuous");
2095
2096        let mut ran = vec!["scalar"];
2097        if std::is_x86_feature_detected!("sse2") {
2098            // SAFETY: probed immediately above.
2099            let got = flags(&|inp, a, b, t, e| unsafe { entropy_flags_sse2(inp, a, b, t, e) });
2100            assert_eq!(got, scalar, "sse2 rung disagrees with the scalar body");
2101            ran.push("sse2");
2102        }
2103        if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("bmi2") {
2104            let got = flags(&|inp, a, b, t, e| unsafe { entropy_flags_avx2(inp, a, b, t, e) });
2105            assert_eq!(got, scalar, "avx2 rung disagrees with the scalar body");
2106            ran.push("avx2");
2107        }
2108        if std::is_x86_feature_detected!("avx512f") && std::is_x86_feature_detected!("avx512bw") {
2109            let got = flags(&|inp, a, b, t, e| unsafe { entropy_flags_avx512(inp, a, b, t, e) });
2110            assert_eq!(got, scalar, "avx512 rung disagrees with the scalar body");
2111            ran.push("avx512");
2112        }
2113        eprintln!("entropy_flags ladder rungs exercised on this host: {}", ran.join(", "));
2114    }
2115
2116    /// The flag pass written as the entropy expression per byte: the same
2117    /// window sum, converted through the same float steps at every position.
2118    /// The production pass compares a full window's sum to a boundary found
2119    /// once from this expression, and must agree with it flag for flag.
2120    fn entropy_flags_by_expression(input: &[u8], threshold_pct: u8) -> Vec<(usize, bool)> {
2121        let w = 64usize;
2122        let mut hist = [0u32; 256];
2123        let mut s = 0i64;
2124        let entropy_norm = (w.min(256) as f32).max(2.0).log2();
2125        let thr = f32::from(threshold_pct) / 100.0;
2126        let lut = fixed_log_table();
2127        let mut out = Vec::with_capacity(input.len());
2128        for i in 0..input.len() {
2129            let b = input[i] as usize;
2130            let nb = hist[b];
2131            hist[b] = nb + 1;
2132            s += fixed_term(lut, nb + 1) - fixed_term(lut, nb);
2133            if i >= w {
2134                let ob = input[i - w] as usize;
2135                let oc = hist[ob];
2136                hist[ob] = oc - 1;
2137                s -= fixed_term(lut, oc) - fixed_term(lut, oc - 1);
2138            }
2139            let nwin = f64::from((i + 1).min(w) as u32);
2140            let h = (nwin.log2() - s as f64 * (1.0 / (nwin * FIXED_LOG_SCALE))) as f32 / entropy_norm;
2141            out.push((i, h >= thr && !input[i].is_ascii_whitespace()));
2142        }
2143        out
2144    }
2145
2146    #[test]
2147    fn the_full_window_boundary_flags_exactly_as_the_expression_does() {
2148        // Prose, blobs of every length around the window, runs of one byte,
2149        // and every byte value in order, so the window sum crosses the
2150        // threshold from both sides many times and sits on it where it can.
2151        let mut input = Vec::new();
2152        for i in 0..80 {
2153            input.extend_from_slice(b"the quick brown fox jumps over the lazy dog ");
2154            match i % 4 {
2155                0 => input.extend(hi_entropy_blob(40 + i)),
2156                1 => input.extend(std::iter::repeat_n(b'x', i)),
2157                2 => input.extend((0..=255u8).skip(i % 7)),
2158                _ => input.extend(hi_entropy_blob(16).iter().chain(b"aaaa").copied()),
2159            }
2160            input.push(b' ');
2161        }
2162        for pct in [50u8, 70, BLOB_ENTROPY_PCT, 95, 100] {
2163            let want = entropy_flags_by_expression(&input, pct);
2164            let mut got = Vec::new();
2165            entropy_flags(&input, 0, input.len(), pct, |i, h| got.push((i, h)));
2166            assert_eq!(got, want, "threshold {pct}: the pass disagrees with the expression");
2167            let highs = want.iter().filter(|(_, h)| *h).count();
2168            assert!(pct == 100 || (highs > 0 && highs < want.len()), "threshold {pct} flags {highs} of {}, so the comparison asserts little", want.len());
2169        }
2170    }
2171
2172    #[test]
2173    fn entropy_flags_from_mid_stream_read_what_the_whole_stream_reads() {
2174        // Prose with blobs dropped in, so the window crosses every kind of
2175        // edge; a pass started at any offset must flag each byte exactly as
2176        // the pass from the start does, which is the fact the parallel form
2177        // rests on.
2178        let mut input = Vec::new();
2179        for i in 0..40 {
2180            input.extend_from_slice(b"the quick brown fox jumps over the lazy dog ");
2181            if i % 7 == 3 {
2182                input.extend(hi_entropy_blob(60 + i));
2183                input.push(b' ');
2184            }
2185        }
2186        let n = input.len();
2187        let mut whole = vec![false; n];
2188        entropy_flags(&input, 0, n, BLOB_ENTROPY_PCT, |i, h| whole[i] = h);
2189        for from in [1, 17, 63, 64, 65, 200, 777, n / 2, n - 70, n - 1] {
2190            let mut part = vec![false; n];
2191            entropy_flags(&input, from, n, BLOB_ENTROPY_PCT, |i, h| part[i] = h);
2192            assert_eq!(&part[from..], &whole[from..], "flags from {from}");
2193        }
2194        // The parallel runs over an input long enough to split are the
2195        // serial runs.
2196        let mut big = Vec::new();
2197        while big.len() < 4 * ENTROPY_FLAGS_MIN_LEAF {
2198            big.extend_from_slice(&input);
2199        }
2200        assert_eq!(
2201            high_entropy_runs_parallel(&big, BLOB_ENTROPY_PCT, BLOB_MIN_LEN),
2202            high_entropy_runs(&big, BLOB_ENTROPY_PCT, BLOB_MIN_LEN),
2203        );
2204    }
2205
2206    #[test]
2207    fn regions_surface_distinct_textures() {
2208        // Prose then a high-bit binary blob: the class flip (alpha prose ->
2209        // high-byte data) plus the entropy rise reliably separates them into
2210        // distinct regions, the blob reading as Data.
2211        let mut doc = Vec::new();
2212        for _ in 0..3 {
2213            doc.extend_from_slice(b"the quick brown fox jumps over the lazy dog near the old stone bridge today ");
2214        }
2215        let mut x = 0x9e37_79b9_7f4a_7c15u64;
2216        for _ in 0..300 {
2217            x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
2218            doc.push(0x80 | ((x >> 56) as u8 & 0x7f)); // 0x80..=0xFF, always high-bit
2219        }
2220        let field = analyze(&doc);
2221        let regs = regions(&field);
2222        let textures: std::collections::HashSet<Texture> = regs.iter().map(|r| r.2).collect();
2223        assert!(regs.len() >= 2, "expected multiple regions, got {regs:?}");
2224        assert!(textures.contains(&Texture::Data), "the blob region should read Data: {regs:?}");
2225    }
2226}