use super::*;
use crate::diagnostic::{FRONTIER_BOUND, QUANTIFIER_BOUND, WALK};
use crate::graph::adapter::{Conjunct, PlanAxis, StepPlan, reference_label};
use crate::value::ambient::{record_debug, record_refusal_coded};
pub(crate) fn navigate_paths(
elems: &[PathElem],
adapter: &impl AstAdapter,
start: NodeId,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
seed_register: &[Reg],
node_level: bool,
) -> Vec<(GraphExt, Vec<Reg>)> {
let mut ctx: Vec<(GraphExt, Vec<Reg>)> = vec![(
GraphExt {
node: start,
bindings: Vec::new(),
arrived: Vec::new(),
unplaced: false,
walk: None,
},
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<(Option<usize>, GraphExt, Vec<Reg>)> = Vec::new();
if let PathElem::Step(step) = elem {
prefetch_hint(adapter, step, ctx.iter().map(|c| c.0.node));
}
for (i, (ext, register)) in ctx.iter().enumerate() {
let node = ext.node;
match elem {
PathElem::Hole(_) | PathElem::Param(_) => {}
PathElem::Step(step) => {
let succs: Vec<(NodeId, Option<EdgeCtx>)> = if let Some(edges) =
link_edges(adapter, node, step, trace, outer, witness, ext, register)
{
edges.into_iter().map(|(s, e)| (s, Some(e))).collect()
} else {
let mut succs = Vec::new();
apply_step(
adapter, node, step, trace, outer, witness, ext, register, &mut succs,
);
succs
.into_iter()
.map(|s| (s, arrived_edge(adapter, node, step, s)))
.collect()
};
next.extend(succs.into_iter().map(|(s, edge)| {
let arrived = edge.into_iter().collect();
(
Some(i),
GraphExt {
node: s,
bindings: Vec::new(),
arrived,
unplaced: false,
walk: None,
},
register.clone(),
)
}));
}
PathElem::Group(group) => {
let anchor = GPath {
ext: GraphExt {
node,
bindings: Vec::new(),
arrived: Vec::new(),
unplaced: false,
walk: None,
},
visited: vec![node],
register: register.clone(),
last_edge: None,
set_walk: true,
node_level,
hops: 0,
total: 0.0,
};
next.extend(
expand_group(adapter, group, anchor, trace, outer, witness)
.into_iter()
.map(|p| {
let arrived = p.last_edge.clone().into_iter().collect();
(None, GraphExt { arrived, ..p.ext }, p.register)
}),
);
}
PathElem::Mark(_) => unreachable!("handled above"),
PathElem::Push { .. } => {}
}
}
let mut merged: Vec<(GraphExt, Vec<Reg>)> = Vec::new();
type Key = (
usize,
NodeId,
Option<(NodeId, String, NodeId, Option<NodeId>)>,
Vec<(Option<String>, String)>,
);
let mut index: HashMap<Key, usize> = HashMap::new();
for (from, ext, r) in next {
let Some(from) = from else {
merged.push((ext, r));
continue;
};
let edge = ext
.arrived
.first()
.map(|e| (e.source, e.label.clone(), e.target, e.row));
match index.entry((from, ext.node, edge, 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> {
navigate_from_level(
elems,
adapter,
start,
trace,
outer,
witness,
seed_register,
false,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn navigate_from_level(
elems: &[PathElem],
adapter: &impl AstAdapter,
start: NodeId,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
seed_register: &[Reg],
node_level: bool,
) -> Vec<NodeId> {
let mut seen = HashSet::new();
navigate_paths(
elems,
adapter,
start,
trace,
outer,
witness,
seed_register,
node_level,
)
.into_iter()
.filter(|(e, r)| seen.insert((e.node, arrival_key(e), reg_key(r))))
.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>,
pub(crate) set_walk: bool,
pub(crate) node_level: bool,
pub(crate) hops: usize,
pub(crate) total: f64,
}
#[derive(Clone, Debug)]
pub(crate) struct BudgetBounds {
pub(crate) floor: f64,
pub(crate) ceiling: Option<f64>,
pub(crate) base: String,
pub(crate) unit: String,
pub(crate) scale: f64,
pub(crate) written: String,
}
impl BudgetBounds {
pub(crate) fn resolve(b: &crate::syntax::ast::Budget) -> Result<BudgetBounds, String> {
let read = |text: &str| -> Result<(f64, String, f64, String), String> {
crate::value::quantity::parse_unit_text(&text.replace('_', " ")).ok_or_else(|| {
if text.trim().parse::<f64>().is_ok() {
format!(
"one dimension per quantifier: '{text}' counts hops where the other \
bound measures a quantity; a budget's bounds are quantities of one \
dimension ('{{;100_km}}'), a count of hops is '{{1;5}}'"
)
} else {
format!(
"the budget's bound '{text}' is no quantity: a budget bounds a sum of an \
edge weight, '{{;100_km}}', '{{2_h;}}' — a count of hops is '{{;5}}'"
)
}
})
};
let floor = b.floor.as_deref().map(read).transpose()?;
let ceiling = b.ceiling.as_deref().map(read).transpose()?;
let (anchor, written) = match (&ceiling, &floor) {
(Some(c), _) => (c.clone(), b.ceiling.clone().unwrap_or_default()),
(None, Some(f)) => (f.clone(), b.floor.clone().unwrap_or_default()),
(None, None) => return Err("a budget needs a floor or a ceiling".into()),
};
if let (Some(c), Some(f)) = (&ceiling, &floor)
&& c.1 != f.1
{
return Err(format!(
"one dimension per quantifier: the floor '{}' measures {} and the ceiling '{}' {}",
b.floor.as_deref().unwrap_or(""),
f.1,
b.ceiling.as_deref().unwrap_or(""),
c.1
));
}
let (bv, base, wmag, wunit) = anchor;
Ok(BudgetBounds {
floor: floor.map(|f| f.0).unwrap_or(0.0),
ceiling: ceiling.map(|c| c.0),
base,
unit: wunit,
scale: if wmag != 0.0 { bv / wmag } else { 1.0 },
written,
})
}
pub(crate) fn facts(&self, path: &GPath) -> WalkFacts {
WalkFacts {
round: path.hops,
hops: path.hops,
total: Some((
path.total,
self.base.clone(),
path.total / self.scale,
self.unit.clone(),
)),
trail: path.trail(),
}
}
pub(crate) fn weight(
&self,
adapter: &impl AstAdapter,
edge: &EdgeCtx,
) -> Result<Option<f64>, String> {
let mut found: Vec<(String, f64)> = Vec::new();
for name in edge.property_names(adapter) {
let Some(v) = edge.property(adapter, &name) else {
continue;
};
let q = match &v {
Value::Quantity { value, base, .. } => Some((*value, base.clone())),
Value::Str(s) => {
crate::value::quantity::parse_unit_text(s).map(|(bv, b, _, _)| (bv, b))
}
_ => None,
};
if let Some((bv, b)) = q
&& b == self.base
{
found.push((name, bv));
}
}
match found.as_slice() {
[] => Ok(None),
[(_, w)] => Ok(Some(*w)),
many => Err(format!(
"two properties of one dimension on the edge '{}': {} — a budget sums one; name \
the weight in the model or the query",
edge.label,
many.iter()
.map(|(n, _)| n.as_str())
.collect::<Vec<_>>()
.join(" and ")
)),
}
}
}
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 !self.set_walk
&& let Err(i) = visited.binary_search(&node)
{
visited.insert(i, node);
}
GPath {
ext: GraphExt {
node,
bindings: Vec::new(),
arrived: Vec::new(),
unplaced: false,
walk: None,
},
visited,
register: self.register.clone(),
last_edge: edge,
set_walk: self.set_walk,
node_level: self.node_level,
hops: self.hops + 1,
total: self.total,
}
}
fn facts(&self) -> WalkFacts {
WalkFacts {
round: self.hops,
hops: self.hops,
total: None,
trail: self.trail(),
}
}
fn trail(&self) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
self.visited.hash(&mut h);
h.finish()
}
fn key(
&self,
) -> (
NodeId,
Vec<NodeId>,
Vec<(Option<String>, String)>,
Option<(NodeId, String, NodeId, Option<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, e.row)),
)
}
}
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,
row: None,
},
Axis::Parent => EdgeCtx {
source: from,
label: PARENT.into(),
target: succ,
row: None,
},
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,
row: None,
}
}
Axis::NextSibling | Axis::FollowingSiblings(_) => EdgeCtx {
source: from,
label: NEXT.into(),
target: succ,
row: None,
},
Axis::PrevSibling | Axis::PrecedingSiblings(_) => EdgeCtx {
source: from,
label: PREV.into(),
target: succ,
row: None,
},
Axis::OutLink => {
let (label, row) = select_links(step, &links_of(adapter, from), |l, _| {
adapter.link_consumed(from, l)
})
.into_iter()
.find(|(_, t, _)| *t == succ)
.map(|(l, _, r)| (l, r))?;
EdgeCtx {
source: from,
label,
target: succ,
row,
}
}
Axis::InLink => {
let (label, row) = select_links(step, &backlinks_of(adapter, from), |l, s| {
adapter.link_consumed(s, l)
})
.into_iter()
.find(|(_, s, _)| *s == succ)
.map(|(l, _, r)| (l, r))?;
EdgeCtx {
source: succ,
label,
target: from,
row,
}
}
Axis::BothLink => {
if let Some((label, row)) = select_links(step, &links_of(adapter, from), |l, _| {
adapter.link_consumed(from, l)
})
.into_iter()
.find(|(_, t, _)| *t == succ)
.map(|(l, _, r)| (l, r))
{
EdgeCtx {
source: from,
label,
target: succ,
row,
}
} else {
let (label, row) = select_links(step, &backlinks_of(adapter, from), |l, s| {
adapter.link_consumed(s, l)
})
.into_iter()
.find(|(_, s, _)| *s == succ)
.map(|(l, _, r)| (l, r))?;
EdgeCtx {
source: succ,
label,
target: from,
row,
}
}
}
Axis::Any | Axis::AnyDeep(_) => {
if let Some((_, e)) = incident_edges(adapter, from, step)
.into_iter()
.find(|(n, _)| *n == succ)
{
return Some(e);
}
if let Some(p) = adapter.parent(succ) {
return Some(EdgeCtx {
source: p,
label: CHILD.into(),
target: succ,
row: None,
});
}
let (label, s, row) = backlinks_of(adapter, succ).first().cloned()?;
EdgeCtx {
source: s,
label,
target: succ,
row,
}
}
Axis::Resolve { property, .. } => EdgeCtx {
source: from,
label: property.clone().unwrap_or_else(|| "ref".to_string()),
target: succ,
row: None,
},
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,
row: None,
},
Axis::ValueMatch {
property, target, ..
} => EdgeCtx {
source: from,
label: format!("{property}={target}"),
target: succ,
row: None,
},
};
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_coded(
QUANTIFIER_BOUND,
"the quantifier",
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_coded(
QUANTIFIER_BOUND,
"the quantifier",
format!(
"the pattern's minimum ({}) exceeds the quantifier bound \
({n_max}); raise --quantifier-bound",
group.quant.min
),
);
return Vec::new();
}
let f_max = frontier_bound(adapter);
let budget = match &group.quant.budget {
Some(b) => match BudgetBounds::resolve(b) {
Ok(bounds) => Some(bounds),
Err(e) => {
record_refusal_coded(crate::diagnostic::REFUSED, "the quantifier", e);
return Vec::new();
}
},
None => None,
};
let node_level = anchor.node_level || group.reach != Reach::All;
let set_walk = anchor.set_walk
&& group_is_plain(group)
&& set_form(group)
&& (group.reach == Reach::Proximal || (group.reach == Reach::All && node_level))
&& budget.is_none();
let anchor_was_set = anchor.set_walk;
let anchor_ground = anchor.visited.clone();
let links_only = !set_walk && group_walks_links(group);
let promise: std::cell::RefCell<Option<String>> = std::cell::RefCell::new(if links_only {
group_promise(adapter, group)
} else {
None
});
let alive = std::cell::Cell::new(links_only);
let merge_ok = node_level && budget.is_none();
let mut anchor = anchor;
anchor.set_walk = set_walk;
let hi = if set_walk {
usize::MAX
} else {
group.quant.max.unwrap_or(n_max)
};
let admits = |p: &GPath| {
group_admits(adapter, group, p, trace, outer, witness, budget.as_ref())
&& budget.as_ref().is_none_or(|b| p.total >= b.floor)
};
let mut seen: HashSet<(NodeId, Vec<(Option<String>, String)>)> = HashSet::new();
if set_walk {
seen.insert((anchor.ext.node, reg_key(&anchor.register)));
}
let mut advance = |frontier: &[GPath], round: usize| -> Vec<GPath> {
let mut next = Vec::new();
let current = promise.borrow().clone();
for path in frontier {
for alt in &group.alts {
next.extend(expand_elems(
adapter,
alt,
vec![path.clone()],
trace,
outer,
witness,
budget.as_ref(),
current.as_deref(),
));
}
}
let next = dedup_paths(next);
if alive.get() {
let mut decl: Option<String> = None;
let held = next.iter().all(|p| match &p.last_edge {
Some(e) => match adapter.acyclic_link(&e.label) {
Some(d) => {
decl.get_or_insert(d);
true
}
None => false,
},
None => true,
});
if held {
if decl.is_some() {
*promise.borrow_mut() = decl;
}
} else {
alive.set(false);
*promise.borrow_mut() = None;
}
}
let next = if merge_ok && alive.get() && promise.borrow().is_some() {
merge_by_position(next)
} else {
next
};
if !set_walk {
record_debug(WALK, format!("round {round}"), || {
format!(
"exact walk: {} expansion path{} in flight after the merge \
(frontier bound {f_max}, quantifier bound {n_max})",
next.len(),
if next.len() == 1 { "" } else { "s" }
)
});
return next;
}
let mut fresh: Vec<GPath> = Vec::new();
for p in next {
let key = (p.ext.node, reg_key(&p.register));
if seen.contains(&key) {
continue;
}
seen.insert(key);
fresh.push(p);
}
record_debug(WALK, format!("round {round}"), || {
format!(
"set walk: {} arrival{} this round, {} position{} spent",
fresh.len(),
if fresh.len() == 1 { "" } else { "s" },
seen.len(),
if seen.len() == 1 { "" } else { "s" }
)
});
fresh
};
let mut matches: Vec<(GPath, usize)> = Vec::new();
if group.quant.min == 0 && admits(&anchor) {
matches.push((anchor.clone(), 0));
}
if let (Some(b), Reach::Proximal) = (&budget, group.reach) {
let mut settled: HashSet<NodeId> = HashSet::from([anchor.ext.node]);
let mut pending: Vec<GPath> = Vec::new();
for alt in &group.alts {
pending.extend(expand_elems(
adapter,
alt,
vec![anchor.clone()],
trace,
outer,
witness,
Some(b),
promise.borrow().as_deref(),
));
}
let mut rounds = 0usize;
while !pending.is_empty() {
rounds += 1;
if rounds > n_max * f_max {
break;
}
let (i, _) = pending
.iter()
.enumerate()
.min_by(|(_, p), (_, q)| {
p.total
.partial_cmp(&q.total)
.unwrap_or(std::cmp::Ordering::Equal)
})
.expect("non-empty");
let path = pending.swap_remove(i);
if !settled.insert(path.ext.node) {
continue;
}
if admits(&path) {
matches.push((path.clone(), 0));
}
if path.hops >= n_max {
continue;
}
for alt in &group.alts {
for p in expand_elems(
adapter,
alt,
vec![path.clone()],
trace,
outer,
witness,
Some(b),
promise.borrow().as_deref(),
) {
if !settled.contains(&p.ext.node) {
pending.push(p);
}
}
}
if crate::value::ambient::refusal_pending() {
return Vec::new();
}
if pending.len() > f_max {
record_refusal_coded(
FRONTIER_BOUND,
format!("round {rounds}"),
format!(
"the frontier bound ({f_max}) is exceeded at round {rounds}: {} \
expansion paths in flight; give the quantifier a ceiling, \
{{;M}}, or raise --frontier-bound",
pending.len()
),
);
return Vec::new();
}
}
return exit_walk(
matches.into_iter().map(|(p, _)| p).collect(),
budget.as_ref(),
);
}
let mut frontier = vec![anchor];
let mut answered = false;
for count in 1..=hi {
frontier = advance(&frontier, count);
if crate::value::ambient::refusal_pending() {
return Vec::new();
}
if frontier.is_empty() {
break;
}
if frontier.len() > f_max {
record_refusal_coded(
FRONTIER_BOUND,
format!("round {count}"),
format!(
"the frontier bound ({f_max}) is exceeded at round {count}: {} \
expansion paths in flight; give the quantifier a ceiling, \
{{;M}}, or raise --frontier-bound",
frontier.len()
),
);
return Vec::new();
}
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 !set_walk && group.quant.max.is_none() && !answered && !frontier.is_empty() {
if !advance(&frontier, n_max + 1).is_empty() {
record_refusal_coded(
QUANTIFIER_BOUND,
format!("round {n_max}"),
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(),
};
let out: Vec<GPath> = if budget.is_some() && group.reach == Reach::Distal {
let heaviest = matches
.iter()
.map(|(p, _)| p.total)
.fold(f64::NEG_INFINITY, f64::max);
matches
.into_iter()
.filter(|(p, _)| p.total == heaviest)
.map(|(p, _)| p)
.collect()
} else {
matches
.into_iter()
.filter(|&(_, c)| extreme.is_none_or(|e| c == e))
.map(|(p, _)| p)
.collect()
};
let out: Vec<GPath> = if group.reach == Reach::All {
out
} else {
let mut once: HashSet<(NodeId, Vec<(Option<String>, String)>)> = HashSet::new();
out.into_iter()
.filter(|p| once.insert((p.ext.node, reg_key(&p.register))))
.collect()
};
let mut out = exit_walk(out, budget.as_ref());
if anchor_was_set && !set_walk {
for p in &mut out {
p.set_walk = true;
p.visited = anchor_ground.clone();
}
}
out
}
fn exit_walk(mut out: Vec<GPath>, budget: Option<&BudgetBounds>) -> Vec<GPath> {
for p in &mut out {
p.ext.walk = Some(match budget {
Some(b) => b.facts(p),
None => p.facts(),
});
}
out
}
pub(crate) fn set_form(group: &Group) -> bool {
group.quant.min <= 1 && group.quant.max.is_none() && group.reach != Reach::Distal
}
pub(crate) fn group_is_plain(group: &Group) -> bool {
group.alts.iter().flatten().all(|e| match e {
PathElem::Step(_) => true,
PathElem::Group(g) => group_is_plain(g),
PathElem::Mark(_) | PathElem::Push { .. } | PathElem::Hole(_) | PathElem::Param(_) => false,
})
}
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
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn group_admits(
adapter: &impl AstAdapter,
group: &Group,
path: &GPath,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
budget: Option<&BudgetBounds>,
) -> 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 facts = match budget {
Some(b) => b.facts(path),
None => path.facts(),
};
let scope = Scope {
prov: None,
outer,
register: &path.register,
crossing: path.last_edge.as_ref(),
walk: Some(&facts),
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,
)
}
fn group_walks_links(group: &Group) -> bool {
group.alts.iter().flatten().all(|elem| match elem {
PathElem::Hole(_) | PathElem::Param(_) => false,
PathElem::Mark(_) | PathElem::Push { .. } => true,
PathElem::Group(g) => group_walks_links(g),
PathElem::Step(step) => {
matches!(step.axis, Axis::OutLink | Axis::InLink | Axis::BothLink)
&& matches!(step.matcher, Matcher::Name(_) | Matcher::Any)
&& step.labels.is_empty()
}
})
}
fn group_promise(adapter: &impl AstAdapter, group: &Group) -> Option<String> {
let mut decl: Option<String> = None;
for alt in &group.alts {
for elem in alt {
match elem {
PathElem::Hole(_) | PathElem::Param(_) => return None,
PathElem::Mark(_) | PathElem::Push { .. } => {}
PathElem::Group(g) => {
let d = group_promise(adapter, g)?;
decl.get_or_insert(d);
}
PathElem::Step(step) => {
if !matches!(step.axis, Axis::OutLink | Axis::InLink | Axis::BothLink) {
return None;
}
let Matcher::Name(label) = &step.matcher else {
return None;
};
let d = adapter.acyclic_link(label)?;
decl.get_or_insert(d);
}
}
}
}
decl
}
pub(crate) fn node_path(adapter: &impl AstAdapter, node: NodeId) -> String {
let mut segments = Vec::new();
let mut cur = Some(node);
while let Some(n) = cur {
if n == adapter.root() {
break;
}
segments.push(adapter.name(n).unwrap_or_else(|| "*".to_string()));
cur = adapter.parent(n);
}
segments.reverse();
format!("/{}", segments.join("/"))
}
fn merge_by_position(paths: Vec<GPath>) -> Vec<GPath> {
let mut kept: Vec<GPath> = Vec::new();
let mut index: HashMap<
(
NodeId,
Vec<(Option<String>, String)>,
Option<(NodeId, String, NodeId)>,
),
usize,
> = HashMap::new();
for p in paths {
let key = (
p.ext.node,
reg_key(&p.register),
p.last_edge
.as_ref()
.map(|e| (e.source, e.label.clone(), e.target)),
);
match index.get(&key) {
Some(&i) => {
let k = &mut kept[i];
for (a, b) in k.register.iter_mut().zip(&p.register) {
a.prov.origins.union(&b.prov.origins);
}
for n in p.visited {
if let Err(j) = k.visited.binary_search(&n) {
k.visited.insert(j, n);
}
}
}
None => {
index.insert(key, kept.len());
kept.push(p);
}
}
}
kept
}
pub(crate) fn expand_elems(
adapter: &impl AstAdapter,
elems: &[PathElem],
mut paths: Vec<GPath>,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
budget: Option<&BudgetBounds>,
promise: Option<&str>,
) -> 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::Hole(_) | PathElem::Param(_) => {}
PathElem::Step(step) => {
let succs: Vec<(NodeId, Option<EdgeCtx>)> = if let Some(edges) = link_edges(
adapter,
path.ext.node,
step,
trace,
outer,
witness,
&path.ext,
&path.register,
) {
edges.into_iter().map(|(s, e)| (s, Some(e))).collect()
} else {
let mut succs = Vec::new();
apply_step(
adapter,
path.ext.node,
step,
trace,
outer,
witness,
&path.ext,
&path.register,
&mut succs,
);
succs
.into_iter()
.map(|s| (s, arrived_edge(adapter, path.ext.node, step, s)))
.collect()
};
for (s, edge) in succs {
if path.blocks(s) {
if let Some(decl) = promise {
record_refusal_coded(
crate::diagnostic::REFUSED,
"the walk",
format!(
"a cycle under the promise '{decl}': the walk from \
{} returns to {} after {} hop{}",
node_path(adapter, path.visited[0]),
node_path(adapter, s),
path.hops + 1,
if path.hops == 0 { "" } else { "s" }
),
);
return Vec::new();
}
continue;
}
let mut p = path.with(s, edge);
if let Some(b) = budget {
let Some(edge) = &p.last_edge else { continue };
match b.weight(adapter, edge) {
Ok(Some(w)) => {
if w < 0.0 {
record_refusal_coded(
crate::diagnostic::REFUSED,
"the quantifier",
format!(
"a negative weight ({w} on '{}') under a budget: the \
lightest walk needs weights of zero or more",
edge.label
),
);
return Vec::new();
}
p.total += w;
if b.ceiling.is_some_and(|c| p.total > c) {
continue;
}
}
Ok(None) => continue,
Err(e) => {
record_refusal_coded(
crate::diagnostic::REFUSED,
"the quantifier",
e,
);
return Vec::new();
}
}
}
next.push(p);
}
}
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 facts = match budget {
Some(b) => b.facts(path),
None => path.facts(),
};
let scope = Scope {
prov: None,
outer,
crossing: path.last_edge.as_ref(),
walk: Some(&facts),
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)) => {
let v = property_or_alias(adapter, node, name);
if matches!(v, Value::Null) && adapter.property_withheld(node, name) {
crate::value::ambient::record_omitted();
crate::pipeline::note_omitted();
}
v
}
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,
row: None,
})
},
));
}
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,
row: None,
})
},
);
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,
row: None,
})
},
));
}
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,
row: None,
})
},
);
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,
row: None,
})
},
));
}
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,
row: None,
})
},
));
}
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,
row: None,
})
},
);
out.extend(by_reach(found, *reach));
}
Axis::OutLink | Axis::InLink => {
out.extend(
link_edges(adapter, node, step, trace, outer, witness, state, register)
.expect("a link axis")
.into_iter()
.map(|(n, _)| n),
);
}
Axis::BothLink => both_link_step(
adapter, node, step, trace, outer, witness, state, register, out,
),
Axis::Any => any_edge_step(
adapter, node, step, trace, outer, witness, state, register, out,
),
Axis::AnyDeep(reach) => any_deep_step(
reach, 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,
row: None,
})
},
);
out.extend(by_reach(found, *reach));
}
pub(crate) fn incident_edges(
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
) -> Vec<(NodeId, EdgeCtx)> {
let mut out = Vec::new();
for c in adapter.children(node) {
if matches_step(adapter, c, step) {
out.push((
c,
EdgeCtx {
source: node,
label: CHILD.into(),
target: c,
row: None,
},
));
}
}
if let Some(p) = adapter.parent(node)
&& matches_step(adapter, p, step)
{
out.push((
p,
EdgeCtx {
source: node,
label: PARENT.into(),
target: p,
row: None,
},
));
}
for (label, t, row) in select_links(step, &links_of(adapter, node), |l, _| {
adapter.link_consumed(node, l)
}) {
if tests_ok(adapter, t, step) {
out.push((
t,
EdgeCtx {
source: node,
label,
target: t,
row,
},
));
}
}
for (label, s, row) in select_links(step, &backlinks_of(adapter, node), |l, s| {
adapter.link_consumed(s, l)
}) {
if tests_ok(adapter, s, step) {
out.push((
s,
EdgeCtx {
source: s,
label,
target: node,
row,
},
));
}
}
out
}
fn wild_edge_step() -> Step {
Step {
axis: Axis::Any,
matcher: Matcher::Any,
labels: Vec::new(),
traits: Vec::new(),
predicates: Vec::new(),
leaf: false,
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn any_edge_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 = incident_edges(adapter, node, step);
let kept = apply_predicates(
adapter,
matched,
step,
trace,
outer,
witness,
state,
register,
|(n, _)| *n,
|(_, e)| Some(e.clone()),
);
out.extend(kept.into_iter().map(|(n, _)| n));
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn any_deep_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 wild = wild_edge_step();
let mut seen: HashSet<NodeId> = HashSet::from([node]);
let mut reached: Vec<(NodeId, usize)> = vec![(node, 0)];
let mut frontier = vec![node];
let mut depth = 0;
while !frontier.is_empty() {
depth += 1;
let mut next = Vec::new();
for n in &frontier {
for (far, _) in incident_edges(adapter, *n, &wild) {
if seen.insert(far) {
next.push(far);
}
}
}
reached.extend(next.iter().map(|n| (*n, depth)));
frontier = next;
}
let mut found: Vec<((NodeId, EdgeCtx), usize)> = Vec::new();
for (n, d) in &reached {
for hit in incident_edges(adapter, *n, step) {
found.push((hit, d + 1));
}
}
let kept = apply_predicates(
adapter,
found,
step,
trace,
outer,
witness,
state,
register,
|((n, _), _)| *n,
|((_, e), _)| Some(e.clone()),
);
out.extend(crate::graph::navigate::dedup(by_reach(
kept.into_iter().map(|((n, _), d)| (n, d)).collect(),
*reach,
)));
}
#[allow(clippy::too_many_arguments)]
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>,
) {
out.extend(
link_edges(adapter, node, step, trace, outer, witness, state, register)
.expect("the headless axis")
.into_iter()
.map(|(n, _)| n),
);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn link_edges(
adapter: &impl AstAdapter,
node: NodeId,
step: &Step,
trace: &Trace,
outer: Option<&Scope<'_, GraphExt>>,
witness: &[Option<NodeId>],
state: &GraphExt,
register: &[Reg],
) -> Option<Vec<(NodeId, EdgeCtx)>> {
let (outgoing, incoming) = match step.axis {
Axis::OutLink => (true, false),
Axis::InLink => (false, true),
Axis::BothLink => (true, true),
_ => return None,
};
let mut matched: Vec<(String, NodeId, bool, Option<NodeId>)> = Vec::new();
if outgoing {
matched.extend(
select_links(step, &links_of(adapter, node), |l, _| {
adapter.link_consumed(node, l)
})
.into_iter()
.map(|(label, target, row)| (label, target, true, row)),
);
}
if incoming {
matched.extend(
select_links(step, &backlinks_of(adapter, node), |l, s| {
adapter.link_consumed(s, l)
})
.into_iter()
.map(|(label, source, row)| (label, source, false, row)),
);
}
matched.retain(|(_, other, _, _)| tests_ok(adapter, *other, step));
let edge_of = |label: &String, other: NodeId, out: bool, row: Option<NodeId>| {
if out {
EdgeCtx {
source: node,
label: label.clone(),
target: other,
row,
}
} else {
EdgeCtx {
source: other,
label: label.clone(),
target: node,
row,
}
}
};
let kept = apply_predicates(
adapter,
matched,
step,
trace,
outer,
witness,
state,
register,
|(_, n, _, _)| *n,
|(label, other, out, row)| Some(edge_of(label, *other, *out, *row)),
);
Some(
kept.into_iter()
.map(|(label, other, out, row)| (other, edge_of(&label, other, out, row)))
.collect(),
)
}
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,
row: None,
})
},
));
}
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(|| reference_label(property).to_string()),
target: t,
row: None,
})
},
));
}
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();
let lookup = if adapter.withheld_properties().iter().any(|w| w == target) {
None
} else {
adapter.index_lookup(&text, target, &own)
};
match lookup {
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,
row: None,
})
},
));
}
#[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(|| reference_label(p).to_string()),
target: node,
row: None,
})
},
)
.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 | Axis::Any | Axis::AnyDeep(_) => &[
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) struct Sought<'a> {
name: Option<&'a str>,
traits: Vec<&'a str>,
hinted: bool,
}
impl<'a> Sought<'a> {
pub(crate) fn of(step: &'a Step) -> Self {
let name = match &step.matcher {
Matcher::Name(n) if step.labels.is_empty() => Some(n.as_str()),
_ => None,
};
let prunable = step.traits.iter().all(|c| {
c.alts
.iter()
.all(|a| !a.starts_with('!') && !crate::syntax::ast::trait_has_wildcard(a))
});
let traits: Vec<&str> = if prunable {
step.traits
.iter()
.flat_map(|c| c.alts.iter().map(String::as_str))
.collect()
} else {
Vec::new()
};
let hinted = name.is_some() || (prunable && !traits.is_empty());
Sought {
name,
traits,
hinted,
}
}
pub(crate) fn may_hold_below(&self, adapter: &impl AstAdapter, node: NodeId) -> bool {
!self.hinted || adapter.may_hold_below(node, self.name, &self.traits)
}
}
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();
let sought = Sought::of(step);
while let Some((child, d)) = stack.pop() {
if matches_step(adapter, child, step) {
out.push((child, d));
}
if !sought.may_hold_below(adapter, child) {
continue;
}
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)
&& algebra_ok(adapter, node, step)
&& tests_ok(adapter, node, step)
}
pub(crate) fn algebra_ok(adapter: &impl AstAdapter, node: NodeId, step: &Step) -> bool {
step.axis.is_link()
|| step.labels.iter().all(|c| {
c.alts
.iter()
.any(|a| matches_name(adapter, node, &a.matcher) != a.neg)
})
}
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,
row: None,
})
};
match elem {
PathElem::Hole(_) | PathElem::Param(_) => false,
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,
walk: 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,
walk: 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(),
walk: None,
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,
links: &[(String, NodeId, Option<NodeId>)],
consumed: impl Fn(&str, NodeId) -> bool,
) -> Vec<(String, NodeId, Option<NodeId>)> {
if step.labels.is_empty() {
let wild = matches!(step.matcher, Matcher::Any);
return links
.iter()
.filter(|(l, n, _)| matches_label(&step.matcher, l) && !(wild && consumed(l, *n)))
.cloned()
.collect();
}
let mut grouped: Vec<((NodeId, Option<NodeId>), Vec<String>)> = Vec::new();
for (l, n, r) in links {
match grouped.iter_mut().find(|(k, _)| *k == (*n, *r)) {
Some((_, ls)) => ls.push(l.clone()),
None => grouped.push(((*n, *r), vec![l.clone()])),
}
}
grouped
.into_iter()
.filter(|(_, ls)| step.labels.iter().all(|c| c.matches(ls)))
.map(|((n, r), mut ls)| (ls.swap_remove(0), n, r))
.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 needs_list = clauses.iter().any(|c| {
c.alts
.iter()
.any(|a| crate::syntax::ast::trait_has_wildcard(a))
});
let traits = if needs_list {
adapter.traits(node)
} else {
Vec::new()
};
clauses
.iter()
.all(|c| c.matches_with(&traits, |name| adapter.has_trait(node, name)))
}