use crate::profile::{AxisCtx, AxisProfile, MagnitudeProfile, Profile};
use crate::token::Token;
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct Action {
pub kinetic: f32,
pub potential: f32,
}
impl Action {
#[must_use]
pub fn total(self) -> f32 {
self.kinetic + self.potential
}
#[must_use]
pub fn lagrangian(self) -> f32 {
self.kinetic - self.potential
}
#[must_use]
pub fn of_profile(p: &Profile) -> Self {
Action { kinetic: p.magnitude.sumsq, potential: p.stress.sum_depth as f32 }
}
}
#[derive(Clone, Debug, Default)]
pub struct ActionField {
pub lagrangian: Vec<f32>,
}
impl ActionField {
#[must_use]
pub fn action(&self, from: usize, to: usize) -> f32 {
let hi = to.min(self.lagrangian.len());
if from >= hi {
return 0.0;
}
self.lagrangian[from..hi].iter().sum()
}
}
#[must_use]
pub fn analyze(toks: &[Token], bytes: &[u8]) -> ActionField {
let stress = crate::stress::analyze(toks, bytes);
let ctx = AxisCtx { stress: Some(&stress), ..AxisCtx::new(bytes) };
let lagrangian: Vec<f32> = toks
.iter()
.enumerate()
.map(|(i, t)| Action::of_profile(&Profile::of_token(i, t, &ctx)).lagrangian())
.collect();
ActionField { lagrangian }
}
#[must_use]
pub fn analyze_bytes(bytes: &[u8]) -> ActionField {
analyze(&crate::lexer::lex(bytes), bytes)
}
#[must_use]
pub fn per_supertoken(toks: &[Token], bytes: &[u8]) -> Vec<Action> {
let stress = crate::stress::analyze(toks, bytes);
let ctx = AxisCtx { stress: Some(&stress), ..AxisCtx::new(bytes) };
let units = crate::supertoken::supertokens_from(toks, bytes);
let n = toks.len();
let mut kinetic: Vec<f64> = Vec::with_capacity(n + 1);
let mut potential: Vec<u64> = Vec::with_capacity(n + 1);
kinetic.push(0.0);
potential.push(0);
for (i, t) in toks.iter().enumerate() {
let m = MagnitudeProfile::of_token(i, t, &ctx);
let depth = stress.frames[i].depth;
kinetic.push(kinetic[i] + f64::from(m.sumsq));
potential.push(potential[i] + u64::from(depth));
}
let mut out = Vec::with_capacity(units.len());
let mut cursor = 0usize;
for u in &units {
while cursor < n && toks[cursor].start() < u.start {
cursor += 1;
}
let lo = cursor;
let hi = lo + toks[lo..].partition_point(|t| t.end() <= u.end);
out.push(Action {
kinetic: (kinetic[hi] - kinetic[lo]) as f32,
potential: (potential[hi] - potential[lo]) as f32,
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profile::fold_tokens;
#[test]
fn per_supertoken_reads_what_the_folded_profile_reads() {
for src in [
&b"{module, [{a, [1, 2, {b, [3, 4]}]}, {c, [5, {d, [6, 7]}]}]}. {x, [{y, 8}]}."[..],
&b"alpha beta gamma delta"[..],
&b"f(x) g(yy) h(zzz)"[..],
] {
let toks = crate::lexer::lex(src);
let stress = crate::stress::analyze(&toks, src);
let ctx = AxisCtx { stress: Some(&stress), ..AxisCtx::new(src) };
let units = crate::supertoken::supertokens_from(&toks, src);
let got = per_supertoken(&toks, src);
assert_eq!(got.len(), units.len(), "one reading per unit for {src:?}");
let mut cursor = 0usize;
for (k, u) in units.iter().enumerate() {
while cursor < toks.len() && toks[cursor].start() < u.start {
cursor += 1;
}
let lo = cursor;
let mut hi = cursor;
while hi < toks.len() && toks[hi].end() <= u.end {
hi += 1;
}
let want = Action::of_profile(&fold_tokens::<Profile>(lo, &toks[lo..hi], &ctx));
assert_eq!(
got[k].potential, want.potential,
"unit {k} potential, {src:?}"
);
assert!(
(got[k].kinetic - want.kinetic).abs() <= 1e-5 * want.kinetic.abs().max(1.0),
"unit {k} kinetic {} against the fold's {}, {src:?}",
got[k].kinetic,
want.kinetic
);
}
}
}
#[test]
fn nesting_raises_the_potential_and_flips_the_lagrangian() {
let flat = analyze_bytes(b"alpha beta gamma");
assert!(flat.lagrangian.iter().all(|&l| l >= 0.0), "no brackets, no potential");
let nested = analyze_bytes(b"alphabet ((( x )))");
assert!(nested.lagrangian.iter().any(|&l| l > 0.0), "the long token carries scale");
assert!(nested.lagrangian.iter().any(|&l| l < 0.0), "the buried one is outweighed");
}
#[test]
fn a_one_character_token_carries_no_kinetic_energy() {
let f = analyze_bytes(b"x");
assert_eq!(f.lagrangian[0], 0.0, "log2(1) is zero, so no kinetic term");
let buried = analyze_bytes(b"((x))");
assert!(
buried.lagrangian.iter().any(|&l| l < 0.0),
"and depth alone drives it negative"
);
}
#[test]
fn a_large_value_raises_the_kinetic_term() {
let small = analyze_bytes(b"(5)");
let large = analyze_bytes(b"(5000000000)");
let ts: f32 = small.lagrangian.iter().sum();
let tl: f32 = large.lagrangian.iter().sum();
assert!(tl > ts, "the larger value carries more kinetic energy at one depth");
}
#[test]
fn action_integrates_over_a_range() {
let f = analyze_bytes(b"a (b) c");
let whole = f.action(0, f.lagrangian.len());
let split = f.action(0, 3) + f.action(3, f.lagrangian.len());
assert!((whole - split).abs() < 1e-3, "the integral is additive over a split");
assert_eq!(f.action(2, 2), 0.0, "an empty range integrates to zero");
assert_eq!(f.action(5, 1), 0.0, "an inverted range integrates to zero");
}
#[test]
fn the_reading_lifts_to_supertokens() {
let bytes = b"let x = 1;\nfoo(2, 3)\nname: bob\n";
let toks = crate::lexer::lex(bytes);
let per = per_supertoken(&toks, bytes);
assert!(!per.is_empty(), "the input has constructs");
assert!(per.iter().any(|a| a.potential > 0.0), "some construct holds load");
assert!(per.iter().any(|a| a.kinetic > 0.0), "some construct carries scale");
let summed: f32 = per.iter().map(|a| a.total()).sum();
assert!(summed > 0.0);
}
#[test]
fn total_energy_is_not_conserved_under_nesting() {
let total = |src: &[u8]| -> (f32, f32) {
let toks = crate::lexer::lex(src);
let stress = crate::stress::analyze(&toks, src);
let ctx = AxisCtx { stress: Some(&stress), ..AxisCtx::new(src) };
let p: Profile = fold_tokens(0, &toks, &ctx);
let a = Action::of_profile(&p);
(a.kinetic, a.potential)
};
let (t0, v0) = total(b"alpha beta");
let (t1, v1) = total(b"( alpha beta )");
let (t2, v2) = total(b"( ( alpha beta ) )");
assert!((t0 - t1).abs() < 1e-3, "kinetic unchanged by one wrap: {t0} vs {t1}");
assert!((t1 - t2).abs() < 1e-3, "and by a second: {t1} vs {t2}");
assert!(v1 > v0, "one wrap raises the potential: {v0} -> {v1}");
assert!(v2 > v1, "two wraps raise it further: {v1} -> {v2}");
assert!(t1 + v1 > t0 + v0, "so the total is not conserved");
assert!(t2 + v2 > t1 + v1);
}
#[test]
fn the_potential_is_extensive_so_it_grows_with_the_span() {
let one = analyze_bytes(b"(a b)");
let two = analyze_bytes(b"(a b a b)");
let v1: f32 = -one.lagrangian.iter().filter(|&&l| l < 0.0).sum::<f32>();
let v2: f32 = -two.lagrangian.iter().filter(|&&l| l < 0.0).sum::<f32>();
assert!(v2 > v1, "a longer held-open span stores more potential");
}
}