use std::collections::HashMap;
use crate::lexer::lex;
use crate::token::{Token, TokenKind};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum RelationKind {
Encloses,
Operator,
Adjacent,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Edge {
pub from: usize,
pub to: usize,
pub kind: RelationKind,
}
#[derive(Clone, Debug, Default)]
pub struct RelationFrame {
pub depth: u16,
pub enclosure: Vec<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Chord {
pub from: usize,
pub to: usize,
pub residual: i32,
}
#[derive(Clone, Debug, Default)]
pub struct RelationField {
pub n_tokens: usize,
pub spans: Vec<(usize, usize)>,
pub frames: Vec<RelationFrame>,
pub edges: Vec<Edge>,
pub chords: Vec<Chord>,
pub nesting_load: usize,
}
impl RelationField {
pub fn edges_of(&self, kind: RelationKind) -> impl Iterator<Item = &Edge> {
self.edges.iter().filter(move |e| e.kind == kind)
}
#[must_use]
pub fn max_depth(&self) -> u16 {
self.frames.iter().map(|f| f.depth).max().unwrap_or(0)
}
#[must_use]
pub fn holonomy(&self) -> u32 {
self.chords.iter().map(|c| c.residual.unsigned_abs()).sum()
}
#[must_use]
pub fn net_holonomy(&self) -> i32 {
self.chords.iter().map(|c| c.residual).sum()
}
#[must_use]
pub fn twisted_chords(&self) -> usize {
self.chords.iter().filter(|c| c.residual != 0).count()
}
}
fn is_operator(text: &[u8]) -> bool {
matches!(text, b"=" | b":")
}
fn prev_significant(toks: &[Token], i: usize) -> Option<usize> {
(0..i).rev().find(|&j| toks[j].is_significant())
}
fn next_significant(toks: &[Token], i: usize) -> Option<usize> {
(i + 1..toks.len()).find(|&j| toks[j].is_significant())
}
#[derive(Clone, Debug)]
pub struct NodeMap {
of_token: Vec<Option<usize>>,
n_nodes: usize,
}
impl NodeMap {
#[must_use]
pub fn per_token(n_tokens: usize) -> Self {
NodeMap { of_token: (0..n_tokens).map(Some).collect(), n_nodes: n_tokens }
}
#[must_use]
pub fn per_supertoken(units: &[crate::supertoken::SuperToken], toks: &[Token]) -> Self {
let mut of_token = vec![None; toks.len()];
let mut u = 0usize;
for (i, t) in toks.iter().enumerate() {
while u < units.len() && units[u].end <= t.start() {
u += 1;
}
if u < units.len() && t.start() >= units[u].start && t.end() <= units[u].end {
of_token[i] = Some(u);
}
}
NodeMap { of_token, n_nodes: units.len() }
}
#[must_use]
pub fn n_nodes(&self) -> usize {
self.n_nodes
}
#[must_use]
pub fn node_of(&self, i: usize) -> Option<usize> {
self.of_token.get(i).copied().flatten()
}
}
impl RelationField {
#[must_use]
pub fn contracted_edges(&self, map: &NodeMap) -> Vec<(usize, usize)> {
let mut out: Vec<(usize, usize)> = Vec::new();
let mut push = |a: usize, b: usize| {
if a != b {
out.push((a.min(b), a.max(b)));
}
};
for e in &self.edges {
if let (Some(u), Some(v)) = (map.node_of(e.from), map.node_of(e.to)) {
push(u, v);
}
}
for c in &self.chords {
if let (Some(u), Some(v)) = (map.node_of(c.from), map.node_of(c.to)) {
push(u, v);
}
}
out.sort_unstable();
out.dedup();
out
}
}
#[must_use]
pub fn analyze(toks: &[Token], bytes: &[u8]) -> RelationField {
let n = toks.len();
let mut frames: Vec<RelationFrame> = Vec::with_capacity(n);
let mut edges: Vec<Edge> = Vec::new();
let mut stack: Vec<(usize, Option<usize>)> = Vec::new();
for i in 0..n {
let kind = toks[i].kind;
if matches!(kind, TokenKind::Close(_)) && toks[i].mate().is_some() {
stack.pop();
}
let enclosure: Vec<usize> = stack.iter().filter_map(|&(_, head)| head).collect();
let depth = stack.len() as u16;
if toks[i].is_significant() && !matches!(kind, TokenKind::Open(_) | TokenKind::Close(_)) {
for &head in &enclosure {
edges.push(Edge { from: head, to: i, kind: RelationKind::Encloses });
}
}
frames.push(RelationFrame { depth, enclosure });
if matches!(kind, TokenKind::Open(_)) && toks[i].mate().is_some() {
let head = prev_significant(toks, i)
.filter(|&j| matches!(toks[j].kind, TokenKind::Word));
stack.push((i, head));
}
}
for i in 0..n {
if toks[i].kind == TokenKind::Punct
&& is_operator(&bytes[toks[i].span()])
&& let (Some(l), Some(r)) = (prev_significant(toks, i), next_significant(toks, i))
{
edges.push(Edge { from: l, to: r, kind: RelationKind::Operator });
}
}
let sig: Vec<usize> = (0..n).filter(|&i| toks[i].is_significant()).collect();
for w in sig.windows(2) {
edges.push(Edge { from: w[0], to: w[1], kind: RelationKind::Adjacent });
}
let mut last_seen: HashMap<&[u8], usize> = HashMap::new();
let mut chords: Vec<Chord> = Vec::new();
for i in 0..n {
let content = toks[i].is_significant()
&& !matches!(toks[i].kind, TokenKind::Open(_) | TokenKind::Close(_) | TokenKind::Punct);
if !content {
continue;
}
let text = &bytes[toks[i].span()];
if let Some(&prev) = last_seen.get(text) {
let residual = i32::from(frames[i].depth) - i32::from(frames[prev].depth);
chords.push(Chord { from: prev, to: i, residual });
}
last_seen.insert(text, i);
}
let nesting_load = frames.iter().filter(|f| f.depth > 0).count();
let spans = toks.iter().map(|t| (t.start(), t.end())).collect();
RelationField { n_tokens: n, spans, frames, edges, chords, nesting_load }
}
#[must_use]
pub fn analyze_bytes(bytes: &[u8]) -> RelationField {
analyze(&lex(bytes), bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unpaired_bracket_encloses_nothing() {
let open = analyze_bytes(b"alpha [BETA;GAMMA, delta epsilon zeta eta theta");
assert!(
open.frames.iter().all(|f| f.depth == 0),
"an unpaired opener is text, so nothing after it is nested"
);
assert!(
open.frames.iter().all(|f| f.enclosure.is_empty()),
"and nothing after it is enclosed by it"
);
assert_eq!(
open.edges.iter().filter(|e| e.kind == RelationKind::Encloses).count(),
0,
"so it emits no enclosure edges"
);
let close = analyze_bytes(b"alpha BETA] gamma delta");
assert!(close.frames.iter().all(|f| f.depth == 0), "an unpaired closer is text too");
}
#[test]
fn a_paired_bracket_still_encloses_what_it_holds() {
let f = analyze_bytes(b"alpha (beta gamma) delta");
assert!(f.frames.iter().any(|fr| fr.depth == 1), "the pair nests its contents");
let encloses = f.edges.iter().filter(|e| e.kind == RelationKind::Encloses).count();
assert!(encloses > 0, "and the head bears on them");
let mixed = analyze_bytes(b"alpha (beta gamma) delta [EPSILON;ZETA, eta");
assert_eq!(
mixed.edges.iter().filter(|e| e.kind == RelationKind::Encloses).count(),
encloses,
"the unpaired opener adds no enclosure of its own"
);
}
#[test]
fn depth_returns_to_zero_when_every_bracket_is_unpaired() {
let src = b"[".repeat(200);
let f = analyze_bytes(&src);
assert!(f.frames.iter().all(|fr| fr.depth == 0), "none of them pair, so none of them nest");
}
#[test]
fn net_holonomy_separates_arrangements_the_magnitude_collapses() {
let inward = field("x (x)");
let outward = field("(x) x");
assert_eq!(inward.chords.len(), 1);
assert_eq!(outward.chords.len(), 1);
assert_eq!(inward.holonomy(), outward.holonomy());
assert_eq!(inward.net_holonomy(), 1);
assert_eq!(outward.net_holonomy(), -1);
assert_eq!(inward.net_holonomy(), -outward.net_holonomy());
}
#[test]
fn net_holonomy_is_zero_where_there_is_no_asymmetry() {
assert_eq!(field("x y x").net_holonomy(), 0);
let mixed = field("a (a) (b) b");
assert_eq!(mixed.net_holonomy(), 0);
assert_eq!(mixed.holonomy(), 2, "the magnitude counts both crossings");
}
fn field(s: &str) -> RelationField {
analyze_bytes(s.as_bytes())
}
fn word_at(f: &RelationField, bytes: &[u8], want: &str) -> usize {
(0..f.n_tokens)
.find(|&i| &bytes[f.spans[i].0..f.spans[i].1] == want.as_bytes())
.expect("word present")
}
#[test]
fn flat_text_has_no_load_and_no_holonomy() {
let f = field("the cat sat on the mat");
assert_eq!(f.nesting_load, 0);
assert_eq!(f.holonomy(), 0);
assert_eq!(f.max_depth(), 0);
assert!(f.edges_of(RelationKind::Encloses).next().is_none());
}
#[test]
fn a_pure_tree_has_zero_holonomy_however_deep() {
for s in ["f(x)", "f(g(x))", "a(b(c(d(e))))"] {
let f = field(s);
assert!(f.nesting_load > 0, "{s} carries nesting load");
assert_eq!(f.holonomy(), 0, "{s} is a tree: zero holonomy");
assert!(f.chords.is_empty(), "{s} has no reuse chords");
}
}
#[test]
fn scope_crossing_reuse_carries_holonomy() {
let twisted = field("a(a)");
assert_eq!(twisted.chords.len(), 1);
assert_eq!(twisted.chords[0].residual, 1);
assert_eq!(twisted.holonomy(), 1);
assert_eq!(twisted.twisted_chords(), 1);
let flat = field("a(b)a");
assert_eq!(flat.chords.len(), 1);
assert_eq!(flat.chords[0].residual, 0);
assert_eq!(flat.holonomy(), 0);
assert_eq!(flat.twisted_chords(), 0);
assert_eq!(field("a((a))").holonomy(), 2);
}
#[test]
fn nesting_direction_is_a_directed_edge() {
let a = field("f(g(x))");
let b = field("g(f(x))");
let has = |f: &RelationField, bytes: &[u8], from: &str, to: &str| {
let fi = word_at(f, bytes, from);
let ti = word_at(f, bytes, to);
f.edges_of(RelationKind::Encloses).any(|e| e.from == fi && e.to == ti)
};
assert!(has(&a, b"f(g(x))", "f", "g"));
assert!(!has(&a, b"f(g(x))", "g", "f"));
assert!(has(&b, b"g(f(x))", "g", "f"));
assert!(!has(&b, b"g(f(x))", "f", "g"));
let letter_depths = |f: &RelationField, bytes: &[u8]| {
let mut d: Vec<u16> = (0..f.n_tokens)
.filter(|&i| bytes[f.spans[i].0].is_ascii_alphabetic())
.map(|i| f.frames[i].depth)
.collect();
d.sort_unstable();
d
};
assert_eq!(letter_depths(&a, b"f(g(x))"), letter_depths(&b, b"g(f(x))"));
}
#[test]
fn operator_direction_is_a_directed_edge() {
let a = field("a = b");
let b = field("b = a");
let ai = word_at(&a, b"a = b", "a");
let bi = word_at(&a, b"a = b", "b");
assert!(a.edges_of(RelationKind::Operator).any(|e| e.from == ai && e.to == bi));
let bi2 = word_at(&b, b"b = a", "b");
let ai2 = word_at(&b, b"b = a", "a");
assert!(b.edges_of(RelationKind::Operator).any(|e| e.from == bi2 && e.to == ai2));
}
#[test]
fn deeper_nesting_raises_the_load() {
assert_eq!(field("a b c").nesting_load, 0);
assert!(field("f(x)").nesting_load > 0);
assert!(field("f(g(x))").nesting_load > field("f(x)").nesting_load);
}
}