use super::*;
use crate::adapter::{Conjunct, PlanAxis, StepPlan};
pub(crate) fn navigate_paths(
elems: &[PathElem],
adapter: &impl AstAdapter,
start: NodeId,
trace: &Trace,
outer: Option<&Scope<'_>>,
witness: &[Option<NodeId>],
seed_marks: &[Mark],
seed_register: &[Reg],
) -> Vec<(NodeId, Vec<Reg>, Vec<Mark>, Vec<EdgeCtx>)> {
let mut ctx: Vec<(NodeId, Vec<Reg>, Vec<Mark>, Vec<EdgeCtx>)> = vec![(
start,
seed_register.to_vec(),
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();
if let PathElem::Step(step) = elem {
prefetch_hint(adapter, step, ctx.iter().map(|c| c.0));
}
for (node, register, marks, _) in &ctx {
match elem {
PathElem::Step(step) => {
let mut succs = Vec::new();
apply_step(
adapter, *node, step, trace, outer, witness, marks, register, &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, witness)
.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<(Option<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 kept = &mut merged[*e.get()];
for (a, b) in kept.1.iter_mut().zip(&r) {
a.prov.origins.union(&b.prov.origins);
}
let slot = &mut kept.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
}
pub(super) fn mark_key(marks: &[Mark]) -> Vec<(Option<String>, u64)> {
marks.iter().map(|m| (m.name.clone(), m.node.0)).collect()
}
pub(super) fn group_value(
adapter: &impl AstAdapter,
node: NodeId,
expr: &Operand,
trace: &Trace,
scope: Scope<'_>,
) -> Value {
if scope.members.is_empty() {
return operand_scalar(adapter, node, expr, trace, scope);
}
let values = eval_operand(adapter, node, expr, trace, &[], scope);
match values.len() {
0 => Value::Null,
1 => values.into_iter().next().unwrap(),
_ => Value::list(values),
}
}
pub(super) fn operand_value(
adapter: &impl AstAdapter,
node: NodeId,
expr: &Operand,
trace: &Trace,
bound: &[Option<NodeId>],
scope: Scope<'_>,
) -> Value {
let values = eval_operand(adapter, node, expr, trace, bound, scope);
match values.len() {
0 => Value::Null,
1 => values.into_iter().next().unwrap(),
_ => Value::list(values),
}
}
pub(super) fn nested_outer<'a>(scope: &'a Scope<'a>) -> Option<&'a Scope<'a>> {
if scope.node.is_some() {
Some(scope)
} else {
scope.outer
}
}
pub(super) fn without_predicates(elems: &[PathElem]) -> Vec<PathElem> {
elems
.iter()
.map(|e| match e {
PathElem::Step(s) => {
let mut s = s.clone();
s.predicates.clear();
PathElem::Step(s)
}
other => other.clone(),
})
.collect()
}
pub(super) fn navigate_from(
elems: &[PathElem],
adapter: &impl AstAdapter,
start: NodeId,
trace: &Trace,
outer: Option<&Scope<'_>>,
witness: &[Option<NodeId>],
seed_marks: &[Mark],
seed_register: &[Reg],
) -> Vec<NodeId> {
dedup(
navigate_paths(
elems,
adapter,
start,
trace,
outer,
witness,
seed_marks,
seed_register,
)
.into_iter()
.map(|(n, _, _, _)| n)
.collect(),
)
}
#[derive(Clone)]
pub(super) struct GPath {
pub(super) node: NodeId,
pub(super) visited: Vec<NodeId>,
pub(super) register: Vec<Reg>,
pub(super) marks: Vec<Mark>,
pub(super) 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)),
)
}
}
pub(super) fn arrived_edge(
adapter: &impl AstAdapter,
from: NodeId,
step: &Step,
succ: NodeId,
) -> Option<EdgeCtx> {
let edge = match &step.axis {
Axis::Child | Axis::Descendant(_) | Axis::Universal => 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 = links_of(adapter, from)
.iter()
.cloned()
.find(|(l, t)| *t == succ && matches_label(&step.matcher, l))
.map(|(l, _)| l)?;
EdgeCtx {
source: from,
label,
target: succ,
}
}
Axis::InLink => {
let label = backlinks_of(adapter, from)
.iter()
.cloned()
.find(|(l, s)| *s == succ && matches_label(&step.matcher, l))
.map(|(l, _)| l)?;
EdgeCtx {
source: succ,
label,
target: from,
}
}
Axis::BothLink => {
if let Some(label) = links_of(adapter, from)
.iter()
.cloned()
.find(|(l, t)| *t == succ && matches_label(&step.matcher, l))
.map(|(l, _)| l)
{
EdgeCtx {
source: from,
label,
target: succ,
}
} else {
let label = backlinks_of(adapter, from)
.iter()
.cloned()
.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().unwrap_or_else(|| "ref".to_string()),
target: succ,
},
Axis::ReverseResolve { property, .. } => EdgeCtx {
source: succ,
label: match property {
RefProp::Named(p) => p.clone(),
_ => adapter
.ref_property(succ)
.unwrap_or_else(|| "ref".to_string()),
},
target: from,
},
Axis::ValueMatch {
property, target, ..
} => EdgeCtx {
source: from,
label: format!("{property}={target}"),
target: succ,
},
};
Some(edge)
}
pub(super) fn expand_group(
adapter: &impl AstAdapter,
group: &Group,
anchor: GPath,
trace: &Trace,
outer: Option<&Scope<'_>>,
witness: &[Option<NodeId>],
) -> Vec<GPath> {
let n_max = quantifier_bound(adapter);
if let Some(n) = group.quant.max {
if n > n_max {
record_refusal(format!(
"the pattern's explicit ceiling ({n}) exceeds the \
quantifier bound ({n_max}); raise --quantifier-bound"
));
return Vec::new();
}
} else if group.quant.min > n_max {
record_refusal(format!(
"the pattern's minimum ({}) exceeds the quantifier bound \
({n_max}); raise --quantifier-bound",
group.quant.min
));
return Vec::new();
}
let hi = group.quant.max.unwrap_or(n_max);
let admits = |p: &GPath| group_admits(adapter, group, p, trace, outer, witness);
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];
let mut answered = false;
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,
witness,
));
}
}
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 {
answered = true;
break;
}
}
}
if group.quant.max.is_none() && !answered && !frontier.is_empty() {
let mut probe = Vec::new();
for path in &frontier {
for alt in &group.alts {
probe.extend(expand_elems(
adapter,
alt,
vec![path.clone()],
trace,
outer,
witness,
));
if !probe.is_empty() {
break;
}
}
if !probe.is_empty() {
break;
}
}
if !probe.is_empty() {
record_refusal(format!(
"quantifier bound reached at round {n_max} with edges \
still to take — the closure would be silently \
incomplete; raise --quantifier-bound"
));
return Vec::new();
}
}
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()
}
pub(super) fn property_or_alias(adapter: &impl AstAdapter, node: NodeId, name: &str) -> Value {
if let Some(v) = adapter.property(node, name) {
return v;
}
if adapter.aliased_metadata(node).contains(&name)
&& let Some(v) = adapter.metadata(node, name)
{
return v;
}
Value::Null
}
pub(super) fn group_admits(
adapter: &impl AstAdapter,
group: &Group,
path: &GPath,
trace: &Trace,
outer: Option<&Scope<'_>>,
witness: &[Option<NodeId>],
) -> bool {
if !group.traits.is_empty() && !trait_clauses_ok(adapter, path.node, &group.traits) {
return false;
}
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();
let scope = Scope {
prov: None,
outer,
edge: path.last_edge.as_ref(),
marks: &path.marks,
bindings: witness,
..NO_SCOPE
};
if !witness.is_empty() {
return exprs
.iter()
.all(|e| eval_pred_expr(adapter, path.node, e, trace, witness, scope));
}
exists_binding(adapter, path.node, &exprs, trace, &mut Vec::new(), scope)
}
pub(super) fn expand_elems(
adapter: &impl AstAdapter,
elems: &[PathElem],
mut paths: Vec<GPath>,
trace: &Trace,
outer: Option<&Scope<'_>>,
witness: &[Option<NodeId>],
) -> 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();
if let PathElem::Step(step) = elem {
prefetch_hint(adapter, step, paths.iter().map(|p| p.node));
}
for path in &paths {
match elem {
PathElem::Step(step) => {
let mut succs = Vec::new();
apply_step(
adapter,
path.node,
step,
trace,
outer,
witness,
&path.marks,
&path.register,
&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,
witness,
));
}
PathElem::Mark(_) => unreachable!("handled above"),
PathElem::Push { name, body } => {
let scope = Scope {
prov: None,
edge: path.last_edge.as_ref(),
outer,
bindings: witness,
..NO_SCOPE
};
let (value, prov) = match body {
PushBody::Query(q) => {
subcontext_traced(q, adapter, path.node, trace, Some(&scope))
}
PushBody::Expr(e) => {
let (v, o) = capture(|| {
operand_value(adapter, path.node, e, trace, witness, scope)
});
(v, Prov::leaf(o))
}
};
let mut p = path.clone();
p.register.push(Reg {
prov,
site: Origin {
node: path.node,
stage: stage_now(),
},
name: name.clone(),
value,
});
next.push(p);
}
}
}
paths = dedup_paths(next);
if paths.is_empty() {
break;
}
}
paths
}
pub(super) fn dedup_paths(paths: Vec<GPath>) -> Vec<GPath> {
let mut kept: Vec<GPath> = Vec::new();
let mut index: HashMap<_, usize> = HashMap::new();
for p in paths {
let key = p.key();
match index.get(&key) {
Some(&i) => {
for (a, b) in kept[i].register.iter_mut().zip(&p.register) {
a.prov.origins.union(&b.prov.origins);
}
}
None => {
index.insert(key, kept.len());
kept.push(p);
}
}
}
kept
}
pub(crate) fn project(adapter: &impl AstAdapter, node: NodeId, proj: &Projection) -> Value {
match proj {
Projection::Property(Some(name)) => property_or_alias(adapter, node, name),
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),
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn apply_step(
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_>>,
witness: &[Option<NodeId>],
marks: &[Mark],
register: &[Reg],
out: &mut Vec<NodeId>,
) {
match &step.axis {
Axis::Child => {
let host = adapter.child_host(node);
let conjuncts = match step.predicates.first() {
Some(Predicate::Expr(e)) => indexable_equality(e)
.map(|(prop, value)| vec![Conjunct::Equals(prop, value)])
.unwrap_or_default(),
_ => Vec::new(),
};
let plan = StepPlan {
axis: PlanAxis::Child,
name: match &step.matcher {
Matcher::Name(n) => Some(n),
_ => None,
},
conjuncts,
limit: None,
order: None,
};
let mut consumed_first = false;
let selected = adapter.select(host, &plan).map(|sel| {
consumed_first = sel.applied.first().copied().unwrap_or(false);
sel.nodes
});
let matched: Vec<NodeId> = match (&step.matcher, selected) {
(Matcher::Name(_), Some(nodes)) => nodes
.into_iter()
.map(|(c, _)| c)
.filter(|&c| tests_ok(adapter, c, step))
.collect(),
(Matcher::Name(n), None) => adapter
.children_named(host, n)
.into_iter()
.filter(|&c| tests_ok(adapter, c, step))
.collect(),
(_, Some(nodes)) => nodes
.into_iter()
.map(|(c, _)| c)
.filter(|&c| matches_step(adapter, c, step))
.collect(),
(_, None) => adapter
.children(host)
.into_iter()
.filter(|&c| matches_step(adapter, c, step))
.collect(),
};
let rest;
let step = if consumed_first {
rest = Step {
predicates: step.predicates[1..].to_vec(),
..step.clone()
};
&rest
} else {
step
};
out.extend(apply_predicates(
adapter,
matched,
step,
trace,
outer,
witness,
marks,
register,
|&n| n,
|&n| {
Some(EdgeCtx {
source: node,
label: CHILD.into(),
target: n,
})
},
));
}
Axis::Descendant(reach) => descendant_step(
reach, adapter, node, step, trace, outer, witness, marks, register, out,
),
Axis::Universal => {
let mut found: Vec<(NodeId, usize)> = Vec::new();
if matches_step(adapter, node, step) {
found.push((node, 0));
}
descendants(adapter, node, 1, step, &mut found);
for n in adapter.non_tree_nodes() {
if matches_step(adapter, n, step) {
found.push((n, 0));
}
}
let found = apply_predicates(
adapter,
found,
step,
trace,
outer,
witness,
marks,
register,
|&(n, _)| n,
|&(n, _)| {
Some(EdgeCtx {
source: adapter.parent(n).unwrap_or(node),
label: CHILD.into(),
target: n,
})
},
);
out.extend(found.into_iter().map(|(n, _)| n));
}
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,
witness,
marks,
register,
|&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,
witness,
marks,
register,
|&(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,
witness,
marks,
register,
|&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,
witness,
marks,
register,
|&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,
witness,
marks,
register,
|&(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)> = links_of(adapter, node)
.iter()
.cloned()
.filter(|(label, target)| {
matches_label(&step.matcher, label) && tests_ok(adapter, *target, step)
})
.collect();
let kept = apply_predicates(
adapter,
matched,
step,
trace,
outer,
witness,
marks,
register,
|(_, 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)> = backlinks_of(adapter, node)
.iter()
.cloned()
.filter(|(label, source)| {
matches_label(&step.matcher, label) && tests_ok(adapter, *source, step)
})
.collect();
let kept = apply_predicates(
adapter,
matched,
step,
trace,
outer,
witness,
marks,
register,
|(_, n)| *n,
|(label, source)| {
Some(EdgeCtx {
source: *source,
label: label.clone(),
target: node,
})
},
);
out.extend(kept.into_iter().map(|(_, source)| source));
}
Axis::BothLink => both_link_step(
adapter, node, step, trace, outer, witness, marks, register, out,
),
Axis::Resolve {
property,
label: hint,
} => resolve_step(
property, hint, adapter, node, step, trace, outer, witness, marks, register, out,
),
Axis::ReverseResolve {
label,
scope,
property,
} => reverse_resolve_step(
label, scope, property, adapter, node, step, trace, outer, witness, marks, register,
out,
),
Axis::ValueMatch {
property,
scope,
target,
} => value_match_step(
property, scope, target, adapter, node, step, trace, outer, witness, marks, register,
out,
),
}
}
pub(super) fn descendant_step(
reach: &Reach,
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_>>,
witness: &[Option<NodeId>],
marks: &[Mark],
register: &[Reg],
out: &mut Vec<NodeId>,
) {
let mut found = Vec::new();
let mut consumed_first = false;
let indexed = match &step.matcher {
Matcher::Name(n) => {
let conjuncts = match step.predicates.first() {
Some(Predicate::Expr(e)) => indexable_equality(e)
.map(|(prop, value)| vec![Conjunct::Equals(prop, value)])
.unwrap_or_default(),
_ => Vec::new(),
};
let plan = StepPlan {
axis: PlanAxis::Descendant,
name: Some(n),
conjuncts,
limit: None,
order: None,
};
adapter.select(node, &plan).map(|sel| {
consumed_first = sel.applied.first().copied().unwrap_or(false);
sel.nodes
})
}
_ => None,
};
match indexed {
Some(v) => found.extend(v.into_iter().filter(|&(n, _)| tests_ok(adapter, n, step))),
None => descendants(adapter, node, 1, step, &mut found),
}
let rest;
let step = if consumed_first {
rest = Step {
predicates: step.predicates[1..].to_vec(),
..step.clone()
};
&rest
} else {
step
};
let found = apply_predicates(
adapter,
found,
step,
trace,
outer,
witness,
marks,
register,
|&(n, _)| n,
|&(n, _)| {
Some(EdgeCtx {
source: adapter.parent(n).unwrap_or(node),
label: CHILD.into(),
target: n,
})
},
);
out.extend(by_reach(found, *reach));
}
pub(super) fn both_link_step(
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_>>,
witness: &[Option<NodeId>],
marks: &[Mark],
register: &[Reg],
out: &mut Vec<NodeId>,
) {
let matched: Vec<(String, NodeId, bool)> = links_of(adapter, node)
.iter()
.cloned()
.map(|(label, target)| (label, target, true))
.chain(
backlinks_of(adapter, node)
.iter()
.cloned()
.map(|(label, source)| (label, source, false)),
)
.filter(|(label, other, _)| {
matches_label(&step.matcher, label) && tests_ok(adapter, *other, step)
})
.collect();
let kept = apply_predicates(
adapter,
matched,
step,
trace,
outer,
witness,
marks,
register,
|(_, n, _)| *n,
|(label, other, outgoing)| {
Some(if *outgoing {
EdgeCtx {
source: node,
label: label.clone(),
target: *other,
}
} else {
EdgeCtx {
source: *other,
label: label.clone(),
target: node,
}
})
},
);
out.extend(kept.into_iter().map(|(_, other, _)| other));
}
pub(super) fn resolve_step(
property: &Option<String>,
hint: &Option<String>,
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_>>,
witness: &[Option<NodeId>],
marks: &[Mark],
register: &[Reg],
out: &mut Vec<NodeId>,
) {
if !adapter.resolves_references() {
record_refusal(
"resolution follows references, and this source defines none \
(no keys, no reference properties): match by value instead — \
'::a=>(/scope/*::b)' — or declare the reference in a model \
('ref /scope/*::a --> target;')"
.into(),
);
return;
}
let prop = property.clone().or_else(|| adapter.ref_property(node));
let Some(prop) = prop else { return };
let property = ∝
if hint.is_none() && adapter.resolve_needs_label(node, property) {
record_refusal(format!(
"'::{property}-->' needs a reference label here: the property \
declares no target, so name the container it points into — \
'::{property}-->target'"
));
return;
}
let mut resolved: Vec<NodeId> = adapter
.resolve(node, property, hint.as_deref())
.into_iter()
.collect();
if resolved.is_empty() {
if let Some(ext) = adapter.external_ref(node, property, hint.as_deref()) {
let frag = ext.within;
match find_tree(
adapter,
&ext.document,
node,
trace,
outer,
witness,
marks,
register,
) {
Ok(root) => {
let landed = frag
.and_then(|f| adapter.resolve_fragment(root, &f))
.unwrap_or(root);
resolved.push(landed);
}
Err(Some(identity)) => record_ref(identity),
Err(None) => {}
}
}
}
let matched: Vec<NodeId> = resolved
.into_iter()
.filter(|&t| tests_ok(adapter, t, step))
.collect();
out.extend(apply_predicates(
adapter,
matched,
step,
trace,
outer,
witness,
marks,
register,
|&n| n,
|&t| {
Some(EdgeCtx {
source: node,
label: adapter
.ref_label(node, property)
.unwrap_or_else(|| property.clone()),
target: t,
})
},
));
}
pub(super) fn reverse_resolve_step(
label: &Option<String>,
scope: &Option<crate::ast::ScopePath>,
property: &RefProp,
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_>>,
witness: &[Option<NodeId>],
marks: &[Mark],
register: &[Reg],
out: &mut Vec<NodeId>,
) {
let indexed = match (scope, property) {
(Some(sp), RefProp::Named(p)) => {
adapter.index_referrers(&sp.text(), p, label.as_deref(), node)
}
_ => None,
};
let implied = label.clone().or_else(|| adapter.reverse_label(node));
let narrowed = |memo: &ScopeMemo| match property {
RefProp::Named(p) => memo_referrers(
adapter,
memo,
p,
|_| vec![p.clone()],
label.as_deref(),
implied.as_deref(),
node,
),
RefProp::Default => memo_referrers(
adapter,
memo,
&format!(
"\u{0}default:{}",
adapter.parent(node).map_or(u64::MAX, |p| p.0)
),
|u| adapter.reference_properties(u, node),
label.as_deref(),
implied.as_deref(),
node,
),
RefProp::Any => memo_referrers(
adapter,
memo,
"\u{0}any",
|u| adapter.property_names(u),
label.as_deref(),
None,
node,
),
};
let mut memo: Option<std::rc::Rc<ScopeMemo>> = None;
let candidates: Vec<NodeId> = match scope {
Some(_) if indexed.is_some() => indexed.unwrap_or_default(),
Some(sp) => {
let text = sp.text();
let walk = || {
navigate_from(
&sp.0,
adapter,
adapter.root(),
trace,
outer,
witness,
marks,
register,
)
};
if scope_is_static(&text) {
let m = scope_memo(adapter, &text, walk);
let c = narrowed(&m);
memo = Some(m);
c
} else {
walk()
}
}
None => {
let named = match property {
RefProp::Named(p) => Some(p.as_str()),
_ => None,
};
match adapter.reverse_resolve(node, named, label.as_deref()) {
Some(sources) if *property != RefProp::Any => {
let found: Vec<(NodeId, String)> = sources
.into_iter()
.filter(|&u| tests_ok(adapter, u, step))
.map(|u| {
let p = named
.map(str::to_string)
.or_else(|| {
adapter.reference_properties(u, node).into_iter().next()
})
.unwrap_or_else(|| "ref".to_string());
(u, p)
})
.collect();
out.extend(reverse_found(
adapter, node, found, step, trace, outer, witness, marks, register,
));
return;
}
_ => {
if !adapter.walks_for_references() {
record_refusal(
"reverse resolution over the whole arbor: this source \
does not open to the walk — give the arrow a scope, \
'<--(/path/*::y)', or declare the reference in a model"
.into(),
);
return;
}
let m = scope_memo(adapter, "", || whole_arbor(adapter));
let c = narrowed(&m);
memo = Some(m);
c
}
}
}
};
if !adapter.resolves_references() {
record_refusal(
"reverse resolution follows references, and this source defines \
none (no keys, no reference properties): match by value \
instead — 't::id=>(/scope/*::y)' — or declare the reference \
in a model ('ref /scope/*::y --> target;')"
.into(),
);
return;
}
let asks_declaration = match property {
RefProp::Any => label.is_none(),
RefProp::Default => true,
RefProp::Named(_) => false,
};
if asks_declaration {
let declares = |u: NodeId| match property {
RefProp::Default => !adapter.reference_properties(u, node).is_empty(),
_ => adapter.can_refer_to(u, node) == Some(true),
};
let declared = |ns: &[NodeId]| ns.iter().any(|&u| u != node && declares(u));
let memo_key = (
*property == RefProp::Any,
adapter.parent(node).map_or(u64::MAX, |p| p.0),
);
let known = memo
.as_ref()
.and_then(|m| m.declared.borrow().get(&memo_key).copied());
let mut ok = match (known, &memo) {
(Some(k), _) => k,
(None, Some(m)) => declared(&m.nodes),
(None, None) => declared(&candidates),
};
if !ok
&& known.is_none()
&& let Some(sp) = scope
{
let bare = without_predicates(&sp.0);
let all = navigate_from(
&bare,
adapter,
adapter.root(),
trace,
outer,
witness,
marks,
register,
);
ok = declared(&all);
if !ok && all.is_empty() && !bare.is_empty() {
let containers = navigate_from(
&bare[..bare.len() - 1],
adapter,
adapter.root(),
trace,
outer,
witness,
marks,
register,
);
let answers: Vec<Option<bool>> = containers
.iter()
.map(|&c| adapter.can_refer_to(c, node))
.collect();
ok = answers.contains(&Some(true)) || !answers.contains(&Some(false));
}
}
if let Some(m) = &memo {
m.declared.borrow_mut().insert(memo_key, ok);
}
if !ok {
let msg = if *property == RefProp::Any {
let remedy = implied
.as_deref()
.map(|c| format!(", or write the reference label, '<--{c}(…::*)'"))
.unwrap_or_default();
format!(
"reverse resolution through any property ('::*'): nothing \
declared on the scoped nodes can refer to this node's \
container, so an empty answer would not mean that nothing \
points here — name the property, '<--(…::y)'{remedy}"
)
} else {
let place = if scope.is_some() {
"the scoped nodes declare"
} else {
"this source declares"
};
format!(
"reverse resolution with no property named: {place} no \
reference property that can point at this node's \
container, so there is nothing to read — name the \
property, '<--(…::y)'"
)
};
record_refusal(msg);
return;
}
}
let mut found: Vec<(NodeId, String)> = Vec::new();
for u in candidates {
if u == node {
continue;
}
let props: Vec<String> = match property {
RefProp::Named(p) => vec![p.clone()],
RefProp::Default => adapter.reference_properties(u, node),
RefProp::Any => adapter.property_names(u),
};
if let Some(p) = props.into_iter().find(|p| {
adapter.resolve(u, p, label.as_deref()) == Some(node)
|| (label.is_none()
&& *property != RefProp::Any
&& adapter.resolve(u, p, implied.as_deref()) == Some(node))
}) && tests_ok(adapter, u, step)
{
found.push((u, p));
}
}
out.extend(reverse_found(
adapter, node, found, step, trace, outer, witness, marks, register,
));
}
pub(super) fn value_match_step(
property: &String,
scope: &Option<crate::ast::ScopePath>,
target: &String,
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_>>,
witness: &[Option<NodeId>],
marks: &[Mark],
register: &[Reg],
out: &mut Vec<NodeId>,
) {
let Some(own) = adapter.property(node, property) else {
return;
};
let candidates: Vec<NodeId> = match scope {
Some(sp) => {
let text = sp.text();
match adapter.index_lookup(&text, target, &own) {
Some(hits) => hits,
None => {
let walk = || {
navigate_from(
&sp.0,
adapter,
adapter.root(),
trace,
outer,
witness,
marks,
register,
)
};
if scope_is_static(&text) {
let memo = scope_memo(adapter, &text, walk);
memo_holders(adapter, &memo, target, &own)
} else {
walk()
}
}
}
}
None => {
if !adapter.walks_for_references() {
record_refusal(
"value match over the whole arbor: this source does not \
open to the walk — give the match a scope, '=>(/path/*::b)'"
.into(),
);
return;
}
let memo = scope_memo(adapter, "", || whole_arbor(adapter));
memo_holders(adapter, &memo, target, &own)
}
};
let scale = |e: &str| adapter.unit_scale(e);
let matched: Vec<NodeId> = candidates
.into_iter()
.filter(|&u| {
adapter
.property(u, target)
.is_some_and(|v| value_eq(&own, &v, &scale))
&& tests_ok(adapter, u, step)
})
.collect();
out.extend(apply_predicates(
adapter,
matched,
step,
trace,
outer,
witness,
marks,
register,
|&n| n,
|&u| {
Some(EdgeCtx {
source: node,
label: format!("{property}={target}"),
target: u,
})
},
));
}
#[allow(clippy::too_many_arguments)]
pub(super) fn reverse_found(
adapter: &impl AstAdapter,
node: NodeId,
found: Vec<(NodeId, String)>,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_>>,
witness: &[Option<NodeId>],
marks: &[Mark],
register: &[Reg],
) -> Vec<NodeId> {
apply_predicates(
adapter,
found,
step,
trace,
outer,
witness,
marks,
register,
|&(n, _)| n,
|&(source, ref p)| {
Some(EdgeCtx {
source,
label: adapter.ref_label(source, p).unwrap_or_else(|| p.clone()),
target: node,
})
},
)
.into_iter()
.map(|(n, _)| n)
.collect()
}
pub(super) fn prefetch_hint(
adapter: &impl AstAdapter,
step: &Step,
nodes: impl Iterator<Item = NodeId>,
) {
let dirs: &[crate::adapter::LinkDir] = match step.axis {
Axis::OutLink => &[crate::adapter::LinkDir::Out],
Axis::InLink => &[crate::adapter::LinkDir::In],
Axis::BothLink => &[crate::adapter::LinkDir::Out, crate::adapter::LinkDir::In],
Axis::Resolve { label: None, .. } => &[crate::adapter::LinkDir::Out],
_ => return,
};
let nodes: Vec<NodeId> = nodes.collect();
if nodes.len() < 2 {
return;
}
for d in dirs {
adapter.prefetch_links(&nodes, *d);
}
}
pub(super) fn collect_subtree(adapter: &impl AstAdapter, node: NodeId, out: &mut Vec<NodeId>) {
let mut stack = vec![node];
while let Some(n) = stack.pop() {
out.push(n);
stack.extend(adapter.children(n).into_iter().rev());
}
}
pub(super) fn descendants(
adapter: &impl AstAdapter,
node: NodeId,
depth: usize,
step: &Step,
out: &mut Vec<(NodeId, usize)>,
) {
let mut stack: Vec<(NodeId, usize)> = adapter
.children(node)
.into_iter()
.rev()
.map(|c| (c, depth))
.collect();
while let Some((child, d)) = stack.pop() {
if matches_step(adapter, child, step) {
out.push((child, d));
}
stack.extend(
adapter
.children(child)
.into_iter()
.rev()
.map(|c| (c, d + 1)),
);
}
}
pub(crate) fn indexable_equality(e: &PredExpr) -> Option<(&str, &Value)> {
let PredExpr::Compare(l, CmpOp::Eq, r) = e else {
return None;
};
let Operand::Rel {
steps,
projection: Some(Projection::Property(Some(prop))),
anchor: Anchor::Current,
} = l
else {
return None;
};
if !steps.is_empty() {
return None;
}
match r {
Operand::Lit(v @ Value::Str(_)) => Some((prop.as_str(), v)),
_ => None,
}
}
pub(super) fn matches_step(adapter: &impl AstAdapter, node: NodeId, step: &Step) -> bool {
matches_name(adapter, node, &step.matcher) && tests_ok(adapter, node, step)
}
pub(super) 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()
}
pub fn node_matches_elem(adapter: &impl AstAdapter, node: NodeId, elem: &PathElem) -> bool {
let trace = Correlation::default();
let edge = |n: &NodeId| {
Some(EdgeCtx {
source: adapter.parent(*n).unwrap_or(*n),
label: CHILD.into(),
target: *n,
})
};
match elem {
PathElem::Step(step) => {
matches_step(adapter, node, step)
&& !apply_predicates(
adapter,
vec![node],
step,
&trace,
None,
&[],
&[],
&[],
|&n| n,
edge,
)
.is_empty()
}
PathElem::Group(g) => {
let any_alt = g.alts.iter().any(|alt| match alt.as_slice() {
[PathElem::Step(s)] => matches_step(adapter, node, s),
_ => false,
});
if !any_alt {
return false;
}
let own = Step {
axis: Axis::Universal,
matcher: Matcher::Any,
traits: g.traits.clone(),
predicates: g.predicates.clone(),
leaf: false,
};
tests_ok(adapter, node, &own)
&& !apply_predicates(
adapter,
vec![node],
&own,
&trace,
None,
&[],
&[],
&[],
|&n| n,
edge,
)
.is_empty()
}
_ => false,
}
}
pub(super) fn apply_predicates<T>(
adapter: &impl AstAdapter,
mut items: Vec<T>,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_>>,
witness: &[Option<NodeId>],
marks: &[Mark],
register: &[Reg],
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);
let scope = Scope {
prov: None,
outer,
edge: edge.as_ref(),
marks,
register,
bindings: witness,
..NO_SCOPE
};
if !witness.is_empty() {
return run.iter().all(|e| {
eval_pred_expr(adapter, node_of(item), e, trace, witness, scope)
});
}
exists_binding(adapter, node_of(item), &run, trace, &mut Vec::new(), scope)
});
}
}
}
items
}
pub(super) 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,
}
}
pub(super) 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
}
}
pub(super) 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
}
pub(super) fn mentions_null_ctx(e: &PredExpr, bound: &[Option<NodeId>]) -> bool {
struct NullCtx<'a> {
bound: &'a [Option<NodeId>],
found: bool,
}
impl visit::Visitor for NullCtx<'_> {
fn operand(&mut self, o: &Operand) {
match o {
Operand::Ctx { index: Some(k), .. }
if matches!(self.bound.get(k.saturating_sub(1)), Some(None)) =>
{
self.found = true;
}
Operand::Piped { expr, .. } => self.operand(expr),
_ => visit::walk_operand(self, o),
}
}
fn elem(&mut self, e: &PathElem) {
if !matches!(
e,
PathElem::Push {
body: PushBody::Query(_),
..
}
) {
visit::walk_elem(self, e);
}
}
}
let mut v = NullCtx {
bound,
found: false,
};
v.pred(e);
v.found
}
pub(super) fn matches_label(matcher: &Matcher, label: &str) -> bool {
matches!(matcher, Matcher::Any) || matcher.matches(label)
}
pub(super) fn traits_ok(adapter: &impl AstAdapter, node: NodeId, step: &Step) -> bool {
trait_clauses_ok(adapter, node, &step.traits)
}
pub(super) fn trait_clauses_ok(
adapter: &impl AstAdapter,
node: NodeId,
clauses: &[crate::ast::TraitClause],
) -> bool {
if clauses.is_empty() {
return true;
}
let any = !adapter.traits(node).is_empty();
clauses
.iter()
.all(|c| c.matches_with(any, |name| adapter.has_trait(node, name)))
}
pub(super) type Captured = (Vec<String>, Vec<(String, String)>);