use std::collections::HashSet;
use std::sync::Arc;
use crate::ast::{AnchorKind, Atom, Grain, OtherInput, Pattern, Scope};
use crate::engine::Match;
use crate::orbit::OrbitGroup;
use crate::token::{Token, TokenKind};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Reading {
pub axis: &'static str,
pub text: String,
pub value: String,
pub parts: Vec<(&'static str, String)>,
}
pub const EXPLAIN_FIELDS: &[(&str, &[&str])] = &[
("kind", &[]),
("guard", &[]),
("route", &[]),
("magnitude", &[]),
("baseline", &["scope", "mean", "spread", "count"]),
("spectral", &["entropy", "period", "strength", "novelty", "texture"]),
("echo", &["count", "occurrence", "period", "rung"]),
("order", &[]),
("template", &["rarity", "cut", "template", "count", "covered"]),
("nesting", &["depth"]),
("seam", &["on", "grain"]),
("ambiguous", &["contested", "grain"]),
("gravity", &["strain", "bound", "class", "grain"]),
("construct", &["role", "depth", "begins"]),
("phase", &["phase", "period"]),
("join", &["occurs", "other", "rung", "key"]),
("field", &["index"]),
];
const WHOLE_MATCH_FIELDS: [&str; 3] = ["kind", "guard", "route"];
pub fn check_explain_field(axis: &str, piece: Option<&str>) -> Result<(), String> {
let Some((_, pieces)) = EXPLAIN_FIELDS.iter().find(|(name, _)| *name == axis) else {
let names: Vec<&str> = EXPLAIN_FIELDS.iter().map(|(name, _)| *name).collect();
return Err(format!("${{@{axis}}} reads no axis; the axes are {}", names.join(", ")));
};
match piece {
None => Ok(()),
Some(p) if pieces.contains(&p) => Ok(()),
Some(p) if pieces.is_empty() => {
Err(format!("${{@{axis}}} states one value and takes no .{p} after it"))
}
Some(p) => {
Err(format!("${{@{axis}.{p}}} reads nothing; {axis} states {}", pieces.join(", ")))
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Explanation {
pub tokens: Vec<(String, String)>,
pub guards: Vec<String>,
pub readings: Vec<Reading>,
pub route: String,
}
impl Explanation {
#[must_use]
pub fn field(&self, axis: &str, piece: Option<&str>, at: Option<usize>) -> String {
let mut values: Vec<String> = if WHOLE_MATCH_FIELDS.contains(&axis) {
match axis {
"kind" => self.tokens.iter().map(|(k, _)| k.clone()).collect(),
"guard" => self.guards.clone(),
_ => vec![self.route.clone()],
}
} else {
self.readings
.iter()
.filter(|r| r.axis == axis)
.filter_map(|r| match piece {
None => Some(r.value.clone()),
Some(p) => {
r.parts.iter().find(|(name, _)| *name == p).map(|(_, v)| v.clone())
}
})
.collect()
};
match at {
Some(i) if i < values.len() => values.swap_remove(i),
Some(_) => String::new(),
None => values.join(", "),
}
}
}
#[must_use]
pub fn route_of(recorded: &[crate::trace::Rung]) -> String {
recorded
.iter()
.rev()
.find(|r| r.ladder == "scan")
.map_or_else(|| "no route recorded".to_string(), |r| r.rung.clone())
}
fn guard_of(kind: TokenKind) -> Option<String> {
let text = match kind {
TokenKind::CreditCard => {
"creditcard: 13 to 19 digits in an issuer's grouping, passing the Luhn check"
}
TokenKind::Jwt => "jwt: three base64url segments opening with the encoded JSON header",
TokenKind::Geo => {
"geo: a latitude within 90 degrees and a longitude within 180, each with four fractional digits, standing alone"
}
TokenKind::Phone => {
"phone: an E.164 country code with 7 to 15 digits in all, or a North American number written nationally"
}
TokenKind::Base64 => {
"base64: a length a multiple of four, at least sixteen, with charset diversity"
}
TokenKind::Custom(id) => {
let name = crate::library::name_of(id)?;
let entry = crate::library::entries().iter().find(|e| e.name == name)?;
if !entry.is_guarded() {
return None;
}
return Some(format!("{name}: {}", entry.what));
}
_ => return None,
};
Some(text.to_string())
}
fn kind_name(kind: TokenKind, shapes: &crate::custom::ShapeSet) -> String {
match kind {
TokenKind::Custom(id) => match shapes.name_of(id) {
Some(name) => name.to_string(),
None => kind.name().to_string(),
},
k => k.name().to_string(),
}
}
#[derive(Default)]
struct Axes {
magnitude: bool,
scopes: Vec<Scope>,
spectral: bool,
echo: Vec<OrbitGroup>,
order: bool,
rare: Vec<crate::templates::Rarity>,
nested: bool,
seam: Vec<Grain>,
ambiguous: Vec<Grain>,
gravity: Vec<Grain>,
construct: bool,
phase: bool,
periods: Vec<crate::ast::PeriodRef>,
field: bool,
joins: Vec<(Arc<OtherInput>, OrbitGroup)>,
}
impl Axes {
fn gather(pat: &Pattern, into: &mut Axes) {
match pat {
Pattern::Atom(a) => Self::atom(a, into),
Pattern::Within(v, _) => {
for e in v {
Self::atom(&e.atom, into);
}
}
Pattern::Anchor(k) => Self::anchor(k, into),
Pattern::Empty | Pattern::Guard(..) => {}
Pattern::Field(_, p) => {
into.field = true;
Self::gather(p, into);
}
Pattern::Bind(_, _, p)
| Pattern::Balanced(_, p)
| Pattern::Assert(p, _, _)
| Pattern::Star(p, _)
| Pattern::Plus(p, _)
| Pattern::Opt(p, _)
| Pattern::Repeat(p, _, _, _)
| Pattern::Atomic(p) => Self::gather(p, into),
Pattern::Concat(v) | Pattern::Alt(v, _) => {
for p in v {
Self::gather(p, into);
}
}
}
}
fn atom(a: &Atom, into: &mut Axes) {
match a {
Atom::Magnitude(p) | Atom::KindMag(_, p) => {
into.magnitude = true;
if let Some(scope) = p.scope()
&& !into.scopes.contains(scope)
{
into.scopes.push(scope.clone());
}
}
Atom::Spectral(_) => into.spectral = true,
Atom::RegisterKin(_, g) => {
if !into.gravity.contains(g) {
into.gravity.push(*g);
}
}
Atom::Class(c) => {
for m in c.members() {
Self::atom(m, into);
}
}
_ => {}
}
}
fn anchor(k: &AnchorKind, into: &mut Axes) {
match k {
AnchorKind::Novel | AnchorKind::Echoed => Self::rung(OrbitGroup::Identity, into),
AnchorKind::Echo(_, g) => Self::rung(*g, into),
AnchorKind::Order(_) => into.order = true,
AnchorKind::Rare(cut) => {
if !into.rare.contains(cut) {
into.rare.push(*cut);
}
}
AnchorKind::Nested(_) => into.nested = true,
AnchorKind::Seam(g) => {
if !into.seam.contains(g) {
into.seam.push(*g);
}
}
AnchorKind::Ambiguous(g) => {
if !into.ambiguous.contains(g) {
into.ambiguous.push(*g);
}
}
AnchorKind::Gravity(_, g, ..) | AnchorKind::Kin(g, _) => {
if !into.gravity.contains(g) {
into.gravity.push(*g);
}
}
AnchorKind::SuperStart | AnchorKind::SuperRole(_) => into.construct = true,
AnchorKind::Phase(_) => into.phase = true,
AnchorKind::PhaseIn(_, period) => {
if !into.periods.contains(period) {
into.periods.push(*period);
}
}
AnchorKind::Joined { other, group, .. } => {
if !into.joins.iter().any(|(o, g)| Arc::ptr_eq(o, other) && g == group) {
into.joins.push((Arc::clone(other), *group));
}
}
AnchorKind::LineStart
| AnchorKind::LineEnd
| AnchorKind::InputStart
| AnchorKind::InputEnd
| AnchorKind::Resume
| AnchorKind::ResetStart => {}
}
}
fn rung(g: OrbitGroup, into: &mut Axes) {
if !into.echo.contains(&g) {
into.echo.push(g);
}
}
}
pub struct Explainer<'a> {
input: &'a [u8],
toks: Vec<Token>,
shapes: crate::custom::ShapeSet,
axes: Axes,
context: Option<crate::context::ContextField>,
relation: Option<crate::context::RelationContext>,
spectral: Option<crate::spectral::SpectralField>,
echo: Vec<(OrbitGroup, crate::echo::EchoField)>,
order: Option<Vec<Option<bool>>>,
templates: Option<crate::templates::Mining>,
stress: Option<crate::stress::StressField>,
seam: Vec<(Grain, Vec<usize>)>,
contested: Vec<(Grain, Vec<usize>)>,
gravity: Vec<crate::gravity::Readings>,
bands: Option<(Vec<u16>, Vec<u32>)>,
supers: Option<crate::supertoken::SuperContext>,
fields: Option<Vec<u32>>,
joins: Vec<(Arc<OtherInput>, OrbitGroup, HashSet<String>)>,
}
fn field_at_token(input: &[u8], toks: &[Token]) -> Vec<u32> {
let starts = crate::engine::field_start_index(input, toks);
let mut here = 0;
starts
.into_iter()
.map(|n| {
if n != 0 {
here = n;
}
here
})
.collect()
}
impl<'a> Explainer<'a> {
#[must_use]
pub fn new(pattern: &Pattern, input: &'a [u8], shapes: &crate::custom::ShapeSet) -> Self {
let shapes = shapes.with_library_shapes(&pattern.library_kinds());
let toks = if shapes.is_empty() {
crate::lexer::lex(input)
} else {
crate::lexer::lex_with_shapes(input, &crate::lexer::blob_runs(input), &shapes, 0)
};
let mut axes = Axes::default();
Axes::gather(pattern, &mut axes);
let reads_window = axes.scopes.contains(&Scope::Window);
let reads_relation = axes.scopes.iter().any(|s| *s != Scope::Window) || axes.phase;
let context = reads_window.then(|| crate::context::analyze(input));
let relation = reads_relation.then(|| crate::context::relate_bytes(input));
let spectral = axes.spectral.then(|| crate::spectral::analyze(input));
let echo = axes
.echo
.iter()
.map(|&g| {
let cfg = crate::echo::EchoConfig { orbit: g, ..Default::default() };
(g, crate::echo::analyze_with(&toks, input, &cfg))
})
.collect();
let order = axes.order.then(|| {
crate::engine::timestamp_order(&toks, input, crate::typed::Clock::current())
});
let templates =
(!axes.rare.is_empty()).then(|| crate::templates::Mining::mine_tokens(&toks, input));
let stress = axes.nested.then(|| crate::stress::analyze(&toks, input));
let seam_cfg = crate::seam::SeamConfig::default();
let seam = axes
.seam
.iter()
.map(|&g| {
let cuts = match g {
Grain::Byte => crate::seam::analyze(input).strong_cuts(),
Grain::Token => crate::seam::analyze_tokens(&toks, &seam_cfg),
Grain::Super => crate::seam::analyze_supertokens(
&crate::supertoken::supertokens_from(&toks, input),
&seam_cfg,
),
};
(g, cuts)
})
.collect();
let obs_cfg = crate::observation::ObservationConfig::default();
let contested = axes
.ambiguous
.iter()
.map(|&g| {
let points = match g {
Grain::Byte => crate::observation::analyze(input).contested,
Grain::Token => crate::observation::contested_tokens(&toks, &obs_cfg),
Grain::Super => crate::observation::contested_supertokens(
&crate::supertoken::supertokens_from(&toks, input),
&obs_cfg,
),
};
(g, points)
})
.collect();
let gravity = axes.gravity.iter().map(|&g| crate::gravity::Readings::read(g, input, &toks)).collect();
let bands = (!axes.periods.is_empty()).then(|| {
let mut next = 0u32;
let sig_index = toks
.iter()
.map(|t| {
if t.is_significant() {
next += 1;
next - 1
} else {
u32::MAX
}
})
.collect();
(crate::context::live_periods(&toks, input), sig_index)
});
let supers = axes.construct.then(|| crate::supertoken::SuperContext::build(&toks, input));
let fields = axes.field.then(|| field_at_token(input, &toks));
let joins = axes
.joins
.iter()
.map(|(other, group)| {
let keys: HashSet<String> = other
.tokens
.iter()
.filter(|t| crate::echo::keyed_kind(t.kind))
.map(|t| crate::orbit::canonical(&other.bytes[t.span()], *group))
.collect();
(Arc::clone(other), *group, keys)
})
.collect();
Explainer {
input,
toks,
shapes,
axes,
context,
relation,
spectral,
echo,
order,
templates,
stress,
seam,
contested,
gravity,
bands,
supers,
fields,
joins,
}
}
#[must_use]
pub fn tokens(&self, at: std::ops::Range<usize>) -> Vec<(String, String)> {
self.toks
.iter()
.filter(|t| t.is_significant() && t.start() >= at.start && t.end() <= at.end)
.map(|t| (kind_name(t.kind, &self.shapes), String::from_utf8_lossy(&self.input[t.span()]).into_owned()))
.collect()
}
#[must_use]
pub fn explain(&self, m: &Match, route: &str) -> Explanation {
let indexes: Vec<usize> = self
.toks
.iter()
.enumerate()
.filter(|(_, t)| t.is_significant() && t.start() >= m.start && t.end() <= m.end)
.map(|(i, _)| i)
.collect();
let text = |i: usize| String::from_utf8_lossy(&self.input[self.toks[i].span()]).into_owned();
let tokens = self.tokens(m.start..m.end);
let guards: Vec<String> = indexes.iter().filter_map(|&i| guard_of(self.toks[i].kind)).collect();
let mut readings: Vec<Reading> = Vec::new();
let mut read = |axis: &'static str,
i: usize,
value: String,
parts: Vec<(&'static str, String)>| {
readings.push(Reading { axis, text: text(i), value, parts });
};
for &i in &indexes {
let t = &self.toks[i];
if self.axes.magnitude {
let mag = crate::magnitude::token_magnitude(t.kind, &self.input[t.span()]);
read("magnitude", i, format!("{mag:.2}"), Vec::new());
for scope in &self.axes.scopes {
if let Some((value, parts)) = self.baseline(scope, i, m) {
read("baseline", i, value, parts);
}
}
}
if let Some(field) = &self.spectral {
let f = field.frame_at(t.start());
let texture = crate::spectral::texture_of(&f).label();
read(
"spectral",
i,
format!(
"entropy {:.2}, period {} (strength {:.2}), novelty {:.2}, texture {texture}",
f.entropy, f.period, f.period_strength, f.novelty,
),
vec![
("entropy", format!("{:.2}", f.entropy)),
("period", f.period.to_string()),
("strength", format!("{:.2}", f.period_strength)),
("novelty", format!("{:.2}", f.novelty)),
("texture", texture.to_string()),
],
);
}
for (g, field) in &self.echo {
let (value, parts) = match field.frames.get(i) {
Some(fr) if fr.keyed => {
let period = if fr.period > 0.0 {
format!(", period {:.0} bytes", fr.period)
} else {
String::new()
};
(
format!("count {}, occurrence {} of {}{period} at the {} rung", fr.count, fr.nth, fr.count, g.label()),
vec![
("count", fr.count.to_string()),
("occurrence", fr.nth.to_string()),
(
"period",
if fr.period > 0.0 {
format!("{:.0}", fr.period)
} else {
String::new()
},
),
("rung", g.label().to_string()),
],
)
}
_ => ("unkeyed".to_string(), Vec::new()),
};
read("echo", i, value, parts);
}
if let Some(order) = &self.order
&& t.kind == TokenKind::Timestamp
{
let value = match order.get(i).copied().flatten() {
Some(true) => "at or after the timestamp before it",
Some(false) => "before the timestamp before it",
None => "the first timestamp, with nothing to stand against",
};
read("order", i, value.to_string(), Vec::new());
}
if let Some(mining) = &self.templates {
for cut in &self.axes.rare {
let (value, parts) = match mining.token_template(i) {
Some(k) => {
let tpl = &mining.templates[k];
let rarity = if mining.is_rare(k, *cut) { "rare" } else { "common" };
(
format!(
"{rarity} under @shape:rare{}: `{}` on {} of {} lines",
cut.label(),
tpl.readable(),
tpl.count(),
mining.covered()
),
vec![
("rarity", rarity.to_string()),
("cut", cut.label().to_string()),
("template", tpl.readable()),
("count", tpl.count().to_string()),
("covered", mining.covered().to_string()),
],
)
}
None => ("no template".to_string(), Vec::new()),
};
read("template", i, value, parts);
}
}
if let Some(field) = &self.stress {
let depth = field.depth_at(t.start());
read("nesting", i, format!("depth {depth}"), vec![("depth", depth.to_string())]);
}
for (g, cuts) in &self.seam {
let at = cuts.binary_search(&t.start()).is_ok();
let on = if at { "on" } else { "not on" };
read(
"seam",
i,
format!("{on} at a {} cut", grain_name(*g)),
vec![("on", on.to_string()), ("grain", grain_name(*g).to_string())],
);
}
for (g, points) in &self.contested {
let k = points.partition_point(|&x| x < t.start());
let contested = points.get(k).is_some_and(|&x| x < t.end());
let state = if contested { "contested" } else { "settled" };
read(
"ambiguous",
i,
format!("{state} at the {} grain", grain_name(*g)),
vec![("contested", state.to_string()), ("grain", grain_name(*g).to_string())],
);
}
for r in &self.gravity {
let grain = grain_name(r.grain());
let Some(u) = r.unit_at(t.start()) else {
read("gravity", i, format!("no unit at the {grain} grain"), Vec::new());
continue;
};
let strain = r.strain[u];
let bound = r.unit_starting_at(t.start()).map(|b| r.binding[b]);
let class = match r.class.get(r.type_of(u) as usize).copied() {
Some(crate::gravity::UNPLACED) | None => "unplaced".to_string(),
Some(c) => c.to_string(),
};
let bound_text = match bound {
Some(b) => format!("bound {b:.2} bits (percentile {:.0})", r.binding_rank(b)),
None => "no cut before it".to_string(),
};
read(
"gravity",
i,
format!(
"strain {strain:.2} bits (percentile {:.0}), {bound_text}, class {class} at the {grain} grain",
r.strain_rank(strain)
),
vec![
("strain", format!("{strain:.2}")),
("bound", bound.map_or_else(String::new, |b| format!("{b:.2}"))),
("class", class),
("grain", grain.to_string()),
],
);
}
if let Some(supers) = &self.supers {
let (value, parts) = match supers.unit_of(i) {
Some(u) => {
let begins = supers.starts_unit(i);
(
format!(
"{} construct at depth {}{}",
u.role.label(),
u.depth,
if begins { ", which this token begins" } else { "" }
),
vec![
("role", u.role.label().to_string()),
("depth", u.depth.to_string()),
("begins", begins.to_string()),
],
)
}
None => ("in no construct".to_string(), Vec::new()),
};
read("construct", i, value, parts);
}
if self.axes.phase
&& let Some(relation) = &self.relation
{
let (value, parts) = match relation.phase_of.get(i) {
Some(&p) if p != u16::MAX => {
(format!("phase {p}"), vec![("phase", p.to_string())])
}
_ => ("no period".to_string(), Vec::new()),
};
read("phase", i, value, parts);
}
if let Some((live, sig_index)) = &self.bands {
for period in &self.axes.periods {
let named = match period {
crate::ast::PeriodRef::Length(len) => live.contains(len).then_some(*len),
crate::ast::PeriodRef::Rank(n) => {
usize::from(*n).checked_sub(1).and_then(|k| live.get(k).copied())
}
};
let (value, parts) = match (named, sig_index.get(i)) {
(Some(len), Some(&at)) if at != u32::MAX => {
let column = at % u32::from(len);
(
format!("phase {column} of the {len}-token period"),
vec![("phase", column.to_string()), ("period", len.to_string())],
)
}
(Some(_) | None, Some(_) | None) => match period {
crate::ast::PeriodRef::Length(len) => (format!("no live {len}-token period"), Vec::new()),
crate::ast::PeriodRef::Rank(n) => (format!("no live period #{n}"), Vec::new()),
},
};
read("phase", i, value, parts);
}
}
if let Some(fields) = &self.fields {
let (value, parts) = match fields.get(i).copied() {
Some(n) if n != 0 => {
(format!("field {n}"), vec![("index", n.to_string())])
}
_ => ("before the first field".to_string(), Vec::new()),
};
read("field", i, value, parts);
}
for (other, group, keys) in &self.joins {
let (value, parts) = if crate::echo::keyed_kind(t.kind) {
let key = crate::orbit::canonical(&self.input[t.span()], *group);
let occurs = if keys.contains(&key) { "occurs in" } else { "absent from" };
let sentence = if key == text(i) {
format!("{occurs} {} at the {} rung", other.name, group.label())
} else {
format!("{occurs} {} at the {} rung, keyed {key}", other.name, group.label())
};
(
sentence,
vec![
("occurs", occurs.to_string()),
("other", other.name.clone()),
("rung", group.label().to_string()),
("key", key),
],
)
} else {
("unkeyed".to_string(), Vec::new())
};
read("join", i, value, parts);
}
}
Explanation { tokens, guards, readings, route: route.to_string() }
}
fn baseline(
&self,
scope: &Scope,
i: usize,
m: &Match,
) -> Option<(String, Vec<(&'static str, String)>)> {
let profile = match scope {
Scope::Window => {
let field = self.context.as_ref()?;
let prev = (0..i).rev().find(|&j| self.toks[j].is_significant())?;
&field.at_token.get(prev)?.magnitude
}
Scope::Phase => &self.relation.as_ref()?.at_token.get(i)?.phase.magnitude,
Scope::Regime => &self.relation.as_ref()?.at_token.get(i)?.regime.magnitude,
Scope::Echo => &self.relation.as_ref()?.at_token.get(i)?.echoing.magnitude,
Scope::Enclosing => &self.relation.as_ref()?.at_token.get(i)?.enclosing.magnitude,
Scope::Key(name) => {
let k = m.names().iter().position(|n| n == name)?;
let span = m.captures().get(k)?;
let key = self.toks.partition_point(|t| t.start() < span.start());
&self.relation.as_ref()?.value_history.get(key)?.magnitude
}
};
Some((
format!(
"{}: mean {:.2}, spread {:.2}, over {}",
scope_name(scope),
profile.mean(),
profile.std_dev(),
profile.count
),
vec![
("scope", scope_name(scope)),
("mean", format!("{:.2}", profile.mean())),
("spread", format!("{:.2}", profile.std_dev())),
("count", profile.count.to_string()),
],
))
}
}
fn grain_name(g: Grain) -> &'static str {
match g {
Grain::Byte => "byte",
Grain::Token => "token",
Grain::Super => "construct",
}
}
fn scope_name(scope: &Scope) -> String {
match scope {
Scope::Window => "window".to_string(),
Scope::Phase => "phase".to_string(),
Scope::Regime => "regime".to_string(),
Scope::Echo => "echo".to_string(),
Scope::Enclosing => "enclosing".to_string(),
Scope::Key(name) => format!("key {name}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_declared_or_library_kind_is_named_by_its_declaration() {
let mut shapes = crate::custom::ShapeSet::new();
shapes.declare("ticket = `[A-Z]{2,4}-\\d{1,4}`", crate::custom::Precedence::Before).expect("a bounded shape");
let pattern = crate::parser::parse_with_shapes(r"\{ticket}", &shapes).expect("valid pattern");
let explainer = Explainer::new(&pattern, b"see AB-12 now", &shapes);
assert_eq!(explainer.tokens(4..9), [("ticket".to_string(), "AB-12".to_string())]);
let iban = b"pay GB82 WEST 1234 5698 7654 32";
let pattern = crate::parse(r"\{iban}").expect("valid pattern");
let explainer = Explainer::new(&pattern, iban, &crate::custom::ShapeSet::new());
assert_eq!(explainer.tokens(4..iban.len()), [("iban".to_string(), "GB82 WEST 1234 5698 7654 32".to_string())]);
}
}