use super::*;
use crate::graph::adapter::Provenance;
use crate::syntax::ast::{ArithOp, PatSeg};
use crate::syntax::lexer::lex;
use crate::syntax::parser::parse;
use std::collections::HashMap;
#[test]
fn glob_match_anchors_and_stars() {
use PatSeg::{Lit, Star};
let lit = |t: &str| Lit(t.to_string());
assert!(glob_match(&[Star, lit("web"), Star], "app-web-2"));
assert!(!glob_match(&[Star, lit("web"), Star], "app"));
assert!(glob_match(&[lit("app"), Star], "app-web"));
assert!(!glob_match(&[lit("app"), Star], "the-app"));
assert!(glob_match(&[Star, lit(".gz")], "data.gz"));
assert!(!glob_match(&[Star, lit(".gz")], "data.gz.bak"));
assert!(glob_match(&[lit("a"), Star, lit("z")], "abcz"));
assert!(glob_match(&[lit("a"), Star, lit("z")], "az"));
assert!(!glob_match(&[lit("a"), Star, lit("z")], "abc"));
assert!(glob_match(&[Star, lit("x"), Star], "x"));
assert!(glob_match(&[Star, lit("ab"), Star, lit("b")], "ab-ab-b"));
assert!(glob_match(&[] as &[PatSeg], ""));
assert!(!glob_match(&[] as &[PatSeg], "x"));
assert!(glob_match(&[Star, lit("gun")], "льюис gun"));
}
#[test]
fn pattern_test_null_semantics() {
use PatSeg::{Lit, Star};
let p = [Star, Lit("x".into()), Star];
assert!(!pattern_test(&p, &Value::Null));
assert!(pattern_test(&p, &Value::Str("axb".into())));
assert!(pattern_test(
&[Star, Lit("42".into()), Star],
&Value::Int(142)
));
}
#[test]
fn quantital_coercion_and_arith() {
let sc: &dyn Fn(&str) -> Option<(f64, String)> = &crate::value::quantity::scale_expr;
let q = |value: f64, base: &str, written: Option<(f64, &str)>| Value::Quantity {
value,
base: base.into(),
written: written.map(|(v, u)| (v, u.to_string())),
};
let len = q(42000.0, "m", Some((42.0, "km")));
assert_eq!(
value_cmp(&len, &Value::Str("5km".into()), sc),
Some(Ordering::Greater)
);
assert!(value_eq(&len, &Value::Str("42km".into()), sc));
assert!(!value_eq(&len, &Value::Float(42.0), sc));
assert!(!value_eq(&len, &Value::Float(42000.0), sc));
assert_eq!(value_cmp(&len, &Value::Float(42.0), sc), None);
assert_eq!(
value_cmp(&len, &Value::Str("30mi".into()), sc),
Some(Ordering::Less)
);
let power = q(290.0, "m^2*kg/s^3", Some((290.0, "W")));
assert_eq!(value_cmp(&power, &Value::Str("5km".into()), sc), None);
assert!(!value_eq(&power, &Value::Str("5km".into()), sc));
assert_eq!(
value_cmp(&power, &Value::Str("0.2kW".into()), sc),
Some(Ordering::Greater)
);
assert_eq!(len.to_string(), "42_km");
let sum = arith(ArithOp::Add, &len, &Value::Str("500 m".into()), sc);
assert_eq!(sum.to_string(), "42.5_km");
let doubled = arith(ArithOp::Mul, &len, &Value::Int(2), sc);
assert_eq!(doubled.to_string(), "84_km");
assert!(matches!(
arith(ArithOp::Add, &len, &Value::Int(5), sc),
Value::Null
));
}
#[test]
fn durational_coercion_and_lift() {
let sc: &dyn Fn(&str) -> Option<(f64, String)> = &crate::value::quantity::scale_expr;
let two_h = Value::seconds(7200, 0);
assert_eq!(
value_cmp(&two_h, &Value::Str("90min".into()), sc),
Some(Ordering::Greater)
);
assert!(value_eq(&two_h, &Value::Str("PT2H".into()), sc));
assert!(!value_eq(&two_h, &Value::Int(7200), sc));
assert_eq!(value_cmp(&two_h, &Value::Str("hello".into()), sc), None);
assert!(!value_eq(&two_h, &Value::Str("hello".into()), sc));
let (s, n, _) = crate::temporal::parse_iso("2026-07-12T09:00:00Z").unwrap();
let now = Value::Instant {
secs: s,
nanos: n,
offset_min: Some(0),
calendar: Default::default(),
};
let earlier = arith(ArithOp::Sub, &now, &Value::Str("12h".into()), sc);
assert_eq!(earlier.to_string(), "2026-07-11T21:00:00Z");
let span = arith(ArithOp::Sub, &now, &Value::Str("2026-07-12".into()), sc);
assert_eq!(span.to_string(), "9h");
assert!(matches!(
arith(ArithOp::Sub, &now, &Value::Int(300), sc),
Value::Null
));
let sum = arith(ArithOp::Add, &two_h, &Value::Str("30min".into()), sc);
assert_eq!(sum.to_string(), "2h 30min");
let five_kg = Value::Quantity {
value: 5.0,
base: "kg".into(),
written: Some((5.0, "kg".into())),
};
assert!(matches!(
arith(ArithOp::Mul, &two_h, &five_kg, sc),
Value::Null
));
assert!(matches!(
arith(ArithOp::Mul, &five_kg, &two_h, sc),
Value::Null
));
assert!(matches!(
arith(ArithOp::Div, &two_h, &five_kg, sc),
Value::Null
));
}
struct MockTree {
names: Vec<Option<String>>,
kids: HashMap<u64, Vec<u64>>,
loose: Vec<u64>,
links: HashMap<u64, Vec<(String, u64)>>,
edge_props: HashMap<(u64, String, u64), Vec<(String, Value)>>,
prov: HashMap<u64, Provenance>,
}
impl MockTree {
fn sample() -> Self {
let names = vec![
None, Some("a".into()), Some("x.rs".into()), Some("y.txt".into()), Some("b".into()), Some("z.rs".into()), Some("deep".into()), Some("w.rs".into()), ];
let mut kids = HashMap::new();
kids.insert(0, vec![1, 4]);
kids.insert(1, vec![2, 3]);
kids.insert(4, vec![5, 6]);
kids.insert(6, vec![7]);
let mut links = HashMap::new();
links.insert(2, vec![("ref".to_string(), 7)]);
MockTree {
names,
kids,
loose: Vec::new(),
links,
edge_props: HashMap::new(),
prov: HashMap::new(),
}
}
fn lattice(n: u64) -> Self {
let mut names = vec![None];
names.extend((1..=n * n).map(|i| Some(format!("c{i}"))));
let mut kids = HashMap::new();
kids.insert(0, (1..=n * n).collect());
let mut links = HashMap::new();
for i in 1..=n * n {
let (x, y) = ((i - 1) % n, (i - 1) / n);
let mut out = Vec::new();
if x < n - 1 {
out.push(("r".to_string(), i + 1));
}
if y < n - 1 {
out.push(("d".to_string(), i + n));
}
links.insert(i, out);
}
MockTree {
names,
kids,
loose: Vec::new(),
links,
edge_props: HashMap::new(),
prov: HashMap::new(),
}
}
fn cyclic() -> Self {
let mut t = MockTree::sample();
t.links = HashMap::new();
t.links.insert(2, vec![("mgr".to_string(), 5)]);
t.links.insert(5, vec![("mgr".to_string(), 7)]);
t.links.insert(7, vec![("mgr".to_string(), 2)]);
t
}
fn lists() -> Self {
let names = vec![
None,
Some("div".into()), Some("ul".into()), Some("li".into()), Some("ul".into()), Some("li".into()), Some("ul".into()), Some("li".into()), ];
let mut kids = HashMap::new();
for (parent, child) in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7)] {
kids.insert(parent, vec![child]);
}
MockTree {
names,
kids,
loose: Vec::new(),
links: HashMap::new(),
edge_props: HashMap::new(),
prov: HashMap::new(),
}
}
fn converging() -> Self {
let mut t = MockTree::weighted();
t.links.get_mut(&3).map(|v| v.push(("e".to_string(), 6)));
t.links.insert(3, vec![("e".to_string(), 6)]);
t.edge_props.insert(
(3, "e".to_string(), 6),
vec![("qty".to_string(), Value::Int(5))],
);
t
}
fn diamond() -> Self {
let mut t = MockTree::sample();
t.links = HashMap::new();
t.links
.insert(1, vec![("e".to_string(), 2), ("e".to_string(), 3)]);
t.links.insert(2, vec![("e".to_string(), 6)]);
t.links.insert(3, vec![("e".to_string(), 6)]);
t.links.insert(6, vec![("e".to_string(), 2)]);
t
}
}
impl MockTree {
fn roads() -> Self {
let mut t = MockTree::diamond();
let mut w = |s: u64, t_: u64, props: Vec<(&str, Value)>| {
t.edge_props.insert(
(s, "e".to_string(), t_),
props.into_iter().map(|(n, v)| (n.to_string(), v)).collect(),
);
};
w(1, 2, vec![("len", Value::Str("4 km".into()))]);
w(
1,
3,
vec![
("len", Value::Str("1 km".into())),
("time", Value::Str("2 h".into())),
],
);
w(2, 6, vec![("len", Value::Str("12 km".into()))]);
w(3, 6, vec![("len", Value::Str("2 km".into()))]);
w(6, 2, vec![("len", Value::Str("3 km".into()))]);
t
}
fn weighted() -> Self {
let mut t = MockTree::sample();
t.links = HashMap::new();
t.links
.insert(1, vec![("e".to_string(), 2), ("e".to_string(), 3)]);
t.links.insert(2, vec![("e".to_string(), 6)]);
let mut w = |s: u64, t_: u64, q: i64| {
t.edge_props.insert(
(s, "e".to_string(), t_),
vec![("qty".to_string(), Value::Int(q))],
);
};
w(1, 2, 4);
w(1, 3, 1);
w(2, 6, 12);
t
}
}
impl AstAdapter for MockTree {
fn root(&self) -> NodeId {
NodeId(0)
}
fn children(&self, node: NodeId) -> Vec<NodeId> {
self.kids
.get(&node.0)
.map(|v| v.iter().map(|&i| NodeId(i)).collect())
.unwrap_or_default()
}
fn name(&self, node: NodeId) -> Option<String> {
self.names[node.0 as usize].clone()
}
fn parent(&self, node: NodeId) -> Option<NodeId> {
self.kids
.iter()
.find(|(_, v)| v.contains(&node.0))
.map(|(&p, _)| NodeId(p))
}
fn non_tree_nodes(&self) -> Vec<NodeId> {
self.loose.iter().map(|&i| NodeId(i)).collect()
}
fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
self.links
.get(&node.0)
.map(|v| v.iter().map(|(l, t)| (l.clone(), NodeId(*t))).collect())
.unwrap_or_default()
}
fn traits(&self, node: NodeId) -> Vec<String> {
match self.names[node.0 as usize].as_deref() {
Some(n) if n.ends_with(".rs") => vec!["code".into(), "file".into()],
Some(n) if n.contains('.') => vec!["text".into(), "file".into()],
_ => Vec::new(),
}
}
fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
self.links
.iter()
.flat_map(|(&src, v)| {
v.iter()
.filter(|(_, t)| *t == node.0)
.map(move |(l, _)| (l.clone(), NodeId(src)))
})
.collect()
}
fn link_property_names(&self, source: NodeId, label: &str, target: NodeId) -> Vec<String> {
self.edge_props
.get(&(source.0, label.to_string(), target.0))
.map(|ps| ps.iter().map(|(n, _)| n.clone()).collect())
.unwrap_or_default()
}
fn link_property(
&self,
source: NodeId,
label: &str,
target: NodeId,
name: &str,
) -> Option<Value> {
self.edge_props
.get(&(source.0, label.to_string(), target.0))?
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v.clone())
}
fn provenance(&self, node: NodeId) -> Provenance {
self.prov.get(&node.0).cloned().unwrap_or_default()
}
}
fn run(q: &str, t: &impl AstAdapter) -> Vec<u64> {
match eval(&parse(&lex(q).unwrap()).unwrap(), t) {
QueryResult::Nodes(ns) => ns.into_iter().map(|n| n.0).collect(),
QueryResult::Values(_) => panic!("expected nodes, got values"),
}
}
fn vals(q: &str, t: &MockTree) -> Vec<Value> {
match eval(&parse(&lex(q).unwrap()).unwrap(), t) {
QueryResult::Values(vs) => vs,
QueryResult::Nodes(_) => panic!("expected values, got nodes"),
}
}
#[test]
fn universal_search_reaches_the_whole_arbor() {
let mut t = MockTree::sample();
t.names.push(Some("x.rs".into())); t.loose.push(8);
t.links.insert(3, vec![("see".to_string(), 8)]);
assert_eq!(run("*//x.rs", &t), vec![2, 8]);
assert_eq!(run("//x.rs", &t), vec![2]);
assert_eq!(run("^//x.rs", &t), vec![2]);
assert_eq!(run("*//*", &t), vec![0, 1, 2, 3, 4, 5, 6, 7, 8]);
assert_eq!(run("//*", &t), vec![1, 2, 3, 4, 5, 6, 7]);
assert_eq!(run("*//*[:is-non-tree]", &t), vec![8]);
assert_eq!(run("*//y.txt->see", &t), vec![8]);
assert!(parse(&lex("*//?x.rs").unwrap()).is_err());
assert!(parse(&lex("/a/*//x.rs").unwrap()).is_ok());
assert_eq!(run("/a/*//x.rs", &t), Vec::<u64>::new());
assert_eq!(
crate::syntax::unparse::unparse(&parse(&lex("*//x.rs[1]").unwrap()).unwrap()),
"*//x.rs[1]"
);
assert_eq!(
vals("/a | %(n = (*//x.rs @| count); d = (//x.rs @| count))", &t),
vec![Value::record(vec![
("n".to_string(), Value::Int(2)),
("d".to_string(), Value::Int(1)),
])]
);
}
#[test]
fn child_and_descendant() {
let t = MockTree::sample();
assert_eq!(run("/a", &t), vec![1]);
assert_eq!(run("/a/x.rs", &t), vec![2]);
assert_eq!(run("//*.rs", &t), vec![2, 5, 7]);
}
#[test]
fn parent_and_ancestor() {
let t = MockTree::sample();
assert_eq!(run("//w.rs\\deep", &t), vec![6]);
assert_eq!(run("//w.rs\\\\b", &t), vec![4]);
}
#[test]
fn proximal_and_distal_descendant() {
let t = MockTree::sample();
assert_eq!(run("/b//*.rs", &t), vec![5, 7]);
assert_eq!(run("/b//?*.rs", &t), vec![5]); assert_eq!(run("/b//!*.rs", &t), vec![7]); }
#[test]
fn siblings() {
let t = MockTree::sample();
assert_eq!(run("/a/x.rs>y.txt", &t), vec![3]);
assert_eq!(run("/a/y.txt<x.rs", &t), vec![2]);
assert!(run("/a/y.txt>*", &t).is_empty()); }
#[test]
fn leaf_anchor() {
let t = MockTree::sample();
assert!(run("/b/deep$", &t).is_empty());
assert_eq!(run("/b/z.rs$", &t), vec![5]);
}
#[test]
fn regex_name() {
let t = MockTree::sample();
assert_eq!(run("//(/.*\\.rs/)", &t), vec![2, 5, 7]);
}
#[test]
fn correlation() {
let t = MockTree::sample();
assert_eq!(run("//* <=> //a[:depth = $$0:depth]", &t), vec![1, 4]);
assert_eq!(run("//*.rs <=> //x.rs[:name != $$0:name]", &t), vec![5, 7]);
assert_eq!(
run("//* <=> //a[:depth = $$0:depth] <=> //x.rs", &t),
vec![1, 4]
);
assert!(
run(
"//* <=> //a[:depth = $$0:depth] <=> //x.rs[:depth = $$1:depth]",
&t
)
.is_empty()
);
}
#[test]
fn nested_predicates_read_the_thread_witness() {
let t = MockTree::sample();
assert_eq!(
run(
"//*.rs <=> //*[:name = $$0:name] \
| [(^//*[:name = $$1:name && :depth = 3])]",
&t
),
vec![7]
);
assert_eq!(
run(
"//*.rs <=> //*[:name = $$0:name] \
| [(^//*[:name = $$1:name && :depth = 2])]",
&t
),
vec![2, 5]
);
assert_eq!(
run(
"//*.rs <=> //*[:name = $$0:name] \
| \\\\*/*.rs[:name != $$1:name && :depth = 2]",
&t
),
vec![5]
);
}
#[test]
fn crosslinks() {
let t = MockTree::sample();
assert_eq!(run("//x.rs->ref", &t), vec![7]);
assert_eq!(run("//x.rs->*", &t), vec![7]);
assert!(run("//x.rs->other", &t).is_empty());
assert_eq!(run("//w.rs<-ref", &t), vec![2]);
assert_eq!(run("//x.rs->ref\\deep", &t), vec![6]);
}
#[test]
fn quantified_child_group() {
let t = MockTree::lists();
assert_eq!(run("/div(/ul/li)+", &t), vec![3, 5, 7]);
assert_eq!(run("/div(/ul/li)+?", &t), vec![3]); assert_eq!(run("/div(/ul/li)+!", &t), vec![7]); assert_eq!(run("/div(/ul/li){2}", &t), vec![5]);
assert_eq!(run("/div(/ul/li){1;2}", &t), vec![3, 5]);
assert_eq!(run("/div(/ul/li){1;2}!", &t), vec![5]);
assert!(run("/div(/ul/li){4;}", &t).is_empty());
}
#[test]
fn quantified_crosslinks_on_a_cycle() {
let t = MockTree::cyclic();
assert_eq!(run("//x.rs(->mgr)+", &t), vec![5, 7]);
assert_eq!(run("//x.rs(->mgr)+?", &t), vec![5]);
assert_eq!(run("//x.rs(->mgr)+!", &t), vec![7]);
assert_eq!(run("//x.rs(->mgr){2}", &t), vec![7]);
assert_eq!(run("//x.rs(->mgr)*", &t), vec![2, 5, 7]);
assert_eq!(run("//x.rs(->mgr)*?", &t), vec![2]);
assert_eq!(run("//x.rs(<-mgr)+", &t), vec![7, 5]);
}
#[test]
fn group_alternation() {
let t = MockTree::sample();
assert_eq!(run("(/a|/b)", &t), vec![1, 4]);
assert_eq!(run("/a/(x.rs|y.txt)", &t), vec![2, 3]);
assert_eq!(run("/b/(z.rs|deep/w.rs)", &t), vec![5, 7]);
assert_eq!(run("(/a/(x.rs|y.txt)|/b/z.rs)", &t), vec![2, 3, 5]);
let toks = lex("/a/(x.rs|/y.txt)").unwrap();
assert!(parse(&toks).is_err());
let toks = lex("(/a|)").unwrap();
assert!(parse(&toks).is_err());
}
#[test]
fn label_algebra() {
let mut t = MockTree::sample();
t.links.insert(
2,
vec![
("ref".to_string(), 7),
("doc".to_string(), 7),
("ref".to_string(), 5),
],
);
assert_eq!(run("/a/x.rs->(ref && doc)", &t), vec![7]);
assert_eq!(run("/a/x.rs->(ref || doc)", &t), vec![7, 5]);
assert_eq!(run("/a/x.rs->(doc || ref)", &t), vec![7, 5]);
assert_eq!(run("/a/x.rs->(!doc)", &t), vec![5]);
assert_eq!(run("/a/x.rs->(!(ref || doc))", &t), Vec::<u64>::new());
assert_eq!(run("/a/x.rs->(ref && !doc)", &t), vec![5]);
assert_eq!(run("/a/x.rs->(r* && !d*)", &t), vec![5]);
assert_eq!(run("/a/x.rs->(!*)", &t), Vec::<u64>::new());
assert_eq!(run("//w.rs<-(ref && doc)", &t), vec![2]);
assert_eq!(run("//z.rs<-(!doc)", &t), vec![2]);
assert_eq!(run("//z.rs<-(ref && doc)", &t), Vec::<u64>::new());
let c = MockTree::cyclic();
assert_eq!(run("/a/x.rs->(mgr && !x){2}", &c), vec![7]);
assert_eq!(run("/a/x.rs(->(mgr || x)){2}", &c), vec![7]);
assert_eq!(run("/a/x.rs->(ref|doc)", &t), vec![7, 5, 7]);
assert_eq!(
run("/a/x.rs->(ref|doc) @| collapse", &t),
run("/a/x.rs->(ref || doc)", &t)
);
assert_eq!(run("/a/x.rs->(ref .and. .not. doc)", &t), vec![5]);
}
#[test]
fn dot_wildcard_and_sugar() {
let t = MockTree::sample();
assert_eq!(run("/b(/.)*/w.rs", &t), run("/b//w.rs", &t));
assert_eq!(run("(/.){2}", &t), vec![2, 3, 5, 6]);
assert_eq!(run("/{2}", &t), run("(/.){2}", &t));
let lists = MockTree::lists();
assert_eq!(run("/div/ul/li(/ul/li){1}", &lists), vec![5]);
assert!(run("/.", &t).is_empty());
}
#[test]
fn zero_repetitions_keep_the_anchor() {
let t = MockTree::sample();
assert_eq!(run("/b(/deep)*", &t), vec![4, 6]);
assert_eq!(run("/b(/deep){0;1}", &t), vec![4, 6]);
assert_eq!(run("/b(/deep){;1}", &t), vec![4, 6]);
}
#[test]
fn quantifier_bound_refuses_out_loud() {
struct Bounded(MockTree);
impl AstAdapter for Bounded {
fn root(&self) -> NodeId {
self.0.root()
}
fn children(&self, node: NodeId) -> Vec<NodeId> {
self.0.children(node)
}
fn name(&self, node: NodeId) -> Option<String> {
self.0.name(node)
}
fn parent(&self, node: NodeId) -> Option<NodeId> {
self.0.parent(node)
}
fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
self.0.links(node)
}
fn quantifier_bound(&self) -> usize {
2
}
}
let t = Bounded(MockTree::lists());
let refusal = |q: &str| {
let out = run(q, &t);
let msg = take_refusal();
(out, msg)
};
let (out, msg) = refusal("/div(/ul/li)+?");
assert_eq!(out, vec![3]);
assert!(msg.is_none());
let (out, msg) = refusal("/div(/ul/li)+");
assert!(out.is_empty());
assert!(msg.unwrap().contains("quantifier bound reached"));
let (out, msg) = refusal("/div(/ul/li)+!");
assert!(out.is_empty());
assert!(msg.unwrap().contains("quantifier bound reached"));
let (out, msg) = refusal("/div(/ul/li){1;3}");
assert!(out.is_empty());
assert!(msg.unwrap().contains("explicit ceiling"));
let (out, msg) = refusal("/div(/ul/li){3;}");
assert!(out.is_empty());
assert!(msg.unwrap().contains("minimum"));
let (out, msg) = refusal("/div(/ul/li){1;2}");
assert_eq!(out, vec![3, 5]);
assert!(msg.is_none());
let looped = |q: &str| match crate::run(q, &t) {
Ok(QueryResult::Values(vs)) => (vs, take_refusal()),
Ok(QueryResult::Nodes(ns)) => (
ns.into_iter().map(|n| Value::Int(n.0 as i64)).collect(),
take_refusal(),
),
Err(e) => {
let _ = take_refusal();
(Vec::new(), Some(e.to_string()))
}
};
let (out, msg) = looped("/div | .n(1){5} | $.n");
assert!(out.is_empty());
assert!(msg.unwrap().contains("loop's explicit ceiling"));
let (out, msg) = looped("/div | .n(1){1;3} | $.n");
assert!(out.is_empty());
assert!(msg.unwrap().contains("loop's explicit ceiling"));
let (out, msg) = looped("/div | .n(1){3;} | $.n");
assert!(out.is_empty());
assert!(msg.unwrap().contains("loop's minimum"));
let (out, msg) = looped("/div | .n(1){1;2} | $.n");
assert_eq!(out, vec![Value::Int(1)]);
assert!(msg.is_none());
}
#[test]
fn bound_hit_with_drained_frontier_is_complete() {
struct Exact(MockTree);
impl AstAdapter for Exact {
fn root(&self) -> NodeId {
self.0.root()
}
fn children(&self, node: NodeId) -> Vec<NodeId> {
self.0.children(node)
}
fn name(&self, node: NodeId) -> Option<String> {
self.0.name(node)
}
fn parent(&self, node: NodeId) -> Option<NodeId> {
self.0.parent(node)
}
fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
self.0.links(node)
}
fn quantifier_bound(&self) -> usize {
3
}
}
let t = Exact(MockTree::lists());
assert_eq!(run("/div(/ul/li)+", &t), vec![3, 5, 7]);
assert!(take_refusal().is_none());
}
#[test]
fn proximal_answer_within_bound_is_complete() {
struct Tight(MockTree);
impl AstAdapter for Tight {
fn root(&self) -> NodeId {
self.0.root()
}
fn children(&self, node: NodeId) -> Vec<NodeId> {
self.0.children(node)
}
fn name(&self, node: NodeId) -> Option<String> {
self.0.name(node)
}
fn parent(&self, node: NodeId) -> Option<NodeId> {
self.0.parent(node)
}
fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
self.0.links(node)
}
fn quantifier_bound(&self) -> usize {
2
}
}
let t = Tight(MockTree::cyclic());
assert_eq!(run("//x.rs(->mgr)+?", &t), vec![5]);
assert!(take_refusal().is_none());
}
#[test]
fn group_in_a_predicate_path() {
let t = MockTree::sample();
assert_eq!(run("/a/*[(->ref)+]", &t), vec![2]);
let cyc = MockTree::cyclic();
assert_eq!(run("//*[(->mgr){2}]", &cyc), vec![2, 5, 7]);
}
#[test]
fn group_predicates_filter_before_reach() {
let t = MockTree::cyclic();
assert_eq!(run("//x.rs(->mgr)+[:name = w.rs]?", &t), vec![7]);
assert_eq!(run("//x.rs(->mgr)+?", &t), vec![5]);
assert_eq!(run("//x.rs(->mgr)+[:name == (/rs/)]!", &t), vec![7]);
assert!(run("//x.rs(->mgr)+[:name = \"nope\"]?", &t).is_empty());
let s = MockTree::sample();
assert_eq!(run("/b/(z.rs|deep/w.rs)[:name = w.rs]", &s), vec![7]);
assert_eq!(run("/a(/x.rs)*[:name = \"a\"]", &s), vec![1]);
let w = MockTree::weighted();
assert_eq!(run("//a(->e)+[$-::qty = 12]", &w), vec![6]);
let toks = lex("/a(/b)+[1]").unwrap();
assert!(parse(&toks).is_err());
}
#[test]
fn arrived_edges_aggregate() {
let t = MockTree::converging();
assert_eq!(
vals("//a->e->e | .(@-::qty) | $.", &t),
vec![
Value::list(vec![Value::Int(12)]),
Value::list(vec![Value::Int(5)])
]
);
assert_eq!(
vals("//a->e->e @| collapse | .(@-::qty) | $.", &t),
vec![Value::list(vec![Value::Int(12), Value::Int(5)])]
);
assert_eq!(
vals("//a->e->e @| collapse | .(@-) | $.", &t),
vec![Value::list(vec![
Value::Str("e".into()),
Value::Str("e".into())
])]
);
assert_eq!(
vals("/a | .(@-) | $.", &t),
vec![Value::list(vec![Value::Str("[child]".into())])]
);
assert_eq!(
vals("//a(->e .($-::qty))->e | .(@-) @| count", &t),
vec![Value::Int(2)]
);
assert_eq!(
vals("//a(->e .(1))->e @| collapse | .(@-::qty) | $.", &t),
vec![Value::list(vec![Value::Int(12), Value::Int(5)])]
);
assert_eq!(
vals("//a->e->e | @-::qty | ... @| sum", &t),
vec![Value::Int(17)]
);
}
#[test]
fn context_accessor_and_inline_pipes() {
let t = MockTree::sample();
assert_eq!(
vals("//*.rs:name | .all((@_ @| join(\", \"))) | $.all", &t),
vec![
Value::Str("x.rs, z.rs, w.rs".into()),
Value::Str("x.rs, z.rs, w.rs".into()),
Value::Str("x.rs, z.rs, w.rs".into()),
]
);
assert_eq!(
vals("//*.rs | [(@@_:name @| count) = 3] @| count", &t),
vec![Value::Int(3)]
);
assert_eq!(
vals("/a/x.rs | .n(((@@_:name | count))) | $.n", &t),
vec![Value::Int(1)]
);
assert_eq!(
vals(
"//*.rs | .top((@@_:name @| sort @| [1..2] @| join(\"-\"))) | $.top",
&t
)[0],
Value::Str("w.rs-x.rs".into())
);
assert_eq!(run("/*[@_ = null]", &t).len(), 2);
let toks = lex("/a | .x((@_ | .bad))").unwrap();
assert!(parse(&toks).is_err());
}
#[test]
fn conditionals() {
let t = MockTree::sample();
assert_eq!(
vals(
"/a/* | %(:name; kind = (:name == (/rs/) ? \"code\" : \"text\"))",
&t
),
vec![
Value::record(vec![
("name".into(), Value::Str("x.rs".into())),
("kind".into(), Value::Str("code".into())),
]),
Value::record(vec![
("name".into(), Value::Str("y.txt".into())),
("kind".into(), Value::Str("text".into())),
]),
]
);
assert_eq!(
vals(
"//w.rs | ((:depth = 1 ? \"top\" : :depth = 2 ? \"mid\" : \"deep\"))",
&t
),
vec![Value::Str("deep".into())]
);
assert_eq!(
vals("/b | ((/deep ? \"has-deep\" : \"flat\"))", &t),
vec![Value::Str("has-deep".into())]
);
assert_eq!(
vals("/a | (((1 = 1 ? \"yes\" : \"no\") | upper))", &t),
vec![Value::Str("YES".into())]
);
assert_eq!(
vals("/a | (($_ ? \"topic\" : \"none\"))", &t),
vec![Value::Str("none".into())]
);
}
#[test]
fn trait_negation() {
let t = MockTree::sample();
assert_eq!(run("//*<file && !code>", &t), vec![3]);
assert_eq!(run("/*<!*>", &t), vec![1, 4]);
assert_eq!(run("/a/*<text || !file>", &t), vec![3]);
assert_eq!(run("//*<(code || text) && !text>", &t), vec![2, 5, 7]);
}
#[test]
fn map_pipe() {
let t = MockTree::converging();
assert_eq!(
vals(
"//a->e->e @| collapse | .(@-::qty) | $. $| ($_ * 10) | json",
&t
),
vec![Value::Str("[120, 50]".into())]
);
assert_eq!(
vals(
"//a->e->e @| collapse | .(@-::qty) | $. $| [$_ > 6] | json",
&t
),
vec![Value::Str("[12]".into())]
);
assert_eq!(
vals(
"//a->e->e @| collapse | .(@-::qty) | $. $| [1..1] | json",
&t
),
vec![Value::Str("[12]".into())]
);
assert_eq!(
vals(
"//a->e->e @| collapse | .(@-::qty) | $. $| (($_ * 100 div (@_ @| sum) | round)) | json",
&t
),
vec![Value::Str("[71, 29]".into())]
);
assert_eq!(vals("/a | $_ $| upper", &t), vec![Value::Null]);
assert_eq!(
vals("/a | :name $| upper", &t),
vec![Value::Str("A".into())]
);
let toks = lex("/a | @. $| .bad").unwrap();
assert!(parse(&toks).is_err());
}
#[test]
fn set_walk_and_exact_walk_on_a_diamond() {
let t = MockTree::diamond();
assert_eq!(run("//a(->e)+", &t), vec![2, 3, 6, 6, 2]);
assert_eq!(run("//a(->e)+ @| collapse", &t), vec![2, 3, 6]);
assert_eq!(run("//a(->e)+?", &t), vec![2, 3]);
assert_eq!(run("//a(->e)*", &t).len(), 6);
assert_eq!(run("//a(->e)* @| collapse", &t).len(), 4);
assert_eq!(run("//a(->e)+!", &t), vec![2]);
assert_eq!(run("//a(->e){3}", &t), vec![2]);
assert_eq!(run("//a(->e){2;3}", &t), vec![6, 6, 2]);
assert_eq!(run("//a(->e){2;}", &t), vec![6, 6, 2]);
assert_eq!(
vals("//a(->e .(1))+! | @. | count", &t),
vec![Value::Int(3)]
);
assert_eq!(
vals("//a(->e .(1)){3}:name", &t),
vec![Value::Str("x.rs".into())]
);
}
#[test]
fn set_walk_over_a_lattice_is_polynomial() {
let t = MockTree::lattice(12);
assert_eq!(run("/c1(->r|->d)+?", &t).len(), 2);
assert_eq!(run("/c1(->r|->d)*?", &t).len(), 1);
assert_eq!(run("/c1(->r|->d)+[:name = \"c144\"]?", &t), vec![144]);
assert!(take_refusal().is_none());
assert!(run("/c1(->r|->d)+", &t).is_empty());
assert!(take_refusal().unwrap().contains("frontier bound"));
let s = MockTree::lattice(6);
let far = run("/c1(->r|->d){10}", &s);
assert_eq!(far.len(), 252);
assert!(far.iter().all(|&n| n == 36));
assert_eq!(run("/c1(->r|->d)+!", &s), vec![36]);
assert_eq!(run("/c1(->r|->d){10} @| collapse", &s), vec![36]);
assert_eq!(run("/c1(->r|->d){5}", &s).len(), 32);
assert_eq!(run("/c1(->r|->d){9;}", &s).len(), 504);
assert!(take_refusal().is_none());
}
#[test]
fn the_degenerate_name_list_is_the_wildcard() {
let t = MockTree::sample();
assert_eq!(run("//a/(*)", &t), run("//a/*", &t));
assert_eq!(run("//a/(*)[..1]", &t), run("//a/*[..1]", &t));
assert_eq!(vals("//a/(*)[..1]:name", &t), vals("//a/*[..1]:name", &t));
assert_eq!(run("/a/(*){1}", &t), run("/a/*", &t));
assert_eq!(run("//(*)", &t), run("//*", &t));
assert_eq!(run("//a->(*)", &t), run("//a->*", &t));
assert!(run("//a/(!*)", &t).is_empty());
assert!(take_refusal().is_none());
}
#[test]
fn the_graph_as_a_table() {
let t = MockTree::roads();
let table = |q: &str| -> (Vec<String>, Vec<Vec<String>>) {
let vs = vals(q, &t);
assert_eq!(vs.len(), 1, "{q}");
let Value::Table(tb) = &vs[0] else {
panic!("{q}: not a table: {}", vs[0].display_form())
};
(
tb.header.clone().unwrap(),
tb.rows
.iter()
.map(|r| r.iter().map(Value::display_form).collect())
.collect(),
)
};
let (h, rows) = table("//a(->e)* @| edges(->e)");
assert_eq!(h, ["source", "target", "label", "directed", "len", "time"]);
assert_eq!(rows.len(), 5);
assert_eq!(rows[0], ["/a", "/a/x.rs", "e", "true", "4_km", "null"]);
assert_eq!(rows[1], ["/a", "/a/y.txt", "e", "true", "1_km", "2h"]);
assert_eq!(table("//a(->e)* @| edges").1.len(), 5);
let (_, rows) = table("//a(->e)* @| edges(--e)");
assert_eq!(rows.len(), 5);
assert!(rows.iter().all(|r| r[3] == "false"));
assert_eq!(table("//a(->e)* @| edges(<-e)").1.len(), 5);
assert_eq!(table("//a(->e)*[:name != \"deep\"] @| edges").1.len(), 2);
let (_, rows) = table("//a(->e)* @| edges(~*)");
assert!(rows.iter().any(|r| r[2] == "[child]"), "{rows:?}");
assert_eq!(rows.iter().filter(|r| r[2] == "e").count(), 5);
let (h, rows) = table("//a(->e)* @| edges(->e; --weight=km)");
assert_eq!(h.last().unwrap(), "weight");
assert_eq!(
rows.iter().map(|r| r[6].as_str()).collect::<Vec<_>>(),
["4_km", "1_km", "12_km", "2_km", "3_km"]
);
let (_, rows) = table("//a(->e)* @| edges(->e; --weight=h)");
assert_eq!(rows[1][6], "2_h");
assert_eq!(rows[0][6], "null");
let (h, rows) = table("//a(->e)* @| nodes");
assert_eq!(h, ["node", "name", "kind"]);
assert_eq!(rows.len(), 4);
assert_eq!(rows[1][0], "/a/x.rs");
assert_eq!(rows[1][1], "x.rs");
assert!(take_refusal().is_none());
assert!(vals("//a(->e)* @| edges(:name)", &t).is_empty());
assert!(take_refusal().unwrap().contains("the collecting hop"));
assert!(vals("//a(->e)* @| edges(->e; --weight=furlong)", &t).is_empty());
assert!(take_refusal().unwrap().contains("no unit"));
}
#[test]
fn pagerank_over_the_induced_subgraph() {
let t = MockTree::diamond();
let table = |q: &str| -> (Vec<String>, Vec<Vec<Value>>) {
let vs = vals(q, &t);
let Value::Table(tb) = &vs[0] else {
panic!("{q}: not a table")
};
(tb.header.clone().unwrap(), tb.rows.clone())
};
let ranks = |q: &str| -> Vec<f64> {
vals(q, &t)
.iter()
.map(|v| match v {
Value::Float(f) => *f,
other => panic!("{other:?}"),
})
.collect()
};
let pr = ranks("//a(->e)* @| collapse @| graph:pagerank(->e)");
assert_eq!(pr.len(), 4);
assert!((pr.iter().sum::<f64>() - 1.0).abs() < 1e-9, "{pr:?}");
assert!((pr[0] - 0.15 / 4.0).abs() < 1e-9, "{pr:?}");
assert!(pr[1] > pr[2] && pr[3] > pr[2], "{pr:?}");
let a = ranks("//a(->e)* @| collapse @| graph:pagerank(->e; --damping=0.5 --rounds=5)")[0];
assert!((a - 0.5 / 4.0).abs() < 1e-9, "{a}");
assert_eq!(
ranks("//a(->e)* @| collapse @| graph:pagerank(->(e || f))").len(),
4
);
let a = ranks("//a(->e)* @| collapse @| graph:pagerank(--e)")[0];
assert!(a > 0.15 / 4.0, "{a}");
assert_eq!(table("//a(->e)* @| edges(->(e || f))").1.len(), 5);
assert_eq!(table("//a(->e)* @| edges((->e|->f))").1.len(), 5);
assert!(take_refusal().is_none());
assert!(vals("//a(->e)* @| graph:pagerank(->e; --damping=2)", &t).is_empty());
assert!(take_refusal().unwrap().contains("outside 0..1"));
let err = parse(&lex("//a(->e)* @| pagerank(->e)").unwrap())
.unwrap_err()
.to_string();
assert!(err.contains("graph:pagerank"), "{err}");
}
#[test]
fn every_walk_is_its_own_thread() {
let t = MockTree::roads();
let show = |q: &str| {
vals(q, &t)
.iter()
.map(Value::display_form)
.collect::<Vec<_>>()
};
assert_eq!(
show("//a(->e){;100_km}[:name = \"deep\"] | $-:total"),
vec!["16_km", "3_km"]
);
assert_eq!(
show("//a(->e){;100_km}[:name = \"deep\"] | $-::len"),
vec!["12 km", "2 km"]
);
assert_eq!(
show("//a(->e){;100_km}[:name = \"deep\"] @| collapse | @-::len"),
vec!["@(\"12 km\"; \"2 km\")"]
);
assert_eq!(
show("//a(->e){;100_km}[:name = \"deep\"] @| collapse @| count"),
vec!["1"]
);
assert_eq!(
show("//a(->e){;100_km}[:name = \"deep\"]? | $-:total"),
vec!["3_km"]
);
assert_eq!(
show("//a(->e){;100_km}[:name = \"x.rs\"] | $-:total"),
vec!["4_km", "6_km"]
);
assert_eq!(
show("//a(->e){;100_km}[:name = \"x.rs\"] | $-:hops"),
vec!["1", "3"]
);
assert_eq!(
show("//a(->e){1;3}[:name = \"x.rs\"] | $-:hops"),
vec!["1", "3"]
);
assert_eq!(show("//a->e->e | :name"), vec!["deep", "deep"]);
assert_eq!(show("//a->e->e @| collapse | :name"), vec!["deep"]);
assert_eq!(
show("//a->e->e @| collapse(-e) | :name"),
vec!["deep", "deep"]
);
assert!(take_refusal().is_none());
}
#[test]
fn a_nested_tree_path_answers_the_node_set() {
let t = MockTree::roads();
let show = |q: &str| {
vals(q, &t)
.iter()
.map(Value::display_form)
.collect::<Vec<_>>()
};
assert_eq!(show("//*//w.rs | :name"), vec!["w.rs"]);
assert_eq!(show("//b | (\\//*//w.rs @| count)"), vec!["1"]);
assert_eq!(
show("//b | %(n = (\\//*//w.rs @| count))"),
vec!["%(n = 1)"]
);
assert_eq!(show("//*\\\\b | :name"), vec!["b"]);
assert_eq!(show("//*\\\\b @| count"), vec!["1"]);
assert_eq!(show("//a | (\\//*\\\\b @| count)"), vec!["1"]);
assert_eq!(
show("//a(->e){;100_km}[:name = \"deep\"] @| count"),
vec!["2"]
);
assert_eq!(
show("//a | ((->e){;100_km}[:name = \"deep\"] @| count)"),
vec!["2"]
);
assert!(take_refusal().is_none());
}
#[test]
fn a_gathered_mark_reads_back_every_node() {
let t = MockTree::sample();
let show = |q: &str| {
vals(q, &t)
.iter()
.map(Value::display_form)
.collect::<Vec<_>>()
};
assert_eq!(show("/a/* .W | \\ | ($$.W @| count)"), vec!["1", "1"]);
assert_eq!(
show("/a/* .W | \\ @| collapse | ($$.W @| count)"),
vec!["2"]
);
assert_eq!(
show("/a/* .W | \\ @| collapse | ($$.W:name @| join(\", \"))"),
vec!["x.rs, y.txt"]
);
assert!(take_refusal().is_none());
}
#[test]
fn the_acyclic_promise_expands_by_node_and_round() {
struct Promised(MockTree, &'static [&'static str]);
impl AstAdapter for Promised {
fn root(&self) -> NodeId {
self.0.root()
}
fn children(&self, node: NodeId) -> Vec<NodeId> {
self.0.children(node)
}
fn name(&self, node: NodeId) -> Option<String> {
self.0.name(node)
}
fn parent(&self, node: NodeId) -> Option<NodeId> {
self.0.parent(node)
}
fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
self.0.links(node)
}
fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
self.0.backlinks(node)
}
fn acyclic_link(&self, label: &str) -> Option<String> {
self.1
.contains(&label)
.then(|| format!("ref /c/*::{label} --> /c acyclic"))
}
fn frontier_bound(&self) -> usize {
30
}
}
let t = Promised(MockTree::lattice(12), &["r", "d", "e"]);
assert!(run("/c1(->r|->d){22}", &t).is_empty());
assert!(take_refusal().unwrap().contains("frontier bound (30)"));
assert_eq!(run("/c1(->r|->d){22} @| collapse", &t), vec![144]);
assert_eq!(run("/c1(->r|->d)+!", &t), vec![144]);
assert_eq!(run("/c1(->r|->d){5} @| collapse", &t).len(), 6);
assert_eq!(run("/c1(->r|->d){20;} @| collapse", &t).len(), 6);
assert_eq!(run("/c1(->r|->d)+ @| collapse", &t).len(), 143);
assert!(take_refusal().is_none());
match eval(
&parse(&lex("/c1(->r .(1)|->d .(1)){22} | @. | count").unwrap()).unwrap(),
&t,
) {
QueryResult::Values(vs) => assert!(vs.is_empty(), "{vs:?}"),
QueryResult::Nodes(ns) => assert!(ns.is_empty()),
}
assert!(take_refusal().unwrap().contains("frontier bound (30)"));
assert_eq!(run("/c1(->*){22} @| collapse", &t), vec![144]);
assert_eq!(run("/c1(->*)+!", &t), vec![144]);
assert!(take_refusal().is_none());
let half = Promised(MockTree::lattice(12), &["r"]);
assert!(run("/c1(->r|->d){5} @| collapse", &half).is_empty());
assert!(take_refusal().unwrap().contains("frontier bound (30)"));
assert!(run("/c1(->*){5} @| collapse", &half).is_empty());
assert!(take_refusal().unwrap().contains("frontier bound (30)"));
assert_eq!(run("/c1(->r){11}", &half), vec![12]);
assert!(take_refusal().is_none());
let plain = MockTree::lattice(12);
assert_eq!(run("/c1(->r|->d)+ @| collapse", &plain).len(), 143);
assert!(take_refusal().is_none());
let d = Promised(MockTree::diamond(), &["e"]);
assert!(run("//a(->e){3}", &d).is_empty());
let msg = take_refusal().unwrap();
assert!(
msg.contains("a cycle under the promise 'ref /c/*::e --> /c acyclic'"),
"{msg}"
);
assert!(
msg.contains("the walk from /a returns to /a/x.rs after 3 hops"),
"{msg}"
);
assert_eq!(run("//a(->e){3}", &MockTree::diamond()), vec![2]);
assert!(take_refusal().is_none());
}
#[test]
fn frontier_bound_refuses_out_loud() {
struct Narrow(MockTree);
impl AstAdapter for Narrow {
fn root(&self) -> NodeId {
self.0.root()
}
fn children(&self, node: NodeId) -> Vec<NodeId> {
self.0.children(node)
}
fn name(&self, node: NodeId) -> Option<String> {
self.0.name(node)
}
fn parent(&self, node: NodeId) -> Option<NodeId> {
self.0.parent(node)
}
fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
self.0.links(node)
}
fn frontier_bound(&self) -> usize {
12
}
}
let t = Narrow(MockTree::lattice(6));
for q in ["/c1(->r .(1)|->d .(1)){4}", "/c1(->r|->d){4}"] {
let out = run(q, &t);
let msg = take_refusal().unwrap();
assert!(out.is_empty());
assert!(
msg.contains("frontier bound (12) is exceeded at round 4"),
"{msg}"
);
assert!(msg.contains("--frontier-bound"));
}
assert_eq!(run("/c1(->r|->d)+?", &t).len(), 2);
assert!(take_refusal().is_none());
assert!(run("/c1(->r|->d)+", &t).is_empty());
assert!(take_refusal().unwrap().contains("frontier bound (12)"));
}
#[test]
fn navop_star_walks_every_edge() {
let t = MockTree::sample();
assert_eq!(run("/a/x.rs~*", &t), vec![1, 7]);
assert_eq!(run("/b/deep/w.rs~*", &t), vec![6, 2]);
assert_eq!(run("/a/x.rs~ref", &t), vec![7]);
assert_eq!(run("/a/x.rs~a", &t), vec![1]);
assert_eq!(run("/a~*", &t), vec![2, 3, 0]);
assert!(run("/a/x.rs~y.txt", &t).is_empty());
assert_eq!(run("/a/x.rs~*[$- = \"ref\"]", &t), vec![7]);
assert_eq!(run("/a/x.rs~*[$- = \"[parent]\"]", &t), vec![1]);
assert_eq!(run("/a/x.rs(~*){2}", &t), vec![3, 0, 6]);
assert_eq!(run("/a/x.rs(~*)+[:name = \"deep\"]?", &t), vec![6]);
assert_eq!(run("/a/x.rs~~w.rs", &t), vec![7]);
assert_eq!(run("/a/x.rs~~?*", &t), vec![1, 7]);
assert_eq!(run("/a/x.rs~~!*", &t), vec![4]);
assert_eq!(
crate::syntax::unparse::unparse(&parse(&lex("/a~~?x(~.)*").unwrap()).unwrap()),
"/a~~?x(~.)*"
);
}
#[test]
fn a_budget_sums_an_edge_weight_along_the_walk() {
let t = MockTree::roads();
assert_eq!(run("//a(->e){;5_km}", &t), vec![1, 2, 3, 6]);
assert_eq!(run("//a(->e){;3_km}", &t), vec![1, 3, 6]);
assert_eq!(run("//a(->e){3_km;7_km}", &t), vec![2, 6, 2]);
assert_eq!(run("//a(->e){;5000_m}", &t), vec![1, 2, 3, 6]);
assert_eq!(run("//a(->e){;100_km}?", &t), vec![1, 3, 6, 2]);
assert_eq!(run("//a(->e){;100_km}[:name = \"deep\"]?", &t), vec![6]);
assert_eq!(run("//a(->e){;7_km}!", &t), vec![2]);
assert_eq!(
vals("//a(->e){;100_km}[:name = \"deep\"]? | $-:total", &t),
vec![Value::Quantity {
value: 3000.0,
base: "m".into(),
written: Some((3.0, "km".into()))
}]
);
assert_eq!(
vals("//a(->e){;100_km}[:name = \"deep\"]? | $-:hops", &t),
vec![Value::Int(2)]
);
assert_eq!(
vals(
"//a(->e){;100_km}[:name = \"deep\"]? | $-:hops | $-:hops",
&t
),
vec![Value::Null]
);
assert_eq!(run("//a(->e){;3_h}", &t), vec![1, 3]);
let mut two = MockTree::roads();
two.edge_props
.get_mut(&(3, "e".to_string(), 6))
.unwrap()
.push(("detour".into(), Value::Str("5 km".into())));
let _ = run("//a(->e){;5_km}", &two);
assert!(take_refusal().unwrap().contains("len and detour"));
assert!(run("//a(->e){1;5_km}", &t).is_empty());
assert!(
take_refusal()
.unwrap()
.contains("one dimension per quantifier")
);
let mut neg = MockTree::roads();
neg.edge_props.insert(
(6, "e".to_string(), 2),
vec![("len".into(), Value::Str("-3 km".into()))],
);
let _ = run("//a(->e){;100_km}?", &neg);
assert!(take_refusal().unwrap().contains("negative weight"));
assert!(lex("//a(->e){5_km}").is_err());
assert_eq!(
vals("//a(->e){2}[:name = \"deep\"] | $-:round", &t),
vec![Value::Int(2), Value::Int(2)]
);
assert_eq!(
vals("//a(->e){2} | $-:total", &t),
vec![Value::Null, Value::Null]
);
assert_eq!(run("//a(->e)+[$-:hops = 2]", &t), vec![6, 6]);
assert_eq!(vals("//a->e | $-:hops", &t), vec![Value::Null, Value::Null]);
assert_eq!(
crate::syntax::unparse::unparse(&parse(&lex("//a(->e){;100_km}?").unwrap()).unwrap()),
"//a(->e){;100_km}?"
);
}
#[test]
fn edge_accessor() {
let t = MockTree::weighted();
assert_eq!(run("//a->e[$-::qty > 1]", &t), vec![2]);
assert_eq!(run("//a->*[$- = \"e\"]", &t), vec![2, 3]);
assert_eq!(run("//deep<-e[$-::qty = 12]", &t), vec![2]);
assert!(run("/a[$-::qty = 4]", &t).is_empty());
assert_eq!(
vals("//a->e[$-::qty > 1]:name", &t),
vec![Value::Str("x.rs".into())]
);
assert_eq!(
vals("//a->e | $-", &t),
vec![Value::Str("e".into()), Value::Str("e".into())]
);
assert_eq!(
vals("//a->e | $-::qty", &t),
vec![Value::Int(4), Value::Int(1)]
);
assert_eq!(vals("//a | $-", &t), vec![Value::Str("[child]".into())]);
}
#[test]
fn headless_hop_is_one_thread_per_edge() {
let t = MockTree::diamond();
let mut reached = run("//x.rs--e", &t);
reached.sort();
assert_eq!(reached, vec![1, 6, 6]);
assert_eq!(
vals("//x.rs--e | $-", &t),
vec![
Value::Str("e".into()),
Value::Str("e".into()),
Value::Str("e".into())
]
);
assert_eq!(run("//x.rs(--e){1} | [:name = \"deep\"]", &t), vec![6, 6]);
assert_eq!(
run("//x.rs(--e){1} | [:name = \"deep\"] @| collapse", &t),
vec![6]
);
}
#[test]
fn pattern_breadcrumbs() {
let t = MockTree::weighted();
assert_eq!(
vals(
"//a(->e .($-::qty))+ | [:name = \"deep\"] | @. | product",
&t
),
vec![Value::Int(48)]
);
assert_eq!(
vals("(/a .($-)){1} | $.", &t),
vec![Value::Str("[child]".into())]
);
assert_eq!(
vals("//a(->e .(1))+! | @. | count", &t),
vec![Value::Int(2)]
);
assert_eq!(
vals("//a(->e .q($-::qty)){1;2}! | $.q", &t),
vec![Value::Int(12)]
);
}
#[test]
fn per_path_results() {
let t = MockTree::diamond();
assert_eq!(
vals(
"//a(->e .(:name))+ | [:name = \"deep\"] | @. | join(\"-\")",
&t
),
vec![
Value::Str("x.rs-deep".into()),
Value::Str("y.txt-deep".into()),
]
);
assert_eq!(run("//a(->e)+ | [:name = \"deep\"]", &t), vec![6, 6]);
assert_eq!(
run("//a(->e)+ | [:name = \"deep\"] @| collapse", &t),
vec![6]
);
}
#[test]
fn structural_edge_labels() {
let t = MockTree::sample();
assert_eq!(run("/a[$- = \"[child]\"]", &t), vec![1]);
assert_eq!(run("//w.rs\\deep[$- = \"[parent]\"]", &t), vec![6]);
assert_eq!(run("/a/x.rs>y.txt[$- = \"[next]\"]", &t), vec![3]);
assert_eq!(run("/a/y.txt<x.rs[$- = \"[prev]\"]", &t), vec![2]);
assert!(run("/a[$- = \"[parent]\"]", &t).is_empty());
}
#[test]
fn pipelines_and_union() {
let t = MockTree::sample();
assert_eq!(vals("//*.rs @| count", &t), vec![Value::Int(3)]);
assert_eq!(
vals("//*.rs:name @| sort @| join(\", \")", &t),
vec![Value::Str("w.rs, x.rs, z.rs".into())]
);
assert_eq!(
vals("//x.rs:name | upper", &t),
vec![Value::Str("X.RS".into())]
);
assert_eq!(run("//x.rs || //z.rs", &t), vec![2, 5]);
assert_eq!(vals("//x.rs || //z.rs @| count", &t), vec![Value::Int(2)]);
}
#[test]
fn registers_and_subcontexts() {
let t = MockTree::sample();
assert_eq!(
vals("/*[:name = \"a\"] | .(//*.rs @| count) | $.", &t),
vec![Value::Int(1)]
);
assert_eq!(
vals("/*[:name = \"b\"] | .(//*.rs @| count) | $.", &t),
vec![Value::Int(2)]
);
assert_eq!(
vals("//x.rs:name | .saved | upper | $.saved", &t),
vec![Value::Str("x.rs".into())]
);
}
#[test]
fn predicates() {
let t = MockTree::sample();
assert_eq!(run("/*[:n-children = 2]", &t), vec![1, 4]);
assert_eq!(run("/*[/z.rs]", &t), vec![4]);
assert_eq!(run("/a/*[1]", &t), vec![2]);
assert_eq!(run("/*[!:is-leaf]", &t), vec![1, 4]);
assert_eq!(run("//*[:is-leaf && :name == (/rs/)]", &t), vec![2, 5, 7]);
assert_eq!(run("//*[:depth > 2]", &t), vec![7]);
}
#[test]
fn index_predicate_positions_among_hop_results() {
let names = vec![
None, Some("h".into()), Some("p".into()), Some("p".into()), Some("sect".into()), Some("title".into()), Some("p".into()), Some("p".into()), ];
let mut kids = HashMap::new();
kids.insert(0, vec![1, 2, 3, 4]);
kids.insert(4, vec![5, 6, 7]);
let t = MockTree {
names,
kids,
loose: Vec::new(),
links: HashMap::new(),
edge_props: HashMap::new(),
prov: HashMap::new(),
};
assert_eq!(run("/p[1]", &t), vec![2]);
assert_eq!(run("/p[2]", &t), vec![3]);
assert_eq!(run("/sect/p[2]", &t), vec![7]);
assert_eq!(run("//p[1]", &t), vec![2]);
assert_eq!(run("//p[4]", &t), vec![7]);
assert_eq!(run("/p[:index = 2]", &t), vec![2]);
assert_eq!(run("/*[:index = 3]", &t), vec![3]);
assert_eq!(run("/p[:index = 1]", &t), Vec::<u64>::new());
assert_eq!(run("/*[:index > 1][1]", &t), vec![2]);
assert_eq!(run("/*[1][:index > 1]", &t), Vec::<u64>::new());
assert_eq!(run("/p[-1]", &t), vec![3]);
assert_eq!(run("//p[-1]", &t), vec![7]);
assert_eq!(run("//p[-4]", &t), vec![2]);
assert_eq!(run("/*[:index > 1][-1]", &t), vec![4]);
assert_eq!(run("//p[0]", &t), Vec::<u64>::new());
assert_eq!(run("//p[5]", &t), Vec::<u64>::new());
assert_eq!(run("//p[-5]", &t), Vec::<u64>::new());
}
#[test]
fn range_predicates() {
let names = vec![
None, Some("h".into()), Some("p".into()), Some("p".into()), Some("sect".into()), Some("title".into()), Some("p".into()), Some("p".into()), ];
let mut kids = HashMap::new();
kids.insert(0, vec![1, 2, 3, 4]);
kids.insert(4, vec![5, 6, 7]);
let t = MockTree {
names,
kids,
loose: Vec::new(),
links: HashMap::new(),
edge_props: HashMap::new(),
prov: HashMap::new(),
};
assert_eq!(run("//p[2..3]", &t), vec![3, 6]);
assert_eq!(run("//p[2..]", &t), vec![3, 6, 7]);
assert_eq!(run("//p[..2]", &t), vec![2, 3]);
assert_eq!(run("//p[2..-1]", &t), vec![3, 6, 7]);
assert_eq!(run("//p[..-2]", &t), vec![2, 3, 6]);
assert_eq!(run("//p[..9]", &t), vec![2, 3, 6, 7]);
assert_eq!(run("//p[5..]", &t), Vec::<u64>::new());
assert_eq!(run("//p[3..2]", &t), Vec::<u64>::new());
assert_eq!(run("/*[..2][:index > 1]", &t), vec![2]);
assert_eq!(run("/*[:index > 1][..2]", &t), vec![2, 3]);
}
#[test]
fn value_expressions() {
let t = MockTree::sample();
assert_eq!(run("/*[1 + 2 * 3 = 7]", &t), vec![1, 4]);
assert_eq!(run("/*[(1 + 2) * 3 = 9]", &t), vec![1, 4]);
assert_eq!(run("/*[:index + 1 = 2]", &t), vec![1]);
assert_eq!(run("/*[:index * 2 = 4]", &t), vec![4]);
assert_eq!(run("/*[:index - 2 = -1]", &t), vec![1]);
assert_eq!(run("/*[- :index = -2]", &t), vec![4]);
assert_eq!(run("/*[7 div 2 = 3.5]", &t), vec![1, 4]);
assert_eq!(run("/*[7 idiv 2 = 3]", &t), vec![1, 4]);
assert_eq!(run("/*[7 mod 2 = 1]", &t), vec![1, 4]);
assert_eq!(run("/*[0 - 7 mod 2 = -1]", &t), vec![1, 4]);
assert_eq!(
run("/*[9007199254740993 + 0 = 9007199254740993]", &t),
vec![1, 4]
);
assert_eq!(run("/*[1 div 0]", &t), run("/*", &t));
assert_eq!(run("/*[1 idiv 0]", &t), run("/*", &t));
assert_eq!(run("/*[:name * 2]", &t), Vec::<u64>::new());
assert_eq!(run("/*[::a-b = 1]", &t), Vec::<u64>::new());
assert_eq!(
run("/*[(:index = 1 || :index = 2) && :is-leaf]", &t),
Vec::<u64>::new()
);
}
#[test]
fn value_expression_commands() {
let t = MockTree::sample();
assert_eq!(vals("/a/x.rs | :index * 2", &t), vec![Value::Int(2)]);
assert_eq!(
vals("/* | .double(:index * 2) | $.double @| sum", &t),
vec![Value::Int(6)]
);
assert_eq!(vals("/a/x.rs | (:index + 1) * 3", &t), vec![Value::Int(6)]);
}
#[test]
fn fused_group_count_matches_the_two_commands() {
let t = MockTree::sample();
let fused = vals("//* @| group(d = :depth) | count", &t);
let unfused = vals("//* @| group(d = :depth) | %(n = ($_ @| count)) | :n", &t);
assert_eq!(fused, unfused);
assert!(fused.len() > 1);
assert_eq!(
fused
.iter()
.map(|v| v.to_string().parse::<i64>().unwrap())
.sum::<i64>(),
vals("//* @| count", &t)[0]
.to_string()
.parse::<i64>()
.unwrap()
);
let recs = vals("//* @| group(d = :depth) | count | %($.d; n = $_)", &t);
let refs = vals("//* @| group(d = :depth) | %($.d; n = ($_ @| count))", &t);
assert_eq!(recs, refs);
assert_eq!(
vals("//* @| group(d = :depth) | count | [$_ > 1] | $.d", &t),
vals(
"//* @| group(d = :depth) | %(d = $.d; n = ($_ @| count)) | [:n > 1] | :d",
&t
)
);
}
#[test]
fn keyed_aggregates() {
let t = MockTree::sample();
assert_eq!(
vals("//*[:is-leaf] @| sort_by(:name) | :name", &t),
vec![
Value::Str("w.rs".into()),
Value::Str("x.rs".into()),
Value::Str("y.txt".into()),
Value::Str("z.rs".into()),
]
);
assert_eq!(
vals(
"//*[:is-leaf] @| sort_by(:name) @| reverse @| [..2] | :name",
&t
),
vec![Value::Str("z.rs".into()), Value::Str("y.txt".into())]
);
assert_eq!(
vals("//*[:is-leaf] @| top(2, :name) | :name", &t),
vec![Value::Str("z.rs".into()), Value::Str("y.txt".into())]
);
assert_eq!(
vals("//*[:is-leaf] @| bottom(1, :name) | :name", &t),
vec![Value::Str("w.rs".into())]
);
assert_eq!(run("//*[:is-leaf] @| max_by(:depth)", &t), vec![7]);
assert_eq!(run("//* @| min_by(:depth)", &t), vec![1, 4]);
assert_eq!(run("//* @| unique_by(:depth)", &t), vec![1, 2, 7]);
assert_eq!(
vals("//*[:is-leaf] @| sort_by(:depth, :name) | :name", &t)[0],
Value::Str("x.rs".into())
);
}
#[test]
fn positional_and_filter_commands() {
let t = MockTree::sample();
assert_eq!(run("//*.rs @| [1]", &t), vec![2]);
assert_eq!(run("//*.rs @| [-1]", &t), vec![7]);
assert_eq!(run("//*.rs @| [2..]", &t), vec![5, 7]);
assert_eq!(
vals("//*.rs @| [..2] | :name", &t),
vec![Value::Str("x.rs".into()), Value::Str("z.rs".into())]
);
assert_eq!(run("//* | [:is-leaf]", &t), vec![2, 3, 5, 7]);
assert_eq!(run("//*.rs @| sort_by(:name) | [:depth > 2]", &t), vec![7]);
assert!(parse(&lex("//* | [1]").unwrap()).is_ok());
assert!(parse(&lex("//* @| [:is-leaf]").unwrap()).is_err());
assert!(parse(&lex("//a | ((::x @| [:is-leaf] @| count))").unwrap()).is_err());
assert!(parse(&lex("//a | ((::x @| bogus))").unwrap()).is_err());
}
#[test]
fn context_snapshot_in_cond_and_agg_keys() {
let t = MockTree::sample();
assert_eq!(
vals("//*.rs | ((@_ @| count) > 2 ? \"many\" : \"few\")", &t),
vec![
Value::Str("many".into()),
Value::Str("many".into()),
Value::Str("many".into()),
]
);
assert_eq!(run("//*.rs @| min_by((@_ @| count) - $ord)", &t), vec![7]);
}
#[test]
fn core_metadata_projection() {
let t = MockTree::sample();
assert_eq!(
vals("/a/*:name", &t),
vec![Value::Str("x.rs".into()), Value::Str("y.txt".into())]
);
assert_eq!(vals("/a:n-children", &t), vec![Value::Int(2)]);
assert_eq!(vals("/b/deep:depth", &t), vec![Value::Int(2)]);
assert_eq!(vals("/a/x.rs:is-leaf", &t), vec![Value::Bool(true)]);
assert_eq!(vals("/b:is-leaf", &t), vec![Value::Bool(false)]);
assert_eq!(vals("/a/y.txt:index", &t), vec![Value::Int(2)]);
assert_eq!(vals("/a/x.rs:parent-id", &t), vec![Value::Int(1)]);
assert_eq!(vals("/a/x.rs:parent-index", &t), vec![Value::Int(1)]);
assert_eq!(vals("/a:is-top-level", &t), vec![Value::Bool(true)]);
assert_eq!(vals("/a/x.rs:is-top-level", &t), vec![Value::Bool(false)]);
assert_eq!(
vals("//w.rs:name-path", &t),
vec![Value::Str("/b/deep/w.rs".into())]
);
assert_eq!(
vals("//w.rs:index-path", &t),
vec![Value::Str("/2/2/1".into())]
);
assert_eq!(
vals("//w.rs:id-path", &t),
vec![Value::Str("/4/6/7".into())]
);
}
#[test]
fn provenance_core_metadata() {
let mut t = MockTree::sample();
let (secs, nanos, off) = crate::temporal::parse_iso("2026-04-07T02:00:01Z").unwrap();
t.prov.insert(
2,
Provenance {
source: Some("api-gateway".into()),
instant: Some((secs, nanos, off)),
dpid: Some("request-42".into()),
..Default::default()
},
);
t.prov.insert(
3,
Provenance {
source: Some("api-gateway".into()),
..Default::default()
},
);
t.prov.insert(
5,
Provenance {
dpid: Some("row-7".into()),
..Default::default()
},
);
assert_eq!(
vals("/a/x.rs:source", &t),
vec![Value::Str("api-gateway".into())]
);
assert_eq!(
vals("/a/x.rs:instant", &t),
vec![Value::Instant {
secs,
nanos,
offset_min: off,
calendar: Default::default()
}]
);
assert_eq!(
vals("/a/x.rs:dpid", &t),
vec![Value::Str("request-42".into())]
);
assert_eq!(
vals("/a/x.rs:provenance", &t),
vec![Value::Str(
"?api-gateway@2026-04-07T02:00:01Z#request-42".into()
)]
);
assert_eq!(
vals("/a/y.txt:provenance", &t),
vec![Value::Str("?api-gateway".into())]
);
assert_eq!(
vals("/b/z.rs:provenance", &t),
vec![Value::Str("?#row-7".into())]
);
assert_eq!(vals("/b/deep:source", &t), vec![Value::Null]);
assert_eq!(vals("/b/deep:instant", &t), vec![Value::Null]);
assert_eq!(vals("/b/deep:provenance", &t), vec![Value::Null]);
assert_eq!(run("//*[:instant > 2020-01-01]", &t), vec![2]);
assert_eq!(run("//*[:instant > 2030-01-01]", &t), Vec::<u64>::new());
let inner = Provenance {
source: Some("leaf".into()),
..Default::default()
};
let outer = Provenance {
source: Some("mount".into()),
dpid: Some("d1".into()),
..Default::default()
};
let merged = inner.or(outer);
assert_eq!(merged.source.as_deref(), Some("leaf"));
assert_eq!(merged.dpid.as_deref(), Some("d1"));
assert!(merged.instant.is_none());
assert_eq!(Provenance::default().canonical(), None);
}
#[test]
fn ancestor_reach() {
let t = MockTree::sample();
assert_eq!(run("//w.rs\\\\*", &t), vec![6, 4, 0]);
assert_eq!(run("//w.rs\\\\?*", &t), vec![6]); assert_eq!(run("//w.rs\\\\!*", &t), vec![0]); assert_eq!(run("//w.rs\\\\", &t), run("//w.rs\\\\*", &t));
assert_eq!(run("//w.rs\\\\?", &t), run("//w.rs\\", &t));
assert_eq!(run("//w.rs\\\\!", &t), run("^", &t));
assert_eq!(run("//w.rs/..", &t), run("//w.rs\\", &t));
assert_eq!(run("//w.rs/...", &t), run("//w.rs\\\\", &t));
}
#[test]
fn marks() {
let t = MockTree::sample();
assert_eq!(run("/a .a /*[:name = $$.a:name]", &t), Vec::<u64>::new());
assert_eq!(run("//deep .d /w.rs[$$.d:name = \"deep\"]", &t), vec![7]);
assert_eq!(
vals("//w.rs .w \\\\!* | $$.w:name", &t),
vec![Value::Str("w.rs".into())]
);
assert_eq!(run("/a/x.rs .x (->ref)[$$.x:name = \"x.rs\"]", &t), vec![7]);
assert_eq!(vals("//*.rs @| count | .n | $.n", &t), vec![Value::Int(3)]);
assert_eq!(run("/a/*[$$.nope:name = \"a\"]", &t), Vec::<u64>::new());
assert_eq!(
run("/a/x.rs .x | [^//w.rs[$$.x:name = \"x.rs\"]]", &t),
vec![2]
);
assert_eq!(
vals("/a/x.rs .x | .n($$.x:name) | $.n", &t),
vec![Value::Str("x.rs".into())]
);
}
#[test]
fn shell_command() {
let t = MockTree::sample();
let q = "/a/x.rs | :name | `tr a-z A-Z`";
assert!(crate::run(q, &t).is_err());
struct Shelly(MockTree);
impl AstAdapter for Shelly {
fn root(&self) -> NodeId {
self.0.root()
}
fn children(&self, n: NodeId) -> Vec<NodeId> {
self.0.children(n)
}
fn name(&self, n: NodeId) -> Option<String> {
self.0.name(n)
}
fn parent(&self, n: NodeId) -> Option<NodeId> {
self.0.parent(n)
}
fn links(&self, n: NodeId) -> Vec<(String, NodeId)> {
self.0.links(n)
}
}
let t = Shelly(MockTree::sample());
let shell = crate::Run {
allow_shell: true,
..Default::default()
};
let run_shell = |q: &str| {
crate::Query::parse(q, &t)
.unwrap()
.run(&t, &shell)
.map(|o| o.result)
};
let got = match run_shell(q).unwrap() {
QueryResult::Values(vs) => vs,
_ => panic!("values"),
};
assert_eq!(got, vec![Value::Str("X.RS".into())]);
let got = match run_shell("/a/x.rs | :name | `false`").unwrap() {
QueryResult::Values(vs) => vs,
_ => panic!("values"),
};
assert_eq!(got, vec![Value::Null]);
}
#[test]
fn node_mode_json() {
let t = MockTree::sample();
let out = match crate::run("/a | json", &t).unwrap() {
QueryResult::Values(vs) => vs,
_ => panic!("values"),
};
assert_eq!(out.len(), 1);
assert!(out[0].to_string().contains("x.rs"));
let out = match crate::run("^ | \"[1,2,3]\" | decode(json) | ...", &t).unwrap() {
QueryResult::Values(vs) => vs,
_ => panic!("values"),
};
assert_eq!(out, vec![Value::Int(1), Value::Int(2), Value::Int(3)]);
let out = match crate::run("/a | xml", &t).unwrap() {
QueryResult::Values(vs) => vs,
_ => panic!("values"),
};
assert!(out[0].to_string().starts_with("<a>") && out[0].to_string().contains("x.rs"));
let out = match crate::run("^ | \"<r><x>1</x></r>\" | decode(xml) | json", &t).unwrap() {
QueryResult::Values(vs) => vs,
_ => panic!("values"),
};
assert_eq!(out, vec![Value::Str("{\"x\": \"1\"}".into())]);
let out = match crate::run("^ | \"{\\\"a\\\":1}\" | decode(json) | json", &t).unwrap() {
QueryResult::Values(vs) => vs,
_ => panic!("values"),
};
assert_eq!(out, vec![Value::Str("{\"a\": 1}".into())]);
}
#[test]
fn value_match() {
let t = MockTree::sample();
assert_eq!(
vals("/a/* | (:name ?= \"x.rs\" ? 1 : \"y.txt\" ? 2 : 0)", &t),
vec![Value::Int(1), Value::Int(2)]
);
assert_eq!(
vals(
"//*.rs @| [..1] | (:name ?= (/^x/) ? \"ex\" : \"other\")",
&t
),
vec![Value::Str("ex".into())]
);
assert_eq!(
vals("/a/*[1] | (:name ?= \"nope\" ? 1 : 9)", &t),
vec![Value::Int(9)]
);
}
#[test]
fn anchored_operands() {
let t = MockTree::sample();
assert_eq!(run("//*[:name = ^/a/*:name]", &t), vec![2, 3]);
assert_eq!(run("/a/*[^//w.rs]", &t), vec![2, 3]);
assert_eq!(
vals("/a/*[1] | (^//*.rs @| count)", &t),
vec![Value::Int(3)]
);
}
#[test]
fn outer_spread() {
let t = MockTree::sample();
assert_eq!(
vals("/a/* | .m(->ref:name) | $.m | ...", &t),
vec![Value::Str("w.rs".into())]
);
assert_eq!(
vals("/a/* | .m(->ref:name) | $.m | ...?", &t),
vec![Value::Str("w.rs".into()), Value::Null]
);
}
#[test]
fn outer_correlation() {
let t = MockTree::sample();
let q = "//*.rs <=> /a/*[:name = $$0:name]";
assert_eq!(run(q, &t), vec![2]);
let q = "//*.rs <=>? /a/*[:name = $$0:name]";
assert_eq!(run(q, &t), vec![2, 5, 7]);
assert_eq!(
vals("//*.rs <=>? /a/*[:name = $$0:name] | $$1:name", &t),
vec![Value::Str("x.rs".into()), Value::Null, Value::Null]
);
let q = "//*.rs <=>? /a/*[:name = $$0:name] | [!$$1:name]";
assert_eq!(run(q, &t), vec![5, 7]);
let q = "//*.rs <=>? /a/*[:name = $$0:name] | [$$1:name]";
assert_eq!(run(q, &t), vec![2]);
}
#[test]
fn outer_correlation_indirect_ctx() {
let t = MockTree::sample();
let q = "//*.rs <=>? /a/*[(:name ?= $$0:name ? 1 : 0) = 1]";
assert_eq!(run(q, &t), vec![2, 5, 7]);
let q = "//*.rs <=>? /a/*[/*[$$0:name = :name]]";
assert_eq!(run(q, &t), vec![2, 5, 7]);
}
#[test]
fn now_operand() {
let t = MockTree::sample();
assert_eq!(vals("^ | (now())", &t), vec![Value::Null]);
struct Clocked(MockTree, i64, u32);
impl AstAdapter for Clocked {
fn root(&self) -> NodeId {
self.0.root()
}
fn children(&self, n: NodeId) -> Vec<NodeId> {
self.0.children(n)
}
fn name(&self, n: NodeId) -> Option<String> {
self.0.name(n)
}
fn parent(&self, n: NodeId) -> Option<NodeId> {
self.0.parent(n)
}
}
let (s, n, _) = crate::temporal::parse_iso("2026-07-12T09:00:00Z").unwrap();
let t = Clocked(MockTree::sample(), s, n);
let clocked = Run {
now: Some((t.1, t.2)),
..Default::default()
};
let got = match eval_with(
&parse(&lex("^ | (now())").unwrap()).unwrap(),
&t,
Some(clocked),
) {
QueryResult::Values(vs) => vs,
_ => panic!("expected values"),
};
assert_eq!(
got,
vec![Value::Instant {
secs: s,
nanos: n,
offset_min: Some(0),
calendar: Default::default(),
}]
);
assert_eq!(got[0].to_string(), "2026-07-12T09:00:00Z");
}
#[test]
fn temporal_arith_overflow_nulls() {
let sc: &dyn Fn(&str) -> Option<(f64, String)> = &crate::value::quantity::scale_expr;
let inst = |secs| Value::Instant {
secs,
nanos: 0,
offset_min: Some(0),
calendar: Default::default(),
};
let dur = |secs| Value::seconds(secs, 0);
assert!(matches!(
arith(ArithOp::Add, &inst(i64::MAX), &dur(1), sc),
Value::Null
));
assert!(matches!(
arith(ArithOp::Add, &dur(i64::MAX), &dur(1), sc),
Value::Quantity { .. }
));
assert!(matches!(
arith(ArithOp::Sub, &inst(i64::MIN), &inst(i64::MAX), sc),
Value::Null
));
assert_eq!(
arith(ArithOp::Add, &inst(0), &dur(60), sc).to_string(),
"1970-01-01T00:01:00Z"
);
}
#[test]
fn contains_and_regex_null_propagate() {
let sc: &dyn Fn(&str) -> Option<(f64, String)> = &crate::value::quantity::scale_expr;
let name = Value::Str("chapter one".into());
assert!(!compare(&name, CmpOp::Contains, &Value::Null, sc));
assert!(compare(
&name,
CmpOp::Contains,
&Value::Str(String::new()),
sc
));
assert!(compare(
&name,
CmpOp::Contains,
&Value::Str("one".into()),
sc
));
assert!(!compare(&name, CmpOp::Match, &Value::Null, sc));
assert!(compare(&name, CmpOp::NotMatch, &Value::Null, sc));
assert!(compare(
&name,
CmpOp::Match,
&Value::Str("^chap".into()),
sc
));
}
#[test]
fn nav_command_resumes_navigation() {
let t = MockTree::sample();
assert_eq!(run("/* | /*", &t), vec![2, 3, 5, 6]);
let vs = vals("/* | .dir(:name) | /* | .file(:name) | %.", &t);
assert_eq!(vs.len(), 4);
let Value::Record(fields) = &vs[0] else {
panic!("expected a record, got {:?}", vs[0]);
};
assert_eq!(
fields[0],
("dir".to_string(), Value::Str("a".into())),
"first column is the ancestor's push"
);
assert_eq!(fields[1], ("file".to_string(), Value::Str("x.rs".into())));
assert_eq!(
vals("/a | /*:name", &t),
vec![Value::Str("x.rs".into()), Value::Str("y.txt".into())]
);
assert_eq!(run("/a/x.rs | ^/b", &t), vec![4]);
assert_eq!(run("/a .m /x.rs | $$.m/y.txt", &t), vec![3]);
assert_eq!(run("/a | .m | /x.rs | $$.m/y.txt", &t), vec![3]);
assert_eq!(run("/a/x.rs | ->ref", &t), vec![7]);
assert_eq!(run("/a | :name | . | /*", &t), vec![2, 3]);
}
#[test]
fn nav_command_mode_discipline() {
let refuse = |q: &str| {
parse(&lex(q).unwrap())
.expect_err("should refuse")
.to_string()
};
assert!(refuse("/a:name | /b").contains("scalar mode"));
assert!(refuse("/a | :name | /b").contains("scalar mode"));
assert!(refuse("/a @| /b").contains("per-thread"));
assert!(refuse("/a | ... $| /b").contains("map pipe"));
}
#[test]
fn named_groups_push_into_the_register() {
let t = MockTree::sample();
let bare = vals(
"/a/* | [:name == (/^(?<stem>\\w+)\\.(?<ext>\\w+)$/)] | %.",
&t,
);
assert!(
bare.iter()
.all(|r| matches!(r, Value::Record(f) if f.is_empty())),
"{bare:?}"
);
let stems = vals(
"/a/* | [:name == (/^(?<stem>\\w+)\\.(?<ext>\\w+)$/)] | %+:stem",
&t,
);
assert!(
stems.iter().all(|v| matches!(v, Value::Str(_))),
"{stems:?}"
);
let q = "/a/* | [:name == (/^(?<stem>\\w+)\\.(?<ext>\\w+)$/)] | %+ | .% | %.";
let rows = vals(q, &t);
assert!(!rows.is_empty());
for r in &rows {
let Value::Record(fields) = r else {
panic!("expected a record, got {r:?}");
};
let keys: Vec<&str> = fields.iter().map(|(k, _)| k.as_str()).collect();
assert_eq!(keys, vec!["stem", "ext"], "{r:?}");
}
let exts = vals(
"/a/* | [:name == (/^(?<stem>\\w+)\\.(?<ext>\\w+)$/)] | %+ | .% | $.ext",
&t,
);
assert!(
exts.iter()
.all(|v| matches!(v, Value::Str(s) if s == "rs" || s == "txt")),
"{exts:?}"
);
let stems = vals("/a/* | [:name == (/^(?<stem>\\w+)\\.(\\w+)$/)] | $1", &t);
assert_eq!(stems.len(), exts.len());
}
#[test]
fn record_fields_and_named_captures() {
let t = MockTree::sample();
let q = "/a/* | [:name == (/^(?<stem>\\w+)\\.(?<ext>\\w+)$/)] | %+:ext";
let exts = vals(q, &t);
assert!(
!exts.is_empty()
&& exts
.iter()
.all(|v| matches!(v, Value::Str(s) if s == "rs" || s == "txt")),
"{exts:?}"
);
let recs = vals("/a/* | [:name == (/^(?<stem>\\w+)/)] | %+", &t);
assert!(
recs.iter()
.all(|r| matches!(r, Value::Record(f) if f.len() == 1 && f[0].0 == "stem")),
"{recs:?}"
);
let ones = vals("/a/* | %(:name; n = 1) | :n", &t);
assert!(
!ones.is_empty() && ones.iter().all(|v| matches!(v, Value::Int(1))),
"{ones:?}"
);
let ones = vals("/a/* | %(:name; n = 1) | .r | $.r:n", &t);
assert!(
!ones.is_empty() && ones.iter().all(|v| matches!(v, Value::Int(1))),
"{ones:?}"
);
let nulls = vals("/a/* | %(:name; n = 1) | :zzz", &t);
assert!(nulls.iter().all(|v| matches!(v, Value::Null)), "{nulls:?}");
let empty = vals("/a/* | %+", &t);
assert!(
empty
.iter()
.all(|v| matches!(v, Value::Record(f) if f.is_empty())),
"{empty:?}"
);
}
#[test]
fn list_literal_gathers_every_value() {
let t = MockTree::sample();
assert_eq!(
vals("/a | @(1; \"x\"; 2 + 1)", &t),
vec![Value::list(vec![
Value::Int(1),
Value::Str("x".into()),
Value::Int(3)
])]
);
let gathered = vals("/a | @(/*:name)", &t);
assert!(
matches!(&gathered[0], Value::List(items) if items.len() > 1),
"{gathered:?}"
);
assert_eq!(vals("/a | @()", &t), vec![Value::list(Vec::new())]);
assert_eq!(vals("/a | *(1; 2)", &t), vals("/a | @(1; 2)", &t));
assert_eq!(
vals("/a | @(1; \"x\")", &t)[0].display_form(),
"@(1; \"x\")"
);
}
#[test]
fn json_and_jsonl_over_the_stream() {
let t = MockTree::sample();
let each = vals("/a/* | :name | json", &t);
assert!(
each.len() > 1
&& each
.iter()
.all(|v| matches!(v, Value::Str(s) if s.starts_with('"'))),
"{each:?}"
);
let whole = vals("/a/* | :name @| json", &t);
assert!(
matches!(&whole[..], [Value::Str(s)] if s.starts_with("[\"") && s.ends_with(']')),
"{whole:?}"
);
let lines = vals("/a/* | :name @| jsonl", &t);
assert!(
matches!(&lines[..], [Value::Str(s)] if s.lines().count() == each.len()),
"{lines:?}"
);
let rec = vals("/a/* | %(n = :name) | json", &t);
assert!(
rec.iter()
.all(|v| matches!(v, Value::Str(s) if s.starts_with("{\"n\": "))),
"{rec:?}"
);
}
#[test]
fn record_push_in_one_step() {
let t = MockTree::sample();
let ones = vals("/a/* | .r%(:name; n = 1) | $.r:n", &t);
assert!(
!ones.is_empty() && ones.iter().all(|v| matches!(v, Value::Int(1))),
"{ones:?}"
);
let views = vals("/a/* | .r%(:name) | %.", &t);
assert!(
views
.iter()
.all(|v| matches!(v, Value::Record(f) if f.len() == 1 && f[0].0 == "r")),
"{views:?}"
);
let anon = vals("/a/* | .%(:name; n = 2) | $.:n", &t);
assert!(
!anon.is_empty() && anon.iter().all(|v| matches!(v, Value::Int(2))),
"{anon:?}"
);
let rich = vals("/a/* | .k(:name) | .%%(n = 3) | $.:k", &t);
assert!(
!rich.is_empty() && rich.iter().all(|v| matches!(v, Value::Str(_))),
"{rich:?}"
);
}
#[test]
fn mark_array_symmetry() {
let t = MockTree::sample();
assert_eq!(run("/a . /x.rs | ((.1))/y.txt", &t), vec![3]);
assert_eq!(run("/a . /x.rs | $$./y.txt", &t), vec![3]);
assert_eq!(run("/a | . | /x.rs | ((.1))/y.txt", &t), vec![3]);
assert_eq!(
vals("/a . /x.rs .m | ((.2)):name", &t),
vec![Value::Str("x.rs".into())]
);
assert_eq!(
vals("/a | . | @. | json", &t),
vec![Value::Str("[]".into())]
);
assert_eq!(run("/a . /*[((.1))/x.rs]", &t), vec![2, 3]);
}
#[test]
fn marks_fan_out() {
let t = MockTree::sample();
assert_eq!(run("/a/x.rs | . | ^/b/z.rs | . | ((@))", &t), vec![2, 5]);
assert_eq!(run("/a | . | ^/b | . | ((@))/*.rs", &t), vec![2, 5]);
assert_eq!(run("/a .s /x.rs | ^/b .s /z.rs | ((@s))", &t), vec![1, 4]);
assert_eq!(run("/a | ((@))/x.rs", &t), Vec::<u64>::new());
}
#[test]
fn numeric_mark_names_refused() {
let refuse = |q: &str| {
parse(&lex(q).unwrap())
.expect_err("should refuse")
.to_string()
};
assert!(refuse("/a .1 /b").contains("starts with a letter"));
assert!(refuse("/a | .1").contains("starts with a letter"));
}
#[test]
fn pushes_return_to_navigation_mode() {
let t = MockTree::sample();
let vs = vals("/* | .dir(:name) | /*[:name = x.rs] | %.", &t);
assert_eq!(vs.len(), 1);
let Value::Record(fields) = &vs[0] else {
panic!("expected a record, got {:?}", vs[0]);
};
assert_eq!(fields[0], ("dir".to_string(), Value::Str("a".into())));
assert_eq!(vals("/a:name | . | $.", &t), vec![Value::Str("a".into())]);
}
fn traced(q: &str, t: &MockTree) -> Vec<Traced> {
eval_traced_prov(&parse(&lex(q).unwrap()).unwrap(), t)
}
fn origins(known: &[(u64, u32)]) -> Origins {
Origins {
known: known
.iter()
.map(|&(n, s)| Origin {
position: (NodeId(n)).into(),
command: s,
})
.collect(),
more: 0,
omitted: false,
}
}
fn provenanced() -> MockTree {
let mut t = MockTree::sample();
let (s2, n2, o2) = crate::temporal::parse_iso("2026-04-07T02:00:01Z").unwrap();
let (s7, n7, o7) = crate::temporal::parse_iso("2026-05-01T00:00:00Z").unwrap();
t.prov.insert(
2,
Provenance {
source: Some("api-gateway".into()),
instant: Some((s2, n2, o2)),
dpid: Some("request-42".into()),
..Default::default()
},
);
t.prov.insert(
7,
Provenance {
source: Some("crm".into()),
instant: Some((s7, n7, o7)),
dpid: Some("row-7".into()),
..Default::default()
},
);
t
}
#[test]
fn push_records_the_read_node_and_command() {
let t = MockTree::sample();
let rows = traced("/a/* | .n(:name) | $.n", &t);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].node, NodeId(2));
assert_eq!(rows[0].prov.origins, origins(&[(2, 1)]));
assert_eq!(rows[1].prov.origins, origins(&[(3, 1)]));
let rows = traced("/a/x.rs:name", &t);
assert_eq!(rows[0].prov.origins, origins(&[(2, 0)]));
}
#[test]
fn expression_unions_its_operands_origins() {
let t = MockTree::sample();
let rows = traced("/a/x.rs | .n(:index) | ->ref | $.n + :index", &t);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].node, NodeId(7));
assert_eq!(rows[0].prov.origins, origins(&[(2, 1), (7, 3)]));
}
#[test]
fn selection_aggregate_keeps_the_winners_origins() {
let t = MockTree::sample();
let rows = traced("/a/*:index @| max", &t);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].prov.origins.known.len(), 1);
let all = traced("/a/*:index @| count", &t);
assert!(
all[0].prov.origins.known.len() > 1,
"a computed reduction keeps the union"
);
}
#[test]
fn a_projected_count_streams_and_keeps_the_counted_nodes() {
let t = MockTree::sample();
let plain = traced("/a/* @| count", &t);
let projected = traced("/a/*:index @| count", &t);
assert_eq!(plain[0].topic, projected[0].topic);
let Some(Value::Int(n)) = projected[0].topic else {
panic!("{projected:?}")
};
assert_eq!(projected[0].prov.origins.known.len(), n as usize);
let missing = traced("/a/*::no-such-property @| count", &t);
assert_eq!(missing[0].topic, projected[0].topic);
}
#[test]
fn a_count_behind_a_descendant_lead_step_streams_and_agrees() {
let t = MockTree::sample();
let streamed = vals("//*//* @| count", &t);
let walked = vals("//*//* @| unique @| count", &t);
assert_eq!(streamed, walked);
let streamed = vals("//a//x.rs @| count", &t);
let walked = vals("//a//x.rs @| unique @| count", &t);
assert_eq!(streamed, walked);
assert_ne!(streamed, vec![Value::Int(0)]);
}
#[test]
fn fork_keeps_the_register_provenance() {
let t = MockTree::sample();
let rows = traced("/a/x.rs | .n(:name) | ->ref | $.n", &t);
assert_eq!(rows[0].node, NodeId(7));
assert_eq!(rows[0].topic, Some(Value::Str("x.rs".into())));
assert_eq!(rows[0].prov.origins, origins(&[(2, 1)]));
}
#[test]
fn record_keeps_a_part_per_field() {
let t = MockTree::sample();
let rows = traced("/a/x.rs | .n(:index) | ->ref | %(a = $.n; b = :name)", &t);
let p = &rows[0].prov;
assert_eq!(p.origins, origins(&[(2, 1), (7, 3)]));
assert_eq!(p.part("a").origins, origins(&[(2, 1)]));
assert_eq!(p.part("b").origins, origins(&[(7, 3)]));
let rows = traced(
"/a/x.rs | .n(:index) | ->ref | %(a = $.n; b = :name) | .% | $.b",
&t,
);
assert_eq!(rows[0].prov.origins, origins(&[(7, 3)]));
let rows = traced(
"/a/x.rs | .n(:index) | ->ref | %(a = $.n; b = :name) | .% | %.",
&t,
);
assert_eq!(rows[0].prov.part("a").origins, origins(&[(2, 1)]));
assert_eq!(rows[0].prov.part("b").origins, origins(&[(7, 3)]));
}
#[test]
fn aggregate_unions_and_bounds() {
let t = MockTree::sample();
let rows = traced("/a/* | :index @| count", &t);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].prov.origins, origins(&[(2, 1), (3, 1)]));
let mut wide = MockTree::sample();
wide.names = (0..13)
.map(|i| if i == 0 { None } else { Some(format!("n{i}")) })
.collect();
wide.kids = HashMap::new();
wide.kids.insert(0, (1..13).collect());
wide.links = HashMap::new();
let rows = traced("/* | :index @| sum", &wide);
assert_eq!(rows[0].prov.origins.known.len(), ORIGIN_CAP);
assert_eq!(rows[0].prov.origins.more, 4);
}
#[test]
fn predicate_reads_are_not_origins() {
let t = MockTree::sample();
let rows = traced(
"/a/x.rs | .n(:index) | ->ref | (:name = \"w.rs\" ? $.n : 0)",
&t,
);
assert_eq!(rows[0].topic, Some(Value::Int(1)));
assert_eq!(rows[0].prov.origins, origins(&[(2, 1)]));
}
#[test]
fn group_and_ungroup_carry_origins() {
let t = MockTree::sample();
let rows = traced("/*/* | .d(\\:name) | :index @| group(d = $.d)", &t);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].prov.origins, origins(&[(2, 2), (3, 2)]));
assert_eq!(rows[1].prov.origins, origins(&[(5, 2), (6, 2)]));
let rows = traced("/*/* | .d(\\:name) | :index @| group(d = $.d) | $.d", &t);
assert_eq!(rows[0].prov.origins, origins(&[(1, 1)]));
let rows = traced(
"/*/* | .d(\\:name) | :index @| group(d = $.d) @| ungroup | $.d",
&t,
);
assert_eq!(rows.len(), 4);
assert_eq!(rows[3].prov.origins, origins(&[(4, 1)]));
}
#[test]
fn a_group_sums_a_record_field() {
let t = MockTree::sample();
let by_value = traced("/*/* | .d(\\:name) | :index @| group(d = $.d) | sum", &t);
let keyed = traced(
"/*/* | .d(\\:name) | %(n = :index) @| group(d = $.d) | sum(--of=:n)",
&t,
);
let read = traced(
"/*/* | .d(\\:name) | %(n = :index) @| group(d = $.d) | :n | sum",
&t,
);
assert_eq!(by_value.len(), 2);
for i in 0..2 {
assert!(by_value[i].topic.is_some());
assert_eq!(keyed[i].topic, by_value[i].topic);
assert_eq!(read[i].topic, by_value[i].topic);
}
}
#[test]
fn piped_operand_reads_its_path() {
let t = MockTree::sample();
let rows = traced("/a/x.rs | .n(->ref @| count) | $.n", &t);
assert_eq!(rows[0].topic, Some(Value::Int(1)));
assert!(
rows[0]
.prov
.origins
.known
.iter()
.any(|o| o.position == NodeId(7).into())
);
}
#[test]
fn value_side_provenance_projection() {
let t = provenanced();
assert_eq!(
vals("/a/x.rs | .n(:index) | $.n:%provenance:text", &t),
vec![Value::Str(
"?api-gateway@2026-04-07T02:00:01Z#request-42".into()
)]
);
assert_eq!(
vals("/a/x.rs | .n(:index) | $.n:%provenance:source", &t),
vec![Value::Str("api-gateway".into())]
);
let q = "/a/x.rs | .n(:index) | ->ref | $.n + :index";
assert_eq!(
vals(&format!("{q} | $_:%provenance:text"), &t),
vec![Value::Str(
"?api-gateway@2026-04-07T02:00:01Z#request-42;crm@2026-05-01T00:00:00Z#row-7".into()
)]
);
assert_eq!(
vals(&format!("{q} | $_:%provenance:source"), &t),
vec![Value::Str("api-gateway".into())]
);
assert_eq!(
vals(&format!("{q} | $_:%provenance:dpid"), &t),
vec![Value::Str("request-42".into())]
);
let (s7, n7, o7) = crate::temporal::parse_iso("2026-05-01T00:00:00Z").unwrap();
assert_eq!(
vals(&format!("{q} | $_:%provenance:instant"), &t),
vec![Value::Instant {
secs: s7,
nanos: n7,
offset_min: o7,
calendar: Default::default()
}]
);
let (s2, n2, o2) = crate::temporal::parse_iso("2026-04-07T02:00:01Z").unwrap();
let rec = |src: &str, path: &str, inst: (i64, u32, Option<i16>), dpid: &str| {
Value::record(vec![
("source".into(), Value::Str(src.into())),
("path".into(), Value::Str(path.into())),
(
"instant".into(),
Value::Instant {
secs: inst.0,
nanos: inst.1,
offset_min: inst.2,
calendar: Default::default(),
},
),
("instant-from".into(), Value::Null),
("dpid".into(), Value::Str(dpid.into())),
])
};
assert_eq!(
vals(&format!("{q} | $_:@provenance"), &t),
vec![Value::list(vec![
rec("api-gateway", "/a/x.rs", (s2, n2, o2), "request-42"),
rec("crm", "/b/deep/w.rs", (s7, n7, o7), "row-7"),
])]
);
let Value::Record(whole) = &vals(&format!("{q} | $_:%provenance"), &t)[0] else {
panic!("a record")
};
let Value::Record(core) = rec("api-gateway", "/a/x.rs", (s7, n7, o7), "request-42") else {
panic!("a record")
};
assert_eq!(&whole[..5], &core[..]);
assert_eq!(whole[5], ("elided".to_string(), Value::Int(0)));
assert!(
matches!(&whole[6], (k, Value::Str(t)) if k == "text" && t.starts_with("?api-gateway@"))
);
assert_eq!(
vals(&format!("{q} | $_:%provenance:path"), &t),
vec![Value::Str("/a/x.rs".into())]
);
assert_eq!(
vals("/a/y.txt:path", &t),
vec![Value::Str("/a/y.txt".into())]
);
assert_eq!(vals("/a/y.txt:instant", &t), vec![Value::Null]);
let coord = |id: i64, command: i64| {
Value::record(vec![
("id".into(), Value::Int(id)),
("command".into(), Value::Int(command)),
])
};
assert_eq!(
vals(&format!("{q} | $_:%origin:id"), &t),
vec![Value::Int(2)]
);
assert_eq!(vals(&format!("{q} | $_:%origin"), &t), vec![coord(2, 1)]);
assert_eq!(
vals(&format!("{q} | $_:@origin"), &t),
vec![Value::list(vec![coord(2, 1), coord(7, 3)])]
);
assert_eq!(
vals(&format!("{q} | $_:%origin:id | . | ((!7)):name"), &t),
vec![Value::Str("w.rs".into())]
);
assert_eq!(
vals(&format!("{q} | $_:%provenance:elided"), &t),
vec![Value::Int(0)]
);
assert_eq!(
vals(
"/a/x.rs | .n(:index) | ->ref | .r%(a = $.n; b = :name) | $.r:b:%provenance:source",
&t
),
vec![Value::Str("crm".into())]
);
let vs = vals(
"/a/x.rs | .n(:index) | %(v = $.n; from = $.n:%provenance:text)",
&t,
);
let Value::Record(fields) = &vs[0] else {
panic!("expected a record")
};
assert_eq!(
fields[1],
(
"from".to_string(),
Value::Str("?api-gateway@2026-04-07T02:00:01Z#request-42".into())
)
);
assert_eq!(vals("/a/x.rs:@provenance | count", &t), vec![Value::Int(1)]);
assert_eq!(vals("/a/x.rs:origin", &t), vec![Value::Int(2)]);
assert_eq!(vals("/a/x.rs:%origin", &t), vec![coord(2, 0)]);
assert_eq!(vals("/a/x.rs:elided", &t), vec![Value::Int(0)]);
assert_eq!(
vals("/a/y.txt | .n(:index) | $.n:%provenance:text", &t),
vec![Value::Null]
);
}
#[test]
fn value_meta_needs_a_value_base() {
assert!(parse(&lex("/a/x.rs | 1:::source").unwrap()).is_err());
assert!(parse(&lex("/a | $.n:%provenance:text").unwrap()).is_ok());
}
fn refused(q: &str, t: &MockTree) -> Option<String> {
match crate::run(q, t) {
Ok(_) => take_refusal(),
Err(e) => Some(e.to_string()),
}
}
#[test]
fn attached_read_is_by_node() {
let t = MockTree::sample();
assert_eq!(
vals("//*<file> | .:v(:index) | ->ref:.v", &t),
vec![Value::Int(1), Value::Null, Value::Null, Value::Null]
);
assert_eq!(
vals("//*<file> | .:v(:index) | <-ref:.v", &t),
vec![Value::Null, Value::Null, Value::Null, Value::Int(1)]
);
assert_eq!(
vals("//*<file> | .v(:index) | ->ref:.v", &t),
vec![Value::Null, Value::Null, Value::Null, Value::Null]
);
assert_eq!(
vals("//*<file> | .:v(:index) | $.v", &t),
vec![Value::Int(1), Value::Int(2), Value::Int(1), Value::Int(1)]
);
assert_eq!(
vals("/a/x.rs | :index | .:v | :.v", &t),
vec![Value::Int(1)]
);
assert_eq!(
vals("/a/x.rs | .:v(1) | .:w(\"k\") | :%.", &t),
vec![Value::record(vec![
("v".to_string(), Value::Int(1)),
("w".to_string(), Value::Str("k".into())),
])]
);
assert_eq!(
vals("/a/x.rs | .:v(1) | ->ref:%.", &t),
vec![Value::record(Vec::new())]
);
}
#[test]
fn attached_read_survives_filters_and_walks() {
let t = MockTree::sample();
assert_eq!(
vals(
"//*<file> | .:v(:index * 10) | [:name = \"x.rs\"] | ->ref:.v",
&t
),
vec![Value::Int(10)]
);
assert_eq!(
vals(
"//*<file> | .:v(:index * 10) | [:name = \"x.rs\"] | ->ref | :.v",
&t
),
vec![Value::Int(10)]
);
assert_eq!(
vals(
"//*<file> | .:v(:index * 10) | [:name = \"x.rs\"] | ->ref | %(n = :name; v = :.v)",
&t
),
vec![Value::record(vec![
("n".to_string(), Value::Str("w.rs".into())),
("v".to_string(), Value::Int(10)),
])]
);
assert_eq!(run("//*<file> | .:v(:index) | ->ref[:.v = 1]", &t), vec![7]);
assert_eq!(
run(
"//*<file> | .:v(:index) | [:name = \"x.rs\"] | .M | ->ref[:.v = $$.M:.v]",
&t
),
vec![7]
);
assert_eq!(
run(
"//*<file> | .:v(:index * :index) | [:name = \"x.rs\"] | .M | ->ref[:.v > $$.M:.v]",
&t
),
Vec::<u64>::new()
);
assert_eq!(
vals("/a/* | .:v(:index) | \\ @| collapse | /x.rs | :.v", &t),
vec![Value::Int(1)]
);
let table = vals("/a/* | .:v(:index) | .:w(\"k\") @| nodes", &t);
let Value::Table(table) = &table[0] else {
panic!("expected a table, got {table:?}");
};
assert_eq!(
table.header.as_ref().unwrap(),
&["node", "name", "kind", "v", "w"]
);
assert_eq!(table.rows[0][3], Value::Int(1));
assert_eq!(table.rows[1][3], Value::Int(2));
assert_eq!(table.rows[1][4], Value::Str("k".into()));
}
#[test]
fn attached_read_sees_the_input_snapshot() {
let t = MockTree::sample();
let pair = "//*<file>[:name = \"x.rs\" || :name = \"w.rs\"]";
let vs = vals(
&format!(
"{pair} | .:v((:name = \"x.rs\" ? 5 : 9)) | .:v((:name = \"x.rs\" ? ->ref:.v : <-ref:.v)) | $.v"
),
&t,
);
assert_eq!(vs, vec![Value::Int(9), Value::Int(5)]);
assert_eq!(
vals(
&format!(
"{pair} | .:v((:name = \"x.rs\" ? 5 : 9)) | .:a(->ref:.v) | .b(<-ref:.a) | $.b"
),
&t
),
vec![Value::Null, Value::Int(9)]
);
assert_eq!(
vals(
&format!(
"{pair} | .:v((:name = \"x.rs\" ? 5 : 9)) | .a(->ref:.v) | .b(<-ref:.a) | $.b"
),
&t
),
vec![Value::Null, Value::Null]
);
assert_eq!(
vals("/a/x.rs | .:v(1) | .:v(2) | :.v", &t),
vec![Value::Int(2)]
);
assert_eq!(
vals("/a/x.rs | .:v(1) | .:v(2) | @. | count", &t),
vec![Value::Int(2)]
);
}
#[test]
fn attaching_threads_at_one_node_must_agree() {
let t = MockTree::sample();
assert_eq!(
vals("/a/* | .v(:index) | \\ | .:w(2) | :.w", &t),
vec![Value::Int(2), Value::Int(2)]
);
let why = refused("/a/* | .v(:index) | \\ | .:w($.v) | :.w", &t).unwrap();
assert!(why.contains("differing values under 'w'"), "{why}");
assert!(why.contains("@| collapse"), "{why}");
assert_eq!(
vals(
"/a/* | .v(:index) | \\ @| collapse | .:w(($.v | count)) | :.w",
&t
),
vec![Value::Int(2)]
);
let why = refused("/a/* | :index @| sum | .:w(1)", &t).unwrap();
assert!(why.contains("stands nowhere"), "{why}");
}
#[test]
fn attached_regula_carries_its_provenance() {
let t = MockTree::sample();
let pair = "//*<file>[:name = \"x.rs\" || :name = \"w.rs\"]";
let rows = traced(&format!("{pair} | .:v(:index) | ->ref:.v"), &t);
assert_eq!(rows[0].node, NodeId(2));
assert_eq!(rows[0].prov.origins, origins(&[(7, 1)]));
let t2 = provenanced();
assert_eq!(
vals(
&format!("{pair} | .:v(:index) | ->ref:.v:%provenance:source"),
&t2
)[0],
Value::Str("crm".into())
);
}
#[test]
fn attached_read_is_null_without_attachments() {
let t = MockTree::sample();
assert_eq!(run("/a/x.rs[->ref:.v = 1]", &t), Vec::<u64>::new());
assert_eq!(run("/a/x.rs[!->ref:.v]", &t), vec![2]);
assert_eq!(run("/a/x.rs[!:.v]", &t), vec![2]);
}
#[test]
fn attached_read_needs_a_node_side_or_none() {
assert!(parse(&lex("/a | $.v:.r").unwrap()).is_err());
assert!(parse(&lex("/a | ->ref::x:.r").unwrap()).is_err());
assert!(parse(&lex("/a | :.r").unwrap()).is_ok());
assert!(parse(&lex("/a | :%.").unwrap()).is_ok());
assert!(parse(&lex("/a[:.r = 1]").unwrap()).is_ok());
assert!(parse(&lex("/a | (::x ? 1 : .5)").unwrap()).is_ok());
for q in [
"/a | ->ref:.",
"/a | ->ref:.1",
"/a | ->ref:..",
"/a | ->ref:@.",
"/a | ->ref:@@.",
"/a | ->ref:%%.",
"/a | ->ref:%.*",
"/a | ->ref:(*.)",
] {
let err = parse(&lex(q).unwrap()).unwrap_err().to_string();
assert!(err.contains("retired"), "{q}: {err}");
}
assert!(parse(&lex("/a | .:(1)").unwrap()).is_err());
assert!(lex("/a | .:v%(a = 1)").and_then(|t| parse(&t)).is_err());
assert!(parse(&lex("/a | .:v(1){3}").unwrap()).is_ok());
assert!(parse(&lex("/a | .:v").unwrap()).is_ok());
}
#[test]
fn edge_ends_are_node_operands() {
let t = MockTree::sample();
assert_eq!(
vals(
"/a/x.rs | ->ref | %(s = $-:source:name; t = $-:target:name)",
&t
),
vec![Value::record(vec![
("s".to_string(), Value::Str("x.rs".into())),
("t".to_string(), Value::Str("w.rs".into())),
])]
);
assert_eq!(
vals(
"//*<file> | .:l(:name) | [:name = \"x.rs\"] | ->ref | %(s = $-:source:.l; t = $-:target:.l)",
&t
),
vec![Value::record(vec![
("s".to_string(), Value::Str("x.rs".into())),
("t".to_string(), Value::Str("w.rs".into())),
])]
);
assert_eq!(
vals("//*<file>[:name = \"w.rs\"] | --ref | $-:source:name", &t),
vec![Value::Str("x.rs".into())]
);
let pair = "//*<file>[:name = \"x.rs\" || :name = \"w.rs\"]";
assert_eq!(
vals(
&format!("{pair} | .:l(:name) @| edges(->ref) | :target:.l"),
&t
),
vec![Value::Str("w.rs".into())]
);
assert_eq!(
vals(
&format!("{pair} | .:l(:name) @| edges(->ref) | [:source:.l = :target:.l] | dim"),
&t
),
vec![Value::list(vec![Value::Int(0), Value::Int(4)])]
);
assert_eq!(
vals(
&format!("{pair} | .:l(\"same\") @| edges(->ref) | [:source:.l = :target:.l] | dim"),
&t
),
vec![Value::list(vec![Value::Int(1), Value::Int(4)])]
);
assert!(parse(&lex("/a | $-::x:.r").unwrap()).is_err());
assert!(parse(&lex("/a | $-:round:.r").unwrap()).is_err());
assert_eq!(
vals("/a/x.rs | ->ref | %(n = :name) | :n:.l", &t),
vec![Value::Null]
);
}
#[test]
fn graph_pagerank_is_row_aligned_over_the_induced_subgraph() {
let t = MockTree::roads();
let six = vals("//a(->e)* @| graph:pagerank(->e)", &t);
assert_eq!(six.len(), 6);
assert_eq!(six[1], six[5]);
assert_eq!(six[3], six[4]);
let ranks = vals("//a(->e)* @| collapse @| graph:pagerank(->e)", &t);
assert_eq!(
ranks,
vec![
six[0].clone(),
six[1].clone(),
six[2].clone(),
six[3].clone()
]
);
let sum: f64 = ranks
.iter()
.map(|v| match v {
Value::Float(f) => *f,
other => panic!("{other:?}"),
})
.sum();
assert!((sum - 1.0).abs() < 1e-12, "{sum}");
assert_eq!(
vals(
"//a(->e)* @| collapse @| graph:pagerank(->e) | %(n = :name; r = $_) | :n",
&t
),
vec![
Value::Str("a".into()),
Value::Str("x.rs".into()),
Value::Str("y.txt".into()),
Value::Str("deep".into())
]
);
assert_eq!(vals("//a(->e)* @| collapse @| graph:pagerank", &t), ranks);
assert_eq!(
vals("/b/deep @| graph:pagerank(->e)", &t),
vec![Value::Float(1.0)]
);
assert_eq!(
vals("/a/* | .v(:index) | \\ @| graph:pagerank(->e)", &t),
vec![Value::Float(1.0), Value::Float(1.0)]
);
let by_unit = vals(
"//a(->e)* @| collapse @| graph:pagerank(->e; --weight=km)",
&t,
);
let by_prop = vals(
"//a(->e)* @| collapse @| graph:pagerank(->e; --weight=::len)",
&t,
);
for (a, b) in by_unit.iter().zip(&by_prop) {
let (Value::Float(a), Value::Float(b)) = (a, b) else {
panic!("{a:?} {b:?}");
};
assert!((a - b).abs() < 1e-12);
}
assert_ne!(by_unit, ranks);
assert_eq!(
vals(
"//a(->e)* @| collapse @| graph:pagerank(->e) | .:pr | [:name = \"deep\"] | :.pr",
&t
),
vec![ranks[3].clone()]
);
assert_eq!(
vals("//a(->e)* @| collapse | :index @| graph:pagerank(->e)", &t),
ranks
);
let why = refused("//a(->e)* | :index @| sum @| graph:pagerank(->e)", &t).unwrap();
assert!(why.contains("stands nowhere"), "{why}");
let table = vals("/a/* | %(n = :name) | .:% @| nodes", &t);
let Value::Table(table) = &table[0] else {
panic!("{table:?}");
};
assert_eq!(table.rows.len(), 2);
assert_eq!(table.header.as_ref().unwrap().last().unwrap(), "n");
let why = refused("//a(->e)* | graph:pagerank(->e)", &t).unwrap();
assert!(why.contains("aggregation pipe"), "{why}");
let why = refused("//a(->e)* @| graph:pagerank((->e|->f))", &t).unwrap();
assert!(why.contains("one hop"), "{why}");
let why = refused("//a(->e)* @| graph:pagerank(->e; --teleport=/a)", &t).unwrap();
assert!(why.contains("reserved"), "{why}");
}
#[test]
fn graph_distance_counts_hops_or_sums_weights_from_the_seeds() {
let t = MockTree::roads();
assert_eq!(
vals(
"//a(->e)* @| collapse @| graph:distance(->e; --from=/a)",
&t
),
vec![Value::Int(0), Value::Int(1), Value::Int(1), Value::Int(2)]
);
assert_eq!(
vals(
"/a .S (->e)* @| collapse @| graph:distance(->e; --from=$$.S)",
&t
),
vec![Value::Int(0), Value::Int(1), Value::Int(1), Value::Int(2)]
);
assert_eq!(
vals("/a/* @| graph:distance(->e; --from=/a)", &t),
vec![Value::Null, Value::Null]
);
assert_eq!(
vals(
"//a(->e)* @| collapse @| graph:distance(->e; --from=/a/x.rs)",
&t
),
vec![Value::Null, Value::Int(0), Value::Null, Value::Int(1)]
);
assert_eq!(
vals(
"//a(->e)* @| collapse @| graph:distance(->e; --from=/a/*)",
&t
),
vec![Value::Null, Value::Int(0), Value::Int(0), Value::Int(1)]
);
let km = vals(
"//a(->e)* @| graph:distance(->e; --from=/a --weight=km)",
&t,
);
assert_eq!(
km[0],
Value::Quantity {
value: 0.0,
base: "m".into(),
written: Some((0.0, "km".into()))
}
);
assert!(
matches!(&km[3], Value::Quantity { value, .. } if (*value - 3000.0).abs() < 1e-9),
"{km:?}"
);
let w = MockTree::weighted();
assert_eq!(
vals(
"//a(->e)* @| graph:distance(->e; --from=/a --weight=::qty)",
&w
),
vec![Value::Int(0), Value::Int(4), Value::Int(1), Value::Int(16)]
);
let why = refused("//a(->e)* @| graph:distance(->e)", &t).unwrap();
assert!(why.contains("--from"), "{why}");
}
#[test]
fn graph_kcore_peels_undirected_distinct_neighbours() {
let t = MockTree::roads();
assert_eq!(
vals("//a(->e)* @| collapse @| graph:kcore(->e)", &t),
vec![Value::Int(2); 4]
);
assert_eq!(
vals("//a(->e)* @| graph:kcore(--e)", &t),
vals("//a(->e)* @| graph:kcore(->e)", &t)
);
let s = MockTree::sample();
assert_eq!(
vals("//*<file> @| graph:kcore(->ref)", &s),
vec![Value::Int(1), Value::Int(0), Value::Int(0), Value::Int(1)]
);
let why = refused("//a(->e)* @| graph:kcore(->e; --weight=km)", &t).unwrap();
assert!(why.contains("weight"), "{why}");
}
#[test]
fn graph_betweenness_and_conncomp() {
let t = MockTree::roads();
let floats = |q: &str| -> Vec<f64> {
vals(q, &t)
.iter()
.map(|v| match v {
Value::Float(f) => *f,
other => panic!("{other:?}"),
})
.collect()
};
let cb = floats("//a(->e)* @| collapse @| graph:betweenness(->e)");
assert_eq!(cb, [0.0, 0.5, 0.5, 1.0]);
let und = floats("//a(->e)* @| collapse @| graph:betweenness(--e; --normalized)");
assert!(und.iter().all(|&c| (c - 1.0 / 6.0).abs() < 1e-9), "{und:?}");
let w = floats("//a(->e)* @| collapse @| graph:betweenness(->e; --weight=km)");
assert!(w[2] > w[1], "{w:?}");
assert_eq!(
vals("//a(->e)* @| collapse @| graph:conncomp(->e)", &t),
vec![Value::Int(1), Value::Int(2), Value::Int(3), Value::Int(2)]
);
assert_eq!(
vals("//a(->e)* @| collapse @| graph:conncomp(--e)", &t),
vec![Value::Int(1); 4]
);
let s = MockTree::sample();
assert_eq!(
vals("//*<file> @| graph:conncomp(--ref)", &s),
vec![Value::Int(1), Value::Int(2), Value::Int(3), Value::Int(1)]
);
let why = refused("//a(->e)* @| graph:conncomp(->e; --weight=km)", &t).unwrap();
assert!(why.contains("weight"), "{why}");
}
#[test]
fn a_mark_re_seat_keeps_every_thread() {
let t = MockTree::roads();
assert_eq!(
vals("/a .A --e .H --e | $$.H @| count", &t),
vec![Value::Int(5)]
);
assert_eq!(
vals("/a .A --e .H --e | $$.H:name @| frequencies | :n", &t),
vec![Value::Int(3), Value::Int(2)]
);
assert_eq!(
vals("/a .A --e .H --e | $$.H @| collapse @| count", &t),
vec![Value::Int(2)]
);
assert_eq!(vals("/a/* | \\ @| count", &t), vec![Value::Int(1)]);
}
#[test]
fn copy_stands_last_and_is_the_tools() {
let t = MockTree::sample();
assert!(parse(&lex("/a/* | json | copy").unwrap()).is_ok());
assert!(parse(&lex("/a/* @| json | copy").unwrap()).is_ok());
let err = parse(&lex("/a/* | copy | first").unwrap())
.unwrap_err()
.to_string();
assert!(err.contains("stands last"), "{err}");
let err = parse(&lex("/a/* | copy(json)").unwrap())
.unwrap_err()
.to_string();
assert!(err.contains("no operand"), "{err}");
let why = refused("/a/* | copy", &t).unwrap();
assert!(why.contains("the tool's"), "{why}");
assert_eq!(
crate::take_copy("//page @| json | copy"),
Some(("//page @| json".to_string(), "JSON"))
);
assert_eq!(
crate::take_copy("/a/* | %(n = :name) | copy "),
Some(("/a/* | %(n = :name)".to_string(), "text"))
);
assert_eq!(
crate::take_copy("/a/* @| csv(a; b) | copy"),
Some(("/a/* @| csv(a; b)".to_string(), "CSV"))
);
assert_eq!(crate::take_copy("/a/* | copy | first"), None);
assert_eq!(
crate::take_copy("/a/* @| json @| copy"),
Some(("/a/* @| json".to_string(), "JSON"))
);
assert!(parse(&lex("/a/* @| copy").unwrap()).is_ok());
let err = parse(&lex("/a/* @| copy | first").unwrap())
.unwrap_err()
.to_string();
assert!(err.contains("stands last"), "{err}");
let why = refused("/a/* @| copy", &t).unwrap();
assert!(why.contains("the tool's"), "{why}");
}
#[test]
fn the_attaching_fields_push_attaches_every_field() {
let t = MockTree::sample();
assert_eq!(
vals("/a/x.rs | %(k = 1; v = \"a\") | .:% | :%.", &t),
vec![Value::record(vec![
("k".to_string(), Value::Int(1)),
("v".to_string(), Value::Str("a".into())),
])]
);
assert_eq!(
vals("/a/x.rs | %(k = 1; v = \"a\") | .:% | $.k", &t),
vec![Value::Int(1)]
);
assert_eq!(
vals(
"//*<file> | %(n = :name) | .:% | [:n = \"x.rs\"] | ->ref:.n",
&t
),
vec![Value::Str("w.rs".into())]
);
assert!(parse(&lex("/a | .:%{2}").unwrap()).is_ok());
assert!(parse(&lex("/a | .:%(a = 1)").unwrap()).is_err());
}
#[test]
fn quantified_attaching_push_floods_to_a_fixpoint() {
let t = MockTree::lattice(2);
assert_eq!(
vals(
"/* | .:l(:index) | .:l((@(:.l; (--*:.l)) | min)){1;} | $.l",
&t
),
vec![Value::Int(1); 4]
);
assert_eq!(
vals(
"/* | .:l(:index) | .:l((@(:.l; (--*:.l)) | min)){1;} | @. | count",
&t
),
vec![Value::Int(4); 4]
);
assert_eq!(
vals(
"/* | .:l((:index = 1 ? 1 : (:index = 4 ? 4 : null))) | .:l($.l ?? (--*:.l @| min)){1;} | $.l",
&t
),
vec![Value::Int(1), Value::Int(1), Value::Int(1), Value::Int(4)]
);
}
#[test]
fn node_id_anchor() {
let t = MockTree::sample();
assert_eq!(run("((!7))", &t), vec![7]);
assert_eq!(vals("((!7)):name", &t), vec![Value::Str("w.rs".into())]);
assert_eq!(
vals("/a | ((!7))\\:name", &t),
vec![Value::Str("deep".into())]
);
assert_eq!(run("((!99))", &t), Vec::<u64>::new());
assert_eq!(vals("/a | ((!99)):name", &t), Vec::<Value>::new());
assert_eq!(vals("/b/deep/w.rs:id", &t), vec![Value::Int(7)]);
assert_eq!(
vals("/a/x.rs | .v(:index) | ->ref | $.v:%origin:id", &t),
vec![Value::Int(2)]
);
assert_eq!(
vals(
"/a/x.rs | .v(:index) | ->ref | .o($.v:%origin:id) | ((!$.o)):name",
&t
),
vec![Value::Str("x.rs".into())]
);
assert_eq!(
vals("/a/x.rs | .v(:index) | ->ref | ((!$.v:%origin)):name", &t),
vec![Value::Str("x.rs".into())]
);
assert_eq!(
vals("/a/* | .v(:index) | ((!$.v + 1)):name", &t),
vec![Value::Str("x.rs".into()), Value::Str("y.txt".into())]
);
assert_eq!(
vals("/a | ((!@(2; 3; 99))):name", &t),
vec![Value::Str("x.rs".into()), Value::Str("y.txt".into())]
);
let rows = traced(
"/a/x.rs | .v(:index) | ->ref | ((!$.v:%origin:id)) | :name",
&t,
);
assert_eq!(rows[0].node, NodeId(2));
assert_eq!(rows[0].prov.origins, origins(&[(2, 4)]));
}
#[test]
fn quantified_push_iterates() {
let t = MockTree::sample();
assert_eq!(
vals("/a | .v(1) | .v($.v * 2){3} | $.v", &t),
vec![Value::Int(8)]
);
assert_eq!(
vals("/a | .v(1) | .v($.v * 2){3} | @. | count", &t),
vec![Value::Int(4)]
);
let pair = "//*<file>[:name = \"x.rs\" || :name = \"w.rs\"]";
assert_eq!(
vals(
&format!(
"{pair} | .:v(:index * 10) | .:v(($.v + (->ref:.v | default(0)) + (<-ref:.v | default(0)))){{2}} | $.v"
),
&t
),
vec![Value::Int(40), Value::Int(40)]
);
assert_eq!(
vals(
"/a | .v(5) | .v(($.v > 8 ? $.v : $.v + 1)){1;} | @. | count",
&t
),
vec![Value::Int(6)]
);
assert_eq!(
vals("/a | .v(5) | .v($.v){3;} | @. | count", &t),
vec![Value::Int(5)]
);
assert_eq!(
vals("/a | .v(5) | .v($.v + 1){2;3} | $.v", &t),
vec![Value::Int(8)]
);
assert!(parse(&lex("/a | upper{3}").unwrap()).is_err());
assert!(parse(&lex("/a | .v(1){3;2}").unwrap()).is_err());
}
fn table_of(q: &str, t: &MockTree) -> (Vec<String>, Vec<Vec<String>>) {
let vs = vals(q, t);
assert_eq!(vs.len(), 1, "{q}");
let Value::Table(tb) = &vs[0] else {
panic!("{q}: not a table: {}", vs[0].display_form())
};
(
tb.header.clone().unwrap(),
tb.rows
.iter()
.map(|r| r.iter().map(Value::display_form).collect())
.collect(),
)
}
fn list_strs(v: &Value) -> Vec<String> {
match v {
Value::List(items) => items.iter().map(|i| i.to_string()).collect(),
other => panic!("a list, not {other}"),
}
}
#[test]
fn artpoints_and_bridges_read_the_hop_undirected() {
let t = MockTree::roads();
let cut = vals("//a(->e)* @| collapse @| graph:artpoints(->e)", &t);
assert_eq!(cut, vec![Value::Bool(false); 4]);
assert_eq!(table_of("//a(->e)* @| graph:bridges(->e)", &t).1.len(), 0);
let cut = vals(
"//a(->e)*[:name != \"deep\"] @| collapse @| graph:artpoints(->e)",
&t,
);
assert_eq!(
cut,
vec![Value::Bool(true), Value::Bool(false), Value::Bool(false)]
);
let (h, rows) = table_of("//a(->e)*[:name != \"deep\"] @| graph:bridges(->e)", &t);
assert_eq!(h, ["source", "target", "label", "directed", "len", "time"]);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0][..2], ["/a", "/a/x.rs"]);
assert_eq!(rows[1][..2], ["/a", "/a/y.txt"]);
assert_eq!(
table_of("//a(->e)*[:name != \"deep\"] @| graph:bridges(<-e)", &t)
.1
.len(),
2
);
assert!(vals("//a(->e)* @| graph:artpoints(->e; --weight=km)", &t).is_empty());
}
#[test]
fn routes_are_lists_of_nodes_seed_first() {
let t = MockTree::roads();
let routes = vals("//a(->e)* @| collapse @| graph:route(->e; --from=/a)", &t);
assert_eq!(list_strs(&routes[0]), ["/a"]);
assert_eq!(list_strs(&routes[1]), ["/a", "/a/x.rs"]);
assert_eq!(list_strs(&routes[2]), ["/a", "/a/y.txt"]);
assert_eq!(list_strs(&routes[3]), ["/a", "/a/x.rs", "/b/deep"]);
let routes = vals(
"//a(->e)* @| collapse @| graph:route(->e; --from=/a --weight=km)",
&t,
);
assert_eq!(list_strs(&routes[3]), ["/a", "/a/y.txt", "/b/deep"]);
let routes = vals(
"//a(->e)* @| collapse @| graph:route(->e; --from=/b/deep)",
&t,
);
assert_eq!(routes[0], Value::Null);
assert_eq!(list_strs(&routes[1]), ["/b/deep", "/a/x.rs"]);
assert_eq!(list_strs(&routes[3]), ["/b/deep"]);
assert_eq!(
vals("//a(->e)* @| graph:route(->e; --from=/a)", &t).len(),
6
);
assert!(vals("//a(->e)* @| graph:route(->e)", &t).is_empty());
}
#[test]
fn edge_betweenness_is_the_edges_table_with_a_column() {
let t = MockTree::roads();
let (h, rows) = table_of("//a(->e)* @| graph:betweenness(->e; --edges)", &t);
assert_eq!(
h,
[
"source",
"target",
"label",
"directed",
"len",
"time",
"betweenness"
]
);
let got: Vec<f64> = rows.iter().map(|r| r[6].parse().unwrap()).collect();
assert_eq!(got, [1.5, 1.5, 1.5, 2.5, 2.0]);
let (_, rows) = table_of(
"//a(->e)* @| graph:betweenness(->e; --edges --normalized)",
&t,
);
let got: Vec<f64> = rows.iter().map(|r| r[6].parse().unwrap()).collect();
assert!((got[3] - 2.5 / 12.0).abs() < 1e-12, "{got:?}");
}
#[test]
fn a_library_command_is_refused_under_the_graph_bound_before_computing() {
let t = MockTree::roads();
let tight = crate::Run {
graph_bound: Some(3),
..Default::default()
};
let run = |q: &str| {
crate::Query::parse(q, &t)
.unwrap()
.run(&t, &tight)
.map(|o| o.result)
};
let err = run("//a(->e)* @| graph:kcore(->e)").err().expect("refused");
let text = format!("{err}");
assert!(
text.contains("graph bound") && text.contains("4 nodes and 5 edges"),
"{text}"
);
assert!(run("//a(->e)* @| count").is_ok());
let err = format!(
"{}",
run("//a(->e)* @| graph:betweenness(->e)").err().unwrap()
);
assert!(err.contains("20 cost units (nodes times edges)"), "{err}");
}
struct Declaring {
inner: MockTree,
kept: std::cell::RefCell<HashMap<(String, String, String), Vec<Value>>>,
asked: std::cell::Cell<usize>,
}
impl AstAdapter for Declaring {
fn root(&self) -> NodeId {
self.inner.root()
}
fn children(&self, n: NodeId) -> Vec<NodeId> {
self.inner.children(n)
}
fn name(&self, n: NodeId) -> Option<String> {
self.inner.name(n)
}
fn parent(&self, n: NodeId) -> Option<NodeId> {
self.inner.parent(n)
}
fn non_tree_nodes(&self) -> Vec<NodeId> {
self.inner.non_tree_nodes()
}
fn links(&self, n: NodeId) -> Vec<(String, NodeId)> {
self.inner.links(n)
}
fn traits(&self, n: NodeId) -> Vec<String> {
self.inner.traits(n)
}
fn backlinks(&self, n: NodeId) -> Vec<(String, NodeId)> {
self.inner.backlinks(n)
}
fn link_property_names(&self, s: NodeId, l: &str, t: NodeId) -> Vec<String> {
self.inner.link_property_names(s, l, t)
}
fn link_property(&self, s: NodeId, l: &str, t: NodeId, p: &str) -> Option<Value> {
self.inner.link_property(s, l, t, p)
}
fn graph_declarations(&self) -> Vec<crate::GraphDeclaration> {
vec![crate::GraphDeclaration {
scope: "//a(->e)*".to_string(),
hop: "->e".to_string(),
measures: vec!["kcore".to_string(), "pagerank(--damping=0.5)".to_string()],
}]
}
fn graph_measure(
&self,
scope: &str,
hop: &str,
measure: &str,
nodes: &[NodeId],
) -> Option<Vec<Value>> {
self.asked.set(self.asked.get() + 1);
let kept = self.kept.borrow();
let v = kept.get(&(scope.to_string(), hop.to_string(), measure.to_string()))?;
(v.len() == nodes.len()).then(|| v.clone())
}
fn store_graph_measure(
&self,
scope: &str,
hop: &str,
measure: &str,
_: &[NodeId],
values: &[Value],
) {
self.kept.borrow_mut().insert(
(scope.to_string(), hop.to_string(), measure.to_string()),
values.to_vec(),
);
}
}
#[test]
fn a_declared_graph_answers_once_the_context_is_the_scope() {
let d = Declaring {
inner: MockTree::roads(),
kept: Default::default(),
asked: Default::default(),
};
let run = |q: &str| match crate::Query::parse(q, &d)
.unwrap()
.run(&d, &Default::default())
.unwrap()
.result
{
QueryResult::Values(vs) => vs,
_ => panic!("values"),
};
let first = run("//a(->e)* @| collapse @| graph:kcore(->e)");
assert_eq!(d.asked.get(), 1);
assert_eq!(d.kept.borrow().len(), 1);
let second = run("//a(->e)* @| collapse @| graph:kcore(->e)");
assert_eq!(first, second);
assert_eq!(d.asked.get(), 2);
assert_eq!(d.kept.borrow().len(), 1);
let narrow = run("//a(->e)*[:name != \"deep\"] @| collapse @| graph:kcore(->e)");
assert_eq!(narrow.len(), 3);
assert_eq!(d.asked.get(), 2);
assert_eq!(d.kept.borrow().len(), 1);
run("//a(->e)* @| collapse @| graph:pagerank(->e; --damping=0.5)");
assert_eq!(d.kept.borrow().len(), 2);
run("//a(->e)* @| collapse @| graph:pagerank(->e)");
assert_eq!(d.kept.borrow().len(), 2);
}
#[test]
fn a_hop_operand_takes_the_walks_predicate() {
let t = MockTree::roads();
let (_, rows) = table_of("//a(->e)* @| edges(->e[:name = \"deep\"])", &t);
assert_eq!(rows.len(), 2, "{rows:?}");
assert!(rows.iter().all(|r| r[1] == "/b/deep"));
let (_, rows) = table_of("//a(->e)* @| edges(->e[$-::len > 2_km])", &t);
assert_eq!(rows.len(), 3, "{rows:?}");
let routes = vals(
"//a(->e)* @| collapse @| graph:route(->e[$-::len < 10_km]; --from=/a)",
&t,
);
assert_eq!(list_strs(&routes[3]), ["/a", "/a/y.txt", "/b/deep"]);
let cut = vals(
"//a(->e)* @| collapse @| graph:artpoints(--e[$-::len < 10_km])",
&t,
);
assert_eq!(cut, vec![Value::Bool(false); 4]);
let cut = vals(
"//a(->e)* @| collapse @| graph:artpoints(--e[$-::len < 3_km])",
&t,
);
assert_eq!(
cut,
vec![
Value::Bool(false),
Value::Bool(false),
Value::Bool(true),
Value::Bool(false)
]
);
}
#[test]
fn the_parser_reads_a_property_through_a_reference() {
let q = parse(&lex("/x | :source::code").unwrap()).unwrap();
assert!(format!("{:?}", q.pipeline).contains("Through"));
let t = MockTree::roads();
let (_, rows) = table_of("//a(->e)* @| edges(->e)", &t);
assert_eq!(rows[0][0], "/a");
let names = vals("//a(->e)* @| edges(->e) | :source:name", &t);
assert_eq!(names.len(), 1);
assert_eq!(list_strs(&names[0]), ["a", "a", "x.rs", "y.txt", "deep"]);
let names = vals(
"//a(->e)* @| collapse @| graph:route(->e; --from=/a) | map(:name)",
&t,
);
assert_eq!(list_strs(&names[3]), ["a", "x.rs", "deep"]);
let names = vals("//a(->e)* @| edges(->e) | (:target | :name)", &t);
assert_eq!(
list_strs(&names[0]),
["x.rs", "y.txt", "deep", "deep", "x.rs"]
);
}
#[test]
fn closeness_communities_modularity_and_triangles() {
let t = MockTree::roads();
let c = vals("//a(->e)* @| collapse @| graph:closeness(->e)", &t);
let Value::Float(a) = c[0] else {
panic!("{c:?}")
};
assert!((a - (3.0 / 3.0) * (3.0 / 4.0)).abs() < 1e-12, "{a}");
let Value::Float(deep) = c[3] else {
panic!("{c:?}")
};
assert!((deep - (1.0 / 3.0) * (1.0 / 1.0)).abs() < 1e-12, "{deep}");
let h = vals(
"//a(->e)* @| collapse @| graph:closeness(->e; --harmonic)",
&t,
);
let Value::Float(ah) = h[0] else {
panic!("{h:?}")
};
assert!((ah - (1.0 + 1.0 + 0.5) / 3.0).abs() < 1e-12, "{ah}");
let c = vals(
"//a(->e)* @| collapse @| graph:closeness(->e; --weight=km)",
&t,
);
let Value::Float(a_km) = c[0] else {
panic!("{c:?}")
};
assert!((a_km - 3.0 / 8.0).abs() < 1e-12, "{a_km}");
let cs = vals("//a(->e)* @| collapse @| graph:communities(--e)", &t);
assert_eq!(cs, vec![Value::Int(1); 4]);
let q = vals(
"//a(->e)* @| collapse @| graph:communities(--e) | .:c @| graph:modularity(--e; --by=:.c)",
&t,
);
assert_eq!(q.len(), 1);
let Value::Float(q0) = q[0] else {
panic!("{q:?}")
};
assert!(q0.abs() < 1e-12, "{q0}");
let q = vals(
"//a(->e)* @| collapse | .:p(:name) @| graph:modularity(--e; --by=:.p)",
&t,
);
let Value::Float(q1) = q[0] else {
panic!("{q:?}")
};
assert!(q1 < 0.0, "{q1}");
assert!(vals("//a(->e)* @| graph:modularity(--e)", &t).is_empty());
assert_eq!(
vals("//a(->e)* @| collapse @| graph:triangles(--e)", &t),
vec![Value::Int(0); 4]
);
assert_eq!(
vals(
"//a(->e)* @| collapse @| graph:triangles(--e; --coefficient)",
&t
),
vec![Value::Float(0.0); 4]
);
assert!(vals("//a(->e)* @| graph:triangles(--e; --weight=km)", &t).is_empty());
}
struct Withholding {
inner: MockTree,
dropped_nodes: bool,
}
impl AstAdapter for Withholding {
fn root(&self) -> NodeId {
self.inner.root()
}
fn children(&self, n: NodeId) -> Vec<NodeId> {
self.inner.children(n)
}
fn name(&self, n: NodeId) -> Option<String> {
self.inner.name(n)
}
fn parent(&self, n: NodeId) -> Option<NodeId> {
self.inner.parent(n)
}
fn non_tree_nodes(&self) -> Vec<NodeId> {
self.inner.non_tree_nodes()
}
fn links(&self, n: NodeId) -> Vec<(String, NodeId)> {
self.inner.links(n)
}
fn traits(&self, n: NodeId) -> Vec<String> {
self.inner.traits(n)
}
fn backlinks(&self, n: NodeId) -> Vec<(String, NodeId)> {
self.inner.backlinks(n)
}
fn property(&self, n: NodeId, name: &str) -> Option<Value> {
match (n.0, name) {
(1, "salary") => Some(Value::Int(10)),
(2, "salary") => Some(Value::Int(20)),
(1 | 2 | 3, "grade") => Some(Value::Str("b".into())),
_ => None,
}
}
fn property_names(&self, n: NodeId) -> Vec<String> {
match n.0 {
1 | 2 => vec!["salary".into(), "grade".into()],
3 => vec!["grade".into()],
_ => Vec::new(),
}
}
fn property_withheld(&self, n: NodeId, name: &str) -> bool {
n.0 == 3 && name == "salary"
}
fn nodes_withheld(&self) -> bool {
self.dropped_nodes
}
}
fn run_omitting(
q: &str,
dropped_nodes: bool,
) -> (
Result<Vec<Value>, String>,
Vec<crate::diagnostic::Diagnostic>,
) {
let w = Withholding {
inner: MockTree::roads(),
dropped_nodes,
};
match crate::Query::parse(q, &w)
.unwrap()
.run(&w, &Default::default())
{
Ok(o) => (
Ok(match o.result {
QueryResult::Values(vs) => vs,
QueryResult::Nodes(ns) => ns.into_iter().map(|_| Value::Null).collect(),
}),
o.diagnostics,
),
Err(e) => (Err(e.to_string()), Vec::new()),
}
}
const OMITTED_TEXT: &str = "some data is omitted due to insufficient permissions";
#[test]
fn withheld_data_reads_absent_with_one_warning() {
let (vs, diags) = run_omitting("//a(->e)* @| collapse | ::salary", false);
assert_eq!(
vs.unwrap(),
vec![Value::Int(10), Value::Int(20), Value::Null, Value::Null]
);
let omitted: Vec<_> = diags.iter().filter(|d| d.code == "OMITTED").collect();
assert_eq!(omitted.len(), 1, "{diags:?}");
assert_eq!(omitted[0].message, OMITTED_TEXT);
assert_eq!(omitted[0].site, "source");
assert_eq!(omitted[0].severity, crate::diagnostic::Severity::Warning);
let (_, diags) = run_omitting(
"//a(->e)* @| collapse | %(::salary; ::grade; s = ::salary)",
false,
);
assert_eq!(diags.iter().filter(|d| d.code == "OMITTED").count(), 1);
let (_, diags) = run_omitting("//a(->e)* @| collapse | ::grade", false);
assert_eq!(diags.iter().filter(|d| d.code == "OMITTED").count(), 0);
let (_, diags) = run_omitting("//a(->e)* @| collapse | ::grade", true);
assert_eq!(diags.iter().filter(|d| d.code == "OMITTED").count(), 1);
let (_, diags) = run_omitting("//a(->e)* @| collapse | ::salary", true);
assert_eq!(diags.iter().filter(|d| d.code == "OMITTED").count(), 1);
let (vs, diags) = run_omitting("//a(->e)* @| collapse | ::salary @| count", false);
assert_eq!(vs.unwrap(), vec![Value::Int(4)]);
assert_eq!(diags.iter().filter(|d| d.code == "OMITTED").count(), 1);
}
#[test]
fn keying_on_a_withheld_property_refuses() {
for q in [
"//a(->e)*[::salary > 5]",
"//a(->e)* @| collapse | [::salary > 5]",
"//a(->e)* @| collapse | ::salary @| sum",
"//a(->e)* @| collapse | ::salary @| sort",
"//a(->e)* @| collapse | ::salary @| unique",
"//a(->e)* @| collapse | %(s = ::salary; n = :name) @| top(1; :s)",
] {
let (r, _) = run_omitting(q, false);
let err = r.err().unwrap_or_else(|| panic!("{q} passed"));
assert!(err.contains(OMITTED_TEXT), "{q}: {err}");
}
assert_eq!(
run_omitting("//a(->e)* @| collapse | [::grade = \"b\"] @| count", false)
.0
.unwrap(),
vec![Value::Int(3)]
);
}
#[test]
fn projector_after_a_pipe() {
let t = MockTree::sample();
let s = |q: &str| {
vals(q, &t)
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(" | ")
};
assert_eq!(
s("#(@(a; b; c); %(1; 2; 3); %(4; 5; 6)) | #(:c; :a)"),
s("#(@(c; a); %(3; 1); %(6; 4))")
);
assert_eq!(
s("%(a = 1; b = 2; c = 3) | %(:a; :b)"),
s("%(a = 1; b = 2)")
);
assert_eq!(
s("@(%(a = 1; b = 2; c = 3); %(a = 4; b = 5; c = 6)) | %(:a; :b)"),
s("@(%(a = 1; b = 2); %(a = 4; b = 5))")
);
assert_eq!(
s("%(a = 1; b = 2) | %(x = :a; b = 2)"),
s("%(x = 1; b = 2)")
);
assert!(parse(&lex("@(1) | #(:a; 2)").unwrap()).is_ok());
}
#[test]
fn round_leaves_what_it_cannot_round() {
let t = MockTree::sample();
let s = |q: &str| {
vals(q, &t)
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(" | ")
};
assert_eq!(
s("%(a = \"x\"; b = 1.23456) | round(2)"),
s("%(a = \"x\"; b = 1.23)")
);
assert_eq!(
s("#(@(a; b); %(\"x\"; 1.23456); %(\"y\"; 2.5)) | round(2)"),
s("#(@(a; b); %(\"x\"; 1.23); %(\"y\"; 2.5))")
);
assert_eq!(s("@(\"n/a\"; 2.5; null) | floor"), s("@(\"n/a\"; 2; null)"));
assert_eq!(s("\"n/a\" | round(2)"), s("\"n/a\""));
assert_eq!(s("@(-1; \"x\") | abs"), s("@(1; \"x\")"));
}