Skip to main content

trex/
context.rs

1//! The rolling context: every axis folded over a window that slides one unit
2//! at a time, at the token rung and the unit rung, and the agreement of the
3//! grains' own boundaries.
4//!
5//! [`crate::profile`] gives each axis a monoid and folds it up the tower; a
6//! fold is a prefix, though, and a position's context is a window - the last
7//! `w` tokens, the last `u` units. Refolding a window at every position is
8//! `w` combines a step. A monoid has no inverse, so the window cannot subtract
9//! the unit leaving it; what it can do is keep two stacks, one holding suffix
10//! folds of the older half and one a running fold of the newer half, so the
11//! window's fold is one combine and each unit is folded at most twice in its
12//! life. That is [`Window`], and it needs nothing of an axis beyond
13//! associativity.
14//!
15//! The context at a token is the fold of the token window ending there; the
16//! context at a unit is the fold of the unit window ending there, where each
17//! unit's own reading is the fold of its tokens - the same fold, one rung up,
18//! which is what the tower promises.
19//!
20//! The grains also each place boundaries: the byte grain's spectral
21//! change-points, the token grain's shape change-points and seam cuts, the
22//! unit grain's unit starts. [`Agreement`] reads, per unit, which lower-grain
23//! boundaries fall at its start. It compares the same reading - a regime
24//! change - re-grounded at three grains, never two different axes, so it has
25//! units; a shuffled stream is the null it is tested against.
26
27use crate::profile::{
28    AxisCtx, AxisProfile, EchoProfile, FlowProfile, MagnitudeProfile, ObservationProfile,
29    SeamProfile, SpectralProfile, StressProfile,
30};
31use crate::supertoken::{Role, SuperContext, SuperToken};
32use crate::token::Token;
33
34/// A sliding window over a monoid: the fold of the last `capacity` pushed
35/// values, in push order, with each value folded at most twice.
36///
37/// `back` holds the newer values with `back_fold` their running fold; `front`
38/// holds suffix folds of the older values, oldest last, so the oldest value's
39/// entry is the fold of everything in `front`. When `front` runs out the
40/// whole of `back` moves across as suffix folds. The window's fold is
41/// `front.last() ⊕ back_fold`.
42pub struct Window<P: AxisProfile> {
43    capacity: usize,
44    back: Vec<P>,
45    back_fold: P,
46    front: Vec<P>,
47}
48
49impl<P: AxisProfile> Window<P> {
50    /// An empty window that holds at most `capacity` values.
51    #[must_use]
52    pub fn new(capacity: usize) -> Self {
53        let capacity = capacity.max(1);
54        Window {
55            capacity,
56            back: Vec::with_capacity(capacity),
57            back_fold: P::identity(),
58            front: Vec::with_capacity(capacity),
59        }
60    }
61
62    /// Values in the window.
63    #[must_use]
64    pub fn len(&self) -> usize {
65        self.front.len() + self.back.len()
66    }
67
68    /// Whether the window holds nothing.
69    #[must_use]
70    pub fn is_empty(&self) -> bool {
71        self.len() == 0
72    }
73
74    /// Push the newest value, dropping the oldest when the window is full.
75    pub fn push(&mut self, value: P) {
76        if self.len() == self.capacity {
77            self.pop();
78        }
79        self.back_fold = self.back_fold.combine(&value);
80        self.back.push(value);
81    }
82
83    /// Drop the oldest value.
84    fn pop(&mut self) {
85        if self.front.is_empty() {
86            let mut suffix = P::identity();
87            for value in self.back.iter().rev() {
88                suffix = value.combine(&suffix);
89                self.front.push(suffix.clone());
90            }
91            self.back.clear();
92            self.back_fold = P::identity();
93        }
94        self.front.pop();
95    }
96
97    /// The fold of the window, oldest to newest.
98    #[must_use]
99    pub fn fold(&self) -> P {
100        match self.front.last() {
101            Some(older) => older.combine(&self.back_fold),
102            None => self.back_fold.clone(),
103        }
104    }
105}
106
107/// The axes whose readings are a fixed number of words, bundled: what a
108/// rolling window folds at a constant cost per step.
109///
110/// The shape axis is left out on purpose. Its monoid is the free monoid, a
111/// span's silhouette being its tokens' silhouettes in order, so a window of
112/// it is the silhouette slice itself and folding would copy it at every step.
113#[derive(Clone, Debug, PartialEq)]
114pub struct WindowProfile {
115    /// Scale.
116    pub magnitude: MagnitudeProfile,
117    /// Structural load.
118    pub stress: StressProfile,
119    /// Temporal texture.
120    pub spectral: SpectralProfile,
121    /// Recurrence.
122    pub echo: EchoProfile,
123    /// Vantage: what the past, the future and a centered view each said.
124    pub observation: ObservationProfile,
125    /// Segmentation, read in both directions.
126    pub seam: SeamProfile,
127    /// Dynamics.
128    pub flow: FlowProfile,
129}
130
131impl AxisProfile for WindowProfile {
132    fn identity() -> Self {
133        WindowProfile {
134            magnitude: MagnitudeProfile::identity(),
135            stress: StressProfile::identity(),
136            spectral: SpectralProfile::identity(),
137            echo: EchoProfile::identity(),
138            observation: ObservationProfile::identity(),
139            seam: SeamProfile::identity(),
140            flow: FlowProfile::identity(),
141        }
142    }
143
144    fn combine(&self, other: &Self) -> Self {
145        WindowProfile {
146            magnitude: self.magnitude.combine(&other.magnitude),
147            stress: self.stress.combine(&other.stress),
148            spectral: self.spectral.combine(&other.spectral),
149            echo: self.echo.combine(&other.echo),
150            observation: self.observation.combine(&other.observation),
151            seam: self.seam.combine(&other.seam),
152            flow: self.flow.combine(&other.flow),
153        }
154    }
155
156    fn of_token(idx: usize, tok: &Token, ctx: &AxisCtx<'_>) -> Self {
157        WindowProfile {
158            magnitude: MagnitudeProfile::of_token(idx, tok, ctx),
159            stress: StressProfile::of_token(idx, tok, ctx),
160            spectral: SpectralProfile::of_token(idx, tok, ctx),
161            echo: EchoProfile::of_token(idx, tok, ctx),
162            observation: ObservationProfile::of_token(idx, tok, ctx),
163            seam: SeamProfile::of_token(idx, tok, ctx),
164            flow: FlowProfile::of_token(idx, tok, ctx),
165        }
166    }
167}
168
169/// Window extents, in units of the rung each applies to.
170#[derive(Clone, Copy, Debug)]
171pub struct ContextConfig {
172    /// Significant tokens the token-rung window spans, the current one
173    /// included.
174    pub token_window: usize,
175    /// Units the unit-rung window spans, the current one included.
176    pub unit_window: usize,
177}
178
179impl Default for ContextConfig {
180    fn default() -> Self {
181        // The token window is the observation reader's, so the two readings
182        // of a position span the same tokens. The unit window covers the
183        // same stretch one rung up: the code corpus in `benches/axis_grain`
184        // lexes to 16500 significant tokens in 4000 units, four to a unit.
185        let token_window = crate::observation::ObservationConfig::default().window;
186        ContextConfig { token_window, unit_window: (token_window / 4).max(1) }
187    }
188}
189
190/// Where each lower grain's nearest boundary sits relative to a unit's
191/// start, in significant tokens: negative before it, zero at it, positive
192/// after. `None` when that grain placed no boundary at all.
193///
194/// A distance rather than a hit: a grain's reader lands its boundary at a
195/// fixed offset from the construct it reads - the token-grain seam cuts one
196/// token into a statement on the code corpus, never at its first token - and
197/// what says the grains are reading the same structure is that the offset
198/// repeats, not that it is zero.
199#[derive(Clone, Copy, Debug, PartialEq, Eq)]
200pub struct Agreement {
201    /// The unit's role, which is what its boundary offsets are read against:
202    /// a grain reads a call and a binding at different places.
203    pub role: Role,
204    /// The byte grain's nearest spectral change-point.
205    pub regime: Option<i32>,
206    /// The token grain's nearest shape change-point.
207    pub shape: Option<i32>,
208    /// The token grain's nearest seam cut.
209    pub seam: Option<i32>,
210}
211
212/// The rolling context of a stream.
213#[derive(Clone, Debug, Default)]
214pub struct ContextField {
215    /// The token-rung window's fold at each token index. A whitespace token
216    /// carries the reading of the significant token before it.
217    pub at_token: Vec<WindowProfile>,
218    /// Each unit's own reading: the fold of its tokens.
219    pub of_unit: Vec<WindowProfile>,
220    /// The unit-rung window's fold at each unit index.
221    pub at_unit: Vec<WindowProfile>,
222    /// Per unit, which lower grains place a boundary at its start.
223    pub agreement: Vec<Agreement>,
224}
225
226impl ContextField {
227    /// How consistently each lower grain's boundaries sit at one offset from
228    /// the starts of units of one role: the share of units whose nearest
229    /// boundary of that grain lies at the modal offset for their role, as
230    /// `(regime, shape, seam)`, each in `[0, 1]`. A grain that reads the
231    /// same construct the unit rung reads lands at one place per role; on a
232    /// stream with no construct to read its offsets spread. Zeros for no
233    /// units.
234    #[must_use]
235    pub fn alignment(&self) -> (f32, f32, f32) {
236        let n = self.agreement.len();
237        if n == 0 {
238            return (0.0, 0.0, 0.0);
239        }
240        let modal_share = |offset: fn(&Agreement) -> Option<i32>| {
241            let mut counts: std::collections::HashMap<(u32, i32), u32> =
242                std::collections::HashMap::new();
243            for a in &self.agreement {
244                if let Some(o) = offset(a) {
245                    *counts.entry((a.role.code(), o)).or_insert(0) += 1;
246                }
247            }
248            let mut modal: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
249            for (&(role, _), &c) in &counts {
250                let m = modal.entry(role).or_insert(0);
251                *m = (*m).max(c);
252            }
253            modal.values().sum::<u32>() as f32 / n as f32
254        };
255        (modal_share(|a| a.regime), modal_share(|a| a.shape), modal_share(|a| a.seam))
256    }
257}
258
259/// Fold the rolling windows over an already-lexed stream.
260///
261/// `ctx` carries the axis fields the readings draw on; a field left out reads
262/// as that axis's identity, as in [`crate::profile`]. `supers` is the unit
263/// rung. The agreement is not read here: it needs the boundary fields, which
264/// [`agreement`] takes.
265#[must_use]
266pub fn fold_windows(
267    toks: &[Token],
268    ctx: &AxisCtx<'_>,
269    supers: &SuperContext,
270    cfg: &ContextConfig,
271) -> ContextField {
272    fold_range(toks, ctx, supers, cfg, 0..toks.len(), 0)
273}
274
275/// Significant tokens a leaf of the parallel fold covers, at the least. A
276/// leaf then takes about what a leaf of the parallel blob pass takes - 128
277/// microseconds, at the 90 nanoseconds a token `benches/context_window`
278/// measures - so the dispatch is amortised the same way.
279const FOLD_MIN_LEAF_TOKENS: usize = 1024;
280
281/// [`fold_windows`] across cores.
282///
283/// The stream is cut at unit starts into a few chunks per core, and each
284/// chunk seeds its token window from the tokens before it and its unit
285/// window from the units before it, so it reads what the one-thread walk
286/// reads there. The readings agree with the serial fold up to the
287/// association of a floating sum, which the seeded window brackets
288/// differently.
289#[must_use]
290pub fn fold_windows_parallel(
291    toks: &[Token],
292    ctx: &AxisCtx<'_>,
293    supers: &SuperContext,
294    cfg: &ContextConfig,
295) -> ContextField {
296    use flynnel::JobPlan;
297    use flynnel::sched::par_iter::for_each_chunk_indexed_min_leaf;
298
299    let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
300    let n_units = supers.units.len();
301    let units_per_chunk = n_units.div_ceil(cores * 4).max(1);
302    // Chunks as (first unit, first token) pairs; a chunk runs to the next
303    // chunk's first token. A chunk holds whole units, at least
304    // `units_per_chunk` of them and at least a leaf's worth of tokens.
305    let mut chunks: Vec<(usize, usize)> = Vec::new();
306    let mut u = 0usize;
307    while u < n_units {
308        let tok_lo = first_token(toks, supers, u);
309        chunks.push((u, tok_lo));
310        let mut v = u + 1;
311        while v < n_units
312            && (v - u < units_per_chunk || first_token(toks, supers, v) - tok_lo < FOLD_MIN_LEAF_TOKENS)
313        {
314            v += 1;
315        }
316        u = v;
317    }
318    if chunks.len() <= 1 || cores <= 1 {
319        return fold_windows(toks, ctx, supers, cfg);
320    }
321    let bounds: Vec<(usize, usize, usize)> = chunks
322        .iter()
323        .enumerate()
324        .map(|(k, &(unit_lo, tok_lo))| {
325            let tok_hi = chunks.get(k + 1).map_or(toks.len(), |&(_, t)| t);
326            (unit_lo, tok_lo, tok_hi)
327        })
328        .collect();
329    let mut parts: Vec<ContextField> = bounds.iter().map(|_| ContextField::default()).collect();
330    // A leaf reads each token's bytes and axis frames once and writes a
331    // reading per token: streaming work, and the shape is what says so. No
332    // cost is named here because none was ever measured for it, and a named
333    // one replaces the scheduler's own probe rather than informing it.
334    let plan = JobPlan::new(0, bounds.len() as u32)
335        .with_leaf_shape(flynnel::LeafShape::Streaming);
336    for_each_chunk_indexed_min_leaf(&plan, &mut parts, 1, |start, slots| {
337        for (i, slot) in slots.iter_mut().enumerate() {
338            let (unit_lo, tok_lo, tok_hi) = bounds[start + i];
339            *slot = fold_range(toks, ctx, supers, cfg, tok_lo..tok_hi, unit_lo);
340        }
341    });
342    // The first chunk starts at the first unit; tokens before it carry the
343    // empty reading, as in the serial walk.
344    let lead = bounds.first().map_or(0, |b| b.1);
345    let mut out = ContextField {
346        at_token: Vec::with_capacity(toks.len()),
347        of_unit: Vec::with_capacity(n_units),
348        at_unit: Vec::with_capacity(n_units),
349        agreement: Vec::new(),
350    };
351    out.at_token.extend(std::iter::repeat_n(WindowProfile::identity(), lead));
352    for part in parts {
353        out.at_token.extend(part.at_token);
354        out.of_unit.extend(part.of_unit);
355        out.at_unit.extend(part.at_unit);
356    }
357    out
358}
359
360/// The index of unit `u`'s first token.
361fn first_token(toks: &[Token], supers: &SuperContext, u: usize) -> usize {
362    toks.partition_point(|t| t.start() < supers.units[u].start)
363}
364
365/// The fold over `tokens`, a range starting at a unit's first token (or at
366/// the stream's start), with `unit_lo` the unit that starts there. The
367/// windows are seeded from what precedes the range so the readings inside it
368/// are those of the whole-stream walk.
369fn fold_range(
370    toks: &[Token],
371    ctx: &AxisCtx<'_>,
372    supers: &SuperContext,
373    cfg: &ContextConfig,
374    tokens: std::ops::Range<usize>,
375    unit_lo: usize,
376) -> ContextField {
377    let mut window: Window<WindowProfile> = Window::new(cfg.token_window);
378    // The token window holds the significant tokens before the range, as
379    // many as it can, oldest first.
380    let seed: Vec<usize> = (0..tokens.start)
381        .rev()
382        .filter(|&i| toks[i].is_significant())
383        .take(cfg.token_window)
384        .collect();
385    for &i in seed.iter().rev() {
386        window.push(WindowProfile::of_token(i, &toks[i], ctx));
387    }
388    let mut current = window.fold();
389    // Each unit's tokens fold as the walk passes them, so a unit's reading is
390    // ready when its last token is; the unit window takes it then. It is
391    // seeded with the units before the range, each folded from its tokens.
392    let mut unit_window: Window<WindowProfile> = Window::new(cfg.unit_window);
393    for u in unit_lo.saturating_sub(cfg.unit_window)..unit_lo {
394        let lo = first_token(toks, supers, u);
395        let hi = first_token(toks, supers, u + 1);
396        let reading = (lo..hi)
397            .filter(|&i| toks[i].is_significant() && supers.index_of(i) == Some(u))
398            .fold(WindowProfile::identity(), |acc, i| acc.combine(&WindowProfile::of_token(i, &toks[i], ctx)));
399        unit_window.push(reading);
400    }
401    let mut at_token: Vec<WindowProfile> = Vec::with_capacity(tokens.len());
402    let mut of_unit: Vec<WindowProfile> = Vec::new();
403    let mut at_unit: Vec<WindowProfile> = Vec::new();
404    let mut unit_fold = WindowProfile::identity();
405    let mut open_unit: Option<usize> = None;
406    let close_unit = |unit_fold: &mut WindowProfile,
407                      of_unit: &mut Vec<WindowProfile>,
408                      at_unit: &mut Vec<WindowProfile>,
409                      unit_window: &mut Window<WindowProfile>| {
410        let reading = std::mem::replace(unit_fold, WindowProfile::identity());
411        unit_window.push(reading.clone());
412        of_unit.push(reading);
413        at_unit.push(unit_window.fold());
414    };
415    for i in tokens {
416        let tok = &toks[i];
417        if tok.is_significant() {
418            let reading = WindowProfile::of_token(i, tok, ctx);
419            window.push(reading.clone());
420            current = window.fold();
421            match (supers.index_of(i), open_unit) {
422                (Some(u), Some(open)) if u == open => {
423                    unit_fold = unit_fold.combine(&reading);
424                }
425                (Some(u), open) => {
426                    if open.is_some() {
427                        close_unit(&mut unit_fold, &mut of_unit, &mut at_unit, &mut unit_window);
428                    }
429                    open_unit = Some(u);
430                    unit_fold = reading;
431                }
432                (None, Some(_)) => {
433                    close_unit(&mut unit_fold, &mut of_unit, &mut at_unit, &mut unit_window);
434                    open_unit = None;
435                }
436                (None, None) => {}
437            }
438        }
439        at_token.push(current.clone());
440    }
441    if open_unit.is_some() {
442        close_unit(&mut unit_fold, &mut of_unit, &mut at_unit, &mut unit_window);
443    }
444    ContextField { at_token, of_unit, at_unit, agreement: Vec::new() }
445}
446
447/// Per unit, where each lower grain's nearest boundary sits relative to the
448/// unit's start.
449///
450/// `regime_cuts` are the byte grain's change-points, `shape_cuts` the token
451/// grain's, both as byte offsets ascending; `seam_cuts` are the token grain's
452/// seam cuts as the byte offsets of token starts, ascending. Every boundary
453/// is placed on the significant token holding it, so the three offsets are
454/// in one unit whatever grain they came from.
455#[must_use]
456pub fn agreement(
457    units: &[SuperToken],
458    toks: &[Token],
459    regime_cuts: &[usize],
460    shape_cuts: &[usize],
461    seam_cuts: &[usize],
462) -> Vec<Agreement> {
463    // Significant-token starts, so a byte offset maps to the significant
464    // token holding it by one search.
465    let starts: Vec<usize> =
466        toks.iter().filter(|t| t.is_significant()).map(Token::start).collect();
467    let sig_index = |byte: usize| starts.partition_point(|&s| s <= byte).saturating_sub(1);
468    let place = |cuts: &[usize]| -> Vec<usize> { cuts.iter().map(|&c| sig_index(c)).collect() };
469    let (regime, shape, seam) = (place(regime_cuts), place(shape_cuts), place(seam_cuts));
470    let nearest = |placed: &[usize], at: usize| -> Option<i32> {
471        if placed.is_empty() {
472            return None;
473        }
474        let i = placed.partition_point(|&p| p < at);
475        let after = placed.get(i).map(|&p| p as i64 - at as i64);
476        let before = i.checked_sub(1).map(|j| placed[j] as i64 - at as i64);
477        let offset = match (before, after) {
478            (Some(b), Some(a)) => if a.abs() < b.abs() { a } else { b },
479            (Some(b), None) => b,
480            (None, Some(a)) => a,
481            (None, None) => return None,
482        };
483        Some(offset.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32)
484    };
485    units
486        .iter()
487        .map(|u| {
488            let at = sig_index(u.start);
489            Agreement {
490                role: u.role,
491                regime: nearest(&regime, at),
492                shape: nearest(&shape, at),
493                seam: nearest(&seam, at),
494            }
495        })
496        .collect()
497}
498
499/// The readings of a token's related context: one fold per relation that
500/// admits earlier tokens, each over every token the relation names.
501///
502/// A window admits by distance. These admit by relation, and because a
503/// reading is a monoid the context needs no members kept and none evicted:
504/// each is a running fold, exact and unbounded in range, that costs one
505/// combine when a token joins it.
506#[derive(Clone, Debug, PartialEq)]
507pub struct RelationReading {
508    /// The heads of the brackets enclosing the token, outermost first: the
509    /// path up the enclosure tree.
510    pub enclosing: WindowProfile,
511    /// Every earlier occurrence of the token's key, the token itself left
512    /// out; identity for a first appearance or an unkeyed token.
513    pub echoing: WindowProfile,
514    /// The significant tokens since the byte grain's last regime change,
515    /// the token itself left out.
516    pub regime: WindowProfile,
517    /// Every earlier significant token at the same phase of the dominant
518    /// token-kind period, the token itself left out; identity when the stream
519    /// has no period.
520    pub phase: WindowProfile,
521}
522
523/// The related context at every token.
524#[derive(Clone, Debug, Default)]
525pub struct RelationContext {
526    /// One reading per token index. A whitespace token carries the reading
527    /// of the significant token before it.
528    pub at_token: Vec<RelationReading>,
529    /// Per token index, the token's phase of the dominant period - its index
530    /// among the significant tokens modulo the period - or `u16::MAX` for an
531    /// insignificant token or a stream with no period.
532    pub phase_of: Vec<u16>,
533    /// Per token index, the fold over the values bound to earlier occurrences
534    /// of the token's key: for each earlier occurrence that is the left
535    /// operand of a binding punctuation (`=`, `:`), the reading of the right
536    /// operand. Identity for an unkeyed token, a first occurrence, or a key
537    /// never bound before.
538    pub value_history: Vec<WindowProfile>,
539    /// The dominant token-kind period the phase fold is keyed on, when the
540    /// stream has one.
541    pub period: Option<u16>,
542}
543
544impl Default for RelationReading {
545    fn default() -> Self {
546        RelationReading {
547            enclosing: WindowProfile::identity(),
548            echoing: WindowProfile::identity(),
549            regime: WindowProfile::identity(),
550            phase: WindowProfile::identity(),
551        }
552    }
553}
554
555/// Fold the related context over an already-lexed stream.
556///
557/// `regime_cuts` are the byte grain's change-points ascending; `echo` is
558/// the recurrence field, whose back lags name each token's previous
559/// occurrence; `period` is the record period over shape classes, from
560/// [`record_period`], which is what both call sites pass. The token readings
561/// are made across cores; the folds themselves are chains and run in one pass
562/// over them.
563#[must_use]
564pub fn relate(
565    toks: &[Token],
566    ctx: &AxisCtx<'_>,
567    regime_cuts: &[usize],
568    echo: &crate::echo::EchoField,
569    period: Option<u16>,
570) -> RelationContext {
571    use crate::token::TokenKind;
572
573    let n = toks.len();
574    let readings = token_readings(toks, ctx);
575    let mut at_token: Vec<RelationReading> = Vec::with_capacity(n);
576    let mut phase_of: Vec<u16> = vec![u16::MAX; n];
577    let mut value_history: Vec<WindowProfile> = vec![WindowProfile::identity(); n];
578    // The enclosure path as a stack of prefix folds: an open bracket pushes
579    // the fold so far combined with its head's reading, a close pops.
580    let mut enclosure: Vec<WindowProfile> = Vec::new();
581    let mut heads: Vec<bool> = Vec::new();
582    // Each keyed token's fold over its earlier occurrences, by token index,
583    // so the next occurrence extends it in one step; likewise the right
584    // operand each token binds, so a later occurrence of its key extends the
585    // key's value history in one step.
586    let mut echo_fold: Vec<WindowProfile> = vec![WindowProfile::identity(); n];
587    let mut operand_of: Vec<Option<usize>> = vec![None; n];
588    let starts: Vec<usize> = toks.iter().map(Token::start).collect();
589    let mut regime = WindowProfile::identity();
590    let mut next_cut = regime_cuts.partition_point(|&c| c == 0);
591    let p = period.map_or(0, usize::from);
592    let mut phase_fold: Vec<WindowProfile> = vec![WindowProfile::identity(); p];
593    let mut sig_index = 0usize;
594    let mut current = RelationReading::default();
595    let mut last_sig: Option<usize> = None;
596    for (i, tok) in toks.iter().enumerate() {
597        // The significant token before this one, which is what heads an
598        // opening bracket here and what a binding punctuation binds.
599        let prev_sig = last_sig;
600        if matches!(tok.kind, TokenKind::Close(_)) && heads.pop().is_some() {
601            enclosure.pop();
602        }
603        if tok.is_significant() {
604            let reading = &readings[i];
605            // A binding punctuation with an operand on each side binds the
606            // right one to the left one, as the relation tier reads it.
607            if tok.kind == TokenKind::Punct
608                && matches!(&ctx.bytes[tok.span()], b"=" | b":")
609                && let Some(l) = prev_sig
610                && let Some(r) = (i + 1..n).find(|&j| toks[j].is_significant())
611            {
612                operand_of[l] = Some(r);
613            }
614            // The regime fold resets at a change-point the token start has
615            // passed; the reading is what came before the token.
616            while next_cut < regime_cuts.len() && regime_cuts[next_cut] <= tok.start() {
617                regime = WindowProfile::identity();
618                next_cut += 1;
619            }
620            let regime_before = regime.clone();
621            regime = regime.combine(reading);
622            let frame = echo.frames.get(i);
623            let echoing = match frame.and_then(|f| f.back_lag) {
624                Some(lag) => {
625                    let prev = starts.partition_point(|&s| s < tok.start() - lag.get() as usize);
626                    let fold = echo_fold[prev].combine(&readings[prev]);
627                    echo_fold[i] = fold.clone();
628                    let bound = &value_history[prev];
629                    value_history[i] = match operand_of[prev] {
630                        Some(r) => bound.combine(&readings[r]),
631                        None => bound.clone(),
632                    };
633                    fold
634                }
635                None => WindowProfile::identity(),
636            };
637            let phase = if p > 0 {
638                let k = sig_index % p;
639                phase_of[i] = k as u16;
640                let before = phase_fold[k].clone();
641                phase_fold[k] = before.combine(reading);
642                before
643            } else {
644                WindowProfile::identity()
645            };
646            sig_index += 1;
647            current = RelationReading {
648                enclosing: enclosure.last().cloned().unwrap_or_else(WindowProfile::identity),
649                echoing,
650                regime: regime_before,
651                phase,
652            };
653            last_sig = Some(i);
654        }
655        if matches!(tok.kind, TokenKind::Open(_)) {
656            // The head is the significant word just before the bracket, as
657            // the relation tier reads it.
658            let head = prev_sig.filter(|&j| matches!(toks[j].kind, TokenKind::Word));
659            let below = enclosure.last().cloned().unwrap_or_else(WindowProfile::identity);
660            enclosure.push(match head {
661                Some(h) => below.combine(&readings[h]),
662                None => below,
663            });
664            heads.push(true);
665        }
666        at_token.push(current.clone());
667    }
668    RelationContext { at_token, phase_of, value_history, period }
669}
670
671/// Every token's reading, made across cores; an insignificant token's is the
672/// identity.
673fn token_readings(toks: &[Token], ctx: &AxisCtx<'_>) -> Vec<WindowProfile> {
674    use flynnel::JobPlan;
675    use flynnel::sched::par_iter::for_each_chunk_indexed_min_leaf;
676
677    let mut readings: Vec<WindowProfile> = vec![WindowProfile::identity(); toks.len()];
678    let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
679    let min_leaf = toks.len().div_ceil(cores * 4).max(FOLD_MIN_LEAF_TOKENS);
680    let plan = JobPlan::new(0, toks.len() as u32)
681        .with_leaf_shape(flynnel::LeafShape::Streaming);
682    for_each_chunk_indexed_min_leaf(&plan, &mut readings, min_leaf, |start, slots| {
683        for (k, slot) in slots.iter_mut().enumerate() {
684            let i = start + k;
685            if toks[i].is_significant() {
686                *slot = WindowProfile::of_token(i, &toks[i], ctx);
687            }
688        }
689    });
690    readings
691}
692
693/// The related context of `bytes` with every field it reads built.
694#[must_use]
695pub fn relate_bytes(bytes: &[u8]) -> RelationContext {
696    let toks = crate::lexer::lex(bytes);
697    let spectral = crate::spectral::analyze(bytes);
698    let stress = crate::stress::analyze(&toks, bytes);
699    let echo = crate::echo::analyze(&toks, bytes);
700    let ctx = AxisCtx {
701        spectral: Some(&spectral),
702        stress: Some(&stress),
703        echo: Some(&echo),
704        ..AxisCtx::new(bytes)
705    };
706    relate(&toks, &ctx, &spectral.boundaries, &echo, record_period(&toks, bytes))
707}
708
709/// Longest record period searched, in significant tokens: the bank the axis
710/// benches read the token stream with.
711pub const PHASE_MAX_PERIOD: u16 = 32;
712
713/// How far above chance a lag's share sits before the reading is kept.
714///
715/// This gate sits beside `spectral::PERIOD_FLOOR` rather than replacing it,
716/// and they refuse different things: the floor demands that the stream repeat
717/// at all, this demands that it repeat more than its own alphabet repeats by
718/// coincidence. The floor alone cannot do the second, because the share a lag
719/// reaches by chance is [`record_period_chance`], which moves with the
720/// silhouette alphabet - 0.10 on code, 0.30 on prose, 0.41 on a log - so a
721/// single share sits above chance on one corpus and below it on another. At
722/// 0.20 it is below the chance rate of prose, a log and a table, and on those
723/// every lag clears it.
724///
725/// Measured over seven real corpora, whole and in slices at 64k, 256k and 1M
726/// by `examples/what_the_resonant_period_changes`: prose reads 1.03 to 1.24 at
727/// every size and offset, and every corpus with structure in it reads 1.61 and
728/// up, with nothing between. This sits in that gap. The gap is clean but it is
729/// seven corpora, so a genre that reads between them is the thing that would
730/// move it.
731pub const PERIOD_LIFT_FLOOR: f32 = 1.4;
732
733/// The period of the stream's records, in significant tokens, when it has
734/// one: the phase fold's key.
735///
736/// The lag at which the stream of token silhouettes best repeats itself:
737/// the share of positions whose silhouette equals the one a lag later, at
738/// the smallest lag reaching the largest share. Silhouettes rather than kinds,
739/// because in kinds a record `word , number , word ;` is an alternation of
740/// content and punctuation and would read as two; the silhouette carries the
741/// glyph. The smallest lag, because a record's multiples repeat as well as it
742/// does. A resonator bank cannot make this reading: every divisor of the
743/// record length is as coherent as the length itself.
744///
745/// The lag is kept only where the stream repeats at it more than chance would,
746/// by [`PERIOD_LIFT_FLOOR`]. The share alone cannot order these corpora: prose
747/// reaches 0.35 where code reaches 0.20, so an absolute floor rates prose the
748/// more periodic of the two. Against its own chance rate prose reads 1.03 and
749/// code 1.61.
750#[must_use]
751pub fn record_period(toks: &[Token], bytes: &[u8]) -> Option<u16> {
752    gated_lag(&period_symbols(toks, bytes))
753}
754
755/// The strongest lag, kept only where the stream repeats at it more than
756/// chance would. The reading every grain's period goes through.
757fn gated_lag(symbols: &[u32]) -> Option<u16> {
758    let (lag, share) = best_lag(symbols)?;
759    let chance = chance_of(symbols);
760    // No symbol repeats at all, so a share above zero is structure rather than
761    // coincidence and there is no baseline to divide by.
762    if chance <= 0.0 {
763        return Some(lag);
764    }
765    (share / chance >= PERIOD_LIFT_FLOOR).then_some(lag)
766}
767
768/// The record period in supertokens, which is the grain a record is actually
769/// found at.
770///
771/// A lag is counted in symbols and the search stops at [`PHASE_MAX_PERIOD`],
772/// so the grain settles what can be FOUND and not only what is read. A clippy
773/// log runs to a median of 74 significant tokens a line against a ceiling of
774/// 32, so no token-grain lag can be its record and [`record_period`] returns
775/// the record's factors instead - 2, 4, 6 and 8, ranked by how short they are.
776/// The same line is 1.64 supertokens.
777///
778/// The role alone does not carry it. Six roles put the chance rate at 0.609,
779/// which is most of what a share can reach, and the strongest unit lag reads
780/// 1.3 times chance - under the gate. Folding the unit's size in by its
781/// magnitude widens the alphabet without keying on a length that varies run to
782/// run, and separates them: on that log the chance rate falls to 0.099 and lag
783/// 20 reads 3.7 times it, clear of the next reading at 1.9. Twenty units is
784/// about twelve lines, which is a diagnostic block - the log's actual record.
785///
786/// Measured over the real corpora beside prose, which still refuses at 1.1,
787/// and a table whose lines are too long to hold units at all, which refuses
788/// too. So this reaches further rather than admitting more.
789#[must_use]
790pub fn unit_record_period(units: &[crate::supertoken::SuperToken]) -> Option<u16> {
791    let symbols = unit_symbols(units);
792    // Units that are all one symbol are period one, and no correlation can say
793    // so: every lag reaches a share of 1.0 and the chance rate is 1.0 with
794    // them, so the ratio is 1.0 and the gate refuses every lag. A uniform log,
795    // where every line is the same shape at the same magnitude, is exactly
796    // this - and its record is one line. The reading is exact rather than a
797    // second floor: one distinct symbol IS a period of one.
798    if symbols.len() > 1 && symbols.iter().all(|s| *s == symbols[0]) {
799        return Some(1);
800    }
801    gated_lag(&symbols)
802}
803
804/// A supertoken's symbol: its role with its size folded in by magnitude.
805///
806/// The magnitude rather than the length, because a record repeats in shape and
807/// not in exact width - two diagnostic blocks differ by a few characters and
808/// must read as the same symbol, while a block and a bare continuation line
809/// must not.
810fn unit_symbols(units: &[crate::supertoken::SuperToken]) -> Vec<u32> {
811    units
812        .iter()
813        .map(|u| {
814            let len = u.end.saturating_sub(u.start).max(1);
815            u.role.code() * 8 + len.ilog2().min(7)
816        })
817        .collect()
818}
819
820/// The token silhouettes the period readings are taken over.
821fn period_symbols(toks: &[Token], bytes: &[u8]) -> Vec<u32> {
822    toks.iter()
823        .filter(|t| t.is_significant())
824        .map(|t| crate::shape::shape_class(t.kind, &bytes[t.span()]))
825        .collect()
826}
827
828/// The smallest lag reaching the largest share above
829/// [`crate::spectral::PERIOD_FLOOR`].
830///
831/// The absolute floor and the lift gate refuse different things and both are
832/// needed. The floor demands that the stream repeat at all; the lift demands
833/// that it repeat more than its own alphabet repeats by coincidence. Dropping
834/// the floor and keeping only the lift admits any short lag standing above a
835/// low chance rate - code's lag of two reaches a share of 0.16 against a
836/// chance rate of 0.10, which is 1.6 times chance and is still an alternation
837/// of identifier and punctuation rather than a record.
838fn best_lag(symbols: &[u32]) -> Option<(u16, f32)> {
839    let n = symbols.len();
840    let mut best: Option<(u16, f32)> = None;
841    for lag in 2..=usize::from(PHASE_MAX_PERIOD) {
842        if lag * 2 > n {
843            break;
844        }
845        let matches = symbols.iter().zip(&symbols[lag..]).filter(|(a, b)| a == b).count();
846        let share = matches as f32 / (n - lag) as f32;
847        if share >= crate::spectral::PERIOD_FLOOR && best.is_none_or(|(_, b)| share > b) {
848            best = Some((lag as u16, share));
849        }
850    }
851    best
852}
853
854/// The chance two positions hold the same silhouette: the sum of the squared
855/// silhouette frequencies.
856fn chance_of(symbols: &[u32]) -> f32 {
857    let n = symbols.len();
858    if n == 0 {
859        return 0.0;
860    }
861    let mut sorted = symbols.to_vec();
862    sorted.sort_unstable();
863    let mut chance = 0.0f64;
864    let mut run = 0usize;
865    for i in 0..n {
866        run += 1;
867        if i + 1 == n || sorted[i] != sorted[i + 1] {
868            let p = run as f64 / n as f64;
869            chance += p * p;
870            run = 0;
871        }
872    }
873    chance as f32
874}
875
876/// [`record_period`] with the share the lag reached, which is how strongly the
877/// stream repeats at it.
878///
879/// The share is what decides the period and is then dropped, so a caller holds
880/// a period and no way to tell a stream that repeats from one where some lag
881/// merely beat the others. This is the raw winner, before
882/// [`record_period`] gates it: the lag won its contest, which says nothing
883/// about whether the contest was between lags that all sat at chance.
884///
885/// Every consumer of the period wants this. `records::periods` cuts a record
886/// every `period` significant tokens whatever the share, so a share that means
887/// nothing still sets what `--count` answers, and `@phase:k` matches against a
888/// period that may be noise.
889#[must_use]
890pub fn record_period_share(toks: &[Token], bytes: &[u8]) -> Option<(u16, f32)> {
891    best_lag(&period_symbols(toks, bytes))
892}
893
894/// The share a lag reaches when the silhouettes carry no order at all: the
895/// chance two positions a lag apart hold the same silhouette, which is the sum
896/// of the squared silhouette frequencies.
897///
898/// This is the null [`record_period_share`] has never been read against. A
899/// share is only evidence of repetition above what shuffling the same tokens
900/// would give, and that baseline is a property of the corpus rather than a
901/// constant: a stream of three distinct silhouettes has a chance collision
902/// rate near a third, which clears a floor of 0.20 without any structure
903/// whatever.
904#[must_use]
905pub fn record_period_chance(toks: &[Token], bytes: &[u8]) -> f32 {
906    chance_of(&period_symbols(toks, bytes))
907}
908
909/// Every lag the reading considers, with the share it reaches, and the chance
910/// rate they are all read against.
911///
912/// [`record_period`] returns one lag out of this profile - the smallest
913/// reaching the largest share - and a caller cannot tell from it whether the
914/// runner-up was a hair behind or half as good, nor whether a longer lag that
915/// repeats just as well was passed over because a factor of it won. A log
916/// whose line is its record repeats at the line AND at every unit the line is
917/// built from, so the profile is what distinguishes them and the single answer
918/// is not.
919///
920/// Lags ascend from 2 to [`PHASE_MAX_PERIOD`], stopping where a lag has fewer
921/// than two periods to compare.
922#[must_use]
923pub fn record_period_profile(toks: &[Token], bytes: &[u8]) -> (f32, Vec<(u16, f32)>) {
924    period_profile_of(&period_symbols(toks, bytes))
925}
926
927/// Every lag the stream repeats at as [`record_period`] demands of the lag it
928/// keeps - a share at the floor and [`PERIOD_LIFT_FLOOR`] times the chance
929/// rate - strongest first by share, and on a tie the shorter, which is the
930/// order [`record_period`] breaks ties in. The first is [`record_period`]'s.
931#[must_use]
932pub fn live_periods(toks: &[Token], bytes: &[u8]) -> Vec<u16> {
933    live_lags(&period_symbols(toks, bytes))
934}
935
936fn live_lags(symbols: &[u32]) -> Vec<u16> {
937    let (chance, profile) = period_profile_of(symbols);
938    let mut live: Vec<(u16, f32)> = profile
939        .into_iter()
940        .filter(|&(_, share)| {
941            share >= crate::spectral::PERIOD_FLOOR && (chance <= 0.0 || share / chance >= PERIOD_LIFT_FLOOR)
942        })
943        .collect();
944    live.sort_by(|a, b| b.1.total_cmp(&a.1).then(a.0.cmp(&b.0)));
945    live.into_iter().map(|(lag, _)| lag).collect()
946}
947
948/// [`record_period_profile`] over symbols the caller chose, so the reading can
949/// be taken at a grain other than the token's.
950///
951/// The grain decides what is reachable, not just what is read. A lag is
952/// counted in symbols and the search stops at [`PHASE_MAX_PERIOD`], so a
953/// record longer than that many symbols cannot be found at all - a log line
954/// runs to a median of 74 significant tokens, so no token-grain lag can BE the
955/// line and the reading returns the line's factors instead. The same line is a
956/// unit or two of [`crate::supertoken`], which is inside the search. The
957/// symbols must be dense, as they are for the resonator bank and for the same
958/// reason.
959#[must_use]
960pub fn period_profile_of(symbols: &[u32]) -> (f32, Vec<(u16, f32)>) {
961    let n = symbols.len();
962    let mut out = Vec::new();
963    for lag in 2..=usize::from(PHASE_MAX_PERIOD) {
964        if lag * 2 > n {
965            break;
966        }
967        let matches = symbols.iter().zip(&symbols[lag..]).filter(|(a, b)| a == b).count();
968        out.push((lag as u16, matches as f32 / (n - lag) as f32));
969    }
970    (chance_of(symbols), out)
971}
972
973/// The rolling context of `bytes` with every field built: the axis fields the
974/// windows read, the unit rung, and the three boundary sets the agreement
975/// compares.
976#[must_use]
977pub fn analyze(bytes: &[u8]) -> ContextField {
978    analyze_with(bytes, &ContextConfig::default())
979}
980
981/// [`analyze`] with the window extents given.
982#[must_use]
983pub fn analyze_with(bytes: &[u8], cfg: &ContextConfig) -> ContextField {
984    let toks = crate::lexer::lex(bytes);
985    let spectral = crate::spectral::analyze(bytes);
986    let stress = crate::stress::analyze(&toks, bytes);
987    let echo = crate::echo::analyze(&toks, bytes);
988    // The vantage field is built here rather than left out: it is the one axis
989    // that reads ahead, so a rolling window without it folds only what the past
990    // said about each position.
991    // Both bidirectional fields are built here rather than left out: they are
992    // the two axes that read ahead, so a rolling window without them folds only
993    // what the past said about each position.
994    let observation = crate::observation::analyze(bytes);
995    let seam_field = crate::seam::analyze(bytes);
996    // Flow reads whichever signal it is given, and the one this window already
997    // folds is magnitude, so the dynamics reported beside a span's scale are
998    // the dynamics OF that scale rather than of some other axis.
999    let flow = crate::flow::analyze(&toks, bytes, crate::flow::Signal::Magnitude);
1000    let ctx = AxisCtx {
1001        spectral: Some(&spectral),
1002        stress: Some(&stress),
1003        echo: Some(&echo),
1004        observation: Some(&observation),
1005        seam: Some(&seam_field),
1006        flow: Some(&flow),
1007        ..AxisCtx::new(bytes)
1008    };
1009    let supers = SuperContext::build(&toks, bytes);
1010    let mut field = fold_windows_parallel(&toks, &ctx, &supers, cfg);
1011    let shape = crate::shape::analyze(&toks, bytes);
1012    let seam_cuts = crate::seam::analyze_tokens(&toks, &crate::seam::SeamConfig::default());
1013    field.agreement =
1014        agreement(&supers.units, &toks, &spectral.boundaries, &shape.boundaries, &seam_cuts);
1015    field
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020    use super::*;
1021    use crate::profile::fold_tokens;
1022
1023    #[test]
1024    fn two_row_shapes_are_both_live_and_the_first_live_period_is_the_record_period() {
1025        let text = format!("{}{}", "k = 1 ;\n".repeat(150), "k = 1 , 2 ;\n".repeat(150));
1026        let toks = crate::lexer::lex(text.as_bytes());
1027        let live = live_periods(&toks, text.as_bytes());
1028        assert!(live.contains(&4) && live.contains(&6), "both row shapes repeat: {live:?}");
1029        assert_eq!(live.first().copied(), record_period(&toks, text.as_bytes()), "{live:?}");
1030        let prose = "the cat sat on a mat while it rained and then it stopped\n".repeat(3);
1031        let toks = crate::lexer::lex(prose.as_bytes());
1032        assert_eq!(live_periods(&toks, prose.as_bytes()).first().copied(), record_period(&toks, prose.as_bytes()));
1033    }
1034
1035    fn close(a: f32, b: f32) -> bool {
1036        (a - b).abs() <= 1e-4 * a.abs().max(b.abs()).max(1.0)
1037    }
1038
1039    fn same_reading(a: &WindowProfile, b: &WindowProfile) -> bool {
1040        close(a.magnitude.sum, b.magnitude.sum)
1041            && close(a.magnitude.sumsq, b.magnitude.sumsq)
1042            && a.magnitude.max == b.magnitude.max
1043            && a.magnitude.count == b.magnitude.count
1044            && a.stress == b.stress
1045            && close(a.spectral.entropy_sum, b.spectral.entropy_sum)
1046            && a.spectral.count == b.spectral.count
1047            && a.spectral.period == b.spectral.period
1048            && a.echo == b.echo
1049            && close(a.observation.anticausal_sum, b.observation.anticausal_sum)
1050            && a.observation.count == b.observation.count
1051            && close(a.seam.fwd_sum, b.seam.fwd_sum)
1052            && a.seam.cuts == b.seam.cuts
1053            && a.flow.reversals == b.flow.reversals
1054            && a.flow.count == b.flow.count
1055    }
1056
1057    /// A field the rolling context builds but its bundle cannot read folds to
1058    /// zeros, and every other test here still passes. So the check is that the
1059    /// readings which look ahead arrive non-empty through the production path,
1060    /// not merely that the fold is self-consistent.
1061    #[test]
1062    fn the_rolling_context_reads_the_fields_it_builds() {
1063        let bytes = corpus(120);
1064        let field = analyze(&bytes);
1065        assert!(!field.at_token.is_empty(), "the corpus folds to something");
1066
1067        let any = |f: fn(&WindowProfile) -> bool| field.at_token.iter().any(f);
1068        assert!(any(|w| w.observation.anticausal_sum > 0.0), "the future vantage arrived");
1069        assert!(any(|w| w.observation.centered_sum > 0.0), "the centered vantage arrived");
1070        assert!(any(|w| w.seam.fwd_sum > 0.0), "the boundary-after signal arrived");
1071        assert!(any(|w| w.seam.cuts > 0), "the stream is cut somewhere");
1072        assert!(any(|w| w.flow.count > 0), "the dynamics reading arrived");
1073
1074        // And a rung up, which is the whole point of folding them.
1075        assert!(!field.at_unit.is_empty(), "the corpus has units");
1076        assert!(
1077            field.at_unit.iter().any(|u| u.observation.anticausal_sum > 0.0),
1078            "the future vantage reaches the unit rung, not only the token one"
1079        );
1080    }
1081
1082    fn corpus(statements: usize) -> Vec<u8> {
1083        let mut s = String::new();
1084        for i in 0..statements {
1085            match i % 4 {
1086                0 => s.push_str(&format!("let value_{i} = {} ;\n", i * 37)),
1087                1 => s.push_str(&format!("call_{i}(alpha, beta, {i}) ;\n")),
1088                2 => s.push_str(&format!("key_{i}: item_{i}, item_{}, item_{} ;\n", i + 1, i + 2)),
1089                _ => s.push_str(&format!("if (cond_{i}) {{ do_{i}(x) ; }}\n")),
1090            }
1091        }
1092        s.into_bytes()
1093    }
1094
1095    #[test]
1096    fn the_window_fold_is_the_direct_fold_at_every_position() {
1097        let bytes = corpus(60);
1098        let toks = crate::lexer::lex(&bytes);
1099        let spectral = crate::spectral::analyze(&bytes);
1100        let stress = crate::stress::analyze(&toks, &bytes);
1101        let echo = crate::echo::analyze(&toks, &bytes);
1102        let ctx =
1103            AxisCtx {
1104                spectral: Some(&spectral),
1105                stress: Some(&stress),
1106                echo: Some(&echo),
1107                ..AxisCtx::new(&bytes)
1108            };
1109        let supers = SuperContext::build(&toks, &bytes);
1110        for w in [1usize, 2, 3, 7, 32, 1000] {
1111            let cfg = ContextConfig { token_window: w, unit_window: 3 };
1112            let field = fold_windows(&toks, &ctx, &supers, &cfg);
1113            let sig: Vec<usize> = (0..toks.len()).filter(|&i| toks[i].is_significant()).collect();
1114            for (k, &i) in sig.iter().enumerate() {
1115                let lo = k + 1 - w.min(k + 1);
1116                // The direct fold over the same significant tokens, in order.
1117                let direct = sig[lo..=k].iter().fold(WindowProfile::identity(), |acc, &j| {
1118                    acc.combine(&WindowProfile::of_token(j, &toks[j], &ctx))
1119                });
1120                assert!(
1121                    same_reading(&field.at_token[i], &direct),
1122                    "window {w} at token {i}: {:?} vs {:?}",
1123                    field.at_token[i],
1124                    direct
1125                );
1126            }
1127        }
1128    }
1129
1130    #[test]
1131    fn a_units_reading_is_the_fold_of_its_tokens_and_the_unit_window_folds_them() {
1132        let bytes = corpus(40);
1133        let toks = crate::lexer::lex(&bytes);
1134        let ctx = AxisCtx::new(&bytes);
1135        let supers = SuperContext::build(&toks, &bytes);
1136        let cfg = ContextConfig { token_window: 8, unit_window: 3 };
1137        let field = fold_windows(&toks, &ctx, &supers, &cfg);
1138        assert_eq!(field.of_unit.len(), supers.units.len());
1139        for (u, unit) in supers.units.iter().enumerate() {
1140            let members: Vec<Token> = toks
1141                .iter()
1142                .enumerate()
1143                .filter(|(i, t)| t.is_significant() && supers.index_of(*i) == Some(u))
1144                .map(|(_, t)| *t)
1145                .collect();
1146            assert!(!members.is_empty(), "unit {u} {unit:?} holds tokens");
1147            let direct: WindowProfile = fold_tokens(0, &members, &ctx);
1148            // Magnitude reads the token's bytes only, so the index offset the
1149            // direct fold lacks changes nothing here.
1150            assert!(same_reading(&field.of_unit[u], &direct), "unit {u}");
1151            let lo = u + 1 - cfg.unit_window.min(u + 1);
1152            let windowed = crate::profile::fold_profiles(&field.of_unit[lo..=u]);
1153            assert!(same_reading(&field.at_unit[u], &windowed), "unit window at {u}");
1154        }
1155    }
1156
1157    #[test]
1158    fn the_window_profile_is_a_monoid() {
1159        let bytes = b"alpha 12 (beta 3456) gamma alpha";
1160        let toks = crate::lexer::lex(bytes);
1161        let ctx = AxisCtx::new(bytes);
1162        let a = WindowProfile::of_token(0, &toks[0], &ctx);
1163        let b = WindowProfile::of_token(2, &toks[2], &ctx);
1164        let c = WindowProfile::of_token(4, &toks[4], &ctx);
1165        assert_eq!(WindowProfile::identity().combine(&a), a);
1166        assert_eq!(a.combine(&WindowProfile::identity()), a);
1167        assert!(same_reading(&a.combine(&b).combine(&c), &a.combine(&b.combine(&c))));
1168    }
1169
1170    #[test]
1171    fn the_grains_agree_on_structure_and_not_on_a_shuffled_stream() {
1172        // Statements whose units start where the byte texture changes
1173        // (a keyword after a newline, a number after an operator). The same
1174        // tokens in a random order keep every grain's reader running but
1175        // leave nothing for their boundaries to agree on, so the shuffled
1176        // stream is the chance level the real one has to clear.
1177        let bytes = corpus(400);
1178        let real = analyze(&bytes);
1179        let toks = crate::lexer::lex(&bytes);
1180        let mut sig: Vec<&[u8]> = toks
1181            .iter()
1182            .filter(|t| t.is_significant())
1183            .map(|t| &bytes[t.span()])
1184            .collect();
1185        let mut x = 0x9e37_79b9_7f4a_7c15u64;
1186        for i in (1..sig.len()).rev() {
1187            x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
1188            sig.swap(i, (x >> 33) as usize % (i + 1));
1189        }
1190        let shuffled: Vec<u8> = sig.join(&b' ');
1191        let null = analyze(&shuffled);
1192        let (_, _, r_seam) = real.alignment();
1193        let (_, _, n_seam) = null.alignment();
1194        assert!(real.agreement.len() > 100 && null.agreement.len() > 100, "enough units on both");
1195        // The seam is the one lower-grain reader dense enough to place a
1196        // boundary near every unit; the byte regime and shape change-points
1197        // are sparse on a uniform corpus, so their offsets spread on both.
1198        // On this corpus a call's nearest seam cut is one token before it,
1199        // a key's two before, a list's and a binding's one after: 0.875 of
1200        // units at their role's offset against 0.372 shuffled
1201        // (`benches/context_window`).
1202        assert!(
1203            r_seam > n_seam * 1.5 && r_seam > 0.6,
1204            "seam offset concentration by role: real {r_seam:.3} vs shuffled {n_seam:.3}"
1205        );
1206    }
1207
1208    #[test]
1209    fn the_parallel_fold_reads_what_the_serial_fold_reads() {
1210        // Enough units and tokens for several chunks on any core count.
1211        let bytes = corpus(3000);
1212        let toks = crate::lexer::lex(&bytes);
1213        let spectral = crate::spectral::analyze(&bytes);
1214        let stress = crate::stress::analyze(&toks, &bytes);
1215        let echo = crate::echo::analyze(&toks, &bytes);
1216        let ctx =
1217            AxisCtx {
1218                spectral: Some(&spectral),
1219                stress: Some(&stress),
1220                echo: Some(&echo),
1221                ..AxisCtx::new(&bytes)
1222            };
1223        let supers = SuperContext::build(&toks, &bytes);
1224        let cfg = ContextConfig::default();
1225        let serial = fold_windows(&toks, &ctx, &supers, &cfg);
1226        let parallel = fold_windows_parallel(&toks, &ctx, &supers, &cfg);
1227        assert_eq!(parallel.at_token.len(), serial.at_token.len());
1228        assert_eq!(parallel.of_unit.len(), serial.of_unit.len());
1229        assert_eq!(parallel.at_unit.len(), serial.at_unit.len());
1230        for (i, (p, s)) in parallel.at_token.iter().zip(&serial.at_token).enumerate() {
1231            assert!(same_reading(p, s), "token {i}: {p:?} vs {s:?}");
1232        }
1233        for (u, (p, s)) in parallel.of_unit.iter().zip(&serial.of_unit).enumerate() {
1234            assert!(same_reading(p, s), "unit {u}");
1235        }
1236        for (u, (p, s)) in parallel.at_unit.iter().zip(&serial.at_unit).enumerate() {
1237            assert!(same_reading(p, s), "unit window {u}");
1238        }
1239    }
1240
1241    #[test]
1242    fn each_relation_fold_is_the_direct_fold_over_what_the_relation_names() {
1243        let bytes = corpus(300);
1244        let toks = crate::lexer::lex(&bytes);
1245        let spectral = crate::spectral::analyze(&bytes);
1246        let stress = crate::stress::analyze(&toks, &bytes);
1247        let echo = crate::echo::analyze(&toks, &bytes);
1248        let ctx =
1249            AxisCtx {
1250                spectral: Some(&spectral),
1251                stress: Some(&stress),
1252                echo: Some(&echo),
1253                ..AxisCtx::new(&bytes)
1254            };
1255        // Four statement shapes cycle through more tokens than the period
1256        // search spans, so this stream has no record period; the phase fold
1257        // is checked on a periodic stream below.
1258        let period = record_period(&toks, &bytes);
1259        assert_eq!(period, None, "no record repeats within the search");
1260        let rel = relate(&toks, &ctx, &spectral.boundaries, &echo, period);
1261        let relation = crate::relation::analyze(&toks, &bytes);
1262        let reading = |i: usize| WindowProfile::of_token(i, &toks[i], &ctx);
1263        let fold = |ix: &[usize]| ix.iter().fold(WindowProfile::identity(), |a, &j| a.combine(&reading(j)));
1264        let sig: Vec<usize> = (0..toks.len()).filter(|&i| toks[i].is_significant()).collect();
1265        let mut echoes_seen = 0usize;
1266        let mut enclosed_seen = 0usize;
1267        for (k, &i) in sig.iter().enumerate() {
1268            let r = &rel.at_token[i];
1269            // Enclosure: the heads of the enclosing brackets, outermost first.
1270            let heads = &relation.frames[i].enclosure;
1271            if !heads.is_empty() {
1272                enclosed_seen += 1;
1273            }
1274            assert!(same_reading(&r.enclosing, &fold(heads)), "enclosure at {i}");
1275            // Echo: every earlier token with the same text.
1276            let text = &bytes[toks[i].span()];
1277            let earlier: Vec<usize> = sig[..k]
1278                .iter()
1279                .copied()
1280                .filter(|&j| echo.frames[j].keyed && &bytes[toks[j].span()] == text)
1281                .collect();
1282            if echo.frames[i].keyed {
1283                if !earlier.is_empty() {
1284                    echoes_seen += 1;
1285                }
1286                assert!(same_reading(&r.echoing, &fold(&earlier)), "echo at {i}");
1287            }
1288            // Regime: the earlier significant tokens since the last change-point.
1289            let cut = spectral.boundaries.partition_point(|&c| c <= toks[i].start());
1290            let since = cut.checked_sub(1).map_or(0, |c| spectral.boundaries[c]);
1291            let members: Vec<usize> =
1292                sig[..k].iter().copied().filter(|&j| toks[j].start() >= since).collect();
1293            assert!(same_reading(&r.regime, &fold(&members)), "regime at {i}");
1294            assert_eq!(r.phase, WindowProfile::identity(), "no period, no phase fold at {i}");
1295            assert_eq!(rel.phase_of[i], u16::MAX, "no period, no phase at {i}");
1296        }
1297        assert!(echoes_seen > 100 && enclosed_seen > 100, "the relations were exercised");
1298    }
1299
1300    #[test]
1301    fn the_phase_fold_is_the_direct_fold_over_the_column() {
1302        // Six significant tokens a row, no header.
1303        let mut rows = String::new();
1304        for i in 0..300 {
1305            rows.push_str(&format!("r{i} , {} , x{} ;\n", i * 3, i % 4));
1306        }
1307        let bytes = rows.as_bytes();
1308        let toks = crate::lexer::lex(bytes);
1309        let period = record_period(&toks, bytes);
1310        assert_eq!(period, Some(6), "the row is the period");
1311        let echo = crate::echo::analyze(&toks, bytes);
1312        let ctx = AxisCtx::new(bytes);
1313        let rel = relate(&toks, &ctx, &[], &echo, period);
1314        let reading = |i: usize| WindowProfile::of_token(i, &toks[i], &ctx);
1315        let sig: Vec<usize> = (0..toks.len()).filter(|&i| toks[i].is_significant()).collect();
1316        for (k, &i) in sig.iter().enumerate() {
1317            let same_phase = (0..k).filter(|&m| m % 6 == k % 6).map(|m| sig[m]).fold(
1318                WindowProfile::identity(),
1319                |a, j| a.combine(&reading(j)),
1320            );
1321            assert!(same_reading(&rel.at_token[i].phase, &same_phase), "phase fold at {i}");
1322            assert_eq!(rel.phase_of[i], (k % 6) as u16, "phase at {i}");
1323        }
1324    }
1325
1326    /// Lines binding values to a few recurring keys, so a key's value history
1327    /// has something in it.
1328    fn log_corpus(lines: usize) -> Vec<u8> {
1329        let mut s = String::new();
1330        for i in 0..lines {
1331            match i % 3 {
1332                0 => s.push_str(&format!("svc_{} latency = {} ms ;\n", i % 7, 90 + (i * 37) % 21)),
1333                1 => s.push_str(&format!("svc_{} size = {} ;\n", i % 7, 1_000_000 + i)),
1334                _ => s.push_str(&format!("state: {} ;\n", if i % 2 == 0 { "ready" } else { "busy" })),
1335            }
1336        }
1337        s.into_bytes()
1338    }
1339
1340    #[test]
1341    fn the_value_history_is_the_fold_over_what_earlier_occurrences_bound() {
1342        let bytes = log_corpus(200);
1343        let toks = crate::lexer::lex(&bytes);
1344        let echo = crate::echo::analyze(&toks, &bytes);
1345        let ctx = AxisCtx::new(&bytes);
1346        let rel = relate(&toks, &ctx, &[], &echo, None);
1347        let reading = |i: usize| WindowProfile::of_token(i, &toks[i], &ctx);
1348        let next_sig = |i: usize| (i + 1..toks.len()).find(|&j| toks[j].is_significant());
1349        // What token `j` binds: the significant token after a `=` or `:`
1350        // that directly follows it.
1351        let bound_by = |j: usize| -> Option<usize> {
1352            let op = next_sig(j)?;
1353            let t = &toks[op];
1354            (t.kind == crate::token::TokenKind::Punct && matches!(&bytes[t.span()], b"=" | b":"))
1355                .then(|| next_sig(op))
1356                .flatten()
1357        };
1358        let mut histories_seen = 0usize;
1359        for i in 0..toks.len() {
1360            if !echo.frames[i].keyed {
1361                continue;
1362            }
1363            let text = &bytes[toks[i].span()];
1364            let values: Vec<usize> = (0..i)
1365                .filter(|&j| echo.frames[j].keyed && &bytes[toks[j].span()] == text)
1366                .filter_map(bound_by)
1367                .collect();
1368            if !values.is_empty() {
1369                histories_seen += 1;
1370            }
1371            let direct = values.iter().fold(WindowProfile::identity(), |a, &v| a.combine(&reading(v)));
1372            assert!(same_reading(&rel.value_history[i], &direct), "value history at {i}");
1373        }
1374        assert!(histories_seen > 100, "keys were bound more than once: {histories_seen}");
1375    }
1376
1377    #[test]
1378    fn a_window_of_one_reads_the_token_alone() {
1379        let bytes = b"alpha 12 beta 3456";
1380        let toks = crate::lexer::lex(bytes);
1381        let ctx = AxisCtx::new(bytes);
1382        let supers = SuperContext::build(&toks, bytes);
1383        let field = fold_windows(&toks, &ctx, &supers, &ContextConfig { token_window: 1, unit_window: 1 });
1384        for (i, t) in toks.iter().enumerate() {
1385            if t.is_significant() {
1386                assert_eq!(field.at_token[i].magnitude.count, 1, "token {i}");
1387            }
1388        }
1389        // The whitespace after a token carries that token's reading.
1390        assert_eq!(field.at_token[1], field.at_token[0]);
1391    }
1392}