use crate::echo::EchoField;
use crate::spectral::SpectralField;
use crate::stress::StressField;
use crate::token::Token;
#[derive(Clone, Copy)]
pub struct AxisCtx<'a> {
pub bytes: &'a [u8],
pub spectral: Option<&'a SpectralField>,
pub stress: Option<&'a StressField>,
pub echo: Option<&'a EchoField>,
pub observation: Option<&'a crate::observation::ObservationField>,
pub seam: Option<&'a crate::seam::SeamField>,
pub flow: Option<&'a crate::flow::FlowField>,
}
impl<'a> AxisCtx<'a> {
#[must_use]
pub fn new(bytes: &'a [u8]) -> Self {
AxisCtx {
bytes,
spectral: None,
stress: None,
echo: None,
observation: None,
seam: None,
flow: None,
}
}
}
pub trait AxisProfile: Clone + PartialEq + std::fmt::Debug {
fn identity() -> Self;
fn combine(&self, other: &Self) -> Self;
fn of_token(idx: usize, tok: &Token, ctx: &AxisCtx<'_>) -> Self;
}
pub fn fold_tokens<P: AxisProfile>(base: usize, toks: &[Token], ctx: &AxisCtx<'_>) -> P {
toks.iter()
.enumerate()
.fold(P::identity(), |acc, (i, t)| acc.combine(&P::of_token(base + i, t, ctx)))
}
pub fn fold_profiles<P: AxisProfile>(parts: &[P]) -> P {
parts.iter().fold(P::identity(), |acc, p| acc.combine(p))
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct MagnitudeProfile {
pub sum: f32,
pub sumsq: f32,
pub max: f32,
pub count: u32,
}
impl MagnitudeProfile {
#[must_use]
pub fn mean(&self) -> f32 {
if self.count == 0 { 0.0 } else { self.sum / self.count as f32 }
}
#[must_use]
pub fn std_dev(&self) -> f32 {
if self.count == 0 {
return 0.0;
}
let mean = self.mean();
(self.sumsq / self.count as f32 - mean * mean).max(0.0).sqrt()
}
}
impl AxisProfile for MagnitudeProfile {
fn identity() -> Self {
MagnitudeProfile { sum: 0.0, sumsq: 0.0, max: f32::NEG_INFINITY, count: 0 }
}
fn combine(&self, other: &Self) -> Self {
MagnitudeProfile {
sum: self.sum + other.sum,
sumsq: self.sumsq + other.sumsq,
max: self.max.max(other.max),
count: self.count + other.count,
}
}
fn of_token(_idx: usize, tok: &Token, ctx: &AxisCtx<'_>) -> Self {
let m = crate::magnitude::token_magnitude(tok.kind, &ctx.bytes[tok.span()]);
MagnitudeProfile { sum: m, sumsq: m * m, max: m, count: 1 }
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct StressProfile {
pub max_depth: u16,
pub sum_depth: u32,
pub count: u32,
}
impl StressProfile {
#[must_use]
pub fn mean_depth(&self) -> f32 {
if self.count == 0 { 0.0 } else { self.sum_depth as f32 / self.count as f32 }
}
}
impl AxisProfile for StressProfile {
fn identity() -> Self {
StressProfile::default()
}
fn combine(&self, other: &Self) -> Self {
StressProfile {
max_depth: self.max_depth.max(other.max_depth),
sum_depth: self.sum_depth + other.sum_depth,
count: self.count + other.count,
}
}
fn of_token(_idx: usize, tok: &Token, ctx: &AxisCtx<'_>) -> Self {
let d = ctx.stress.map_or(0, |f| f.depth_at(tok.start()));
StressProfile { max_depth: d, sum_depth: u32::from(d), count: 1 }
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct ShapeProfile {
pub silhouette: Vec<u32>,
}
impl AxisProfile for ShapeProfile {
fn identity() -> Self {
ShapeProfile::default()
}
fn combine(&self, other: &Self) -> Self {
let mut s = Vec::with_capacity(self.silhouette.len() + other.silhouette.len());
s.extend_from_slice(&self.silhouette);
s.extend_from_slice(&other.silhouette);
ShapeProfile { silhouette: s }
}
fn of_token(_idx: usize, tok: &Token, ctx: &AxisCtx<'_>) -> Self {
ShapeProfile {
silhouette: vec![crate::shape::shape_class(tok.kind, &ctx.bytes[tok.span()])],
}
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct SpectralProfile {
pub entropy_sum: f32,
pub count: u32,
pub period: u16,
pub period_strength: f32,
}
impl SpectralProfile {
#[must_use]
pub fn mean_entropy(&self) -> f32 {
if self.count == 0 { 0.0 } else { self.entropy_sum / self.count as f32 }
}
}
impl AxisProfile for SpectralProfile {
fn identity() -> Self {
SpectralProfile::default()
}
fn combine(&self, other: &Self) -> Self {
let (period, period_strength) = if other.period_strength > self.period_strength {
(other.period, other.period_strength)
} else {
(self.period, self.period_strength)
};
SpectralProfile {
entropy_sum: self.entropy_sum + other.entropy_sum,
count: self.count + other.count,
period,
period_strength,
}
}
fn of_token(_idx: usize, tok: &Token, ctx: &AxisCtx<'_>) -> Self {
let Some(f) = ctx.spectral else {
return SpectralProfile { count: 1, ..SpectralProfile::default() };
};
let mut want = crate::spectral::Needs::none();
want.entropy = true;
want.period = true;
f.assert_carries(want, "the spectral profile");
let sig = f.signature(tok.start(), tok.end());
SpectralProfile {
entropy_sum: sig.entropy,
count: 1,
period: sig.period,
period_strength: sig.period_strength,
}
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct ObservationProfile {
pub causal_sum: f32,
pub anticausal_sum: f32,
pub centered_sum: f32,
pub disagreement_sum: f32,
pub disagreement_max: f32,
pub contested: u32,
pub count: u32,
}
impl ObservationProfile {
#[must_use]
pub fn mean_causal(&self) -> f32 {
if self.count == 0 { 0.0 } else { self.causal_sum / self.count as f32 }
}
#[must_use]
pub fn mean_anticausal(&self) -> f32 {
if self.count == 0 { 0.0 } else { self.anticausal_sum / self.count as f32 }
}
#[must_use]
pub fn mean_centered(&self) -> f32 {
if self.count == 0 { 0.0 } else { self.centered_sum / self.count as f32 }
}
#[must_use]
pub fn mean_disagreement(&self) -> f32 {
if self.count == 0 { 0.0 } else { self.disagreement_sum / self.count as f32 }
}
#[must_use]
pub fn revision(&self) -> f32 {
self.mean_anticausal() - self.mean_causal()
}
}
impl AxisProfile for ObservationProfile {
fn identity() -> Self {
ObservationProfile::default()
}
fn combine(&self, other: &Self) -> Self {
ObservationProfile {
causal_sum: self.causal_sum + other.causal_sum,
anticausal_sum: self.anticausal_sum + other.anticausal_sum,
centered_sum: self.centered_sum + other.centered_sum,
disagreement_sum: self.disagreement_sum + other.disagreement_sum,
disagreement_max: self.disagreement_max.max(other.disagreement_max),
contested: self.contested + other.contested,
count: self.count + other.count,
}
}
fn of_token(_idx: usize, tok: &Token, ctx: &AxisCtx<'_>) -> Self {
let Some(f) = ctx.observation else {
return ObservationProfile { count: 1, ..ObservationProfile::default() };
};
let at = tok.start();
ObservationProfile {
causal_sum: f.causal_at(at),
anticausal_sum: f.anticausal_at(at),
centered_sum: f.centered_at(at),
disagreement_sum: f.disagreement_at(at),
disagreement_max: f.disagreement_at(at),
contested: u32::from(f.any_contested_in(tok.start(), tok.end())),
count: 1,
}
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct SeamProfile {
pub fwd_sum: f32,
pub bwd_sum: f32,
pub strength_sum: f32,
pub strength_max: f32,
pub cuts: u32,
pub count: u32,
}
impl SeamProfile {
#[must_use]
pub fn mean_fwd(&self) -> f32 {
if self.count == 0 { 0.0 } else { self.fwd_sum / self.count as f32 }
}
#[must_use]
pub fn mean_bwd(&self) -> f32 {
if self.count == 0 { 0.0 } else { self.bwd_sum / self.count as f32 }
}
#[must_use]
pub fn cut_density(&self) -> f32 {
if self.count == 0 { 0.0 } else { self.cuts as f32 / self.count as f32 }
}
#[must_use]
pub fn asymmetry(&self) -> f32 {
self.mean_fwd() - self.mean_bwd()
}
}
impl AxisProfile for SeamProfile {
fn identity() -> Self {
SeamProfile::default()
}
fn combine(&self, other: &Self) -> Self {
SeamProfile {
fwd_sum: self.fwd_sum + other.fwd_sum,
bwd_sum: self.bwd_sum + other.bwd_sum,
strength_sum: self.strength_sum + other.strength_sum,
strength_max: self.strength_max.max(other.strength_max),
cuts: self.cuts + other.cuts,
count: self.count + other.count,
}
}
fn of_token(_idx: usize, tok: &Token, ctx: &AxisCtx<'_>) -> Self {
let Some(f) = ctx.seam else {
return SeamProfile { count: 1, ..SeamProfile::default() };
};
let at = tok.start();
SeamProfile {
fwd_sum: f.fwd_at(at),
bwd_sum: f.bwd_at(at),
strength_sum: f.strength_at(at),
strength_max: f.strength_at(at),
cuts: u32::from(f.starts_segment(at)),
count: 1,
}
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct FlowProfile {
pub slope_sum: f32,
pub rising: u32,
pub falling: u32,
pub steady: u32,
pub momentum_max: u16,
pub reversals: u32,
pub first: i8,
pub last: i8,
pub count: u32,
}
impl FlowProfile {
#[must_use]
pub fn mean_slope(&self) -> f32 {
if self.count == 0 { 0.0 } else { self.slope_sum / self.count as f32 }
}
#[must_use]
pub fn drift(&self) -> f32 {
if self.count == 0 {
return 0.0;
}
(self.rising as f32 - self.falling as f32) / self.count as f32
}
#[must_use]
pub fn volatility(&self) -> f32 {
if self.count < 2 { 0.0 } else { self.reversals as f32 / (self.count - 1) as f32 }
}
}
impl AxisProfile for FlowProfile {
fn identity() -> Self {
FlowProfile::default()
}
fn combine(&self, other: &Self) -> Self {
if self.count == 0 {
return other.clone();
}
if other.count == 0 {
return self.clone();
}
FlowProfile {
slope_sum: self.slope_sum + other.slope_sum,
rising: self.rising + other.rising,
falling: self.falling + other.falling,
steady: self.steady + other.steady,
momentum_max: self.momentum_max.max(other.momentum_max),
reversals: self.reversals + other.reversals + u32::from(self.last != other.first),
first: self.first,
last: other.last,
count: self.count + other.count,
}
}
fn of_token(_idx: usize, tok: &Token, ctx: &AxisCtx<'_>) -> Self {
let Some(f) = ctx.flow else {
return FlowProfile { count: 1, ..FlowProfile::default() };
};
let at = tok.start();
let dir = f.direction_at(at);
FlowProfile {
slope_sum: f.slope_at(at),
rising: u32::from(dir > 0),
falling: u32::from(dir < 0),
steady: u32::from(dir == 0),
momentum_max: f.momentum_at(at),
reversals: 0,
first: dir,
last: dir,
count: 1,
}
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct EchoProfile {
pub novel: u32,
pub echoed: u32,
pub count: u32,
}
impl AxisProfile for EchoProfile {
fn identity() -> Self {
EchoProfile::default()
}
fn combine(&self, other: &Self) -> Self {
EchoProfile {
novel: self.novel + other.novel,
echoed: self.echoed + other.echoed,
count: self.count + other.count,
}
}
fn of_token(idx: usize, _tok: &Token, ctx: &AxisCtx<'_>) -> Self {
let frame = ctx.echo.and_then(|f| f.frames.get(idx));
EchoProfile {
novel: u32::from(frame.is_some_and(crate::echo::EchoFrame::novel)),
echoed: u32::from(frame.is_some_and(crate::echo::EchoFrame::echoed)),
count: 1,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Profile {
pub magnitude: MagnitudeProfile,
pub stress: StressProfile,
pub shape: ShapeProfile,
pub spectral: SpectralProfile,
pub echo: EchoProfile,
pub observation: ObservationProfile,
pub seam: SeamProfile,
pub flow: FlowProfile,
}
impl Profile {
#[must_use]
pub fn kinetic(&self) -> f32 {
self.magnitude.sumsq
}
#[must_use]
pub fn potential(&self) -> f32 {
self.stress.sum_depth as f32
}
#[must_use]
pub fn lagrangian(&self) -> f32 {
self.kinetic() - self.potential()
}
#[must_use]
pub fn total_energy(&self) -> f32 {
self.kinetic() + self.potential()
}
}
impl AxisProfile for Profile {
fn identity() -> Self {
Profile {
magnitude: MagnitudeProfile::identity(),
stress: StressProfile::identity(),
shape: ShapeProfile::identity(),
spectral: SpectralProfile::identity(),
echo: EchoProfile::identity(),
observation: ObservationProfile::identity(),
seam: SeamProfile::identity(),
flow: FlowProfile::identity(),
}
}
fn combine(&self, other: &Self) -> Self {
Profile {
magnitude: self.magnitude.combine(&other.magnitude),
stress: self.stress.combine(&other.stress),
shape: self.shape.combine(&other.shape),
spectral: self.spectral.combine(&other.spectral),
echo: self.echo.combine(&other.echo),
observation: self.observation.combine(&other.observation),
seam: self.seam.combine(&other.seam),
flow: self.flow.combine(&other.flow),
}
}
fn of_token(idx: usize, tok: &Token, ctx: &AxisCtx<'_>) -> Self {
Profile {
magnitude: MagnitudeProfile::of_token(idx, tok, ctx),
stress: StressProfile::of_token(idx, tok, ctx),
shape: ShapeProfile::of_token(idx, tok, ctx),
spectral: SpectralProfile::of_token(idx, tok, ctx),
echo: EchoProfile::of_token(idx, tok, ctx),
observation: ObservationProfile::of_token(idx, tok, ctx),
seam: SeamProfile::of_token(idx, tok, ctx),
flow: FlowProfile::of_token(idx, tok, ctx),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::token::TokenKind;
fn toks(s: &str) -> Vec<Token> {
crate::lexer::lex(s.as_bytes())
}
fn assert_monoid<P: AxisProfile>(a: &P, b: &P, c: &P) {
assert_eq!(P::identity().combine(a), *a, "left identity");
assert_eq!(a.combine(&P::identity()), *a, "right identity");
assert_eq!(
a.combine(b).combine(c),
a.combine(&b.combine(c)),
"associativity"
);
}
#[test]
fn every_axis_is_a_monoid() {
let bytes = b"alpha 12 (beta 3456) gamma alpha";
let tk = crate::lexer::lex(bytes);
let stress = crate::stress::analyze(&tk, bytes);
let spectral = crate::spectral::analyze(bytes);
let echo = crate::echo::analyze(&tk, bytes);
let observation = crate::observation::analyze(bytes);
let seam = crate::seam::analyze(bytes);
let flow = crate::flow::analyze(&tk, bytes, crate::flow::Signal::Magnitude);
let ctx = AxisCtx {
bytes,
spectral: Some(&spectral),
stress: Some(&stress),
echo: Some(&echo),
observation: Some(&observation),
seam: Some(&seam),
flow: Some(&flow),
};
let sig: Vec<(usize, &Token)> =
tk.iter().enumerate().filter(|(_, t)| t.is_significant()).collect();
assert!(sig.len() >= 3, "need three readings to test associativity");
let (i0, t0) = sig[0];
let (i1, t1) = sig[1];
let (i2, t2) = sig[2];
assert_monoid(
&MagnitudeProfile::of_token(i0, t0, &ctx),
&MagnitudeProfile::of_token(i1, t1, &ctx),
&MagnitudeProfile::of_token(i2, t2, &ctx),
);
assert_monoid(
&StressProfile::of_token(i0, t0, &ctx),
&StressProfile::of_token(i1, t1, &ctx),
&StressProfile::of_token(i2, t2, &ctx),
);
assert_monoid(
&ShapeProfile::of_token(i0, t0, &ctx),
&ShapeProfile::of_token(i1, t1, &ctx),
&ShapeProfile::of_token(i2, t2, &ctx),
);
assert_monoid(
&SpectralProfile::of_token(i0, t0, &ctx),
&SpectralProfile::of_token(i1, t1, &ctx),
&SpectralProfile::of_token(i2, t2, &ctx),
);
assert_monoid(
&EchoProfile::of_token(i0, t0, &ctx),
&EchoProfile::of_token(i1, t1, &ctx),
&EchoProfile::of_token(i2, t2, &ctx),
);
assert_monoid(
&ObservationProfile::of_token(i0, t0, &ctx),
&ObservationProfile::of_token(i1, t1, &ctx),
&ObservationProfile::of_token(i2, t2, &ctx),
);
assert_monoid(
&SeamProfile::of_token(i0, t0, &ctx),
&SeamProfile::of_token(i1, t1, &ctx),
&SeamProfile::of_token(i2, t2, &ctx),
);
assert_monoid(
&FlowProfile::of_token(i0, t0, &ctx),
&FlowProfile::of_token(i1, t1, &ctx),
&FlowProfile::of_token(i2, t2, &ctx),
);
assert_monoid(
&Profile::of_token(i0, t0, &ctx),
&Profile::of_token(i1, t1, &ctx),
&Profile::of_token(i2, t2, &ctx),
);
}
#[test]
fn the_forward_reading_survives_the_fold() {
let mut src = String::new();
for i in 0..40 {
src.push_str(&format!("let value_{i} = {} ;\n", i * 37));
}
for i in 0..40 {
src.push_str(&format!("the quiet meadow held its breath for the {i}th time\n"));
}
let bytes = src.as_bytes();
let tk = crate::lexer::lex(bytes);
let observation = crate::observation::analyze(bytes);
let seam = crate::seam::analyze(bytes);
let ctx =
AxisCtx { observation: Some(&observation), seam: Some(&seam), ..AxisCtx::new(bytes) };
let whole: ObservationProfile = fold_tokens(0, &tk, &ctx);
assert!(whole.count > 0, "the document has tokens");
assert!(
whole.anticausal_sum > 0.0,
"the future-only vantage reached the fold, not just the past-only one"
);
let seams: SeamProfile = fold_tokens(0, &tk, &ctx);
assert!(seams.fwd_sum > 0.0, "the boundary-after signal reached the fold");
assert!(seams.cuts > 0, "the document is cut somewhere");
let mid = tk.len() / 2;
let left: ObservationProfile = fold_tokens(0, &tk[..mid], &ctx);
let right: ObservationProfile = fold_tokens(mid, &tk[mid..], &ctx);
let joined = left.combine(&right);
assert_eq!(joined.count, whole.count, "token count");
assert_eq!(joined.contested, whole.contested, "contested count");
assert!(
(joined.disagreement_max - whole.disagreement_max).abs() < f32::EPSILON,
"peak disagreement is a max, so it regroups exactly"
);
for (a, b, what) in [
(joined.causal_sum, whole.causal_sum, "causal"),
(joined.anticausal_sum, whole.anticausal_sum, "anticausal"),
(joined.centered_sum, whole.centered_sum, "centered"),
] {
assert!((a - b).abs() <= b.abs() * 1e-5, "{what} sum regroups to within rounding");
}
}
#[test]
fn a_reversal_at_a_split_survives_the_fold() {
let mut src = String::new();
for i in 1..12 {
src.push_str(&format!("{} ", 10i64.pow(i.min(9))));
}
for i in (1..12).rev() {
src.push_str(&format!("{} ", 10i64.pow(i.min(9))));
}
let bytes = src.as_bytes();
let tk = crate::lexer::lex(bytes);
let flow = crate::flow::analyze(&tk, bytes, crate::flow::Signal::Magnitude);
let ctx = AxisCtx { flow: Some(&flow), ..AxisCtx::new(bytes) };
let whole: FlowProfile = fold_tokens(0, &tk, &ctx);
assert!(whole.count > 2, "the corpus has tokens to turn between");
assert!(whole.reversals > 0, "the signal turns, so the whole span sees a reversal");
for mid in 1..tk.len() {
let left: FlowProfile = fold_tokens(0, &tk[..mid], &ctx);
let right: FlowProfile = fold_tokens(mid, &tk[mid..], &ctx);
let joined = left.combine(&right);
assert_eq!(
joined.reversals, whole.reversals,
"a split at token {mid} lost or invented a reversal"
);
assert_eq!(joined.count, whole.count, "split at {mid}");
assert_eq!(joined.first, whole.first, "split at {mid}");
assert_eq!(joined.last, whole.last, "split at {mid}");
}
}
#[test]
fn the_bundle_derives_the_action_axis_rather_than_folding_it() {
let bytes = b"let a = 100 ; f(b, 2000) ; { deep(30000) }";
let tk = crate::lexer::lex(bytes);
let stress = crate::stress::analyze(&tk, bytes);
let ctx = AxisCtx { stress: Some(&stress), ..AxisCtx::new(bytes) };
let folded: Profile = fold_tokens(0, &tk, &ctx);
let field = crate::action::analyze(&tk, bytes);
let direct = field.action(0, tk.len());
assert!(folded.kinetic() > 0.0, "the span carries kinetic energy");
let slack = direct.abs().max(1.0) * 1e-4;
assert!(
(folded.lagrangian() - direct).abs() <= slack,
"bundle {} against the action field {direct}",
folded.lagrangian()
);
}
#[test]
fn a_three_level_fold_uses_no_level_specific_code() {
let bytes = b"let x = 1;\nfoo(2, 3)\nname: bob\n";
let tk = crate::lexer::lex(bytes);
let units = crate::supertoken::supertokens_from(&tk, bytes);
let ctx = AxisCtx::new(bytes);
let per_unit: Vec<Profile> = units
.iter()
.map(|u| {
let lo = tk.iter().position(|t| t.start() >= u.start).unwrap_or(0);
let hi = tk.iter().rposition(|t| t.end() <= u.end).map_or(lo, |i| i + 1);
fold_tokens::<Profile>(lo, &tk[lo..hi.max(lo)], &ctx)
})
.collect();
let document: Profile = fold_profiles(&per_unit);
let flat: Profile = fold_tokens(0, &tk, &ctx);
assert_eq!(document.magnitude.count, flat.magnitude.count.min(document.magnitude.count));
assert!(document.magnitude.count > 0, "the document has tokens");
assert!(!per_unit.is_empty(), "the input has supertokens");
}
#[test]
fn magnitude_carries_three_readings_not_one() {
let bytes = b"5 5000000000";
let tk = toks("5 5000000000");
let ctx = AxisCtx::new(bytes);
let p: MagnitudeProfile = fold_tokens(0, &tk, &ctx);
assert_eq!(p.count, 3, "two numbers and the whitespace between them");
assert!(p.max > 9.0, "the large value sets the peak: {}", p.max);
assert!(p.mean() < p.max, "the mean is pulled down by the small value");
assert!(p.sumsq > p.max, "energy accumulates rather than peaking");
}
#[test]
fn shape_is_the_free_monoid_over_silhouettes() {
let bytes = b"foo(a, b)";
let tk = toks("foo(a, b)");
let ctx = AxisCtx::new(bytes);
let whole: ShapeProfile = fold_tokens(0, &tk, &ctx);
let (l, r) = tk.split_at(3);
let left: ShapeProfile = fold_tokens(0, l, &ctx);
let right: ShapeProfile = fold_tokens(3, r, &ctx);
assert_eq!(whole, left.combine(&right), "splitting anywhere gives the same silhouette");
assert_eq!(whole.silhouette.len(), tk.len());
}
#[test]
fn an_axis_nobody_asked_for_costs_nothing() {
let bytes = b"a (b)";
let tk = toks("a (b)");
let ctx = AxisCtx::new(bytes);
let s: StressProfile = fold_tokens(0, &tk, &ctx);
assert_eq!(s.max_depth, 0);
let e: EchoProfile = fold_tokens(0, &tk, &ctx);
assert_eq!(e.novel, 0);
assert_eq!(e.echoed, 0);
assert_eq!(e.count as usize, tk.len());
}
#[test]
fn kind_codes_keep_silhouettes_distinct() {
let bytes = b"12 ab";
let tk = toks("12 ab");
let ctx = AxisCtx::new(bytes);
let p: ShapeProfile = fold_tokens(0, &tk, &ctx);
let num = crate::shape::shape_class(TokenKind::Number, b"12");
assert_eq!(p.silhouette[0], num);
assert_ne!(p.silhouette[0], p.silhouette[2], "a number and a word differ");
}
}