Skip to main content

libxml_rs/xml/xpath/
eval.rs

1//! XPath 1.0 Evaluation Engine (§25).
2//!
3//! Evaluates compiled XPath expressions against XML trees using the
4//! internal Rust representation.
5//!
6//! # UPSTREAM-PARITY
7//!
8//! Full XPath 1.0 evaluation semantics: location paths, axes, node tests,
9//! predicates, functions, operators, type conversions, comparison semantics.
10//!
11//! # Courts
12//!
13//! XPATH-EVAL-*
14//!
15//! # Upstream contract
16//!
17//! Mirrors the evaluation core of upstream `xpath.c`
18//! (`SRC-LIBXML2-2.15.0-XPATH-C`, parity target libxml2 2.15.3 oracle):
19//! xmlXPathEvalExpr semantics for location paths, predicates, operators,
20//! type conversions and comparison rules.
21//!
22//! # Conceptual behavior
23//!
24//! Walks the AST with an explicit recursion guard (`push_recursion` /
25//! `pop_recursion`). Absolute paths start at the document node (the
26//! `_xmlDoc` cast to `_xmlNode`, type 9), NOT at the root element — the
27//! R-000102 fix that aligns `/` with XPath 1.0. Predicates are evaluated
28//! per-step with live context position/size; node-sets are deduplicated
29//! in document order.
30//!
31//! # Ownership & safety invariants
32//!
33//! Evaluation borrows the tree and the context: node-sets hold borrowed
34//! `_xmlNode` pointers that stay valid while the document lives; the
35//! engine never frees tree nodes. Values returned to the C ABI are
36//! converted in exports.rs and owned by the `_xmlXPathObject`. Context
37//! mutation is confined to the evaluation call (no global state).
38//!
39//! # Historical quirks & epochs
40//!
41//! R-000102: absolute paths originally evaluated from the root element
42//! (candidate bug) and were fixed to the document node. R-000159:
43//! `//book[position() <= 2]` matched one extra node until the predicate
44//! context-size semantics were aligned with the oracle. Both were sealed
45//! during the 11.1 XPath closure against the 2.15.3 oracle.
46//!
47//! # Deliberate oddities
48//!
49//! The engine evaluates predicates eagerly per step (upstream
50//! xmlXPathCompOpEval style) rather than collecting then filtering —
51//! position()/last() depend on that order, so it is deliberate.
52//!
53//! # Proving courts
54//!
55//! XPATH-EVAL-* and the XPATH differential probes compare evaluation
56//! results byte-identical against the oracle; the XSLT courts
57//! (CLI-XSLTPROC-*) run compiled templates through this engine.
58//!
59//! # Tempting simplifications that would break parity
60//!
61//! Do not defer predicate evaluation to a post-filter pass: position() /
62//! last() are order-sensitive (R-000159). Do not drop the recursion
63//! guard, and do not evaluate absolute paths from the root element
64//! (R-000102) — both are observable through xmlXPathEval results.
65
66use crate::abi::structs::_xmlNode;
67use crate::xml::xpath::ast::{Axis, BinaryOp, Expr, NameTest, NodeTest, Step};
68use crate::xml::xpath::axes;
69use crate::xml::xpath::context::XPathContext;
70use crate::xml::xpath::parser_context::{
71    free_parser_context, new_parser_context, value_pop, value_push,
72};
73use crate::xml::xpath::types::{node_string_value, string_to_number, NodeSet, XPathValue};
74use core::ffi::c_void;
75use std::os::raw::c_int;
76
77// ═══════════════════════════════════════════════════════════════════════════════
78// Evaluation
79// ═══════════════════════════════════════════════════════════════════════════════
80
81/// Evaluate an XPath expression in the given context.
82pub fn eval(ctx: &mut XPathContext, expr: &Expr) -> Result<XPathValue, String> {
83    ctx.push_recursion()?;
84
85    let result = match expr {
86        Expr::Step(step) => eval_step(ctx, step),
87        Expr::AbsolutePath(expr) => eval_absolute_path(ctx, expr),
88        Expr::RelativePath(left, right) => eval_relative_path(ctx, left, right),
89        Expr::Filter(expr, predicates) => eval_filter(ctx, expr, predicates),
90        Expr::Variable(name) => eval_variable(ctx, name),
91        Expr::StringLiteral(s) => Ok(XPathValue::String(s.clone())),
92        Expr::NumberLiteral(n) => Ok(XPathValue::Number(*n)),
93        Expr::BooleanLiteral(b) => Ok(XPathValue::Boolean(*b)),
94        Expr::FunctionCall { name, args } => eval_function_call(ctx, name, args),
95        Expr::BinaryOp { op, left, right } => eval_binary_op(ctx, op, left, right),
96        Expr::UnaryMinus(expr) => {
97            let val = eval(ctx, expr)?;
98            Ok(XPathValue::Number(-val.as_number()))
99        }
100        Expr::Union(left, right) => eval_union(ctx, left, right),
101    };
102
103    ctx.pop_recursion();
104    result
105}
106
107// ═══════════════════════════════════════════════════════════════════════════════
108// Location Path Evaluation
109// ═══════════════════════════════════════════════════════════════════════════════
110
111/// Evaluate an absolute location path: `/foo/bar`.
112fn eval_absolute_path(ctx: &mut XPathContext, expr: &Expr) -> Result<XPathValue, String> {
113    // Start from the document root
114    let doc = ctx.document;
115    if doc.is_null() {
116        return Ok(XPathValue::NodeSet(NodeSet::new()));
117    }
118
119    {
120        // The document node is the _xmlDoc cast to _xmlNode (type 9).
121        // Absolute paths like `/root/item` select relative to the
122        // document node itself, NOT the root element. This matches
123        // XPath 1.0: `/` selects the document root node.
124        let doc_node = doc as *mut _xmlNode;
125
126        // Set context to document node and evaluate the path
127        let saved_node = ctx.context_node;
128        let saved_list = ctx.context_list.clone();
129        ctx.context_node = doc_node;
130        ctx.set_context_list(vec![doc_node]);
131
132        let result = eval(ctx, expr);
133
134        ctx.context_node = saved_node;
135        ctx.context_list = saved_list;
136
137        result
138    }
139}
140
141/// Evaluate a relative location path: `foo/bar`.
142fn eval_relative_path(
143    ctx: &mut XPathContext,
144    left: &Expr,
145    right: &Expr,
146) -> Result<XPathValue, String> {
147    // Evaluate left side to get a node-set
148    let left_val = eval(ctx, left)?;
149    let left_ns = left_val.as_node_set().clone();
150
151    let mut result = NodeSet::new();
152
153    for node in left_ns.iter() {
154        // For each node in the left result, evaluate the right step
155        let saved_node = ctx.context_node;
156        let saved_list = ctx.context_list.clone();
157        ctx.context_node = node;
158        ctx.set_context_list(left_ns.iter().collect());
159
160        match eval(ctx, right) {
161            Ok(val) => {
162                if let XPathValue::NodeSet(ns) = val {
163                    for n in ns.iter() {
164                        result.push(n);
165                    }
166                }
167            }
168            Err(e) => {
169                ctx.context_node = saved_node;
170                ctx.context_list = saved_list;
171                return Err(e);
172            }
173        }
174
175        ctx.context_node = saved_node;
176        ctx.context_list = saved_list;
177    }
178
179    Ok(XPathValue::NodeSet(result))
180}
181
182/// Evaluate a single step.
183/// Evaluate one axis step, applying predicates to the traversed node-set.
184///
185/// # Safety
186///
187/// - `ctx.context_node` must be NULL or a valid `_xmlNode` that stays
188///   alive for the call; `traverse_axis` reads the node's subtree, so the
189///   whole reachable tree must be valid and stable while it runs; the
190///   predicate evaluation temporarily mutates the context fields, which
191///   must not be observed by other threads during the call.
192fn eval_step(ctx: &mut XPathContext, step: &Step) -> Result<XPathValue, String> {
193    let context_node = ctx.context_node;
194    if context_node.is_null() {
195        return Ok(XPathValue::NodeSet(NodeSet::new()));
196    }
197
198    // UPSTREAM-PARITY (xpath.c xmlXPathCompQName): a QName / prefix:* node
199    // test resolves its prefix through the context's REGISTERED namespaces
200    // (ctxt->nsHash) — never the document's in-scope declarations. An
201    // unregistered prefix raises XPATH_UNDEF_PREFIX_ERROR
202    // ("Undefined namespace prefix: prefix").
203    let node_test = resolve_step_prefix(&step.node_test, &ctx.namespaces)?;
204
205    // Traverse the axis. Attribute nodes are included for every axis step;
206    // namespace nodes only when the step is explicitly the namespace axis
207    // (upstream collects namespace nodes only on AXIS_NAMESPACE).
208    let include_namespaces = step.axis == Axis::Namespace;
209    let mut result = unsafe {
210        axes::traverse_axis(
211            context_node,
212            step.axis,
213            &node_test,
214            true, // include attributes
215            include_namespaces,
216        )
217    };
218
219    // Apply predicates
220    for predicate in &step.predicates {
221        let mut filtered = NodeSet::new();
222
223        for (i, node) in result.iter().enumerate() {
224            // Set context position and size for this node
225            let saved_node = ctx.context_node;
226            let saved_pos = ctx.context_position;
227            let saved_prox = ctx.proximity_position;
228            let saved_size = ctx.context_size;
229            let saved_list = ctx.context_list.clone();
230
231            ctx.context_node = node;
232            ctx.context_position = (i + 1) as i32;
233            // UPSTREAM-PARITY: position() reads proximityPosition — both
234            // must track the predicate position (R-000159).
235            ctx.proximity_position = (i + 1) as i32;
236            ctx.context_size = result.len() as i32;
237
238            // Evaluate predicate
239            let pred_val = eval(ctx, predicate)?;
240
241            // Predicate is true if:
242            // - It's a number and equals the context position
243            // - It's a boolean and is true
244            // - It converts to true
245            let matches = match pred_val {
246                XPathValue::Number(n) => {
247                    // Number predicate: match if n == context_position
248                    (n - (i as f64 + 1.0)).abs() < f64::EPSILON
249                        || (n.round() as i32) == (i + 1) as i32
250                }
251                _ => pred_val.as_boolean(),
252            };
253
254            if matches {
255                filtered.push(node);
256            }
257
258            ctx.context_node = saved_node;
259            ctx.context_position = saved_pos;
260            ctx.proximity_position = saved_prox;
261            ctx.context_size = saved_size;
262            ctx.context_list = saved_list;
263        }
264
265        result = filtered;
266    }
267
268    Ok(XPathValue::NodeSet(result))
269}
270
271/// Resolve a step's node-test prefix through the registered namespaces,
272/// replacing prefix-based tests with URI-based ones (upstream
273/// `xmlXPathCompQName`/`xmlXPathCompNodeTest` do this at compile time; the
274/// candidate's AST is built without the context, so the resolution happens
275/// here at the start of each step evaluation).
276///
277/// An unregistered prefix is an error (XPATH_UNDEF_PREFIX_ERROR).
278fn resolve_step_prefix(
279    node_test: &NodeTest,
280    namespaces: &std::collections::HashMap<String, String>,
281) -> Result<NodeTest, String> {
282    match node_test {
283        NodeTest::NameTest(NameTest::QName { prefix, local }) if !prefix.is_empty() => {
284            let uri = namespaces
285                .get(prefix)
286                .ok_or_else(|| format!("Undefined namespace prefix: {}", prefix))?;
287            Ok(NodeTest::NameTest(NameTest::QNameUri {
288                uri: uri.clone(),
289                local: local.clone(),
290            }))
291        }
292        NodeTest::NsWildcard(prefix) if !prefix.is_empty() => {
293            let uri = namespaces
294                .get(prefix)
295                .ok_or_else(|| format!("Undefined namespace prefix: {}", prefix))?;
296            Ok(NodeTest::NsWildcardUri(uri.clone()))
297        }
298        _ => Ok(node_test.clone()),
299    }
300}
301
302/// Evaluate a filter expression: `primary[pred1][pred2]`.
303fn eval_filter(
304    ctx: &mut XPathContext,
305    expr: &Expr,
306    predicates: &[Expr],
307) -> Result<XPathValue, String> {
308    // Evaluate the primary expression
309    let mut result = eval(ctx, expr)?;
310
311    // Apply predicates
312    let ns = match &mut result {
313        XPathValue::NodeSet(ns) => ns,
314        _ => return Ok(result), // Non-node-set can't have predicates
315    };
316
317    for predicate in predicates {
318        let mut filtered = NodeSet::new();
319        let nodes: Vec<_> = ns.iter().collect();
320
321        for (i, node) in nodes.iter().enumerate() {
322            let saved_node = ctx.context_node;
323            let saved_pos = ctx.context_position;
324            let saved_prox = ctx.proximity_position;
325            let saved_size = ctx.context_size;
326            let saved_list = ctx.context_list.clone();
327
328            ctx.context_node = *node;
329            ctx.context_position = (i + 1) as i32;
330            // UPSTREAM-PARITY: position() reads proximityPosition (R-000159).
331            ctx.proximity_position = (i + 1) as i32;
332            ctx.context_size = nodes.len() as i32;
333
334            let pred_val = eval(ctx, predicate)?;
335
336            let matches = match pred_val {
337                XPathValue::Number(n) => {
338                    (n - (i as f64 + 1.0)).abs() < f64::EPSILON
339                        || (n.round() as i32) == (i + 1) as i32
340                }
341                _ => pred_val.as_boolean(),
342            };
343
344            if matches {
345                filtered.push(*node);
346            }
347
348            ctx.context_node = saved_node;
349            ctx.context_position = saved_pos;
350            ctx.proximity_position = saved_prox;
351            ctx.context_size = saved_size;
352            ctx.context_list = saved_list;
353        }
354
355        *ns = filtered;
356    }
357
358    Ok(result)
359}
360
361// ═══════════════════════════════════════════════════════════════════════════════
362// Variable / Function Call Evaluation
363// ═══════════════════════════════════════════════════════════════════════════════
364
365/// Evaluate a variable reference.
366fn eval_variable(ctx: &mut XPathContext, name: &str) -> Result<XPathValue, String> {
367    ctx.resolve_variable(name)
368        .ok_or_else(|| format!("Undefined variable: ${}", name))
369}
370
371/// Resolve a function-call name to the qualified `{URI}local` key used by
372/// `xmlXPathRegisterFuncNS`. Unprefixed names stay as-is; a `prefix:local`
373/// name is expanded using the context's registered namespace map. Returns
374/// `None` when the prefix is undeclared or the name is unprefixed.
375fn resolve_function_qualified_name(ctx: &XPathContext, name: &str) -> Option<String> {
376    let (prefix, local) = name.split_once(':')?;
377    let uri = ctx.namespaces.get(prefix)?;
378    Some(format!("{{{}}}{}", uri, local))
379}
380
381/// Evaluate a function call.
382fn eval_function_call(
383    ctx: &mut XPathContext,
384    name: &str,
385    args: &[Expr],
386) -> Result<XPathValue, String> {
387    // Evaluate arguments first
388    let mut evaluated_args = Vec::new();
389    for arg in args {
390        evaluated_args.push(eval(ctx, arg)?);
391    }
392
393    // Look up the function. Namespaced function calls (nokogiri's
394    // `nokogiri-builtin:css-class` registered via xmlXPathRegisterFuncNS with
395    // URI `https://www.nokogiri.org/default_ns/ruby/builtins`) are stored under
396    // the `{URI}local` qualified key. The raw name (e.g. `exsl:node-set`) is
397    // tried first so the XSLT function_lookup closure (which splits on ':') and
398    // prefix-string-registered functions resolve; the qualified key falls back.
399    //
400    // UPSTREAM-PARITY (functions.c xsltXPathFunctionLookup): for a PREFIXED
401    // call whose prefix is declared, the per-context extension registry is
402    // consulted BEFORE the builtin/native tables — xslt gives "priority to
403    // context-level functions" (xmlHashLookup2 on the funcHash, which
404    // xsltRegisterExtFunction fills) over the module registry and the XSLT
405    // builtins. PHP exploits this to OVERRIDE EXSLT builtins
406    // (registerPHPFunctionNS on the EXSLT namespace); native Rust registrations
407    // (e.g. the candidate's EXSLT library) must therefore yield to a
408    // per-context C registration for the same prefix:local name.
409    let namespaced_c_first: Option<crate::xml::xpath::context::BoxedXPathFunction> = {
410        let prefixed = name
411            .split_once(':')
412            .is_some_and(|(p, _)| !p.is_empty() && ctx.namespaces.contains_key(p));
413        if prefixed {
414            ctx.function_lookup.as_ref().and_then(|lk| lk(ctx, name))
415        } else {
416            None
417        }
418    };
419    if let Some(f) = namespaced_c_first {
420        return f(ctx, &evaluated_args);
421    }
422    let mut func_ptr: Option<*const crate::xml::xpath::context::BoxedXPathFunction> = None;
423    if let Some(f) = ctx.lookup_function(name) {
424        func_ptr = Some(f as *const _);
425    } else if let Some(qname) = resolve_function_qualified_name(ctx, name) {
426        if let Some(f) = ctx.lookup_function(&qname) {
427            func_ptr = Some(f as *const _);
428        }
429    }
430    match func_ptr {
431        Some(p) => {
432            // SAFETY: `p` points into `ctx.functions`, which is alive for the
433            // duration of this call and is not mutated during evaluation.
434            let f: &crate::xml::xpath::context::BoxedXPathFunction = unsafe { &*p };
435            f(ctx, &evaluated_args)
436        }
437        None => {
438            // C-registered extension function (xmlXPathRegisterFuncLookup):
439            // consumers like nokogiri register a lookup callback that returns
440            // an xmlXPathFunction; the candidate must invoke it with the
441            // upstream parser-context protocol (name on ctxt->function, args
442            // on the value stack, result popped off the stack).
443            if ctx.func_lookup_func.is_some() {
444                if let Some(result) = invoke_c_extension_function(ctx, name, &evaluated_args) {
445                    return Ok(result);
446                }
447            }
448            // UPSTREAM-PARITY (xpath.c xmlXPathCompFunction): an unknown
449            // function reports "Unregistered function: name"; when the name
450            // carries a prefix whose namespace was never declared, the error
451            // is "Undefined namespace prefix: prefix" instead (both are
452            // XPATH_UNKNOWN_FUNC / XPATH_UNDEF_PREFIX_ERROR, delivered as
453            // "XPath error : ...").
454            let msg = match name.split_once(':') {
455                Some((prefix, _)) if !ctx.namespaces.contains_key(prefix) => {
456                    format!("Undefined namespace prefix: {}", prefix)
457                }
458                _ => format!("Unregistered function: {}", name),
459            };
460            Err(msg)
461        }
462    }
463}
464
465/// Invoke a C-registered XPath extension function through the upstream
466/// parser-context protocol.
467///
468/// UPSTREAM-PARITY (xpath.c `xmlXPathFunctionCall` / `xmlXPathCompFunction`):
469/// the function-lookup callback (`xmlXPathFuncLookupFunc`) is called with a
470/// NUL-terminated name; when it returns a function pointer, the candidate
471/// builds a fresh `xmlXPathParserContext`, sets `ctxt->function` to the
472/// function name (the invoker reads it), pushes the evaluated arguments in
473/// REVERSE order (so the first argument is on top — the function pops them in
474/// order), calls `func(pc, argc)`, and pops the result. Returns `None` when
475/// the callback is absent, returns NULL, or the C context is unavailable.
476///
477/// # Safety
478///
479/// - `ctx.func_lookup_func` / `ctx.func_lookup_data` come from
480///   `xmlXPathRegisterFuncLookup`; `ctx.c_context` must be the owning
481///   `_xmlXPathContext`.
482fn invoke_c_extension_function(
483    ctx: &mut XPathContext,
484    name: &str,
485    args: &[XPathValue],
486) -> Option<XPathValue> {
487    let lookup = ctx.func_lookup_func?;
488    let c_ctxt = ctx.c_context;
489    if c_ctxt.is_null() {
490        return None;
491    }
492
493    // UPSTREAM-PARITY (xpath.c xmlXPathCompFunction): the lookup callback
494    // receives the LOCAL name and the prefix's RESOLVED namespace URI, not
495    // the raw `prefix:local` token — xslt's xsltXPathFunctionLookup and
496    // php's php:function registration (module table keyed by
497    // {http://php.net/xsl}function) match on that. When the prefix is not
498    // declared the raw name + NULL URI are passed (consumers that key on
499    // the raw token, e.g. nokogiri's per-context lookup, keep working).
500    let (lookup_name, lookup_uri): (Vec<u8>, Option<Vec<u8>>) = match name.split_once(':') {
501        Some((prefix, local)) => match ctx.namespaces.get(prefix) {
502            Some(uri) => (local.as_bytes().to_vec(), Some(uri.as_bytes().to_vec())),
503            None => (name.as_bytes().to_vec(), None),
504        },
505        None => (name.as_bytes().to_vec(), None),
506    };
507    let mut lookup_name_nul: Vec<crate::abi::types::xmlChar> = lookup_name.clone();
508    lookup_name_nul.push(0);
509    let lookup_uri_nul: Option<Vec<crate::abi::types::xmlChar>> = lookup_uri.as_ref().map(|u| {
510        let mut v = u.clone();
511        v.push(0);
512        v
513    });
514    // SAFETY: the callback contract returns an xmlXPathFunction (fn pointer)
515    // stored as void*, or NULL when the handler does not provide the name.
516    let fp = unsafe {
517        lookup(
518            ctx.func_lookup_data,
519            lookup_name_nul.as_ptr() as *const crate::abi::types::xmlChar,
520            lookup_uri_nul.as_ref().map_or(std::ptr::null(), |v| {
521                v.as_ptr() as *const crate::abi::types::xmlChar
522            }),
523        )
524    };
525    if fp.is_null() {
526        return None;
527    }
528    let func: unsafe extern "C" fn(*mut c_void, c_int) = unsafe { std::mem::transmute(fp) };
529
530    unsafe {
531        let saved_function = (*c_ctxt).function;
532        (*c_ctxt).function = lookup_name_nul.as_ptr() as *const crate::abi::types::xmlChar;
533        let pc = new_parser_context(std::ptr::null(), c_ctxt);
534        if pc.is_null() {
535            (*c_ctxt).function = saved_function;
536            return None;
537        }
538
539        // Push the evaluated arguments in reverse order (upstream
540        // xmlXPathFunctionCall evaluates and pushes args from the last to the
541        // first, so the first argument sits on top of the stack).
542        let mut ok = true;
543        for val in args.iter().rev() {
544            let obj = crate::abi::exports_xml2::xpath_to_object_pub(val.clone());
545            if obj.is_null() {
546                ok = false;
547                break;
548            }
549            value_push(pc, obj);
550        }
551
552        let result = if ok {
553            func(pc as *mut c_void, args.len() as c_int);
554            let res = value_pop(pc);
555            if res.is_null() {
556                None
557            } else {
558                let val = crate::abi::exports_xml2::object_to_xpathvalue_pub(res);
559                crate::abi::exports_xml2::xmlXPathFreeObject(res);
560                Some(val)
561            }
562        } else {
563            None
564        };
565
566        // free_parser_context frees any remaining stack values and the
567        // context itself; the popped result was already freed above.
568        free_parser_context(pc);
569        (*c_ctxt).function = saved_function;
570        result
571    }
572}
573
574// ═══════════════════════════════════════════════════════════════════════════════
575// Operators
576// ═══════════════════════════════════════════════════════════════════════════════
577
578/// Evaluate a binary operation.
579fn eval_binary_op(
580    ctx: &mut XPathContext,
581    op: &BinaryOp,
582    left: &Expr,
583    right: &Expr,
584) -> Result<XPathValue, String> {
585    match op {
586        BinaryOp::Or => {
587            // Short-circuit: evaluate left, if true return true
588            let left_val = eval(ctx, left)?;
589            if left_val.as_boolean() {
590                return Ok(XPathValue::Boolean(true));
591            }
592            let right_val = eval(ctx, right)?;
593            Ok(XPathValue::Boolean(right_val.as_boolean()))
594        }
595        BinaryOp::And => {
596            // Short-circuit: evaluate left, if false return false
597            let left_val = eval(ctx, left)?;
598            if !left_val.as_boolean() {
599                return Ok(XPathValue::Boolean(false));
600            }
601            let right_val = eval(ctx, right)?;
602            Ok(XPathValue::Boolean(right_val.as_boolean()))
603        }
604        BinaryOp::Eq | BinaryOp::Ne => {
605            let left_val = eval(ctx, left)?;
606            let right_val = eval(ctx, right)?;
607            let eq = compare_equal(ctx, &left_val, &right_val);
608            Ok(match op {
609                BinaryOp::Eq => XPathValue::Boolean(eq),
610                BinaryOp::Ne => XPathValue::Boolean(!eq),
611                _ => unreachable!(),
612            })
613        }
614        BinaryOp::Lt | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Ge => {
615            let left_val = eval(ctx, left)?;
616            let right_val = eval(ctx, right)?;
617            let cmp = compare_ordered(ctx, &left_val, &right_val);
618            let result = match op {
619                BinaryOp::Lt => cmp == std::cmp::Ordering::Less,
620                BinaryOp::Gt => cmp == std::cmp::Ordering::Greater,
621                BinaryOp::Le => cmp != std::cmp::Ordering::Greater,
622                BinaryOp::Ge => cmp != std::cmp::Ordering::Less,
623                _ => unreachable!(),
624            };
625            Ok(XPathValue::Boolean(result))
626        }
627        BinaryOp::Add => {
628            let left_val = eval(ctx, left)?;
629            let right_val = eval(ctx, right)?;
630            Ok(XPathValue::Number(
631                left_val.as_number() + right_val.as_number(),
632            ))
633        }
634        BinaryOp::Sub => {
635            let left_val = eval(ctx, left)?;
636            let right_val = eval(ctx, right)?;
637            Ok(XPathValue::Number(
638                left_val.as_number() - right_val.as_number(),
639            ))
640        }
641        BinaryOp::Mul => {
642            let left_val = eval(ctx, left)?;
643            let right_val = eval(ctx, right)?;
644            Ok(XPathValue::Number(
645                left_val.as_number() * right_val.as_number(),
646            ))
647        }
648        BinaryOp::Div => {
649            let left_val = eval(ctx, left)?;
650            let right_val = eval(ctx, right)?;
651            Ok(XPathValue::Number(
652                left_val.as_number() / right_val.as_number(),
653            ))
654        }
655        BinaryOp::Mod => {
656            let left_val = eval(ctx, left)?;
657            let right_val = eval(ctx, right)?;
658            Ok(XPathValue::Number(
659                left_val.as_number() % right_val.as_number(),
660            ))
661        }
662        BinaryOp::Union => {
663            // Union is handled at the Expr level, not BinaryOp level
664            unreachable!("Union operator should be handled by Expr::Union")
665        }
666    }
667}
668
669/// Evaluate a union expression: `left | right`.
670fn eval_union(ctx: &mut XPathContext, left: &Expr, right: &Expr) -> Result<XPathValue, String> {
671    let left_val = eval(ctx, left)?;
672    let right_val = eval(ctx, right)?;
673
674    let mut result = left_val.as_node_set().clone();
675    result.extend(right_val.as_node_set());
676    result.sort();
677
678    Ok(XPathValue::NodeSet(result))
679}
680
681// ═══════════════════════════════════════════════════════════════════════════════
682// Comparison Semantics
683// ═══════════════════════════════════════════════════════════════════════════════
684
685/// Compare two XPath values for equality (XPath 1.0 §3.4).
686fn compare_equal(_ctx: &mut XPathContext, a: &XPathValue, b: &XPathValue) -> bool {
687    match (a, b) {
688        // If both are node-sets, compare by set intersection
689        (XPathValue::NodeSet(ns_a), XPathValue::NodeSet(ns_b)) => {
690            for node_a in ns_a.iter() {
691                let val_a = node_string_value(node_a);
692                for node_b in ns_b.iter() {
693                    let val_b = node_string_value(node_b);
694                    if val_a == val_b {
695                        return true;
696                    }
697                }
698            }
699            false
700        }
701        // If one is a node-set and the other is not
702        (XPathValue::NodeSet(ns), other) | (other, XPathValue::NodeSet(ns)) => {
703            for node in ns.iter() {
704                let node_str = node_string_value(node);
705                match other {
706                    XPathValue::Boolean(_) => {
707                        // Compare boolean(node-set) == other
708                        return (!ns.is_empty()) == other.as_boolean();
709                    }
710                    XPathValue::Number(_) => {
711                        let node_num = string_to_number(&node_str);
712                        if (node_num - other.as_number()).abs() < f64::EPSILON {
713                            return true;
714                        }
715                        continue;
716                    }
717                    XPathValue::String(_) => {
718                        if node_str == other.as_string() {
719                            return true;
720                        }
721                        continue;
722                    }
723                    _ => continue,
724                };
725            }
726            false
727        }
728        // Neither is a node-set
729        _ => match (a, b) {
730            (XPathValue::Boolean(_), _) | (_, XPathValue::Boolean(_)) => {
731                a.as_boolean() == b.as_boolean()
732            }
733            (XPathValue::Number(_), _) | (_, XPathValue::Number(_)) => {
734                let na = a.as_number();
735                let nb = b.as_number();
736                if na.is_nan() || nb.is_nan() {
737                    false
738                } else {
739                    (na - nb).abs() < f64::EPSILON || na == nb
740                }
741            }
742            _ => a.as_string() == b.as_string(),
743        },
744    }
745}
746
747/// Compare two XPath values for ordering (XPath 1.0 §3.4).
748fn compare_ordered(_ctx: &mut XPathContext, a: &XPathValue, b: &XPathValue) -> std::cmp::Ordering {
749    match (a, b) {
750        // If both are node-sets, compare pairwise
751        (XPathValue::NodeSet(ns_a), XPathValue::NodeSet(ns_b)) => {
752            for node_a in ns_a.iter() {
753                let num_a = string_to_number(&node_string_value(node_a));
754                for node_b in ns_b.iter() {
755                    let num_b = string_to_number(&node_string_value(node_b));
756                    if num_a < num_b {
757                        return std::cmp::Ordering::Less;
758                    }
759                    if num_a > num_b {
760                        return std::cmp::Ordering::Greater;
761                    }
762                }
763            }
764            std::cmp::Ordering::Equal
765        }
766        // If one is a node-set
767        (XPathValue::NodeSet(ns), other) | (other, XPathValue::NodeSet(ns)) => {
768            let other_num = other.as_number();
769            for node in ns.iter() {
770                let node_num = string_to_number(&node_string_value(node));
771                if node_num < other_num {
772                    return std::cmp::Ordering::Less;
773                }
774                if node_num > other_num {
775                    return std::cmp::Ordering::Greater;
776                }
777            }
778            std::cmp::Ordering::Equal
779        }
780        // Neither is a node-set: compare as numbers
781        _ => {
782            let na = a.as_number();
783            let nb = b.as_number();
784            if na.is_nan() || nb.is_nan() {
785                std::cmp::Ordering::Equal // NaN comparisons return false, so equal for ordering
786            } else if na < nb {
787                std::cmp::Ordering::Less
788            } else if na > nb {
789                std::cmp::Ordering::Greater
790            } else {
791                std::cmp::Ordering::Equal
792            }
793        }
794    }
795}
796
797// ═══════════════════════════════════════════════════════════════════════════════
798// Top-level evaluation API
799// ═══════════════════════════════════════════════════════════════════════════════
800
801/// Evaluate an XPath expression string against a document.
802///
803/// This is the main entry point for XPath evaluation.
804pub fn eval_xpath(ctx: &mut XPathContext, expression: &str) -> Result<XPathValue, String> {
805    let expr = crate::xml::xpath::parser::parse_xpath(expression).map_err(|e| e.message)?;
806    eval(ctx, &expr)
807}
808
809// ═══════════════════════════════════════════════════════════════════════════════
810// Tests
811// ═══════════════════════════════════════════════════════════════════════════════
812
813#[cfg(test)]
814mod tests {
815    use super::*;
816    use crate::xml::xpath::context::XPathContext;
817    use crate::xml::xpath::functions;
818
819    fn setup_context() -> XPathContext {
820        let mut ctx = XPathContext::new(std::ptr::null_mut());
821        // Register core functions
822        let funcs = functions::core_functions();
823        for (name, func) in funcs {
824            ctx.register_function(&name, func);
825        }
826        ctx
827    }
828
829    #[test]
830    fn test_eval_string_literal() {
831        let mut ctx = setup_context();
832        let result = eval_xpath(&mut ctx, "'hello'").unwrap();
833        assert_eq!(result.as_string(), "hello");
834    }
835
836    #[test]
837    fn test_eval_number_literal() {
838        let mut ctx = setup_context();
839        let result = eval_xpath(&mut ctx, "42").unwrap();
840        assert_eq!(result.as_number(), 42.0);
841    }
842
843    #[test]
844    fn test_eval_addition() {
845        let mut ctx = setup_context();
846        let result = eval_xpath(&mut ctx, "1 + 2").unwrap();
847        assert_eq!(result.as_number(), 3.0);
848    }
849
850    #[test]
851    fn test_eval_subtraction() {
852        let mut ctx = setup_context();
853        let result = eval_xpath(&mut ctx, "5 - 3").unwrap();
854        assert_eq!(result.as_number(), 2.0);
855    }
856
857    #[test]
858    fn test_eval_multiplication() {
859        let mut ctx = setup_context();
860        let result = eval_xpath(&mut ctx, "3 * 4").unwrap();
861        assert_eq!(result.as_number(), 12.0);
862    }
863
864    #[test]
865    fn test_eval_division() {
866        let mut ctx = setup_context();
867        let result = eval_xpath(&mut ctx, "10 div 3").unwrap();
868        assert!((result.as_number() - 3.3333333333333335).abs() < 1e-10);
869    }
870
871    #[test]
872    fn test_eval_modulo() {
873        let mut ctx = setup_context();
874        let result = eval_xpath(&mut ctx, "10 mod 3").unwrap();
875        assert_eq!(result.as_number(), 1.0);
876    }
877
878    #[test]
879    fn test_eval_equality() {
880        let mut ctx = setup_context();
881        assert!(eval_xpath(&mut ctx, "1 = 1").unwrap().as_boolean());
882        assert!(!eval_xpath(&mut ctx, "1 = 2").unwrap().as_boolean());
883        assert!(eval_xpath(&mut ctx, "1 != 2").unwrap().as_boolean());
884    }
885
886    #[test]
887    fn test_eval_comparison() {
888        let mut ctx = setup_context();
889        assert!(eval_xpath(&mut ctx, "1 < 2").unwrap().as_boolean());
890        assert!(eval_xpath(&mut ctx, "2 > 1").unwrap().as_boolean());
891        assert!(eval_xpath(&mut ctx, "1 <= 1").unwrap().as_boolean());
892        assert!(eval_xpath(&mut ctx, "2 >= 2").unwrap().as_boolean());
893    }
894
895    #[test]
896    fn test_eval_and_or() {
897        let mut ctx = setup_context();
898        assert!(eval_xpath(&mut ctx, "true() and true()")
899            .unwrap()
900            .as_boolean());
901        assert!(!eval_xpath(&mut ctx, "true() and false()")
902            .unwrap()
903            .as_boolean());
904        assert!(eval_xpath(&mut ctx, "true() or false()")
905            .unwrap()
906            .as_boolean());
907        assert!(!eval_xpath(&mut ctx, "false() or false()")
908            .unwrap()
909            .as_boolean());
910    }
911
912    #[test]
913    fn test_eval_not() {
914        let mut ctx = setup_context();
915        assert!(!eval_xpath(&mut ctx, "not(true())").unwrap().as_boolean());
916        assert!(eval_xpath(&mut ctx, "not(false())").unwrap().as_boolean());
917    }
918
919    #[test]
920    fn test_eval_boolean() {
921        let mut ctx = setup_context();
922        assert!(eval_xpath(&mut ctx, "boolean('hello')")
923            .unwrap()
924            .as_boolean());
925        assert!(!eval_xpath(&mut ctx, "boolean('')").unwrap().as_boolean());
926        assert!(!eval_xpath(&mut ctx, "boolean(0)").unwrap().as_boolean());
927        assert!(eval_xpath(&mut ctx, "boolean(1)").unwrap().as_boolean());
928    }
929
930    #[test]
931    fn test_eval_number() {
932        let mut ctx = setup_context();
933        assert_eq!(
934            eval_xpath(&mut ctx, "number('42')").unwrap().as_number(),
935            42.0
936        );
937    }
938
939    #[test]
940    fn test_eval_string() {
941        let mut ctx = setup_context();
942        assert_eq!(
943            eval_xpath(&mut ctx, "string(42)").unwrap().as_string(),
944            "42"
945        );
946    }
947
948    #[test]
949    fn test_eval_concat() {
950        let mut ctx = setup_context();
951        assert_eq!(
952            eval_xpath(&mut ctx, "concat('a', 'b', 'c')")
953                .unwrap()
954                .as_string(),
955            "abc"
956        );
957    }
958
959    #[test]
960    fn test_eval_starts_with() {
961        let mut ctx = setup_context();
962        assert!(eval_xpath(&mut ctx, "starts-with('hello', 'he')")
963            .unwrap()
964            .as_boolean());
965    }
966
967    #[test]
968    fn test_eval_contains() {
969        let mut ctx = setup_context();
970        assert!(eval_xpath(&mut ctx, "contains('hello', 'ell')")
971            .unwrap()
972            .as_boolean());
973    }
974
975    #[test]
976    fn test_eval_substring() {
977        let mut ctx = setup_context();
978        assert_eq!(
979            eval_xpath(&mut ctx, "substring('12345', 1, 3)")
980                .unwrap()
981                .as_string(),
982            "123"
983        );
984        assert_eq!(
985            eval_xpath(&mut ctx, "substring('12345', 2)")
986                .unwrap()
987                .as_string(),
988            "2345"
989        );
990    }
991
992    #[test]
993    fn test_eval_string_length() {
994        let mut ctx = setup_context();
995        assert_eq!(
996            eval_xpath(&mut ctx, "string-length('hello')")
997                .unwrap()
998                .as_number(),
999            5.0
1000        );
1001    }
1002
1003    #[test]
1004    fn test_eval_normalize_space() {
1005        let mut ctx = setup_context();
1006        assert_eq!(
1007            eval_xpath(&mut ctx, "normalize-space('  hello   world  ')")
1008                .unwrap()
1009                .as_string(),
1010            "hello world"
1011        );
1012    }
1013
1014    #[test]
1015    fn test_eval_floor_ceiling_round() {
1016        let mut ctx = setup_context();
1017        assert_eq!(eval_xpath(&mut ctx, "floor(3.7)").unwrap().as_number(), 3.0);
1018        assert_eq!(
1019            eval_xpath(&mut ctx, "ceiling(3.2)").unwrap().as_number(),
1020            4.0
1021        );
1022        assert_eq!(eval_xpath(&mut ctx, "round(3.5)").unwrap().as_number(), 4.0);
1023    }
1024
1025    #[test]
1026    fn test_eval_sum() {
1027        // sum() on an empty node-set should return 0
1028        let mut ctx = setup_context();
1029        ctx.document = std::ptr::null_mut();
1030        // Can't test sum directly without a node-set, but we can test it
1031        // with literal values when we add node-set construction
1032    }
1033
1034    #[test]
1035    fn test_eval_variable_not_found() {
1036        let mut ctx = setup_context();
1037        let result = eval_xpath(&mut ctx, "$undefined_var");
1038        assert!(result.is_err());
1039    }
1040
1041    #[test]
1042    fn test_eval_variable_found() {
1043        let mut ctx = setup_context();
1044        ctx.register_variable("x", XPathValue::Number(42.0));
1045        let result = eval_xpath(&mut ctx, "$x").unwrap();
1046        assert_eq!(result.as_number(), 42.0);
1047    }
1048
1049    #[test]
1050    fn test_eval_union() {
1051        // Union requires node-sets, which need a document.
1052        // Test basic union of numbers (should error since numbers aren't node-sets).
1053        // Actually, union on non-node-sets would panic at as_node_set().
1054        // For now, this is a placeholder for when we have document support.
1055    }
1056
1057    #[test]
1058    fn test_eval_operator_precedence() {
1059        let mut ctx = setup_context();
1060        // 1 + 2 * 3 should be 7 (multiplication before addition)
1061        let result = eval_xpath(&mut ctx, "1 + 2 * 3").unwrap();
1062        assert_eq!(result.as_number(), 7.0);
1063
1064        // (1 + 2) * 3 should be 9
1065        let result = eval_xpath(&mut ctx, "(1 + 2) * 3").unwrap();
1066        assert_eq!(result.as_number(), 9.0);
1067    }
1068
1069    #[test]
1070    fn test_eval_unary_minus() {
1071        let mut ctx = setup_context();
1072        let result = eval_xpath(&mut ctx, "-5").unwrap();
1073        assert_eq!(result.as_number(), -5.0);
1074
1075        let result = eval_xpath(&mut ctx, "--5").unwrap();
1076        assert_eq!(result.as_number(), 5.0);
1077    }
1078
1079    #[test]
1080    fn test_eval_true_false() {
1081        let mut ctx = setup_context();
1082        assert!(eval_xpath(&mut ctx, "true()").unwrap().as_boolean());
1083        assert!(!eval_xpath(&mut ctx, "false()").unwrap().as_boolean());
1084    }
1085
1086    #[test]
1087    fn test_eval_translate() {
1088        let mut ctx = setup_context();
1089        assert_eq!(
1090            eval_xpath(
1091                &mut ctx,
1092                "translate('hello', 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')"
1093            )
1094            .unwrap()
1095            .as_string(),
1096            "HELLO"
1097        );
1098    }
1099
1100    #[test]
1101    fn test_eval_empty_expression() {
1102        let mut ctx = setup_context();
1103        let result = eval_xpath(&mut ctx, "");
1104        assert!(result.is_err());
1105    }
1106}