use crate::adapter::{AstAdapter, NodeId};
use crate::ast::{
Arg, ArithOp, Axis, Branch, CmpOp, FnCall, Group, InterpSeg, Matcher, Operand, PathElem,
PredExpr, Predicate, Projection, PushBody, Query, Reach, RegRef, Stage, Step,
};
use crate::stdlib;
use crate::value::Value;
use regex::Regex;
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
#[derive(Clone)]
struct Reg {
name: Option<String>,
value: Value,
}
#[derive(Clone, PartialEq)]
struct Mark {
name: String,
node: NodeId,
}
#[derive(Clone)]
struct Capsa {
node: NodeId,
register: Vec<Reg>,
topic: Option<Value>,
members: Vec<Capsa>,
captures: Vec<String>,
marks: Vec<Mark>,
bindings: Vec<Option<NodeId>>,
arrived: Vec<EdgeCtx>,
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub(crate) struct EdgeCtx {
source: NodeId,
label: String,
target: NodeId,
}
const CHILD: &str = "[child]";
const PARENT: &str = "[parent]";
const NEXT: &str = "[next]";
const PREV: &str = "[prev]";
#[derive(Clone, Copy)]
struct Scope<'a> {
register: &'a [Reg],
topic: Option<&'a Value>,
ordinal: Option<usize>,
captures: &'a [String],
marks: &'a [Mark],
bindings: &'a [Option<NodeId>],
edge: Option<&'a EdgeCtx>,
arrived: &'a [EdgeCtx],
peers: Option<&'a [Capsa]>,
outer: Option<&'a Scope<'a>>,
}
const NO_SCOPE: Scope<'static> = Scope {
register: &[],
topic: None,
ordinal: None,
captures: &[],
marks: &[],
bindings: &[],
edge: None,
arrived: &[],
peers: None,
outer: None,
};
#[derive(Debug, Clone, PartialEq)]
pub enum QueryResult {
Nodes(Vec<NodeId>),
Values(Vec<Value>),
}
#[derive(Default)]
struct Correlation {
outer: Vec<bool>,
contexts: Vec<Vec<NodeId>>,
witnesses: std::cell::RefCell<std::collections::HashMap<u64, Vec<Option<NodeId>>>>,
}
type Trace = Correlation;
pub(crate) fn gate_shell(query: &Query, adapter: &impl AstAdapter) -> crate::Result<()> {
if uses_shell_query(query) && !adapter.allow_shell() {
return Err(crate::QuarbError::Unsupported(
"the sh(...) stage runs external commands; pass --allow-shell".into(),
));
}
Ok(())
}
fn uses_shell_query(q: &Query) -> bool {
q.correlations.iter().any(uses_shell_query)
|| q.branches
.iter()
.any(|b| b.steps.iter().any(uses_shell_elem))
|| q.pipeline.iter().any(uses_shell_stage)
}
fn uses_shell_stage(stage: &Stage) -> bool {
let args = |call: &FnCall| {
call.args.iter().any(|a| match a {
Arg::Expr(e) => uses_shell_operand(e),
_ => false,
})
};
match stage {
Stage::Func(call) | Stage::Agg(call) => call.name == "sh" || args(call),
Stage::Subcontext { body, .. } => uses_shell_query(body),
Stage::Map(inner) => uses_shell_stage(inner),
Stage::Filter(e) => uses_shell_pred(e),
Stage::Select(p) => match p {
Predicate::Expr(e) => uses_shell_pred(e),
_ => false,
},
Stage::Expr(o) | Stage::ExprPush { expr: o, .. } => uses_shell_operand(o),
_ => false,
}
}
fn uses_shell_pred(e: &PredExpr) -> bool {
match e {
PredExpr::Or(a, b) | PredExpr::And(a, b) => uses_shell_pred(a) || uses_shell_pred(b),
PredExpr::Not(a) => uses_shell_pred(a),
PredExpr::Compare(l, _, r) => uses_shell_operand(l) || uses_shell_operand(r),
PredExpr::Truthy(o) => uses_shell_operand(o),
}
}
fn uses_shell_elem(e: &PathElem) -> bool {
match e {
PathElem::Mark(_) => false,
PathElem::Step(st) => st.predicates.iter().any(|p| match p {
Predicate::Expr(e) => uses_shell_pred(e),
_ => false,
}),
PathElem::Group(g) => {
g.alts.iter().flatten().any(uses_shell_elem)
|| g.predicates.iter().any(|p| match p {
Predicate::Expr(e) => uses_shell_pred(e),
_ => false,
})
}
PathElem::Push { body, .. } => match body {
PushBody::Query(q) => uses_shell_query(q),
PushBody::Expr(e) => uses_shell_operand(e),
},
}
}
fn uses_shell_operand(o: &Operand) -> bool {
match o {
Operand::Piped { expr, stages } => {
uses_shell_operand(expr) || stages.iter().any(uses_shell_stage)
}
Operand::Neg(i) | Operand::Outer(i) => uses_shell_operand(i),
Operand::Group(e) => uses_shell_pred(e),
Operand::Arith { left, right, .. } => uses_shell_operand(left) || uses_shell_operand(right),
Operand::Cond { cond, then, other } => {
uses_shell_pred(cond) || uses_shell_operand(then) || uses_shell_operand(other)
}
Operand::Match {
scrutinee,
arms,
other,
} => {
uses_shell_operand(scrutinee)
|| arms
.iter()
.any(|(t, _, r)| uses_shell_operand(t) || uses_shell_operand(r))
|| uses_shell_operand(other)
}
Operand::Interp(segs) => segs.iter().any(|seg| match seg {
InterpSeg::Expr(e) => uses_shell_operand(e),
InterpSeg::Text(_) => false,
}),
Operand::Rel { steps, .. } | Operand::Ctx { steps, .. } => {
steps.iter().any(uses_shell_elem)
}
_ => false,
}
}
pub fn eval(query: &Query, adapter: &impl AstAdapter) -> QueryResult {
eval_query(query, adapter, adapter.root(), &Correlation::default())
}
pub fn eval_traced(query: &Query, adapter: &impl AstAdapter) -> Vec<(NodeId, Option<Value>)> {
let (caps, projected) =
eval_query_caps(query, adapter, adapter.root(), &Correlation::default());
caps.into_iter()
.map(|c| {
let topic = if projected {
Some(c.topic.unwrap_or(Value::Null))
} else {
c.topic
};
(c.node, topic)
})
.collect()
}
fn eval_query(
query: &Query,
adapter: &impl AstAdapter,
start: NodeId,
base: &Trace,
) -> QueryResult {
eval_query_outer(query, adapter, start, base, None)
}
fn eval_query_outer(
query: &Query,
adapter: &impl AstAdapter,
start: NodeId,
base: &Trace,
outer: Option<&Scope<'_>>,
) -> QueryResult {
let mut contexts = base.contexts.clone();
let mut outers = base.outer.clone();
for corr in &query.correlations {
let prior = Correlation {
contexts: contexts.clone(),
outer: outers.clone(),
witnesses: Default::default(),
};
let ctx = match eval_query(corr, adapter, start, &prior) {
QueryResult::Nodes(ns) => ns,
QueryResult::Values(_) => Vec::new(),
};
contexts.push(ctx);
outers.push(corr.outer);
}
let trace = Correlation {
contexts,
outer: outers,
witnesses: Default::default(),
};
let mut caps = union_branches(&query.branches, adapter, start, &trace, outer);
for stage in &query.pipeline {
caps = apply_stage(stage, caps, adapter, &trace, outer);
}
to_result(caps, pipeline_projected(query))
}
fn eval_query_caps(
query: &Query,
adapter: &impl AstAdapter,
start: NodeId,
base: &Trace,
) -> (Vec<Capsa>, bool) {
eval_query_caps_outer(query, adapter, start, base, None)
}
fn eval_query_caps_outer(
query: &Query,
adapter: &impl AstAdapter,
start: NodeId,
base: &Trace,
outer: Option<&Scope<'_>>,
) -> (Vec<Capsa>, bool) {
let mut contexts = base.contexts.clone();
let mut outers = base.outer.clone();
for corr in &query.correlations {
let prior = Correlation {
contexts: contexts.clone(),
outer: outers.clone(),
witnesses: Default::default(),
};
let ctx = match eval_query(corr, adapter, start, &prior) {
QueryResult::Nodes(ns) => ns,
QueryResult::Values(_) => Vec::new(),
};
contexts.push(ctx);
outers.push(corr.outer);
}
let trace = Correlation {
contexts,
outer: outers,
witnesses: Default::default(),
};
let mut caps = union_branches(&query.branches, adapter, start, &trace, outer);
for stage in &query.pipeline {
caps = apply_stage(stage, caps, adapter, &trace, outer);
}
(caps, pipeline_projected(query))
}
fn stage_projects(stage: &Stage) -> bool {
match stage {
Stage::Select(_) | Stage::Filter(_) => false,
Stage::Agg(call) => !stdlib::known_keyed(&call.name) || call.name == "group",
_ => true,
}
}
fn pipeline_projected(query: &Query) -> bool {
let mut projected = query.branches.iter().any(|b| b.projection.is_some());
let mut before_group: Vec<bool> = Vec::new();
for stage in &query.pipeline {
match stage {
Stage::Agg(call) if matches!(call.name.as_str(), "group" | "window") => {
before_group.push(projected);
projected = true;
}
Stage::Agg(call) if call.name == "ungroup" => {
projected = before_group.pop().unwrap_or(false);
}
s => projected = projected || stage_projects(s),
}
}
projected
}
fn union_branches(
branches: &[Branch],
adapter: &impl AstAdapter,
start: NodeId,
trace: &Trace,
outer: Option<&Scope<'_>>,
) -> Vec<Capsa> {
let mut caps = Vec::new();
for branch in branches {
let seed: &[Mark] = outer.map(|s| s.marks).unwrap_or(&[]);
let from = if let Some(m) = &branch.mark {
match seed.iter().rev().find(|k| &k.name == m) {
Some(k) => k.node,
None => continue,
}
} else if branch.anchored {
adapter.root()
} else {
start
};
for (node, register, marks, arrived) in
navigate_paths(&branch.steps, adapter, from, trace, outer, seed)
{
caps.push(Capsa {
node,
arrived,
marks,
register,
topic: branch
.projection
.as_ref()
.map(|p| project(adapter, node, p)),
members: Vec::new(),
captures: Vec::new(),
bindings: trace
.witnesses
.borrow()
.get(&node.0)
.cloned()
.unwrap_or_default(),
});
}
}
let mut seen = HashSet::new();
caps.retain(|c| seen.insert((c.node, reg_key(&c.register))));
caps
}
fn stage_reads_context(stage: &Stage) -> bool {
fn op(o: &Operand) -> bool {
match o {
Operand::Capsae { .. } => true,
Operand::Piped { expr, stages } => op(expr) || stages.iter().any(stage_reads_context),
Operand::Arith { left, right, .. } => op(left) || op(right),
Operand::Neg(inner) | Operand::Outer(inner) => op(inner),
Operand::Group(e) => pred(e),
Operand::Interp(segs) => segs.iter().any(|s| match s {
InterpSeg::Expr(e) => op(e),
InterpSeg::Text(_) => false,
}),
Operand::Rel { steps, .. } | Operand::Ctx { steps, .. } => steps.iter().any(elem),
_ => false,
}
}
fn pred(e: &PredExpr) -> bool {
match e {
PredExpr::Or(a, b) | PredExpr::And(a, b) => pred(a) || pred(b),
PredExpr::Not(a) => pred(a),
PredExpr::Compare(l, _, r) => op(l) || op(r),
PredExpr::Truthy(o) => op(o),
}
}
fn elem(e: &PathElem) -> bool {
match e {
PathElem::Mark(_) => false,
PathElem::Step(st) => st.predicates.iter().any(|p| match p {
Predicate::Expr(e) => pred(e),
_ => false,
}),
PathElem::Group(g) => {
g.alts.iter().flatten().any(elem)
|| g.predicates.iter().any(|p| match p {
Predicate::Expr(e) => pred(e),
_ => false,
})
}
PathElem::Push { body, .. } => match body {
PushBody::Expr(e) => op(e),
PushBody::Query(_) => false,
},
}
}
match stage {
Stage::Map(inner) => stage_reads_context(inner),
Stage::Expr(e) | Stage::ExprPush { expr: e, .. } => op(e),
Stage::Filter(e) => pred(e),
Stage::Select(Predicate::Expr(e)) => pred(e),
Stage::Func(call) | Stage::Agg(call) => call.args.iter().any(|a| match a {
Arg::Expr(e) => op(e),
_ => false,
}),
_ => false,
}
}
fn to_result(caps: Vec<Capsa>, projected: bool) -> QueryResult {
if projected || caps.iter().any(|c| c.topic.is_some()) {
QueryResult::Values(
caps.into_iter()
.map(|c| c.topic.unwrap_or(Value::Null))
.collect(),
)
} else {
QueryResult::Nodes(caps.into_iter().map(|c| c.node).collect())
}
}
fn apply_stage(
stage: &Stage,
caps: Vec<Capsa>,
adapter: &impl AstAdapter,
trace: &Trace,
outer: Option<&Scope<'_>>,
) -> Vec<Capsa> {
let snapshot = stage_reads_context(stage).then(|| caps.clone());
let peers = snapshot.as_deref();
match stage {
Stage::Func(call) if call.name == "json" => caps
.into_iter()
.map(|mut c| {
let v = match &c.topic {
Some(t) => Value::Str(t.to_json()),
None => Value::Str(node_to_json(adapter, c.node).to_json()),
};
c.topic = Some(v);
c
})
.collect(),
Stage::Func(call) if call.name == "xml" => caps
.into_iter()
.map(|mut c| {
let xml = match &c.topic {
Some(t) => value_to_xml(t, "item"),
None => node_to_xml(adapter, c.node),
};
c.topic = Some(Value::Str(xml));
c
})
.collect(),
Stage::Func(call) if call.name == "sh" => caps
.into_iter()
.enumerate()
.map(|(i, mut c)| {
let scope = Scope {
register: &c.register,
topic: c.topic.as_ref(),
ordinal: Some(i + 1),
captures: &c.captures,
marks: &c.marks,
bindings: &c.bindings,
edge: None,
arrived: &c.arrived,
peers,
outer,
};
let cmd = call.args.first().map(|a| match a {
Arg::Lit(v) => v.to_string(),
Arg::Expr(e) => operand_scalar(adapter, c.node, e, trace, scope).to_string(),
Arg::Range(..) => String::new(),
});
c.topic = Some(match cmd {
Some(cmd) if !cmd.is_empty() => {
run_shell(&cmd, c.topic.as_ref()).unwrap_or(Value::Null)
}
_ => Value::Null,
});
c
})
.collect(),
Stage::Func(call) if matches!(call.name.as_str(), "record" | "rec") => caps
.into_iter()
.enumerate()
.map(|(i, mut c)| {
let scope = Scope {
register: &c.register,
topic: c.topic.as_ref(),
ordinal: Some(i + 1),
captures: &c.captures,
marks: &c.marks,
bindings: &c.bindings,
edge: None,
arrived: &c.arrived,
peers,
outer,
};
let value = build_record(call, adapter, c.node, trace, scope);
c.topic = Some(value);
c
})
.collect(),
Stage::Func(call) if stdlib::known_keyed(&call.name) => caps
.into_iter()
.map(|mut c| {
if c.members.is_empty() {
return c;
}
let members =
keyed_agg(call, std::mem::take(&mut c.members), adapter, trace, outer);
c.topic = Some(Value::List(
members
.iter()
.map(|m| {
m.topic
.clone()
.unwrap_or_else(|| node_scalar(adapter, m.node))
})
.collect(),
));
c.members = members;
c
})
.collect(),
Stage::Func(call) if stdlib::known_agg(&call.name) => caps
.into_iter()
.map(|mut c| {
let items = match c.topic.take() {
Some(Value::List(items)) => items,
Some(other) => vec![other],
None => vec![node_scalar(adapter, c.node)],
};
let mut out = stdlib::apply(call, items);
c.topic = Some(match out.len() {
1 => out.pop().expect("len checked"),
_ => Value::List(out),
});
c.members = Vec::new();
c
})
.collect(),
Stage::Func(call) => caps
.into_iter()
.flat_map(|c| {
let topic = c.topic.clone().unwrap_or(Value::Null);
stdlib::apply_scalar(call, topic, &|e| adapter.unit_scale(e))
.into_iter()
.map(move |v| Capsa {
node: c.node,
register: c.register.clone(),
topic: Some(v),
members: Vec::new(),
captures: c.captures.clone(),
marks: c.marks.clone(),
bindings: c.bindings.clone(),
arrived: c.arrived.clone(),
})
})
.collect(),
Stage::Map(inner) => caps
.into_iter()
.map(|mut c| {
let (items, was_list) = match c.topic.take() {
Some(Value::List(items)) => (items, true),
Some(Value::Null) | None => {
c.topic = Some(Value::Null);
return c;
}
Some(other) => (vec![other], false),
};
let pseudo: Vec<Capsa> = items
.into_iter()
.map(|v| Capsa {
node: c.node,
register: c.register.clone(),
topic: Some(v),
members: Vec::new(),
captures: c.captures.clone(),
marks: c.marks.clone(),
bindings: c.bindings.clone(),
arrived: c.arrived.clone(),
})
.collect();
let mut out: Vec<Value> = apply_stage(inner, pseudo, adapter, trace, outer)
.into_iter()
.map(|p| p.topic.unwrap_or(Value::Null))
.collect();
c.topic = Some(if was_list {
Value::List(out)
} else {
match out.len() {
1 => out.pop().expect("len checked"),
_ => Value::List(out),
}
});
c
})
.collect(),
Stage::Spread { outer } => caps
.into_iter()
.flat_map(|c| {
let mut values = match c.topic.clone() {
Some(Value::List(items)) => items,
Some(Value::Null) | None => Vec::new(),
Some(other) => vec![other],
};
if *outer && values.is_empty() {
values.push(Value::Null);
}
values.into_iter().map(move |v| Capsa {
node: c.node,
register: c.register.clone(),
topic: Some(v),
members: Vec::new(),
captures: c.captures.clone(),
marks: c.marks.clone(),
bindings: c.bindings.clone(),
arrived: c.arrived.clone(),
})
})
.collect(),
Stage::Push(name) => caps
.into_iter()
.map(|mut c| {
match (c.topic.clone(), name.clone()) {
(None, Some(label)) => c.marks.push(Mark {
name: label,
node: c.node,
}),
(topic, name) => c.register.push(Reg {
name,
value: topic.unwrap_or(Value::Null),
}),
}
c
})
.collect(),
Stage::Subcontext { name, body } => caps
.into_iter()
.enumerate()
.map(|(i, mut c)| {
let scope = Scope {
register: &c.register,
topic: c.topic.as_ref(),
ordinal: Some(i + 1),
captures: &c.captures,
marks: &c.marks,
bindings: &c.bindings,
edge: None,
arrived: &c.arrived,
peers,
outer,
};
let value = subcontext_scalar(body, adapter, c.node, trace, Some(&scope));
c.register.push(Reg {
name: name.clone(),
value: value.clone(),
});
c.topic = Some(value);
c
})
.collect(),
Stage::Expr(expr) => caps
.into_iter()
.enumerate()
.map(|(i, mut c)| {
let scope = Scope {
register: &c.register,
topic: c.topic.as_ref(),
ordinal: Some(i + 1),
captures: &c.captures,
marks: &c.marks,
bindings: &c.bindings,
edge: None,
arrived: &c.arrived,
peers,
outer,
};
let value = operand_scalar(adapter, c.node, expr, trace, scope);
c.topic = Some(value);
c
})
.collect(),
Stage::ExprPush { name, expr } => caps
.into_iter()
.enumerate()
.map(|(i, mut c)| {
let scope = Scope {
register: &c.register,
topic: c.topic.as_ref(),
ordinal: Some(i + 1),
captures: &c.captures,
marks: &c.marks,
bindings: &c.bindings,
edge: None,
arrived: &c.arrived,
peers,
outer,
};
let value = operand_scalar(adapter, c.node, expr, trace, scope);
c.register.push(Reg {
name: name.clone(),
value: value.clone(),
});
c.topic = Some(value);
c
})
.collect(),
Stage::Agg(call) if call.name == "ungroup" => caps
.into_iter()
.flat_map(|c| {
if c.members.is_empty() {
return vec![c];
}
let key_regs = c.register;
c.members
.into_iter()
.map(|mut m| {
m.register.extend(key_regs.iter().cloned());
m
})
.collect()
})
.collect(),
Stage::Agg(call) if call.name == "window" => {
window_stage(call, caps, adapter, trace, outer)
}
Stage::Agg(call) if call.name == "shift" => shift_stage(call, caps, adapter, trace, outer),
Stage::Agg(call) if stdlib::known_keyed(&call.name) => {
keyed_agg(call, caps, adapter, trace, outer)
}
Stage::Agg(call)
if matches!(
call.name.as_str(),
"sort" | "unique" | "reverse" | "first" | "last"
) =>
{
let mut caps: Vec<Capsa> = caps
.into_iter()
.map(|mut c| {
c.topic = Some(
c.topic
.take()
.unwrap_or_else(|| node_scalar(adapter, c.node)),
);
c
})
.collect();
let topic_of = |c: &Capsa| c.topic.clone().unwrap_or(Value::Null);
match call.name.as_str() {
"sort" => match stdlib::collator_for(call) {
Some(coll) => caps.sort_by(|a, b| {
coll.compare(&topic_of(a).to_string(), &topic_of(b).to_string())
}),
None => caps.sort_by(|a, b| topic_of(a).compare(&topic_of(b))),
},
"reverse" => caps.reverse(),
"unique" => {
let mut seen: Vec<String> = Vec::new();
caps.retain(|c| {
let key = topic_of(c).to_string();
if seen.contains(&key) {
false
} else {
seen.push(key);
true
}
});
}
"first" => caps.truncate(1),
"last" => {
let n = caps.len().saturating_sub(1);
caps.drain(..n);
}
_ => {}
}
caps
}
Stage::Select(pred @ (Predicate::Index(_) | Predicate::Range(_, _))) => {
positional(caps, pred)
}
Stage::Select(Predicate::Expr(e)) => {
apply_stage(&Stage::Filter(e.clone()), caps, adapter, trace, outer)
}
Stage::Filter(cond) => caps
.into_iter()
.enumerate()
.filter_map(|(i, mut c)| {
let scope = Scope {
register: &c.register,
topic: c.topic.as_ref(),
ordinal: Some(i + 1),
captures: &c.captures,
marks: &c.marks,
bindings: &c.bindings,
edge: None,
arrived: &c.arrived,
peers,
outer,
};
let hit = if c.bindings.is_empty() {
exists_binding(adapter, c.node, &[cond], trace, &mut Vec::new(), scope)
} else {
eval_pred_expr(adapter, c.node, cond, trace, &c.bindings, scope)
};
if !hit {
return None;
}
let groups = extract_captures(cond, adapter, c.node, trace, scope);
if let Some(groups) = groups {
c.captures = groups;
}
Some(c)
})
.collect(),
Stage::Agg(call) => {
let topics: Vec<Value> = caps
.iter()
.map(|c| {
c.topic
.clone()
.unwrap_or_else(|| node_scalar(adapter, c.node))
})
.collect();
let node = caps.first().map_or(adapter.root(), |c| c.node);
stdlib::apply(call, topics)
.into_iter()
.map(|v| Capsa {
node,
register: Vec::new(),
topic: Some(v),
members: Vec::new(),
captures: Vec::new(),
marks: Vec::new(),
bindings: Vec::new(),
arrived: Vec::new(),
})
.collect()
}
Stage::Recall(r) => caps
.into_iter()
.map(|mut c| {
c.topic = Some(recall(&c.register, r));
c
})
.collect(),
}
}
fn keyed_agg(
call: &FnCall,
caps: Vec<Capsa>,
adapter: &impl AstAdapter,
trace: &Trace,
outer: Option<&Scope<'_>>,
) -> Vec<Capsa> {
let (count, key_args): (Option<usize>, &[Arg]) = match call.args.split_first() {
Some((Arg::Lit(Value::Int(n)), rest)) if matches!(call.name.as_str(), "top" | "bottom") => {
(Some((*n).max(0) as usize), rest)
}
_ => (None, &call.args),
};
let key_of = |c: &Capsa, i: usize| -> Vec<Value> {
key_args
.iter()
.filter_map(|a| match a {
Arg::Expr(e) => Some(operand_scalar(
adapter,
c.node,
e,
trace,
Scope {
register: &c.register,
topic: c.topic.as_ref(),
ordinal: Some(i + 1),
captures: &c.captures,
marks: &c.marks,
bindings: &c.bindings,
edge: None,
arrived: &c.arrived,
peers: None,
outer,
},
)),
Arg::Lit(_) | Arg::Range(_, _) => None,
})
.collect()
};
let compare_keys = |a: &[Value], b: &[Value]| -> Ordering {
a.iter()
.zip(b.iter())
.map(|(x, y)| x.compare(y))
.find(|o| *o != Ordering::Equal)
.unwrap_or(Ordering::Equal)
};
let has_null = |k: &[Value]| k.iter().any(|v| matches!(v, Value::Null));
if call.name == "group" {
type Group = (Vec<Value>, Vec<(String, Value)>, Vec<Capsa>);
let mut groups: Vec<Group> = Vec::new();
for (i, c) in caps.into_iter().enumerate() {
let scope = Scope {
register: &c.register,
topic: c.topic.as_ref(),
ordinal: Some(i + 1),
captures: &c.captures,
marks: &c.marks,
bindings: &c.bindings,
edge: None,
arrived: &c.arrived,
peers: None,
outer,
};
let fields = record_fields(&call.args, adapter, c.node, trace, scope);
let key: Vec<Value> = fields.iter().map(|(_, v)| v.clone()).collect();
if key.iter().any(|v| matches!(v, Value::Null)) {
continue;
}
match groups.iter_mut().find(|(k, _, _)| {
k.len() == key.len()
&& k.iter()
.zip(&key)
.all(|(a, b)| a.compare(b) == Ordering::Equal)
}) {
Some((_, _, members)) => members.push(c),
None => groups.push((key, fields, vec![c])),
}
}
return groups
.into_iter()
.map(|(_, fields, members)| {
let node = members[0].node;
let register = fields
.into_iter()
.map(|(name, value)| Reg {
name: Some(name),
value,
})
.collect();
let topics: Vec<Value> = members
.iter()
.map(|m| {
m.topic
.clone()
.unwrap_or_else(|| node_scalar(adapter, m.node))
})
.collect();
Capsa {
node,
register,
topic: Some(Value::List(topics)),
members,
captures: Vec::new(),
marks: Vec::new(),
bindings: Vec::new(),
arrived: Vec::new(),
}
})
.collect();
}
let mut keyed: Vec<(Vec<Value>, Capsa)> = caps
.into_iter()
.enumerate()
.map(|(i, c)| (key_of(&c, i), c))
.collect();
match call.name.as_str() {
"sort_by" => {
keyed.sort_by(|(a, _), (b, _)| {
has_null(a)
.cmp(&has_null(b))
.then_with(|| compare_keys(a, b))
});
}
"top" => {
keyed.retain(|(k, _)| !has_null(k));
keyed.sort_by(|(a, _), (b, _)| compare_keys(b, a));
keyed.truncate(count.unwrap_or(0));
}
"bottom" => {
keyed.retain(|(k, _)| !has_null(k));
keyed.sort_by(|(a, _), (b, _)| compare_keys(a, b));
keyed.truncate(count.unwrap_or(0));
}
"unique_by" => {
let mut seen: Vec<Vec<Value>> = Vec::new();
keyed.retain(|(k, _)| {
if seen.iter().any(|s| compare_keys(s, k) == Ordering::Equal) {
false
} else {
seen.push(k.clone());
true
}
});
}
"min_by" | "max_by" => {
keyed.retain(|(k, _)| !has_null(k));
let want = if call.name == "min_by" {
Ordering::Less
} else {
Ordering::Greater
};
let extreme = keyed
.iter()
.map(|(k, _)| k.clone())
.reduce(|a, b| if compare_keys(&b, &a) == want { b } else { a });
if let Some(extreme) = extreme {
keyed.retain(|(k, _)| compare_keys(k, &extreme) == Ordering::Equal);
}
}
_ => {}
}
keyed.into_iter().map(|(_, c)| c).collect()
}
fn effective_topic(c: &Capsa, adapter: &impl AstAdapter) -> Value {
c.topic
.clone()
.unwrap_or_else(|| node_scalar(adapter, c.node))
}
fn peer_lists(
caps: &[Capsa],
key: Option<&Operand>,
adapter: &impl AstAdapter,
trace: &Trace,
outer: Option<&Scope<'_>>,
) -> (Vec<Vec<usize>>, Vec<(usize, usize)>) {
let Some(key) = key else {
let all: Vec<usize> = (0..caps.len()).collect();
let of = (0..caps.len()).map(|i| (0, i)).collect();
return (vec![all], of);
};
let mut lists: Vec<(Option<Value>, Vec<usize>)> = Vec::new();
let mut of = Vec::with_capacity(caps.len());
for (i, c) in caps.iter().enumerate() {
let scope = Scope {
register: &c.register,
topic: c.topic.as_ref(),
ordinal: Some(i + 1),
captures: &c.captures,
marks: &c.marks,
bindings: &c.bindings,
edge: None,
arrived: &c.arrived,
peers: None,
outer,
};
let k = operand_scalar(adapter, c.node, key, trace, scope);
let slot = if matches!(k, Value::Null) {
None
} else {
lists
.iter()
.position(|(lk, _)| matches!(lk, Some(lk) if lk.compare(&k) == Ordering::Equal))
};
match slot {
Some(li) => {
lists[li].1.push(i);
of.push((li, lists[li].1.len() - 1));
}
None => {
let owner = if matches!(k, Value::Null) {
None
} else {
Some(k)
};
lists.push((owner, vec![i]));
of.push((lists.len() - 1, 0));
}
}
}
(lists.into_iter().map(|(_, l)| l).collect(), of)
}
fn window_args(call: &FnCall) -> (Option<i64>, Option<i64>, Option<&Operand>) {
let key = match call.args.get(1) {
Some(Arg::Expr(e)) => Some(e),
_ => None,
};
match call.args.first() {
Some(Arg::Range(a, b)) => (*a, *b, key),
Some(Arg::Lit(Value::Int(n))) => (Some(-(n - 1)), Some(0), key),
_ => (None, None, key),
}
}
fn window_stage(
call: &FnCall,
caps: Vec<Capsa>,
adapter: &impl AstAdapter,
trace: &Trace,
outer: Option<&Scope<'_>>,
) -> Vec<Capsa> {
let (from, to, key) = window_args(call);
let (lists, of) = peer_lists(&caps, key, adapter, trace, outer);
let windows: Vec<Vec<usize>> = of
.iter()
.map(|&(li, p)| {
let list = &lists[li];
let p = p as i64;
let lo = from.map_or(0, |a| (p + a).max(0)) as usize;
let hi = to.map_or(list.len() as i64 - 1, |b| {
(p + b).min(list.len() as i64 - 1)
});
if hi < lo as i64 {
return Vec::new();
}
list[lo..=hi as usize].to_vec()
})
.collect();
let members: Vec<Vec<Capsa>> = windows
.iter()
.map(|w| w.iter().map(|&j| caps[j].clone()).collect())
.collect();
caps.into_iter()
.zip(members)
.map(|(mut c, members)| {
c.topic = Some(Value::List(
members
.iter()
.map(|m| effective_topic(m, adapter))
.collect(),
));
c.members = members;
c
})
.collect()
}
fn shift_stage(
call: &FnCall,
caps: Vec<Capsa>,
adapter: &impl AstAdapter,
trace: &Trace,
outer: Option<&Scope<'_>>,
) -> Vec<Capsa> {
let (n, key) = match call.args.split_first() {
Some((Arg::Lit(Value::Int(n)), rest)) => (
*n,
match rest.first() {
Some(Arg::Expr(e)) => Some(e),
_ => None,
},
),
_ => (1, None),
};
let (lists, of) = peer_lists(&caps, key, adapter, trace, outer);
let topics: Vec<Value> = of
.iter()
.map(|&(li, p)| {
let list = &lists[li];
let idx = p as i64 - n;
if idx < 0 || idx >= list.len() as i64 {
Value::Null
} else {
effective_topic(&caps[list[idx as usize]], adapter)
}
})
.collect();
caps.into_iter()
.zip(topics)
.map(|(mut c, t)| {
c.topic = Some(t);
c.members = Vec::new();
c
})
.collect()
}
fn build_record(
call: &FnCall,
adapter: &impl AstAdapter,
node: NodeId,
trace: &Trace,
scope: Scope<'_>,
) -> Value {
Value::Record(record_fields(&call.args, adapter, node, trace, scope))
}
fn record_fields(
args: &[Arg],
adapter: &impl AstAdapter,
node: NodeId,
trace: &Trace,
scope: Scope<'_>,
) -> Vec<(String, Value)> {
let mut fields: Vec<(String, Value)> = Vec::new();
let mut args = args.iter().peekable();
while let Some(arg) = args.next() {
match arg {
Arg::Lit(Value::Str(name)) => {
let value = match args.next() {
Some(Arg::Expr(e)) => operand_scalar_bound(adapter, node, e, trace, &[], scope),
Some(Arg::Lit(v)) => v.clone(),
Some(Arg::Range(_, _)) | None => Value::Null,
};
fields.push((name.clone(), value));
}
Arg::Expr(e) => {
let name = crate::ast::auto_field_name(e)
.unwrap_or_default()
.to_string();
fields.push((
name,
operand_scalar_bound(adapter, node, e, trace, &[], scope),
));
}
Arg::Lit(v) => {
fields.push((String::new(), v.clone()));
}
Arg::Range(_, _) => {}
}
}
fields
}
fn subcontext_scalar(
body: &Query,
adapter: &impl AstAdapter,
node: NodeId,
trace: &Trace,
outer: Option<&Scope<'_>>,
) -> Value {
match eval_query_outer(body, adapter, node, trace, outer) {
QueryResult::Values(mut vs) => match vs.len() {
0 => Value::Null,
1 => vs.pop().unwrap(),
_ => Value::List(vs),
},
QueryResult::Nodes(ns) => Value::Int(ns.len() as i64),
}
}
fn node_scalar(adapter: &impl AstAdapter, node: NodeId) -> Value {
adapter.name(node).map_or(Value::Null, Value::Str)
}
fn operand_scalar(
adapter: &impl AstAdapter,
node: NodeId,
expr: &Operand,
trace: &Trace,
scope: Scope<'_>,
) -> Value {
operand_scalar_bound(adapter, node, expr, trace, &[], scope)
}
fn operand_scalar_bound(
adapter: &impl AstAdapter,
node: NodeId,
expr: &Operand,
trace: &Trace,
bound: &[Option<NodeId>],
scope: Scope<'_>,
) -> Value {
eval_operand(adapter, node, expr, trace, bound, scope)
.into_iter()
.next()
.unwrap_or(Value::Null)
}
fn temporal_arith(op: ArithOp, left: &Value, right: &Value) -> Option<Value> {
let lift = |v: &Value| -> Option<Value> {
match v {
Value::Instant { .. } | Value::Duration { .. } => Some(v.clone()),
Value::Str(s) => crate::temporal::parse_iso(s)
.map(|(secs, nanos, offset_min)| Value::Instant {
secs,
nanos,
offset_min,
})
.or_else(|| {
crate::temporal::parse_span(s)
.map(|(secs, nanos)| Value::Duration { secs, nanos })
}),
_ => None,
}
};
let typed = |v: &Value| matches!(v, Value::Instant { .. } | Value::Duration { .. });
if !(matches!(op, ArithOp::Mul) && (typed(left) || typed(right))
|| matches!(op, ArithOp::Div) && typed(left))
{
match (typed(left), typed(right)) {
(true, false) => {
if let Some(l) = lift(right) {
return temporal_arith(op, left, &l);
}
}
(false, true) => {
if let Some(l) = lift(left) {
return temporal_arith(op, &l, right);
}
}
_ => {}
}
}
use Value::{Duration, Instant};
let add = |s: i64, n: u32, ds: i64, dn: u32, sign: i64| -> Option<(i64, u32)> {
let mut secs = s.checked_add(sign.checked_mul(ds)?)?;
let total = n as i64 + sign * dn as i64;
let mut nanos = total;
if nanos < 0 {
nanos += 1_000_000_000;
secs = secs.checked_sub(1)?;
} else if nanos >= 1_000_000_000 {
nanos -= 1_000_000_000;
secs = secs.checked_add(1)?;
}
Some((secs, nanos as u32))
};
match (left, right, op) {
(
Instant {
secs: a, nanos: an, ..
},
Instant {
secs: b, nanos: bn, ..
},
ArithOp::Sub,
) => {
let Some((secs, nanos)) = add(*a, *an, *b, *bn, -1) else {
return Some(Value::Null);
};
Some(Duration { secs, nanos })
}
(
Instant {
secs,
nanos,
offset_min,
},
Duration { secs: d, nanos: dn },
ArithOp::Add | ArithOp::Sub,
) => {
let sign = if matches!(op, ArithOp::Add) { 1 } else { -1 };
let Some((secs, nanos)) = add(*secs, *nanos, *d, *dn, sign) else {
return Some(Value::Null);
};
Some(Instant {
secs,
nanos,
offset_min: *offset_min,
})
}
(
Duration { secs: d, nanos: dn },
Instant {
secs,
nanos,
offset_min,
},
ArithOp::Add,
) => {
let Some((secs, nanos)) = add(*secs, *nanos, *d, *dn, 1) else {
return Some(Value::Null);
};
Some(Instant {
secs,
nanos,
offset_min: *offset_min,
})
}
(
Duration { secs: a, nanos: an },
Duration { secs: b, nanos: bn },
ArithOp::Add | ArithOp::Sub,
) => {
let sign = if matches!(op, ArithOp::Add) { 1 } else { -1 };
let Some((secs, nanos)) = add(*a, *an, *b, *bn, sign) else {
return Some(Value::Null);
};
Some(Duration { secs, nanos })
}
(Duration { secs, nanos }, n, ArithOp::Mul)
| (n, Duration { secs, nanos }, ArithOp::Mul) => {
let k = n.numeric()?;
let total = (*secs as f64 + *nanos as f64 / 1e9) * k;
Some(Duration {
secs: total.floor() as i64,
nanos: ((total - total.floor()) * 1e9) as u32,
})
}
(Duration { secs, nanos }, n, ArithOp::Div) => {
let k = n.numeric()?;
if k == 0.0 {
return Some(Value::Null);
}
let total = (*secs as f64 + *nanos as f64 / 1e9) / k;
Some(Duration {
secs: total.floor() as i64,
nanos: ((total - total.floor()) * 1e9) as u32,
})
}
(Instant { .. } | Duration { .. }, _, _) | (_, Instant { .. } | Duration { .. }, _) => {
Some(Value::Null)
}
_ => None,
}
}
fn quantital_arith(op: ArithOp, left: &Value, right: &Value, scale: UnitScale) -> Option<Value> {
use Value::Quantity;
let typed = |v: &Value| matches!(v, Quantity { .. });
if !typed(left) && !typed(right) {
return None;
}
let rescale = |q: &Value, k: f64| -> Value {
let Quantity {
value,
base,
written,
} = q
else {
unreachable!("rescale is called on quantities only");
};
Quantity {
value: value * k,
base: base.clone(),
written: written.as_ref().map(|(v, u)| (v * k, u.clone())),
}
};
match op {
ArithOp::Mul => {
return match (typed(left), typed(right)) {
(true, false) => right
.numeric()
.map(|k| rescale(left, k))
.or(Some(Value::Null)),
(false, true) => left
.numeric()
.map(|k| rescale(right, k))
.or(Some(Value::Null)),
_ => Some(Value::Null), };
}
ArithOp::Div if typed(left) && !typed(right) => {
return match right.numeric() {
Some(k) if k != 0.0 => Some(rescale(left, 1.0 / k)),
_ => Some(Value::Null),
};
}
_ => {}
}
let lift = |v: &Value| -> Option<(f64, String)> {
match v {
Quantity { value, base, .. } => Some((*value, base.clone())),
Value::Str(s) => {
crate::quantity::parse_unit_text_with(s, scale).map(|(bv, b, ..)| (bv, b))
}
_ => None,
}
};
match op {
ArithOp::Add | ArithOp::Sub => {
let (Some((a, ba)), Some((b, bb))) = (lift(left), lift(right)) else {
return Some(Value::Null);
};
if ba != bb {
return Some(Value::Null);
}
let v = if matches!(op, ArithOp::Add) {
a + b
} else {
a - b
};
Some(Value::Quantity {
value: v,
base: ba,
written: None,
})
}
_ => Some(Value::Null),
}
}
fn arith(op: ArithOp, left: &Value, right: &Value, scale: UnitScale) -> Value {
if let Some(v) = temporal_arith(op, left, right) {
return v;
}
if let Some(v) = quantital_arith(op, left, right, scale) {
return v;
}
let (Some(l), Some(r)) = (left.numeric_reading(), right.numeric_reading()) else {
return Value::Null;
};
if let (Value::Int(a), Value::Int(b)) = (&l, &r) {
let (a, b) = (*a, *b);
let promoted = |f: fn(f64, f64) -> f64| Value::Float(f(a as f64, b as f64));
return match op {
ArithOp::Add => a
.checked_add(b)
.map(Value::Int)
.unwrap_or_else(|| promoted(|x, y| x + y)),
ArithOp::Sub => a
.checked_sub(b)
.map(Value::Int)
.unwrap_or_else(|| promoted(|x, y| x - y)),
ArithOp::Mul => a
.checked_mul(b)
.map(Value::Int)
.unwrap_or_else(|| promoted(|x, y| x * y)),
ArithOp::Div => {
if b == 0 {
Value::Null
} else {
Value::Float(a as f64 / b as f64)
}
}
ArithOp::IDiv => {
if b == 0 {
Value::Null
} else {
a.checked_div(b)
.map(Value::Int)
.unwrap_or_else(|| promoted(|x, y| (x / y).trunc()))
}
}
ArithOp::Mod => {
if b == 0 {
Value::Null
} else {
Value::Int(a.checked_rem(b).unwrap_or(0))
}
}
};
}
let (Some(a), Some(b)) = (l.numeric(), r.numeric()) else {
return Value::Null;
};
match op {
ArithOp::Add => Value::Float(a + b),
ArithOp::Sub => Value::Float(a - b),
ArithOp::Mul => Value::Float(a * b),
ArithOp::Div | ArithOp::IDiv | ArithOp::Mod if b == 0.0 => Value::Null,
ArithOp::Div => Value::Float(a / b),
ArithOp::IDiv => {
let t = (a / b).trunc();
if t >= i64::MIN as f64 && t <= i64::MAX as f64 {
Value::Int(t as i64)
} else {
Value::Float(t)
}
}
ArithOp::Mod => Value::Float(a % b),
}
}
fn recall(register: &[Reg], r: &RegRef) -> Value {
match r {
RegRef::Top => register
.last()
.map(|r| r.value.clone())
.unwrap_or(Value::Null),
RegRef::Index(n) => register
.get(n.saturating_sub(1))
.map(|r| r.value.clone())
.unwrap_or(Value::Null),
RegRef::Named(name) => register
.iter()
.rev()
.find(|r| r.name.as_deref() == Some(name.as_str()))
.map(|r| r.value.clone())
.unwrap_or(Value::Null),
RegRef::Whole => Value::List(register.iter().map(|r| r.value.clone()).collect()),
RegRef::Record => {
let mut fields: Vec<(String, Value)> = Vec::new();
for reg in register {
if let Some(name) = ®.name {
match fields.iter_mut().find(|(n, _)| n == name) {
Some((_, v)) => *v = reg.value.clone(),
None => fields.push((name.clone(), reg.value.clone())),
}
}
}
Value::Record(fields)
}
}
}
fn reg_key(register: &[Reg]) -> Vec<(Option<String>, String)> {
register
.iter()
.map(|r| (r.name.clone(), r.value.to_string()))
.collect()
}
fn navigate_paths(
elems: &[PathElem],
adapter: &impl AstAdapter,
start: NodeId,
trace: &Trace,
outer: Option<&Scope<'_>>,
seed_marks: &[Mark],
) -> Vec<(NodeId, Vec<Reg>, Vec<Mark>, Vec<EdgeCtx>)> {
let mut ctx: Vec<(NodeId, Vec<Reg>, Vec<Mark>, Vec<EdgeCtx>)> =
vec![(start, Vec::new(), seed_marks.to_vec(), Vec::new())];
for elem in elems {
if let PathElem::Mark(name) = elem {
for (node, _, marks, _) in &mut ctx {
marks.push(Mark {
name: name.clone(),
node: *node,
});
}
continue;
}
let mut next: Vec<(NodeId, Vec<Reg>, Vec<Mark>, Vec<EdgeCtx>)> = Vec::new();
for (node, register, marks, _) in &ctx {
match elem {
PathElem::Step(step) => {
let mut succs = Vec::new();
apply_step(adapter, *node, step, trace, outer, marks, &mut succs);
next.extend(succs.into_iter().map(|s| {
let arrived = arrived_edge(adapter, *node, step, s).into_iter().collect();
(s, register.clone(), marks.clone(), arrived)
}));
}
PathElem::Group(group) => {
let anchor = GPath {
node: *node,
visited: vec![*node],
register: register.clone(),
marks: marks.clone(),
last_edge: None,
};
next.extend(
expand_group(adapter, group, anchor, trace, outer)
.into_iter()
.map(|p| {
let arrived = p.last_edge.clone().into_iter().collect();
(p.node, p.register, p.marks, arrived)
}),
);
}
PathElem::Mark(_) => unreachable!("handled above"),
PathElem::Push { .. } => {}
}
}
let mut merged: Vec<(NodeId, Vec<Reg>, Vec<Mark>, Vec<EdgeCtx>)> = Vec::new();
let mut index: HashMap<(NodeId, Vec<(Option<String>, String)>, Vec<(String, u64)>), usize> =
HashMap::new();
for (n, r, m, arrived) in next {
match index.entry((n, reg_key(&r), mark_key(&m))) {
std::collections::hash_map::Entry::Occupied(e) => {
let slot = &mut merged[*e.get()].3;
for edge in arrived {
if !slot.contains(&edge) {
slot.push(edge);
}
}
}
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(merged.len());
merged.push((n, r, m, arrived));
}
}
}
ctx = merged;
if ctx.is_empty() {
break;
}
}
ctx
}
fn mark_key(marks: &[Mark]) -> Vec<(String, u64)> {
marks.iter().map(|m| (m.name.clone(), m.node.0)).collect()
}
fn navigate_from(
elems: &[PathElem],
adapter: &impl AstAdapter,
start: NodeId,
trace: &Trace,
outer: Option<&Scope<'_>>,
seed_marks: &[Mark],
) -> Vec<NodeId> {
dedup(
navigate_paths(elems, adapter, start, trace, outer, seed_marks)
.into_iter()
.map(|(n, _, _, _)| n)
.collect(),
)
}
#[derive(Clone)]
struct GPath {
node: NodeId,
visited: Vec<NodeId>,
register: Vec<Reg>,
marks: Vec<Mark>,
last_edge: Option<EdgeCtx>,
}
impl GPath {
fn blocks(&self, node: NodeId) -> bool {
self.visited.binary_search(&node).is_ok()
}
fn with(&self, node: NodeId, edge: Option<EdgeCtx>) -> GPath {
let mut visited = self.visited.clone();
if let Err(i) = visited.binary_search(&node) {
visited.insert(i, node);
}
GPath {
node,
visited,
register: self.register.clone(),
marks: self.marks.clone(),
last_edge: edge,
}
}
fn key(
&self,
) -> (
NodeId,
Vec<NodeId>,
Vec<(Option<String>, String)>,
Option<(NodeId, String, NodeId)>,
) {
(
self.node,
self.visited.clone(),
reg_key(&self.register),
self.last_edge
.as_ref()
.map(|e| (e.source, e.label.clone(), e.target)),
)
}
}
fn arrived_edge(
adapter: &impl AstAdapter,
from: NodeId,
step: &Step,
succ: NodeId,
) -> Option<EdgeCtx> {
let edge = match &step.axis {
Axis::Child | Axis::Descendant(_) => EdgeCtx {
source: adapter.parent(succ).unwrap_or(from),
label: CHILD.into(),
target: succ,
},
Axis::Parent => EdgeCtx {
source: from,
label: PARENT.into(),
target: succ,
},
Axis::Ancestor(_) => {
let mut child = from;
while let Some(p) = adapter.parent(child) {
if p == succ {
break;
}
child = p;
}
EdgeCtx {
source: child,
label: PARENT.into(),
target: succ,
}
}
Axis::NextSibling | Axis::FollowingSiblings(_) => EdgeCtx {
source: from,
label: NEXT.into(),
target: succ,
},
Axis::PrevSibling | Axis::PrecedingSiblings(_) => EdgeCtx {
source: from,
label: PREV.into(),
target: succ,
},
Axis::OutLink => {
let label = adapter
.links(from)
.into_iter()
.find(|(l, t)| *t == succ && matches_label(&step.matcher, l))
.map(|(l, _)| l)?;
EdgeCtx {
source: from,
label,
target: succ,
}
}
Axis::InLink => {
let label = adapter
.backlinks(from)
.into_iter()
.find(|(l, s)| *s == succ && matches_label(&step.matcher, l))
.map(|(l, _)| l)?;
EdgeCtx {
source: succ,
label,
target: from,
}
}
Axis::Resolve { property, .. } => EdgeCtx {
source: from,
label: property.clone(),
target: succ,
},
Axis::ReverseResolve { property, .. } => EdgeCtx {
source: succ,
label: property.clone(),
target: from,
},
};
Some(edge)
}
fn expand_group(
adapter: &impl AstAdapter,
group: &Group,
anchor: GPath,
trace: &Trace,
outer: Option<&Scope<'_>>,
) -> Vec<GPath> {
let n_max = adapter.quantifier_bound();
let hi = group.quant.max.map_or(n_max, |n| n.min(n_max));
let admits = |p: &GPath| group_admits(adapter, group, p, trace, outer);
let mut matches: Vec<(GPath, usize)> = Vec::new();
if group.quant.min == 0 && admits(&anchor) {
matches.push((anchor.clone(), 0));
}
let mut frontier = vec![anchor];
for count in 1..=hi {
let mut next = Vec::new();
for path in &frontier {
for alt in &group.alts {
next.extend(expand_elems(adapter, alt, vec![path.clone()], trace, outer));
}
}
frontier = dedup_paths(next);
if frontier.is_empty() {
break;
}
if count >= group.quant.min {
let before = matches.len();
matches.extend(
frontier
.iter()
.filter(|p| admits(p))
.map(|p| (p.clone(), count)),
);
if group.reach == Reach::Proximal && matches.len() > before {
break;
}
}
}
let extreme = match group.reach {
Reach::All => None,
Reach::Proximal => matches.iter().map(|&(_, c)| c).min(),
Reach::Distal => matches.iter().map(|&(_, c)| c).max(),
};
matches
.into_iter()
.filter(|&(_, c)| extreme.is_none_or(|e| c == e))
.map(|(p, _)| p)
.collect()
}
fn group_admits(
adapter: &impl AstAdapter,
group: &Group,
path: &GPath,
trace: &Trace,
outer: Option<&Scope<'_>>,
) -> bool {
if group.predicates.is_empty() {
return true;
}
let exprs: Vec<&PredExpr> = group
.predicates
.iter()
.filter_map(|p| match p {
Predicate::Expr(e) => Some(e),
_ => None,
})
.collect();
exists_binding(
adapter,
path.node,
&exprs,
trace,
&mut Vec::new(),
Scope {
outer,
edge: path.last_edge.as_ref(),
marks: &path.marks,
..NO_SCOPE
},
)
}
fn expand_elems(
adapter: &impl AstAdapter,
elems: &[PathElem],
mut paths: Vec<GPath>,
trace: &Trace,
outer: Option<&Scope<'_>>,
) -> Vec<GPath> {
for elem in elems {
if let PathElem::Mark(name) = elem {
for path in &mut paths {
path.marks.push(Mark {
name: name.clone(),
node: path.node,
});
}
continue;
}
let mut next = Vec::new();
for path in &paths {
match elem {
PathElem::Step(step) => {
let mut succs = Vec::new();
apply_step(
adapter,
path.node,
step,
trace,
outer,
&path.marks,
&mut succs,
);
next.extend(succs.into_iter().filter(|&s| !path.blocks(s)).map(|s| {
let edge = arrived_edge(adapter, path.node, step, s);
path.with(s, edge)
}));
}
PathElem::Group(inner) => {
next.extend(expand_group(adapter, inner, path.clone(), trace, outer));
}
PathElem::Mark(_) => unreachable!("handled above"),
PathElem::Push { name, body } => {
let scope = Scope {
edge: path.last_edge.as_ref(),
outer,
..NO_SCOPE
};
let value = match body {
PushBody::Query(q) => {
subcontext_scalar(q, adapter, path.node, trace, Some(&scope))
}
PushBody::Expr(e) => {
operand_scalar_bound(adapter, path.node, e, trace, &[], scope)
}
};
let mut p = path.clone();
p.register.push(Reg {
name: name.clone(),
value,
});
next.push(p);
}
}
}
paths = dedup_paths(next);
if paths.is_empty() {
break;
}
}
paths
}
fn dedup_paths(paths: Vec<GPath>) -> Vec<GPath> {
let mut seen = HashSet::new();
paths.into_iter().filter(|p| seen.insert(p.key())).collect()
}
fn project(adapter: &impl AstAdapter, node: NodeId, proj: &Projection) -> Value {
match proj {
Projection::Property(Some(name)) => adapter.property(node, name).unwrap_or(Value::Null),
Projection::Property(None) => adapter.default_value(node).unwrap_or(Value::Null),
Projection::CoreMeta(key) => core_meta(adapter, node, key).unwrap_or(Value::Null),
Projection::AdapterMeta(key) => adapter.metadata(node, key).unwrap_or(Value::Null),
}
}
fn core_meta(adapter: &impl AstAdapter, node: NodeId, key: &str) -> Option<Value> {
let index = || index_of(adapter, node);
let n_siblings = || {
adapter
.parent(node)
.map(|p| adapter.children(p).len().saturating_sub(1))
};
match key {
"name" => Some(adapter.name(node).map_or(Value::Null, Value::Str)),
"id" => Some(Value::Int(node.0 as i64)),
"index" => Some(index().map_or(Value::Null, |i| Value::Int(i as i64))),
"depth" => Some(Value::Int(depth_of(adapter, node) as i64)),
"parent-name" => Some(
adapter
.parent(node)
.and_then(|p| adapter.name(p))
.map_or(Value::Null, Value::Str),
),
"parent-id" => Some(
adapter
.parent(node)
.map_or(Value::Null, |p| Value::Int(p.0 as i64)),
),
"parent-index" => Some(
adapter
.parent(node)
.and_then(|p| index_of(adapter, p))
.map_or(Value::Null, |i| Value::Int(i as i64)),
),
"n-children" => Some(Value::Int(adapter.children(node).len() as i64)),
"n-descendants" => Some(Value::Int(n_descendants(adapter, node) as i64)),
"n-siblings" => Some(n_siblings().map_or(Value::Null, |n| Value::Int(n as i64))),
"traits" => Some(Value::List(
adapter.traits(node).into_iter().map(Value::Str).collect(),
)),
"is-leaf" => Some(Value::Bool(adapter.children(node).is_empty())),
"is-root" => Some(Value::Bool(adapter.parent(node).is_none())),
"is-first-child" => Some(Value::Bool(index() == Some(1))),
"is-last-child" => Some(Value::Bool(
matches!((index(), n_siblings()), (Some(i), Some(n)) if i == n + 1),
)),
"is-top-level" => Some(Value::Bool(depth_of(adapter, node) == 1)),
"is-non-tree" => Some(Value::Bool(false)),
"name-path" => Some(Value::Str(ancestor_path(adapter, node, |a, n| a.name(n)))),
"index-path" => Some(Value::Str(ancestor_path(adapter, node, |a, n| {
index_of(a, n).map(|i| i.to_string())
}))),
"id-path" => Some(Value::Str(ancestor_path(adapter, node, |_, n| {
Some(n.0.to_string())
}))),
_ => None,
}
}
fn ancestor_path<A: AstAdapter>(
adapter: &A,
node: NodeId,
token: impl Fn(&A, NodeId) -> Option<String>,
) -> String {
let mut parts = Vec::new();
let mut cur = Some(node);
while let Some(n) = cur {
if adapter.parent(n).is_none() {
break; }
if let Some(t) = token(adapter, n) {
parts.push(t);
}
cur = adapter.parent(n);
}
parts.reverse();
format!("/{}", parts.join("/"))
}
fn index_of(adapter: &impl AstAdapter, node: NodeId) -> Option<usize> {
let parent = adapter.parent(node)?;
adapter
.children(parent)
.iter()
.position(|&c| c == node)
.map(|i| i + 1)
}
fn depth_of(adapter: &impl AstAdapter, node: NodeId) -> usize {
let mut depth = 0;
let mut cur = adapter.parent(node);
while let Some(p) = cur {
depth += 1;
cur = adapter.parent(p);
}
depth
}
fn n_descendants(adapter: &impl AstAdapter, node: NodeId) -> usize {
adapter
.children(node)
.into_iter()
.map(|c| 1 + n_descendants(adapter, c))
.sum()
}
#[allow(clippy::too_many_arguments)]
fn apply_step(
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_>>,
marks: &[Mark],
out: &mut Vec<NodeId>,
) {
match &step.axis {
Axis::Child => {
let matched: Vec<NodeId> = match &step.matcher {
Matcher::Name(n) => adapter
.children_named(node, n)
.into_iter()
.filter(|&c| tests_ok(adapter, c, step))
.collect(),
_ => adapter
.children(node)
.into_iter()
.filter(|&c| matches_step(adapter, c, step))
.collect(),
};
out.extend(apply_predicates(
adapter,
matched,
step,
trace,
outer,
marks,
|&n| n,
|&n| {
Some(EdgeCtx {
source: node,
label: CHILD.into(),
target: n,
})
},
));
}
Axis::Descendant(reach) => {
let mut found = Vec::new();
descendants(adapter, node, 1, step, &mut found);
let found = apply_predicates(
adapter,
found,
step,
trace,
outer,
marks,
|&(n, _)| n,
|&(n, _)| {
Some(EdgeCtx {
source: adapter.parent(n).unwrap_or(node),
label: CHILD.into(),
target: n,
})
},
);
out.extend(by_reach(found, *reach));
}
Axis::Parent => {
let matched: Vec<NodeId> = adapter
.parent(node)
.into_iter()
.filter(|&p| matches_step(adapter, p, step))
.collect();
out.extend(apply_predicates(
adapter,
matched,
step,
trace,
outer,
marks,
|&n| n,
|&p| {
Some(EdgeCtx {
source: node,
label: PARENT.into(),
target: p,
})
},
));
}
Axis::Ancestor(reach) => {
let mut found = Vec::new();
let mut preds: HashMap<NodeId, NodeId> = HashMap::new();
let mut prev = node;
let mut cur = adapter.parent(node);
let mut dist = 1usize;
while let Some(anc) = cur {
if matches_step(adapter, anc, step) {
found.push((anc, dist));
preds.insert(anc, prev);
}
prev = anc;
cur = adapter.parent(anc);
dist += 1;
}
let found = apply_predicates(
adapter,
found,
step,
trace,
outer,
marks,
|&(n, _)| n,
|&(n, _)| {
Some(EdgeCtx {
source: preds.get(&n).copied().unwrap_or(node),
label: PARENT.into(),
target: n,
})
},
);
out.extend(by_reach(found, *reach));
}
Axis::NextSibling => {
let matched: Vec<NodeId> = sibling(adapter, node, 1)
.into_iter()
.filter(|&s| matches_step(adapter, s, step))
.collect();
out.extend(apply_predicates(
adapter,
matched,
step,
trace,
outer,
marks,
|&n| n,
|&sib| {
Some(EdgeCtx {
source: node,
label: NEXT.into(),
target: sib,
})
},
));
}
Axis::PrevSibling => {
let matched: Vec<NodeId> = sibling(adapter, node, -1)
.into_iter()
.filter(|&s| matches_step(adapter, s, step))
.collect();
out.extend(apply_predicates(
adapter,
matched,
step,
trace,
outer,
marks,
|&n| n,
|&sib| {
Some(EdgeCtx {
source: node,
label: PREV.into(),
target: sib,
})
},
));
}
Axis::FollowingSiblings(reach) | Axis::PrecedingSiblings(reach) => {
let following = matches!(step.axis, Axis::FollowingSiblings(_));
let mut found: Vec<(NodeId, usize)> = Vec::new();
if let Some(parent) = adapter.parent(node) {
let sibs = adapter.children(parent);
if let Some(pos) = sibs.iter().position(|&s| s == node) {
let range: Vec<NodeId> = if following {
sibs[pos + 1..].to_vec()
} else {
sibs[..pos].to_vec()
};
for (i, s) in range.iter().enumerate() {
if matches_step(adapter, *s, step) {
let dist = if following { i + 1 } else { range.len() - i };
found.push((*s, dist));
}
}
}
}
let found = apply_predicates(
adapter,
found,
step,
trace,
outer,
marks,
|&(n, _)| n,
|&(sib, _)| {
Some(EdgeCtx {
source: node,
label: if following { NEXT.into() } else { PREV.into() },
target: sib,
})
},
);
out.extend(by_reach(found, *reach));
}
Axis::OutLink => {
let matched: Vec<(String, NodeId)> = adapter
.links(node)
.into_iter()
.filter(|(label, target)| {
matches_label(&step.matcher, label) && tests_ok(adapter, *target, step)
})
.collect();
let kept = apply_predicates(
adapter,
matched,
step,
trace,
outer,
marks,
|(_, n)| *n,
|(label, target)| {
Some(EdgeCtx {
source: node,
label: label.clone(),
target: *target,
})
},
);
out.extend(kept.into_iter().map(|(_, target)| target));
}
Axis::InLink => {
let matched: Vec<(String, NodeId)> = adapter
.backlinks(node)
.into_iter()
.filter(|(label, source)| {
matches_label(&step.matcher, label) && tests_ok(adapter, *source, step)
})
.collect();
let kept = apply_predicates(
adapter,
matched,
step,
trace,
outer,
marks,
|(_, n)| *n,
|(label, source)| {
Some(EdgeCtx {
source: *source,
label: label.clone(),
target: node,
})
},
);
out.extend(kept.into_iter().map(|(_, source)| source));
}
Axis::Resolve { property, hint } => {
let matched: Vec<NodeId> = adapter
.resolve(node, property, hint.as_deref())
.into_iter()
.filter(|&t| tests_ok(adapter, t, step))
.collect();
out.extend(apply_predicates(
adapter,
matched,
step,
trace,
outer,
marks,
|&n| n,
|&t| {
Some(EdgeCtx {
source: node,
label: property.clone(),
target: t,
})
},
));
}
Axis::ReverseResolve { property, hint } => {
let mut all = Vec::new();
collect_subtree(adapter, adapter.root(), &mut all);
let matched: Vec<NodeId> = all
.into_iter()
.filter(|&source| {
adapter.resolve(source, property, hint.as_deref()) == Some(node)
&& tests_ok(adapter, source, step)
})
.collect();
out.extend(apply_predicates(
adapter,
matched,
step,
trace,
outer,
marks,
|&n| n,
|&source| {
Some(EdgeCtx {
source,
label: property.clone(),
target: node,
})
},
));
}
}
}
fn collect_subtree(adapter: &impl AstAdapter, node: NodeId, out: &mut Vec<NodeId>) {
out.push(node);
for child in adapter.children(node) {
collect_subtree(adapter, child, out);
}
}
fn descendants(
adapter: &impl AstAdapter,
node: NodeId,
depth: usize,
step: &Step,
out: &mut Vec<(NodeId, usize)>,
) {
for child in adapter.children(node) {
if matches_step(adapter, child, step) {
out.push((child, depth));
}
descendants(adapter, child, depth + 1, step, out);
}
}
fn matches_step(adapter: &impl AstAdapter, node: NodeId, step: &Step) -> bool {
matches_name(adapter, node, &step.matcher) && tests_ok(adapter, node, step)
}
fn tests_ok(adapter: &impl AstAdapter, node: NodeId, step: &Step) -> bool {
if !traits_ok(adapter, node, step) {
return false;
}
!step.leaf || adapter.children(node).is_empty()
}
fn apply_predicates<T>(
adapter: &impl AstAdapter,
mut items: Vec<T>,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_>>,
marks: &[Mark],
node_of: impl Fn(&T) -> NodeId,
edge_of: impl Fn(&T) -> Option<EdgeCtx>,
) -> Vec<T> {
let mut i = 0;
while i < step.predicates.len() {
match &step.predicates[i] {
pred @ (Predicate::Index(_) | Predicate::Range(_, _)) => {
items = positional(items, pred);
i += 1;
}
Predicate::Expr(_) => {
let mut run: Vec<&PredExpr> = Vec::new();
while let Some(Predicate::Expr(e)) = step.predicates.get(i) {
run.push(e);
i += 1;
}
items.retain(|item| {
let edge = edge_of(item);
exists_binding(
adapter,
node_of(item),
&run,
trace,
&mut Vec::new(),
Scope {
outer,
edge: edge.as_ref(),
marks,
..NO_SCOPE
},
)
});
}
}
}
items
}
fn positional<T>(items: Vec<T>, pred: &Predicate) -> Vec<T> {
match pred {
Predicate::Index(n) => match resolve_pos(*n, items.len()) {
Some(idx) => items.into_iter().nth(idx).into_iter().collect(),
None => Vec::new(),
},
Predicate::Range(start, end) => {
let len = items.len();
let s = match start {
None | Some(0) => 0,
Some(v) if *v > 0 => (*v - 1) as usize,
Some(v) => (len as i64 + *v).max(0) as usize,
};
let e = match end {
None => len,
Some(0) => 0,
Some(v) if *v > 0 => (*v as usize).min(len),
Some(v) => (len as i64 + *v + 1).max(0) as usize,
};
if s < e {
items.into_iter().skip(s).take(e - s).collect()
} else {
Vec::new()
}
}
Predicate::Expr(_) => items,
}
}
fn resolve_pos(i: i64, len: usize) -> Option<usize> {
if i > 0 && i as usize <= len {
Some(i as usize - 1)
} else if i < 0 && len as i64 + i >= 0 {
Some((len as i64 + i) as usize)
} else {
None
}
}
fn exists_binding(
adapter: &impl AstAdapter,
node: NodeId,
exprs: &[&PredExpr],
trace: &Trace,
bound: &mut Vec<Option<NodeId>>,
scope: Scope<'_>,
) -> bool {
if bound.len() == trace.contexts.len() {
let hit = exprs.iter().all(|e| {
mentions_null_ctx(e, bound) || eval_pred_expr(adapter, node, e, trace, bound, scope)
});
if hit && !bound.is_empty() {
trace
.witnesses
.borrow_mut()
.entry(node.0)
.or_insert_with(|| bound.clone());
}
return hit;
}
for &t in &trace.contexts[bound.len()] {
bound.push(Some(t));
let hit = exists_binding(adapter, node, exprs, trace, bound, scope);
bound.pop();
if hit {
return true;
}
}
if trace.outer.get(bound.len()).copied().unwrap_or(false) {
bound.push(None);
let hit = exists_binding(adapter, node, exprs, trace, bound, scope);
bound.pop();
if hit {
return true;
}
}
false
}
fn mentions_null_ctx(e: &PredExpr, bound: &[Option<NodeId>]) -> bool {
fn op_mentions(op: &Operand, bound: &[Option<NodeId>]) -> bool {
match op {
Operand::Ctx { index, steps, .. } => {
matches!(index, Some(k) if matches!(bound.get(k.saturating_sub(1)), Some(None)))
|| steps_mention(steps, bound)
}
Operand::Rel { steps, .. } => steps_mention(steps, bound),
Operand::Neg(inner) | Operand::Outer(inner) => op_mentions(inner, bound),
Operand::Arith { left, right, .. } => {
op_mentions(left, bound) || op_mentions(right, bound)
}
Operand::Group(inner) => mentions_null_ctx(inner, bound),
Operand::Piped { expr, .. } => op_mentions(expr, bound),
Operand::Cond { cond, then, other } => {
mentions_null_ctx(cond, bound)
|| op_mentions(then, bound)
|| op_mentions(other, bound)
}
Operand::Match {
scrutinee,
arms,
other,
} => {
op_mentions(scrutinee, bound)
|| arms.iter().any(|(test, _, result)| {
op_mentions(test, bound) || op_mentions(result, bound)
})
|| op_mentions(other, bound)
}
Operand::Interp(segs) => segs.iter().any(|seg| match seg {
InterpSeg::Expr(inner) => op_mentions(inner, bound),
InterpSeg::Text(_) => false,
}),
_ => false,
}
}
fn steps_mention(steps: &[PathElem], bound: &[Option<NodeId>]) -> bool {
steps.iter().any(|elem| match elem {
PathElem::Step(s) => preds_mention(&s.predicates, bound),
PathElem::Group(g) => {
preds_mention(&g.predicates, bound)
|| g.alts.iter().any(|alt| steps_mention(alt, bound))
}
PathElem::Push { body, .. } => match body {
PushBody::Expr(o) => op_mentions(o, bound),
PushBody::Query(_) => false,
},
PathElem::Mark(_) => false,
})
}
fn preds_mention(preds: &[Predicate], bound: &[Option<NodeId>]) -> bool {
preds.iter().any(|p| match p {
Predicate::Expr(e) => mentions_null_ctx(e, bound),
Predicate::Index(_) | Predicate::Range(_, _) => false,
})
}
match e {
PredExpr::Or(a, b) | PredExpr::And(a, b) => {
mentions_null_ctx(a, bound) || mentions_null_ctx(b, bound)
}
PredExpr::Not(a) => mentions_null_ctx(a, bound),
PredExpr::Compare(l, _, r) => op_mentions(l, bound) || op_mentions(r, bound),
PredExpr::Truthy(o) => op_mentions(o, bound),
}
}
fn matches_label(matcher: &Matcher, label: &str) -> bool {
matches!(matcher, Matcher::Any) || matcher.matches(label)
}
fn traits_ok(adapter: &impl AstAdapter, node: NodeId, step: &Step) -> bool {
if step.traits.is_empty() {
return true;
}
let node_traits = adapter.traits(node);
step.traits.iter().all(|c| c.matches(&node_traits))
}
fn extract_captures(
e: &PredExpr,
adapter: &impl AstAdapter,
node: NodeId,
trace: &Trace,
scope: Scope<'_>,
) -> Option<Vec<String>> {
match e {
PredExpr::Or(a, b) | PredExpr::And(a, b) => {
let first = extract_captures(a, adapter, node, trace, scope);
extract_captures(b, adapter, node, trace, scope).or(first)
}
PredExpr::Not(a) => extract_captures(a, adapter, node, trace, scope),
PredExpr::Compare(l, CmpOp::Match, r) => {
let pattern = operand_scalar_bound(adapter, node, r, trace, &[], scope);
let re = Regex::new(&pattern.to_string()).ok()?;
if re.captures_len() <= 1 {
return None;
}
eval_operand(adapter, node, l, trace, &[], scope)
.iter()
.find_map(|v| {
let text = v.to_string();
re.captures(&text).map(|caps| {
caps.iter()
.skip(1)
.map(|g| g.map_or(String::new(), |m| m.as_str().to_string()))
.collect()
})
})
}
PredExpr::Compare(..) | PredExpr::Truthy(_) => None,
}
}
fn eval_pred_expr(
adapter: &impl AstAdapter,
node: NodeId,
e: &PredExpr,
trace: &Trace,
bound: &[Option<NodeId>],
scope: Scope<'_>,
) -> bool {
match e {
PredExpr::Or(a, b) => {
eval_pred_expr(adapter, node, a, trace, bound, scope)
|| eval_pred_expr(adapter, node, b, trace, bound, scope)
}
PredExpr::And(a, b) => {
eval_pred_expr(adapter, node, a, trace, bound, scope)
&& eval_pred_expr(adapter, node, b, trace, bound, scope)
}
PredExpr::Not(a) => !eval_pred_expr(adapter, node, a, trace, bound, scope),
PredExpr::Compare(l, op, r) => {
let lhs = eval_operand(adapter, node, l, trace, bound, scope);
let rhs = eval_operand(adapter, node, r, trace, bound, scope);
lhs.iter().any(|a| {
rhs.iter()
.any(|b| compare(a, *op, b, &|e| adapter.unit_scale(e)))
})
}
PredExpr::Truthy(o) => eval_operand(adapter, node, o, trace, bound, scope)
.iter()
.any(Value::is_truthy),
}
}
fn eval_operand(
adapter: &impl AstAdapter,
node: NodeId,
operand: &Operand,
trace: &Trace,
bound: &[Option<NodeId>],
scope: Scope<'_>,
) -> Vec<Value> {
match operand {
Operand::Lit(v) => vec![v.clone()],
Operand::Match {
scrutinee,
arms,
other,
} => {
let subject = operand_scalar_bound(adapter, node, scrutinee, trace, bound, scope);
for (test, regex, result) in arms {
let hit = if *regex {
let pat = operand_scalar_bound(adapter, node, test, trace, bound, scope);
regex_test(&subject, &pat, true)
} else {
let t = operand_scalar_bound(adapter, node, test, trace, bound, scope);
value_eq(&subject, &t, &|e| adapter.unit_scale(e))
};
if hit {
return eval_operand(adapter, node, result, trace, bound, scope);
}
}
eval_operand(adapter, node, other, trace, bound, scope)
}
Operand::Now => vec![
adapter
.invocation_instant()
.map(|(secs, nanos)| Value::Instant {
secs,
nanos,
offset_min: Some(0),
})
.unwrap_or(Value::Null),
],
Operand::Recall(r) => vec![recall(scope.register, r)],
Operand::Topic => vec![scope.topic.cloned().unwrap_or(Value::Null)],
Operand::Ordinal => vec![
scope
.ordinal
.map(|i| Value::Int(i as i64))
.unwrap_or(Value::Null),
],
Operand::Cond { cond, then, other } => {
let taken = if eval_pred_expr(adapter, node, cond, trace, bound, scope) {
then
} else {
other
};
eval_operand(adapter, node, taken, trace, bound, scope)
}
Operand::Capsae { projection } => {
let Some(peers) = scope.peers else {
return vec![Value::Null];
};
let vs: Vec<Value> = peers
.iter()
.map(|p| match projection {
None => p
.topic
.clone()
.unwrap_or_else(|| node_scalar(adapter, p.node)),
Some(proj) => project(adapter, p.node, proj),
})
.collect();
vec![Value::List(vs)]
}
Operand::Piped { expr, stages } => {
let mut state = eval_operand(adapter, node, expr, trace, bound, scope);
for stage in stages.iter() {
if matches!(stage, Stage::Agg(_) | Stage::Select(_))
&& state.len() == 1
&& matches!(state[0], Value::List(_))
{
let Some(Value::List(items)) = state.pop() else {
unreachable!("matched above");
};
state = items;
}
let caps: Vec<Capsa> = state
.into_iter()
.map(|v| Capsa {
node,
register: scope.register.to_vec(),
topic: Some(v),
members: Vec::new(),
captures: scope.captures.to_vec(),
marks: scope.marks.to_vec(),
bindings: bound.to_vec(),
arrived: scope.arrived.to_vec(),
})
.collect();
state = apply_stage(stage, caps, adapter, trace, Some(&scope))
.into_iter()
.map(|c| c.topic.unwrap_or(Value::Null))
.collect();
}
state
}
Operand::Edges { projection } => {
let vs: Vec<Value> = scope
.arrived
.iter()
.map(|edge| match projection {
None | Some(Projection::Property(None)) => Value::Str(edge.label.clone()),
Some(Projection::Property(Some(prop))) => adapter
.link_property(edge.source, &edge.label, edge.target, prop)
.unwrap_or(Value::Null),
Some(_) => Value::Null,
})
.collect();
vec![Value::List(vs)]
}
Operand::Edge { projection } => {
let Some(edge) = scope.edge else {
return vec![Value::Null];
};
let v = match projection {
None | Some(Projection::Property(None)) => Value::Str(edge.label.clone()),
Some(Projection::Property(Some(prop))) => adapter
.link_property(edge.source, &edge.label, edge.target, prop)
.unwrap_or(Value::Null),
Some(_) => Value::Null,
};
vec![v]
}
Operand::Param(_) => vec![Value::Null],
Operand::Outer(inner) => match scope.outer {
Some(o) => eval_operand(adapter, node, inner, trace, bound, *o),
None => vec![Value::Null],
},
Operand::Interp(segs) => {
let mut out = String::new();
for seg in segs {
match seg {
InterpSeg::Text(t) => out.push_str(t),
InterpSeg::Expr(e) => {
let v = operand_scalar_bound(adapter, node, e, trace, bound, scope);
out.push_str(&v.to_string());
}
}
}
vec![Value::Str(out)]
}
Operand::Capture(n) => vec![
scope
.captures
.get(n - 1)
.map(|s| Value::Str(s.clone()))
.unwrap_or(Value::Null),
],
Operand::Rel {
steps,
projection,
anchored,
mark,
} => {
let from = if let Some(m) = mark {
match scope.marks.iter().rev().find(|k| &k.name == m) {
Some(k) => k.node,
None => return Vec::new(),
}
} else if *anchored {
adapter.root()
} else {
node
};
let nodes = navigate_from(steps, adapter, from, trace, scope.outer, scope.marks);
match projection {
Some(p) => nodes.iter().map(|&n| project(adapter, n, p)).collect(),
None => vec![Value::Bool(true); nodes.len()],
}
}
Operand::Arith { op, left, right } => {
let l = operand_scalar_bound(adapter, node, left, trace, bound, scope);
let r = operand_scalar_bound(adapter, node, right, trace, bound, scope);
vec![arith(*op, &l, &r, &|e| adapter.unit_scale(e))]
}
Operand::Neg(inner) => {
let v = operand_scalar_bound(adapter, node, inner, trace, bound, scope);
vec![match v.numeric_reading() {
Some(Value::Int(n)) => n
.checked_neg()
.map(Value::Int)
.unwrap_or(Value::Float(-(n as f64))),
Some(Value::Float(f)) => Value::Float(-f),
_ => Value::Null,
}]
}
Operand::Group(e) => vec![Value::Bool(eval_pred_expr(
adapter, node, e, trace, bound, scope,
))],
Operand::Ctx {
index,
steps,
projection,
} => {
let bound = if bound.is_empty() {
scope.bindings
} else {
bound
};
let base = match index {
Some(k) => bound.get(k.saturating_sub(1)).copied(),
None => Some(Some(node)),
};
let base = match base {
None => return Vec::new(),
Some(None) => return vec![Value::Null],
Some(Some(n)) => n,
};
let nodes = if steps.is_empty() {
vec![base]
} else {
navigate_from(steps, adapter, base, trace, scope.outer, scope.marks)
};
match projection {
Some(p) => nodes.iter().map(|&n| project(adapter, n, p)).collect(),
None => vec![Value::Bool(true); nodes.len()],
}
}
}
}
pub(crate) type UnitScale<'a> = &'a dyn Fn(&str) -> Option<(f64, String)>;
fn compare(a: &Value, op: CmpOp, b: &Value, scale: UnitScale) -> bool {
match op {
CmpOp::Eq => value_eq(a, b, scale),
CmpOp::Ne => !value_eq(a, b, scale),
CmpOp::Lt => value_cmp(a, b, scale) == Some(Ordering::Less),
CmpOp::Le => matches!(
value_cmp(a, b, scale),
Some(Ordering::Less | Ordering::Equal)
),
CmpOp::Gt => value_cmp(a, b, scale) == Some(Ordering::Greater),
CmpOp::Ge => matches!(
value_cmp(a, b, scale),
Some(Ordering::Greater | Ordering::Equal)
),
CmpOp::Match => regex_test(a, b, true),
CmpOp::NotMatch => regex_test(a, b, false),
CmpOp::Contains => match (a, b) {
(Value::Null, _) | (_, Value::Null) => false,
_ => a.to_string().contains(&b.to_string()),
},
}
}
fn value_eq(a: &Value, b: &Value, scale: UnitScale) -> bool {
if matches!(a, Value::Instant { .. }) || matches!(b, Value::Instant { .. }) {
return match (a.temporal_reading(), b.temporal_reading()) {
(Some(x), Some(y)) => x == y,
_ => false,
};
}
if matches!(a, Value::Duration { .. }) || matches!(b, Value::Duration { .. }) {
return match (a.durational_reading(), b.durational_reading()) {
(Some(x), Some(y)) => x == y,
_ => false,
};
}
if matches!(a, Value::Quantity { .. }) || matches!(b, Value::Quantity { .. }) {
return match crate::value::quantital_pair_with(a, b, scale) {
Some((x, y)) => x == y,
None => false,
};
}
if let (Some(x), Some(y)) = (a.numeric(), b.numeric()) {
return x == y;
}
match (a, b) {
(Value::Str(x), Value::Str(y)) => x == y,
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::Null, Value::Null) => true,
_ => false,
}
}
fn value_cmp(a: &Value, b: &Value, scale: UnitScale) -> Option<Ordering> {
if matches!(a, Value::Instant { .. }) || matches!(b, Value::Instant { .. }) {
return match (a.temporal_reading(), b.temporal_reading()) {
(Some(x), Some(y)) => Some(x.cmp(&y)),
_ => None,
};
}
if matches!(a, Value::Duration { .. }) || matches!(b, Value::Duration { .. }) {
return match (a.durational_reading(), b.durational_reading()) {
(Some(x), Some(y)) => Some(x.cmp(&y)),
_ => None,
};
}
if matches!(a, Value::Quantity { .. }) || matches!(b, Value::Quantity { .. }) {
return crate::value::quantital_pair_with(a, b, scale).and_then(|(x, y)| x.partial_cmp(&y));
}
if let (Some(x), Some(y)) = (a.numeric(), b.numeric()) {
return x.partial_cmp(&y);
}
match (a, b) {
(Value::Str(x), Value::Str(y)) => Some(x.cmp(y)),
_ => None,
}
}
fn node_to_json(adapter: &impl AstAdapter, node: NodeId) -> Value {
let children = adapter.children(node);
if children.is_empty() {
return adapter.default_value(node).unwrap_or(Value::Null);
}
let names: Vec<Option<String>> = children.iter().map(|&c| adapter.name(c)).collect();
let is_array = names
.iter()
.enumerate()
.all(|(i, n)| n.as_deref() == Some(i.to_string().as_str()));
if is_array {
Value::List(children.iter().map(|&c| node_to_json(adapter, c)).collect())
} else {
Value::Record(
children
.iter()
.zip(&names)
.map(|(&c, n)| (n.clone().unwrap_or_default(), node_to_json(adapter, c)))
.collect(),
)
}
}
fn value_to_xml(v: &Value, tag: &str) -> String {
match v {
Value::Record(fields) => {
let mut attrs = String::new();
let mut body = String::new();
for (k, val) in fields {
if let Some(a) = k.strip_prefix('@') {
attrs.push_str(&format!(" {a}=\"{}\"", xml_escape(&val.to_string())));
} else if k == "#text" {
body.push_str(&xml_escape(&val.to_string()));
} else if let Value::List(items) = val {
for it in items {
body.push_str(&value_to_xml(it, &xml_tag(k)));
}
} else {
body.push_str(&value_to_xml(val, &xml_tag(k)));
}
}
format!("<{tag}{attrs}>{body}</{tag}>")
}
Value::List(items) => items.iter().map(|it| value_to_xml(it, tag)).collect(),
other => format!("<{tag}>{}</{tag}>", xml_escape(&other.to_string())),
}
}
fn node_to_xml(adapter: &impl AstAdapter, node: NodeId) -> String {
match adapter.name(node) {
Some(tag) => node_to_xml_named(adapter, node, &xml_tag(&tag)),
None => adapter
.children(node)
.iter()
.map(|&c| {
let t = adapter
.name(c)
.map(|n| xml_tag(&n))
.unwrap_or_else(|| "item".into());
node_to_xml_named(adapter, c, &t)
})
.collect(),
}
}
fn node_to_xml_named(adapter: &impl AstAdapter, node: NodeId, tag: &str) -> String {
let children = adapter.children(node);
if children.is_empty() {
let body = adapter
.default_value(node)
.map(|v| xml_escape(&v.to_string()))
.unwrap_or_default();
format!("<{tag}>{body}</{tag}>")
} else {
let inner: String = children
.iter()
.map(|&c| {
let t = adapter
.name(c)
.map(|n| xml_tag(&n))
.unwrap_or_else(|| "item".into());
node_to_xml_named(adapter, c, &t)
})
.collect();
format!("<{tag}>{inner}</{tag}>")
}
}
fn xml_tag(name: &str) -> String {
let mut chars = name.chars();
let ok = matches!(chars.next(), Some(c) if c.is_alphabetic() || c == '_')
&& chars.all(|c| c.is_alphanumeric() || matches!(c, '_' | '-' | '.' | ':'));
if ok {
name.to_string()
} else {
"item".to_string()
}
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn run_shell(cmd: &str, topic: Option<&Value>) -> Option<Value> {
use std::io::Write;
let mut child = std::process::Command::new("sh")
.arg("-c")
.arg(cmd)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.ok()?;
if let Some(v) = topic
&& let Some(mut stdin) = child.stdin.take()
{
let text = v.to_string();
std::thread::spawn(move || {
let _ = stdin.write_all(text.as_bytes());
if !text.ends_with('\n') {
let _ = stdin.write_all(b"\n");
}
});
}
drop(child.stdin.take());
let out = child.wait_with_output().ok()?;
if !out.status.success() {
return None;
}
let mut text = String::from_utf8_lossy(&out.stdout).into_owned();
if text.ends_with('\n') {
text.pop();
}
Some(Value::Str(text))
}
fn regex_test(a: &Value, b: &Value, want: bool) -> bool {
if matches!(a, Value::Null) || matches!(b, Value::Null) {
return !want;
}
match Regex::new(&b.to_string()) {
Ok(re) => re.is_match(&a.to_string()) == want,
Err(_) => false,
}
}
fn matches_name(adapter: &impl AstAdapter, node: NodeId, matcher: &Matcher) -> bool {
match matcher {
Matcher::Any | Matcher::Dot => true,
_ => adapter
.name(node)
.is_some_and(|name| matcher.matches(&name)),
}
}
fn sibling(adapter: &impl AstAdapter, node: NodeId, offset: isize) -> Option<NodeId> {
let parent = adapter.parent(node)?;
let sibs = adapter.children(parent);
let i = sibs.iter().position(|&s| s == node)? as isize + offset;
if i < 0 {
return None;
}
sibs.get(i as usize).copied()
}
fn by_reach(matches: Vec<(NodeId, usize)>, reach: Reach) -> Vec<NodeId> {
let extreme = match reach {
Reach::All => None,
Reach::Proximal => matches.iter().map(|&(_, d)| d).min(),
Reach::Distal => matches.iter().map(|&(_, d)| d).max(),
};
matches
.into_iter()
.filter(|&(_, d)| extreme.is_none_or(|e| d == e))
.map(|(n, _)| n)
.collect()
}
fn dedup(nodes: Vec<NodeId>) -> Vec<NodeId> {
let mut seen = HashSet::new();
nodes.into_iter().filter(|n| seen.insert(*n)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::lex;
use crate::parser::parse;
use std::collections::HashMap;
#[test]
fn quantital_coercion_and_arith() {
let sc: &dyn Fn(&str) -> Option<(f64, String)> = &crate::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(42000.0), sc));
assert_eq!(
value_cmp(&len, &Value::Str("30mi".into()), sc),
Some(Ordering::Less)
);
let power = q(290.0, "kg*m^2/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(), "42500 m");
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::quantity::scale_expr;
let two_h = Value::Duration {
secs: 7200,
nanos: 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),
};
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(), "PT9H");
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(), "PT2H30M");
}
struct MockTree {
names: Vec<Option<String>>,
kids: HashMap<u64, Vec<u64>>,
links: HashMap<u64, Vec<(String, u64)>>,
edge_props: HashMap<(u64, String, u64), Vec<(String, Value)>>,
}
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,
links,
edge_props: 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,
links: HashMap::new(),
edge_props: 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 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 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(
&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 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 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 = $*1:::depth]", &t), vec![1, 4]);
assert_eq!(
run("//x.rs <=> //*.rs[:::name != $*1:::name]", &t),
vec![5, 7]
);
assert_eq!(
run("//a <=> //x.rs <=> //*[:::depth = $*1:::depth]", &t),
vec![1, 4]
);
}
#[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 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]);
}
#[test]
fn quantifier_bound_clamps_open_forms() {
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());
assert_eq!(run("/div(/ul/li)+", &t), vec![3, 5]);
assert_eq!(run("/div(/ul/li){1,3}", &t), vec![3, 5]);
assert!(run("/div(/ul/li){3,}", &t).is_empty());
}
#[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::Int(5)])]
);
assert_eq!(
vals("//a->e->e | .(@-)", &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 | .(@-::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(', ')))", &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)))", &t),
vec![Value::Int(1)]
);
assert_eq!(
vals(
"//*.rs | .top((@*:::name @| sort @| [1..2] @| join('-')))",
&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/* | rec(:::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 | .(@-::qty) $| ($_ * 10) | json", &t),
vec![Value::Str("[120, 50]".into())]
);
assert_eq!(
vals("//a->e->e | .(@-::qty) $| [$_ > 6] | json", &t),
vec![Value::Str("[12]".into())]
);
assert_eq!(
vals("//a->e->e | .(@-::qty) $| [1..1] | json", &t),
vec![Value::Str("[12]".into())]
);
assert_eq!(
vals(
"//a->e->e | .(@-::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 simple_path_dedup_is_by_visited_set() {
let t = MockTree::diamond();
assert_eq!(run("//a(->e)+", &t), vec![2, 3, 6]);
assert_eq!(run("//a(->e)+!", &t), vec![2]);
}
#[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())]
);
}
#[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]);
}
#[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("/*[not :::is-leaf]", &t), vec![1, 4]);
assert_eq!(
run("//*[:::is-leaf and :::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,
links: HashMap::new(),
edge_props: 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,
links: HashMap::new(),
edge_props: 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), Vec::<u64>::new());
assert_eq!(run("/*[1 idiv 0]", &t), Vec::<u64>::new());
assert_eq!(run("/*[:::name * 2]", &t), Vec::<u64>::new());
assert_eq!(run("/*[::a-b = 1]", &t), Vec::<u64>::new());
assert_eq!(
run("/*[(:::index = 1 or :::index = 2) and :::is-leaf]", &t),
Vec::<u64>::new()
);
}
#[test]
fn value_expression_stages() {
let t = MockTree::sample();
assert_eq!(vals("/a/x.rs | :::index * 2", &t), vec![Value::Int(2)]);
assert_eq!(
vals("/* | .double(:::index * 2) @| sum", &t),
vec![Value::Int(6)]
);
assert_eq!(
vals("/a/x.rs | (:::index + 1) * 3", &t),
vec![Value::Int(6)]
);
}
#[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_stages() {
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_err());
assert!(parse(&lex("//* @| [:::is-leaf]").unwrap()).is_err());
}
#[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 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]); }
#[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_stage() {
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)
}
fn allow_shell(&self) -> bool {
true
}
}
let t = Shelly(MockTree::sample());
let got = match crate::run(q, &t).unwrap() {
QueryResult::Values(vs) => vs,
_ => panic!("values"),
};
assert_eq!(got, vec![Value::Str("X.RS".into())]);
let got = match crate::run("/a/x.rs | :::name | `false`", &t).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 = "/a/* <=> ^//*.rs[:::name = $*1:::name]";
assert_eq!(run(q, &t), vec![2]);
let q = "/a/* <=>? ^//*.rs[:::name = $*1:::name]";
assert_eq!(run(q, &t), vec![2, 5, 7]);
assert_eq!(
vals("/a/* <=>? ^//*.rs[:::name = $*1:::name] | $*1:::name", &t),
vec![Value::Str("x.rs".into()), Value::Null, Value::Null]
);
let q = "/a/* <=>? ^//*.rs[:::name = $*1:::name] | [not $*1:::name]";
assert_eq!(run(q, &t), vec![5, 7]);
let q = "/a/* <=>? ^//*.rs[:::name = $*1:::name] | [$*1:::name]";
assert_eq!(run(q, &t), vec![2]);
}
#[test]
fn outer_correlation_indirect_ctx() {
let t = MockTree::sample();
let q = "/a/* <=>? ^//*.rs[(:::name ?= $*1:::name ? 1 : 0) = 1]";
assert_eq!(run(q, &t), vec![2, 5, 7]);
let q = "/a/* <=>? ^//*.rs[/*[$*1:::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)
}
fn invocation_instant(&self) -> Option<(i64, u32)> {
Some((self.1, self.2))
}
}
let (s, n, _) = crate::temporal::parse_iso("2026-07-12T09:00:00Z").unwrap();
let t = Clocked(MockTree::sample(), s, n);
let got = match eval(&parse(&lex("^ | (now())").unwrap()).unwrap(), &t) {
QueryResult::Values(vs) => vs,
_ => panic!("expected values"),
};
assert_eq!(
got,
vec![Value::Instant {
secs: s,
nanos: n,
offset_min: Some(0),
}]
);
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::quantity::scale_expr;
let inst = |secs| Value::Instant {
secs,
nanos: 0,
offset_min: Some(0),
};
let dur = |secs| Value::Duration { secs, nanos: 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::Null
));
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::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
));
}
}