use super::*;
use crate::graph::adapter::{Conjunct, PlanAxis, StepPlan};
pub(crate) fn navigate_paths(
elems: &[PathElem],
adapter: &impl AstAdapter,
start: NodeId,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
seed_register: &[Reg],
) -> Vec<(GraphExt, Vec<Reg>)> {
let mut ctx: Vec<(GraphExt, Vec<Reg>)> = vec![(
GraphExt {
node: start,
bindings: Vec::new(),
arrived: Vec::new(),
unplaced: false,
},
seed_register.to_vec(),
)];
for elem in elems {
if let PathElem::Mark(name) = elem {
for (ext, register) in &mut ctx {
register.push(mark_reg(adapter, ext.node, name.clone()));
}
continue;
}
let mut next: Vec<(GraphExt, Vec<Reg>)> = Vec::new();
if let PathElem::Step(step) = elem {
prefetch_hint(adapter, step, ctx.iter().map(|c| c.0.node));
}
for (ext, register) in &ctx {
let node = ext.node;
match elem {
PathElem::Step(step) => {
let mut succs = Vec::new();
apply_step(
adapter, node, step, trace, outer, witness, ext, register, &mut succs,
);
next.extend(succs.into_iter().map(|s| {
let arrived = arrived_edge(adapter, node, step, s).into_iter().collect();
(
GraphExt {
node: s,
bindings: Vec::new(),
arrived,
unplaced: false,
},
register.clone(),
)
}));
}
PathElem::Group(group) => {
let anchor = GPath {
ext: GraphExt {
node,
bindings: Vec::new(),
arrived: Vec::new(),
unplaced: false,
},
visited: vec![node],
register: register.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();
(GraphExt { arrived, ..p.ext }, p.register)
}),
);
}
PathElem::Mark(_) => unreachable!("handled above"),
PathElem::Push { .. } => {}
}
}
let mut merged: Vec<(GraphExt, Vec<Reg>)> = Vec::new();
let mut index: HashMap<
(NodeId, Vec<(Option<String>, String)>),
usize,
> = HashMap::new();
for (ext, r) in next {
match index.entry((ext.node, reg_key(&r))) {
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.0.arrived;
for edge in ext.arrived {
if !slot.contains(&edge) {
slot.push(edge);
}
}
}
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(merged.len());
merged.push((ext, r));
}
}
}
ctx = merged;
if ctx.is_empty() {
break;
}
}
ctx
}
pub(crate) fn nested_outer<'a>(scope: &'a Scope<'a, GraphExt>) -> Option<&'a Scope<'a, GraphExt>> {
if scope.view.node.is_some() {
Some(scope)
} else {
scope.outer
}
}
pub(crate) 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(crate) fn navigate_from(
elems: &[PathElem],
adapter: &impl AstAdapter,
start: NodeId,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
seed_register: &[Reg],
) -> Vec<NodeId> {
dedup(
navigate_paths(
elems,
adapter,
start,
trace,
outer,
witness,
seed_register,
)
.into_iter()
.map(|(e, _)| e.node)
.collect(),
)
}
#[derive(Clone)]
pub(crate) struct GPath {
pub(crate) ext: GraphExt,
pub(crate) visited: Vec<NodeId>,
pub(crate) register: Vec<Reg>,
pub(crate) 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 {
ext: GraphExt {
node,
bindings: Vec::new(),
arrived: Vec::new(),
unplaced: false,
},
visited,
register: self.register.clone(),
last_edge: edge,
}
}
fn key(
&self,
) -> (
NodeId,
Vec<NodeId>,
Vec<(Option<String>, String)>,
Option<(NodeId, String, NodeId)>,
) {
(
self.ext.node,
self.visited.clone(),
reg_key(&self.register),
self.last_edge
.as_ref()
.map(|e| (e.source, e.label.clone(), e.target)),
)
}
}
pub(crate) 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 = select_links(step, &links_of(adapter, from))
.into_iter()
.find(|(_, t)| *t == succ)
.map(|(l, _)| l)?;
EdgeCtx {
source: from,
label,
target: succ,
}
}
Axis::InLink => {
let label = select_links(step, &backlinks_of(adapter, from))
.into_iter()
.find(|(_, s)| *s == succ)
.map(|(l, _)| l)?;
EdgeCtx {
source: succ,
label,
target: from,
}
}
Axis::BothLink => {
if let Some(label) = select_links(step, &links_of(adapter, from))
.into_iter()
.find(|(_, t)| *t == succ)
.map(|(l, _)| l)
{
EdgeCtx {
source: from,
label,
target: succ,
}
} else {
let label = select_links(step, &backlinks_of(adapter, from))
.into_iter()
.find(|(_, s)| *s == succ)
.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(crate) fn expand_group(
adapter: &impl AstAdapter,
group: &Group,
anchor: GPath,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
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(crate) 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(crate) fn group_admits(
adapter: &impl AstAdapter,
group: &Group,
path: &GPath,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
) -> bool {
if !group.traits.is_empty() && !trait_clauses_ok(adapter, path.ext.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,
register: &path.register,
crossing: path.last_edge.as_ref(),
witness: (!witness.is_empty()).then_some(witness),
..Scope::at(&path.ext)
};
if !witness.is_empty() {
return exprs
.iter()
.all(|e| eval_pred_expr_at(adapter, path.ext.node, e, trace, witness, scope));
}
exists_binding(
adapter,
path.ext.node,
&exprs,
trace,
&mut Vec::new(),
scope,
)
}
pub(crate) fn expand_elems(
adapter: &impl AstAdapter,
elems: &[PathElem],
mut paths: Vec<GPath>,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
) -> Vec<GPath> {
for elem in elems {
if let PathElem::Mark(name) = elem {
for path in &mut paths {
path.register.push(mark_reg(adapter, path.ext.node, name.clone()));
}
continue;
}
let mut next = Vec::new();
if let PathElem::Step(step) = elem {
prefetch_hint(adapter, step, paths.iter().map(|p| p.ext.node));
}
for path in &paths {
match elem {
PathElem::Step(step) => {
let mut succs = Vec::new();
apply_step(
adapter,
path.ext.node,
step,
trace,
outer,
witness,
&path.ext,
&path.register,
&mut succs,
);
next.extend(succs.into_iter().filter(|&s| !path.blocks(s)).map(|s| {
let edge = arrived_edge(adapter, path.ext.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 state = GraphExt::at(path.ext.node);
let scope = Scope {
prov: None,
outer,
crossing: path.last_edge.as_ref(),
witness: (!witness.is_empty()).then_some(witness),
..Scope::at(&state)
};
let (value, prov) = match body {
PushBody::Query(q) => {
subcontext_traced(q, adapter, path.ext.node, trace, Some(&scope))
}
PushBody::Expr(e) => {
let (v, o) = capture(|| {
operand_value_at(adapter, path.ext.node, e, trace, witness, scope)
});
(v, Prov::leaf(o))
}
};
let mut p = path.clone();
p.register.push(Reg {
prov,
site: Origin {
position: (path.ext.node).into(),
command: command_now(),
},
name: name.clone(),
value,
});
next.push(p);
}
}
}
paths = dedup_paths(next);
if paths.is_empty() {
break;
}
}
paths
}
pub(crate) 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_else(|| {
crate::value::ambient::record_refusal(format!(
"':{key}' is no core field; the core fields are {} (':fields' lists them); a property is '::{key}', adapter metadata ':::{key}'",
crate::graph::navigate::CORE_FIELDS.join(", ")
));
Value::Null
}),
Projection::AdapterMeta(key) => adapter.metadata(node, key).unwrap_or(Value::Null),
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn apply_step(
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
state: &GraphExt,
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 {
traits: Vec::new(),
cap: Default::default(),
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,
state,
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, state, 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,
state,
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,
state,
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,
state,
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,
state,
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,
state,
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,
state,
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)> = select_links(step, &links_of(adapter, node))
.into_iter()
.filter(|(_, target)| tests_ok(adapter, *target, step))
.collect();
let kept = apply_predicates(
adapter,
matched,
step,
trace,
outer,
witness,
state,
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)> = select_links(step, &backlinks_of(adapter, node))
.into_iter()
.filter(|(_, source)| tests_ok(adapter, *source, step))
.collect();
let kept = apply_predicates(
adapter,
matched,
step,
trace,
outer,
witness,
state,
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, state, register, out,
),
Axis::Resolve {
property,
label: hint,
} => resolve_step(
property, hint, adapter, node, step, trace, outer, witness, state, register, out,
),
Axis::ReverseResolve {
label,
scope,
property,
} => reverse_resolve_step(
label, scope, property, adapter, node, step, trace, outer, witness, state, register,
out,
),
Axis::ValueMatch {
property,
scope,
target,
} => value_match_step(
property, scope, target, adapter, node, step, trace, outer, witness, state, register,
out,
),
}
}
pub(crate) fn descendant_step(
reach: &Reach,
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
state: &GraphExt,
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 {
traits: Vec::new(),
cap: Default::default(),
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,
state,
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(crate) fn both_link_step(
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
state: &GraphExt,
register: &[Reg],
out: &mut Vec<NodeId>,
) {
let matched: Vec<(String, NodeId, bool)> = select_links(step, &links_of(adapter, node))
.into_iter()
.map(|(label, target)| (label, target, true))
.chain(
select_links(step, &backlinks_of(adapter, node))
.into_iter()
.map(|(label, source)| (label, source, false)),
)
.filter(|(_, other, _)| tests_ok(adapter, *other, step))
.collect();
let kept = apply_predicates(
adapter,
matched,
step,
trace,
outer,
witness,
state,
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(crate) fn resolve_step(
property: &Option<String>,
hint: &Option<String>,
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
state: &GraphExt,
register: &[Reg],
out: &mut Vec<NodeId>,
) {
if let Some(p) = property
&& let Some(Value::Node(n)) = adapter.property(node, p)
{
let target = NodeId(n.id);
if adapter.has_node(target) && tests_ok(adapter, target, step) {
out.extend(apply_predicates(
adapter,
vec![target],
step,
trace,
outer,
witness,
state,
register,
|&n| n,
|&t| {
Some(EdgeCtx {
source: node,
label: p.clone(),
target: t,
})
},
));
}
return;
}
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,
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,
state,
register,
|&n| n,
|&t| {
Some(EdgeCtx {
source: node,
label: adapter
.ref_label(node, property)
.unwrap_or_else(|| property.clone()),
target: t,
})
},
));
}
pub(crate) fn reverse_resolve_step(
label: &Option<String>,
scope: &Option<crate::syntax::ast::ScopePath>,
property: &RefProp,
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
state: &GraphExt,
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,
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, state, 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,
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,
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, state, register,
));
}
pub(crate) fn value_match_step(
property: &String,
scope: &Option<crate::syntax::ast::ScopePath>,
target: &String,
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
state: &GraphExt,
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,
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,
state,
register,
|&n| n,
|&u| {
Some(EdgeCtx {
source: node,
label: format!("{property}={target}"),
target: u,
})
},
));
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn reverse_found(
adapter: &impl AstAdapter,
node: NodeId,
found: Vec<(NodeId, String)>,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
state: &GraphExt,
register: &[Reg],
) -> Vec<NodeId> {
apply_predicates(
adapter,
found,
step,
trace,
outer,
witness,
state,
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(crate) fn prefetch_hint(
adapter: &impl AstAdapter,
step: &Step,
nodes: impl Iterator<Item = NodeId>,
) {
let dirs: &[crate::graph::adapter::LinkDir] = match step.axis {
Axis::OutLink => &[crate::graph::adapter::LinkDir::Out],
Axis::InLink => &[crate::graph::adapter::LinkDir::In],
Axis::BothLink => &[
crate::graph::adapter::LinkDir::Out,
crate::graph::adapter::LinkDir::In,
],
Axis::Resolve { label: None, .. } => &[crate::graph::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(crate) 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(crate) 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_comparison(
e: &PredExpr,
) -> Option<(&str, crate::graph::adapter::Cmp, &Value)> {
use crate::graph::adapter::Cmp;
if let Some((p, v)) = indexable_equality(e) {
return Some((p, Cmp::Eq, v));
}
let PredExpr::Compare(l, op, 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;
}
let Operand::Lit(v) = r else {
return None;
};
if matches!(v, Value::Null | Value::List(_)) {
return None;
}
let cmp = match op {
CmpOp::Eq => Cmp::Eq,
CmpOp::Ne => Cmp::Ne,
CmpOp::Lt => Cmp::Lt,
CmpOp::Le => Cmp::Le,
CmpOp::Gt => Cmp::Gt,
CmpOp::Ge => Cmp::Ge,
CmpOp::Contains => Cmp::Contains,
_ => return None,
};
Some((prop.as_str(), cmp, v))
}
pub(crate) fn literal_traits(step: &Step) -> Vec<&str> {
let mut out = Vec::new();
for clause in &step.traits {
match clause.alts.as_slice() {
[one] if !one.starts_with('!') && !crate::syntax::ast::trait_has_wildcard(one) => {
out.push(one.as_str())
}
_ => return Vec::new(),
}
}
out
}
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(crate) fn matches_step(adapter: &impl AstAdapter, node: NodeId, step: &Step) -> bool {
matches_name(adapter, node, &step.matcher) && tests_ok(adapter, node, step)
}
pub(crate) 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,
&[],
&GraphExt::at(adapter.root()),
&[],
|&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,
labels: Vec::new(),
traits: g.traits.clone(),
predicates: g.predicates.clone(),
leaf: false,
};
tests_ok(adapter, node, &own)
&& !apply_predicates(
adapter,
vec![node],
&own,
&trace,
None,
&[],
&GraphExt::at(adapter.root()),
&[],
|&n| n,
edge,
)
.is_empty()
}
_ => false,
}
}
pub(crate) fn apply_predicates<T>(
adapter: &impl AstAdapter,
mut items: Vec<T>,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
state: &GraphExt,
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(_, _) | Predicate::Positions(_)) => {
items = positional(items, pred);
i += 1;
}
Predicate::Computed(op) => {
i += 1;
let Some(first) = items.first() else { continue };
let scope = Scope {
prov: None,
outer,
register,
crossing: None,
witness: (!witness.is_empty()).then_some(witness),
..Scope::at(state)
};
let value = super::operand::operand_value_at(
adapter,
node_of(first),
op,
trace,
witness,
scope,
);
match crate::pipeline::positions_of(&value) {
Some(pred) => items = positional(items, &pred),
None => {
let truthy = PredExpr::Truthy(op.clone());
items.retain(|item| {
let scope = Scope {
prov: None,
outer,
register,
crossing: None,
witness: (!witness.is_empty()).then_some(witness),
..Scope::at(state)
};
eval_pred_expr_at(adapter, node_of(item), &truthy, trace, witness, scope)
});
}
}
}
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,
register,
crossing: edge.as_ref(),
witness: (!witness.is_empty()).then_some(witness),
..Scope::at(&state)
};
if !witness.is_empty() {
return run.iter().all(|e| {
eval_pred_expr_at(adapter, node_of(item), e, trace, witness, scope)
});
}
exists_binding(adapter, node_of(item), &run, trace, &mut Vec::new(), scope)
});
}
}
}
items
}
pub(crate) fn exists_binding(
adapter: &impl AstAdapter,
node: NodeId,
exprs: &[&PredExpr],
trace: &Trace,
bound: &mut Vec<Option<NodeId>>,
scope: Scope<'_, GraphExt>,
) -> bool {
if bound.len() == trace.contexts.len() {
let hit = exprs.iter().all(|e| {
mentions_null_ctx(e, bound) || eval_pred_expr_at(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(crate) 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(crate) fn matches_label(matcher: &Matcher, label: &str) -> bool {
matches!(matcher, Matcher::Any) || matcher.matches(label)
}
pub(crate) fn select_links(step: &Step, pairs: &[(String, NodeId)]) -> Vec<(String, NodeId)> {
if step.labels.is_empty() {
return pairs
.iter()
.filter(|(l, _)| matches_label(&step.matcher, l))
.cloned()
.collect();
}
let mut grouped: Vec<(NodeId, Vec<String>)> = Vec::new();
for (l, n) in pairs {
match grouped.iter_mut().find(|(m, _)| m == n) {
Some((_, ls)) => ls.push(l.clone()),
None => grouped.push((*n, vec![l.clone()])),
}
}
grouped
.into_iter()
.filter(|(_, ls)| step.labels.iter().all(|c| c.matches(ls)))
.map(|(n, mut ls)| (ls.swap_remove(0), n))
.collect()
}
pub(crate) fn traits_ok(adapter: &impl AstAdapter, node: NodeId, step: &Step) -> bool {
trait_clauses_ok(adapter, node, &step.traits)
}
pub(crate) fn trait_clauses_ok(
adapter: &impl AstAdapter,
node: NodeId,
clauses: &[crate::syntax::ast::TraitClause],
) -> bool {
if clauses.is_empty() {
return true;
}
let traits = adapter.traits(node);
clauses
.iter()
.all(|c| c.matches_with(&traits, |name| adapter.has_trait(node, name)))
}