use super::ast::{Axis, BinaryOp, Expr, NodeTest, PathExpr, PathStart, Step};
use super::context::EvalContext;
use super::functions;
use super::value::Value;
use crate::xml::{Document, NodeId, NodeKind};
#[derive(Debug, Clone)]
pub struct EvalError {
pub message: String,
}
impl EvalError {
pub(crate) fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl std::fmt::Display for EvalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for EvalError {}
pub fn evaluate(expr: &Expr, context: &EvalContext<'_>) -> Result<Value, EvalError> {
match expr {
Expr::Literal(text) => Ok(Value::String(text.clone())),
Expr::Number(value) => Ok(Value::Number(*value)),
Expr::Variable(name) => {
let key = name.to_string();
context.variables.lookup(&key).cloned().ok_or_else(|| {
let known: Vec<&str> = context.variables.names().collect();
EvalError::new(format!(
"variable ${key} is not bound{}",
if known.is_empty() {
String::new()
} else {
format!("; bound here: {}", known.join(", "))
}
))
})
}
Expr::Negate(inner) => {
let value = evaluate(inner, context)?;
Ok(Value::Number(-value.to_number(context.document)))
}
Expr::Binary(op, left, right) => evaluate_binary(*op, left, right, context),
Expr::Function { name, args } => {
let mut values = Vec::with_capacity(args.len());
for arg in args {
values.push(evaluate(arg, context)?);
}
functions::call(name, &values, context)
}
Expr::Path(path) => Ok(Value::NodeSet(evaluate_path(path, context)?)),
Expr::If {
condition,
then_branch,
else_branch,
} => {
if evaluate(condition, context)?.to_boolean() {
evaluate(then_branch, context)
} else {
evaluate(else_branch, context)
}
}
}
}
fn evaluate_binary(
op: BinaryOp,
left: &Expr,
right: &Expr,
context: &EvalContext<'_>,
) -> Result<Value, EvalError> {
match op {
BinaryOp::And => {
if !evaluate(left, context)?.to_boolean() {
return Ok(Value::Boolean(false));
}
return Ok(Value::Boolean(evaluate(right, context)?.to_boolean()));
}
BinaryOp::Or => {
if evaluate(left, context)?.to_boolean() {
return Ok(Value::Boolean(true));
}
return Ok(Value::Boolean(evaluate(right, context)?.to_boolean()));
}
_ => {}
}
let left = evaluate(left, context)?;
let right = evaluate(right, context)?;
let document = context.document;
match op {
BinaryOp::And | BinaryOp::Or => unreachable!("handled above"),
BinaryOp::Union => match (left, right) {
(Value::NodeSet(a), Value::NodeSet(b)) => {
let mut merged = a;
merged.extend(b);
Ok(Value::NodeSet(sort_and_deduplicate(merged, document)))
}
(a, b) => Err(EvalError::new(format!(
"the union operator | requires two node-sets, but was given a {} and a {}",
a.type_name(),
b.type_name()
))),
},
BinaryOp::Equal => Ok(Value::Boolean(compare_equality(&left, &right, document, true))),
BinaryOp::NotEqual => Ok(Value::Boolean(compare_equality(
&left, &right, document, false,
))),
BinaryOp::Less | BinaryOp::LessEqual | BinaryOp::Greater | BinaryOp::GreaterEqual => {
Ok(Value::Boolean(compare_relational(op, &left, &right, document)))
}
BinaryOp::Add
| BinaryOp::Subtract
| BinaryOp::Multiply
| BinaryOp::Divide
| BinaryOp::Modulo => {
let a = left.to_number(document);
let b = right.to_number(document);
Ok(Value::Number(match op {
BinaryOp::Add => a + b,
BinaryOp::Subtract => a - b,
BinaryOp::Multiply => a * b,
BinaryOp::Divide => a / b,
BinaryOp::Modulo => a % b,
_ => unreachable!("outer match limits the operators"),
}))
}
}
}
fn compare_equality(left: &Value, right: &Value, document: &Document, want_equal: bool) -> bool {
let test = |a: &str, b: &str| (a == b) == want_equal;
match (left, right) {
(Value::NodeSet(a), Value::NodeSet(b)) => {
let b_strings: Vec<String> = b.iter().map(|&n| document.string_value(n)).collect();
a.iter().any(|&node| {
let a_string = document.string_value(node);
b_strings.iter().any(|b_string| test(&a_string, b_string))
})
}
(Value::NodeSet(nodes), other) | (other, Value::NodeSet(nodes)) => match other {
Value::Boolean(b) => (nodes.is_empty() != *b) == want_equal,
Value::Number(n) => nodes.iter().any(|&node| {
let value = super::value::parse_number(&document.string_value(node));
(value == *n) == want_equal
}),
Value::String(s) => nodes
.iter()
.any(|&node| test(&document.string_value(node), s)),
Value::NodeSet(_) => unreachable!("matched by the arm above"),
},
(a, b) => {
if matches!(a, Value::Boolean(_)) || matches!(b, Value::Boolean(_)) {
(a.to_boolean() == b.to_boolean()) == want_equal
} else if matches!(a, Value::Number(_)) || matches!(b, Value::Number(_)) {
(a.to_number(document) == b.to_number(document)) == want_equal
} else {
test(&a.to_xpath_string(document), &b.to_xpath_string(document))
}
}
}
}
fn compare_relational(op: BinaryOp, left: &Value, right: &Value, document: &Document) -> bool {
let compare = |a: f64, b: f64| match op {
BinaryOp::Less => a < b,
BinaryOp::LessEqual => a <= b,
BinaryOp::Greater => a > b,
BinaryOp::GreaterEqual => a >= b,
_ => unreachable!("caller limits the operators"),
};
let numbers = |value: &Value| -> Vec<f64> {
match value {
Value::NodeSet(nodes) => nodes
.iter()
.map(|&n| super::value::parse_number(&document.string_value(n)))
.collect(),
other => vec![other.to_number(document)],
}
};
let left_numbers = numbers(left);
let right_numbers = numbers(right);
left_numbers
.iter()
.any(|&a| right_numbers.iter().any(|&b| compare(a, b)))
}
fn sort_and_deduplicate(mut nodes: Vec<NodeId>, document: &Document) -> Vec<NodeId> {
nodes.sort_unstable_by_key(|&n| document.order(n));
nodes.dedup();
nodes
}
fn evaluate_path(path: &PathExpr, context: &EvalContext<'_>) -> Result<Vec<NodeId>, EvalError> {
let mut current = match &path.start {
PathStart::Root => vec![context.document.root_of(context.node)],
PathStart::Context => vec![context.node],
PathStart::Expr(expr, predicates) => {
let value = evaluate(expr, context)?;
let Value::NodeSet(nodes) = value else {
return Err(EvalError::new(format!(
"a path can only continue from a node-set, but the left side is a {}",
value.type_name()
)));
};
let mut nodes = sort_and_deduplicate(nodes, context.document);
for predicate in predicates {
nodes = filter_by_predicate(nodes, predicate, context)?;
}
nodes
}
};
for step in &path.steps {
current = evaluate_step(step, ¤t, context)?;
}
Ok(current)
}
fn evaluate_step(
step: &Step,
input: &[NodeId],
context: &EvalContext<'_>,
) -> Result<Vec<NodeId>, EvalError> {
let document = context.document;
let mut output: Vec<NodeId> = Vec::new();
for &node in input {
let mut candidates: Vec<NodeId> = collect_axis(document, node, step.axis)
.into_iter()
.filter(|&candidate| {
matches_node_test(document, candidate, step.axis, &step.node_test, context)
})
.collect();
for predicate in &step.predicates {
candidates = filter_by_predicate(candidates, predicate, context)?;
}
output.extend(candidates);
}
Ok(sort_and_deduplicate(output, document))
}
fn filter_by_predicate(
nodes: Vec<NodeId>,
predicate: &Expr,
context: &EvalContext<'_>,
) -> Result<Vec<NodeId>, EvalError> {
let size = nodes.len();
let mut kept = Vec::with_capacity(size);
for (index, node) in nodes.into_iter().enumerate() {
let position = index + 1;
let inner = context.focus(node, position, size);
let value = evaluate(predicate, &inner)?;
let keep = match value {
Value::Number(n) => {
#[allow(clippy::cast_precision_loss)]
let position = position as f64;
(n - position).abs() < f64::EPSILON
}
other => other.to_boolean(),
};
if keep {
kept.push(node);
}
}
Ok(kept)
}
fn collect_axis(document: &Document, node: NodeId, axis: Axis) -> Vec<NodeId> {
match axis {
Axis::SelfAxis => vec![node],
Axis::Child => document.children(node).to_vec(),
Axis::Attribute => document.attributes(node).to_vec(),
Axis::Namespace => document.namespaces(node).to_vec(),
Axis::Parent => document.parent(node).into_iter().collect(),
Axis::Ancestor => document.ancestors(node),
Axis::AncestorOrSelf => {
let mut nodes = vec![node];
nodes.extend(document.ancestors(node));
nodes
}
Axis::Descendant => {
let mut nodes = document.descendants_or_self(node);
nodes.remove(0);
nodes
}
Axis::DescendantOrSelf => document.descendants_or_self(node),
Axis::FollowingSibling => siblings(document, node, true),
Axis::PrecedingSibling => siblings(document, node, false),
Axis::Following => {
let after = document.subtree_end(node);
document
.descendants_or_self(document.root_of(node))
.into_iter()
.filter(|&n| document.order(n) > after)
.collect()
}
Axis::Preceding => {
let order = document.order(node);
let mut nodes: Vec<NodeId> = document
.descendants_or_self(document.root_of(node))
.into_iter()
.filter(|&n| document.order(n) < order && document.subtree_end(n) < order)
.collect();
nodes.reverse();
nodes
}
}
}
fn siblings(document: &Document, node: NodeId, following: bool) -> Vec<NodeId> {
if matches!(
document.kind(node),
NodeKind::Attribute | NodeKind::Namespace
) {
return Vec::new();
}
let Some(parent) = document.parent(node) else {
return Vec::new();
};
let children = document.children(parent);
let Some(index) = children.iter().position(|&n| n == node) else {
return Vec::new();
};
if following {
children[index + 1..].to_vec()
} else {
let mut before = children[..index].to_vec();
before.reverse();
before
}
}
const fn principal_kind(axis: Axis) -> NodeKind {
match axis {
Axis::Attribute => NodeKind::Attribute,
Axis::Namespace => NodeKind::Namespace,
_ => NodeKind::Element,
}
}
fn matches_node_test(
document: &Document,
node: NodeId,
axis: Axis,
test: &NodeTest,
context: &EvalContext<'_>,
) -> bool {
let kind = document.kind(node);
match test {
NodeTest::AnyNode => true,
NodeTest::Text => kind == NodeKind::Text,
NodeTest::Comment => kind == NodeKind::Comment,
NodeTest::ProcessingInstruction(target) => {
kind == NodeKind::ProcessingInstruction
&& match target {
None => true,
Some(wanted) => {
document.name(node).is_some_and(|n| &n.local == wanted)
}
}
}
NodeTest::Wildcard => kind == principal_kind(axis),
NodeTest::NamespaceWildcard(prefix) => {
kind == principal_kind(axis)
&& match context.namespaces.resolve(prefix) {
None => false,
Some(uri) => document
.name(node)
.is_some_and(|n| n.uri.as_deref() == Some(uri)),
}
}
NodeTest::Name(name) => {
if kind != principal_kind(axis) {
return false;
}
if axis == Axis::Namespace {
return document.name(node).is_some_and(|n| n.local == name.local);
}
let uri = match &name.prefix {
Some(prefix) => match context.namespaces.resolve(prefix) {
Some(uri) => Some(uri),
None => return false,
},
None => None,
};
document
.name(node)
.is_some_and(|n| n.matches_parts(uri, &name.local))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xpath::{parse, Namespaces, Variables};
fn eval(source: &str, expression: &str) -> Value {
let document = Document::from_str(source).unwrap();
let expr = parse(expression).unwrap();
let variables = Variables::new();
let namespaces = Namespaces::new();
let context = EvalContext::new(
&document,
document.document_element().unwrap(),
&variables,
&namespaces,
);
evaluate(&expr, &context).unwrap()
}
fn number(source: &str, expression: &str) -> f64 {
let document = Document::from_str(source).unwrap();
eval(source, expression).to_number(&document)
}
fn boolean(source: &str, expression: &str) -> bool {
eval(source, expression).to_boolean()
}
fn text(source: &str, expression: &str) -> String {
let document = Document::from_str(source).unwrap();
eval(source, expression).to_xpath_string(&document)
}
const DOC: &str = "<a><b id='1'>one</b><b id='2'>two</b><c><d/></c></a>";
#[test]
fn child_axis_selects_named_children() {
assert_eq!(number(DOC, "count(b)"), 2.0);
assert_eq!(number(DOC, "count(child::b)"), 2.0);
}
#[test]
fn descendant_axis_reaches_grandchildren() {
assert_eq!(number(DOC, "count(descendant::*)"), 4.0);
assert_eq!(number(DOC, "count(.//d)"), 1.0);
}
#[test]
fn attribute_axis_needs_the_attribute_principal_type() {
assert_eq!(number(DOC, "count(b/@id)"), 2.0);
assert_eq!(number(DOC, "count(b/id)"), 0.0);
}
#[test]
fn parent_and_ancestor_axes() {
assert_eq!(number(DOC, "count(c/d/parent::c)"), 1.0);
assert_eq!(number(DOC, "count(c/d/ancestor::*)"), 2.0);
assert_eq!(number(DOC, "count(c/d/ancestor-or-self::*)"), 3.0);
}
#[test]
fn sibling_axes_are_side_specific() {
assert_eq!(number(DOC, "count(c/preceding-sibling::*)"), 2.0);
assert_eq!(number(DOC, "count(b[1]/following-sibling::*)"), 2.0);
}
#[test]
fn following_and_preceding_exclude_ancestors_and_descendants() {
assert_eq!(number(DOC, "count(b[1]/following::*)"), 3.0);
assert_eq!(number(DOC, "count(c/d/preceding::*)"), 2.0);
}
#[test]
fn predicates_number_positions_from_one() {
assert_eq!(text(DOC, "b[1]"), "one");
assert_eq!(text(DOC, "b[2]"), "two");
assert_eq!(text(DOC, "b[last()]"), "two");
assert_eq!(number(DOC, "count(b[position() = 1])"), 1.0);
}
#[test]
fn reverse_axis_predicates_count_outwards() {
assert_eq!(text(DOC, "c/preceding-sibling::b[1]"), "two");
}
#[test]
fn node_set_equality_is_existential() {
assert!(boolean(DOC, "b/@id = '1'"));
assert!(boolean(DOC, "b/@id != '1'"));
}
#[test]
fn empty_node_set_compares_false_both_ways() {
assert!(!boolean(DOC, "nothing = 'x'"));
assert!(!boolean(DOC, "nothing != 'x'"));
}
#[test]
fn relational_comparison_is_numeric() {
assert!(boolean(DOC, "b/@id > 1"));
assert!(!boolean(DOC, "b/@id > 2"));
assert!(!boolean(DOC, "'x' > 0"));
}
#[test]
fn arithmetic_follows_ieee_754() {
assert_eq!(number(DOC, "1 div 0"), f64::INFINITY);
assert!(number(DOC, "0 div 0").is_nan());
assert_eq!(number(DOC, "5 mod 3"), 2.0);
assert_eq!(number(DOC, "-5 mod 3"), -2.0);
}
#[test]
fn union_merges_into_document_order() {
assert_eq!(number(DOC, "count(b | c)"), 3.0);
assert_eq!(number(DOC, "count(b | b)"), 2.0);
}
#[test]
fn union_of_non_node_sets_is_an_error() {
let document = Document::from_str(DOC).unwrap();
let expr = parse("1 | 2").unwrap();
let variables = Variables::new();
let namespaces = Namespaces::new();
let context = EvalContext::new(&document, document.root(), &variables, &namespaces);
assert!(evaluate(&expr, &context).is_err());
}
#[test]
fn string_functions() {
assert_eq!(text(DOC, "concat('a', 'b')"), "ab");
assert_eq!(text(DOC, "substring('12345', 2, 3)"), "234");
assert_eq!(text(DOC, "substring('12345', 1.5, 2.6)"), "234");
assert_eq!(text(DOC, "substring-before('a/b', '/')"), "a");
assert_eq!(text(DOC, "substring-after('a/b', '/')"), "b");
assert_eq!(text(DOC, "normalize-space(' a b ')"), "a b");
assert_eq!(text(DOC, "translate('bar', 'abc', 'ABC')"), "BAr");
assert_eq!(number(DOC, "string-length('abc')"), 3.0);
assert!(boolean(DOC, "starts-with('abc', 'a')"));
assert!(boolean(DOC, "contains('abc', 'b')"));
}
#[test]
fn numeric_functions() {
assert_eq!(number(DOC, "sum(b/@id)"), 3.0);
assert_eq!(number(DOC, "floor(1.9)"), 1.0);
assert_eq!(number(DOC, "ceiling(1.1)"), 2.0);
assert_eq!(number(DOC, "round(1.5)"), 2.0);
assert_eq!(number(DOC, "round(-1.5)"), -1.0);
}
#[test]
fn name_functions_use_the_context_node() {
assert_eq!(text(DOC, "name()"), "a");
assert_eq!(text(DOC, "local-name(b[1])"), "b");
assert_eq!(text(DOC, "name(b[2]/@id)"), "id");
}
#[test]
fn unbound_variable_is_an_error_not_an_empty_node_set() {
let document = Document::from_str(DOC).unwrap();
let expr = parse("$missing").unwrap();
let variables = Variables::new();
let namespaces = Namespaces::new();
let context = EvalContext::new(&document, document.root(), &variables, &namespaces);
let error = evaluate(&expr, &context).unwrap_err();
assert!(error.message.contains("$missing"), "{error}");
}
#[test]
fn variables_resolve_when_bound() {
let document = Document::from_str(DOC).unwrap();
let expr = parse("$n + 1").unwrap();
let mut variables = Variables::new();
variables.bind("n", Value::Number(41.0));
let namespaces = Namespaces::new();
let context = EvalContext::new(&document, document.root(), &variables, &namespaces);
assert_eq!(evaluate(&expr, &context).unwrap(), Value::Number(42.0));
}
#[test]
fn prefixed_names_resolve_through_the_namespace_bindings() {
let document = Document::from_str(r#"<a xmlns="urn:n"><b/></a>"#).unwrap();
let mut namespaces = Namespaces::new();
namespaces.insert("p", "urn:n");
let variables = Variables::new();
let context = EvalContext::new(
&document,
document.document_element().unwrap(),
&variables,
&namespaces,
);
let prefixed = parse("count(p:b)").unwrap();
assert_eq!(evaluate(&prefixed, &context).unwrap(), Value::Number(1.0));
let bare = parse("count(b)").unwrap();
assert_eq!(evaluate(&bare, &context).unwrap(), Value::Number(0.0));
}
#[test]
fn text_and_comment_node_tests() {
let source = "<a>x<!--c--><?pi d?></a>";
assert_eq!(number(source, "count(text())"), 1.0);
assert_eq!(number(source, "count(comment())"), 1.0);
assert_eq!(number(source, "count(processing-instruction())"), 1.0);
assert_eq!(number(source, "count(processing-instruction('pi'))"), 1.0);
assert_eq!(number(source, "count(processing-instruction('other'))"), 0.0);
assert_eq!(number(source, "count(node())"), 3.0);
}
#[test]
fn absolute_paths_start_at_the_root() {
assert_eq!(number(DOC, "count(/a/b)"), 2.0);
assert_eq!(number(DOC, "count(//b)"), 2.0);
assert_eq!(number(DOC, "count(/)"), 1.0);
}
#[test]
fn boolean_operators_short_circuit() {
let document = Document::from_str(DOC).unwrap();
let variables = Variables::new();
let namespaces = Namespaces::new();
let context = EvalContext::new(&document, document.root(), &variables, &namespaces);
let expr = parse("false() and $missing").unwrap();
assert_eq!(evaluate(&expr, &context).unwrap(), Value::Boolean(false));
let expr = parse("true() or $missing").unwrap();
assert_eq!(evaluate(&expr, &context).unwrap(), Value::Boolean(true));
}
#[test]
fn lang_matches_inherited_xml_lang() {
let source = r#"<a xml:lang="en"><b/></a>"#;
assert!(boolean(source, "b[lang('en')]"));
assert!(!boolean(source, "b[lang('fr')]"));
}
#[test]
fn wrong_arity_is_an_error_not_a_panic() {
let document = Document::from_str(DOC).unwrap();
let variables = Variables::new();
let namespaces = Namespaces::new();
let context = EvalContext::new(&document, document.root(), &variables, &namespaces);
for source in ["not()", "translate('a')", "substring('a')", "concat('a')", "boolean()"] {
let expr = parse(source).unwrap();
let error = evaluate(&expr, &context)
.expect_err(&format!("{source} should be an arity error"));
assert!(error.message.contains("argument"), "{source}: {error}");
}
}
#[test]
fn id_function_finds_elements_by_id_attribute() {
assert_eq!(number(DOC, "count(id('1'))"), 1.0);
assert_eq!(number(DOC, "count(id('1 2'))"), 2.0);
}
}