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::{BinaryOp, Expr, Step};
68use crate::xml::xpath::axes;
69use crate::xml::xpath::context::XPathContext;
70use crate::xml::xpath::types::{node_string_value, string_to_number, NodeSet, XPathValue};
71
72// ═══════════════════════════════════════════════════════════════════════════════
73// Evaluation
74// ═══════════════════════════════════════════════════════════════════════════════
75
76/// Evaluate an XPath expression in the given context.
77pub fn eval(ctx: &mut XPathContext, expr: &Expr) -> Result<XPathValue, String> {
78    ctx.push_recursion()?;
79
80    let result = match expr {
81        Expr::Step(step) => eval_step(ctx, step),
82        Expr::AbsolutePath(expr) => eval_absolute_path(ctx, expr),
83        Expr::RelativePath(left, right) => eval_relative_path(ctx, left, right),
84        Expr::Filter(expr, predicates) => eval_filter(ctx, expr, predicates),
85        Expr::Variable(name) => eval_variable(ctx, name),
86        Expr::StringLiteral(s) => Ok(XPathValue::String(s.clone())),
87        Expr::NumberLiteral(n) => Ok(XPathValue::Number(*n)),
88        Expr::BooleanLiteral(b) => Ok(XPathValue::Boolean(*b)),
89        Expr::FunctionCall { name, args } => eval_function_call(ctx, name, args),
90        Expr::BinaryOp { op, left, right } => eval_binary_op(ctx, op, left, right),
91        Expr::UnaryMinus(expr) => {
92            let val = eval(ctx, expr)?;
93            Ok(XPathValue::Number(-val.as_number()))
94        }
95        Expr::Union(left, right) => eval_union(ctx, left, right),
96    };
97
98    ctx.pop_recursion();
99    result
100}
101
102// ═══════════════════════════════════════════════════════════════════════════════
103// Location Path Evaluation
104// ═══════════════════════════════════════════════════════════════════════════════
105
106/// Evaluate an absolute location path: `/foo/bar`.
107fn eval_absolute_path(ctx: &mut XPathContext, expr: &Expr) -> Result<XPathValue, String> {
108    // Start from the document root
109    let doc = ctx.document;
110    if doc.is_null() {
111        return Ok(XPathValue::NodeSet(NodeSet::new()));
112    }
113
114    {
115        // The document node is the _xmlDoc cast to _xmlNode (type 9).
116        // Absolute paths like `/root/item` select relative to the
117        // document node itself, NOT the root element. This matches
118        // XPath 1.0: `/` selects the document root node.
119        let doc_node = doc as *mut _xmlNode;
120
121        // Set context to document node and evaluate the path
122        let saved_node = ctx.context_node;
123        let saved_list = ctx.context_list.clone();
124        ctx.context_node = doc_node;
125        ctx.set_context_list(vec![doc_node]);
126
127        let result = eval(ctx, expr);
128
129        ctx.context_node = saved_node;
130        ctx.context_list = saved_list;
131
132        result
133    }
134}
135
136/// Evaluate a relative location path: `foo/bar`.
137fn eval_relative_path(
138    ctx: &mut XPathContext,
139    left: &Expr,
140    right: &Expr,
141) -> Result<XPathValue, String> {
142    // Evaluate left side to get a node-set
143    let left_val = eval(ctx, left)?;
144    let left_ns = left_val.as_node_set().clone();
145
146    let mut result = NodeSet::new();
147
148    for node in left_ns.iter() {
149        // For each node in the left result, evaluate the right step
150        let saved_node = ctx.context_node;
151        let saved_list = ctx.context_list.clone();
152        ctx.context_node = node;
153        ctx.set_context_list(left_ns.iter().collect());
154
155        match eval(ctx, right) {
156            Ok(val) => {
157                if let XPathValue::NodeSet(ns) = val {
158                    for n in ns.iter() {
159                        result.push(n);
160                    }
161                }
162            }
163            Err(e) => {
164                ctx.context_node = saved_node;
165                ctx.context_list = saved_list;
166                return Err(e);
167            }
168        }
169
170        ctx.context_node = saved_node;
171        ctx.context_list = saved_list;
172    }
173
174    Ok(XPathValue::NodeSet(result))
175}
176
177/// Evaluate a single step.
178/// Evaluate one axis step, applying predicates to the traversed node-set.
179///
180/// # Safety
181///
182/// - `ctx.context_node` must be NULL or a valid `_xmlNode` that stays
183///   alive for the call; `traverse_axis` reads the node's subtree, so the
184///   whole reachable tree must be valid and stable while it runs; the
185///   predicate evaluation temporarily mutates the context fields, which
186///   must not be observed by other threads during the call.
187fn eval_step(ctx: &mut XPathContext, step: &Step) -> Result<XPathValue, String> {
188    let context_node = ctx.context_node;
189    if context_node.is_null() {
190        return Ok(XPathValue::NodeSet(NodeSet::new()));
191    }
192
193    // Traverse the axis
194    let mut result = unsafe {
195        axes::traverse_axis(
196            context_node,
197            step.axis,
198            &step.node_test,
199            true,  // include attributes
200            false, // include namespaces
201        )
202    };
203
204    // Apply predicates
205    for predicate in &step.predicates {
206        let mut filtered = NodeSet::new();
207
208        for (i, node) in result.iter().enumerate() {
209            // Set context position and size for this node
210            let saved_node = ctx.context_node;
211            let saved_pos = ctx.context_position;
212            let saved_prox = ctx.proximity_position;
213            let saved_size = ctx.context_size;
214            let saved_list = ctx.context_list.clone();
215
216            ctx.context_node = node;
217            ctx.context_position = (i + 1) as i32;
218            // UPSTREAM-PARITY: position() reads proximityPosition — both
219            // must track the predicate position (R-000159).
220            ctx.proximity_position = (i + 1) as i32;
221            ctx.context_size = result.len() as i32;
222
223            // Evaluate predicate
224            let pred_val = eval(ctx, predicate)?;
225
226            // Predicate is true if:
227            // - It's a number and equals the context position
228            // - It's a boolean and is true
229            // - It converts to true
230            let matches = match pred_val {
231                XPathValue::Number(n) => {
232                    // Number predicate: match if n == context_position
233                    (n - (i as f64 + 1.0)).abs() < f64::EPSILON
234                        || (n.round() as i32) == (i + 1) as i32
235                }
236                _ => pred_val.as_boolean(),
237            };
238
239            if matches {
240                filtered.push(node);
241            }
242
243            ctx.context_node = saved_node;
244            ctx.context_position = saved_pos;
245            ctx.proximity_position = saved_prox;
246            ctx.context_size = saved_size;
247            ctx.context_list = saved_list;
248        }
249
250        result = filtered;
251    }
252
253    Ok(XPathValue::NodeSet(result))
254}
255
256/// Evaluate a filter expression: `primary[pred1][pred2]`.
257fn eval_filter(
258    ctx: &mut XPathContext,
259    expr: &Expr,
260    predicates: &[Expr],
261) -> Result<XPathValue, String> {
262    // Evaluate the primary expression
263    let mut result = eval(ctx, expr)?;
264
265    // Apply predicates
266    let ns = match &mut result {
267        XPathValue::NodeSet(ns) => ns,
268        _ => return Ok(result), // Non-node-set can't have predicates
269    };
270
271    for predicate in predicates {
272        let mut filtered = NodeSet::new();
273        let nodes: Vec<_> = ns.iter().collect();
274
275        for (i, node) in nodes.iter().enumerate() {
276            let saved_node = ctx.context_node;
277            let saved_pos = ctx.context_position;
278            let saved_prox = ctx.proximity_position;
279            let saved_size = ctx.context_size;
280            let saved_list = ctx.context_list.clone();
281
282            ctx.context_node = *node;
283            ctx.context_position = (i + 1) as i32;
284            // UPSTREAM-PARITY: position() reads proximityPosition (R-000159).
285            ctx.proximity_position = (i + 1) as i32;
286            ctx.context_size = nodes.len() as i32;
287
288            let pred_val = eval(ctx, predicate)?;
289
290            let matches = match pred_val {
291                XPathValue::Number(n) => {
292                    (n - (i as f64 + 1.0)).abs() < f64::EPSILON
293                        || (n.round() as i32) == (i + 1) as i32
294                }
295                _ => pred_val.as_boolean(),
296            };
297
298            if matches {
299                filtered.push(*node);
300            }
301
302            ctx.context_node = saved_node;
303            ctx.context_position = saved_pos;
304            ctx.proximity_position = saved_prox;
305            ctx.context_size = saved_size;
306            ctx.context_list = saved_list;
307        }
308
309        *ns = filtered;
310    }
311
312    Ok(result)
313}
314
315// ═══════════════════════════════════════════════════════════════════════════════
316// Variable / Function Call Evaluation
317// ═══════════════════════════════════════════════════════════════════════════════
318
319/// Evaluate a variable reference.
320fn eval_variable(ctx: &mut XPathContext, name: &str) -> Result<XPathValue, String> {
321    ctx.resolve_variable(name)
322        .ok_or_else(|| format!("Undefined variable: ${}", name))
323}
324
325/// Evaluate a function call.
326fn eval_function_call(
327    ctx: &mut XPathContext,
328    name: &str,
329    args: &[Expr],
330) -> Result<XPathValue, String> {
331    // Evaluate arguments first
332    let mut evaluated_args = Vec::new();
333    for arg in args {
334        evaluated_args.push(eval(ctx, arg)?);
335    }
336
337    // Look up the function
338    // Take a raw pointer to the boxed function so the immutable borrow of
339    // `ctx` ends before we call it with `&mut ctx`.
340    let func_ptr: Option<*const crate::xml::xpath::context::BoxedXPathFunction> =
341        ctx.lookup_function(name).map(|f| f as *const _);
342    match func_ptr {
343        Some(p) => {
344            // SAFETY: `p` points into `ctx.functions`, which is alive for the
345            // duration of this call and is not mutated during evaluation.
346            let f: &crate::xml::xpath::context::BoxedXPathFunction = unsafe { &*p };
347            f(ctx, &evaluated_args)
348        }
349        None => {
350            // UPSTREAM-PARITY (xpath.c xmlXPathCompFunction): an unknown
351            // function reports "Unregistered function: name"; when the name
352            // carries a prefix whose namespace was never declared, the error
353            // is "Undefined namespace prefix: prefix" instead (both are
354            // XPATH_UNKNOWN_FUNC / XPATH_UNDEF_PREFIX_ERROR, delivered as
355            // "XPath error : ...").
356            let msg = match name.split_once(':') {
357                Some((prefix, _)) if !ctx.namespaces.contains_key(prefix) => {
358                    format!("Undefined namespace prefix: {}", prefix)
359                }
360                _ => format!("Unregistered function: {}", name),
361            };
362            Err(msg)
363        }
364    }
365}
366
367// ═══════════════════════════════════════════════════════════════════════════════
368// Operators
369// ═══════════════════════════════════════════════════════════════════════════════
370
371/// Evaluate a binary operation.
372fn eval_binary_op(
373    ctx: &mut XPathContext,
374    op: &BinaryOp,
375    left: &Expr,
376    right: &Expr,
377) -> Result<XPathValue, String> {
378    match op {
379        BinaryOp::Or => {
380            // Short-circuit: evaluate left, if true return true
381            let left_val = eval(ctx, left)?;
382            if left_val.as_boolean() {
383                return Ok(XPathValue::Boolean(true));
384            }
385            let right_val = eval(ctx, right)?;
386            Ok(XPathValue::Boolean(right_val.as_boolean()))
387        }
388        BinaryOp::And => {
389            // Short-circuit: evaluate left, if false return false
390            let left_val = eval(ctx, left)?;
391            if !left_val.as_boolean() {
392                return Ok(XPathValue::Boolean(false));
393            }
394            let right_val = eval(ctx, right)?;
395            Ok(XPathValue::Boolean(right_val.as_boolean()))
396        }
397        BinaryOp::Eq | BinaryOp::Ne => {
398            let left_val = eval(ctx, left)?;
399            let right_val = eval(ctx, right)?;
400            let eq = compare_equal(ctx, &left_val, &right_val);
401            Ok(match op {
402                BinaryOp::Eq => XPathValue::Boolean(eq),
403                BinaryOp::Ne => XPathValue::Boolean(!eq),
404                _ => unreachable!(),
405            })
406        }
407        BinaryOp::Lt | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Ge => {
408            let left_val = eval(ctx, left)?;
409            let right_val = eval(ctx, right)?;
410            let cmp = compare_ordered(ctx, &left_val, &right_val);
411            let result = match op {
412                BinaryOp::Lt => cmp == std::cmp::Ordering::Less,
413                BinaryOp::Gt => cmp == std::cmp::Ordering::Greater,
414                BinaryOp::Le => cmp != std::cmp::Ordering::Greater,
415                BinaryOp::Ge => cmp != std::cmp::Ordering::Less,
416                _ => unreachable!(),
417            };
418            Ok(XPathValue::Boolean(result))
419        }
420        BinaryOp::Add => {
421            let left_val = eval(ctx, left)?;
422            let right_val = eval(ctx, right)?;
423            Ok(XPathValue::Number(
424                left_val.as_number() + right_val.as_number(),
425            ))
426        }
427        BinaryOp::Sub => {
428            let left_val = eval(ctx, left)?;
429            let right_val = eval(ctx, right)?;
430            Ok(XPathValue::Number(
431                left_val.as_number() - right_val.as_number(),
432            ))
433        }
434        BinaryOp::Mul => {
435            let left_val = eval(ctx, left)?;
436            let right_val = eval(ctx, right)?;
437            Ok(XPathValue::Number(
438                left_val.as_number() * right_val.as_number(),
439            ))
440        }
441        BinaryOp::Div => {
442            let left_val = eval(ctx, left)?;
443            let right_val = eval(ctx, right)?;
444            Ok(XPathValue::Number(
445                left_val.as_number() / right_val.as_number(),
446            ))
447        }
448        BinaryOp::Mod => {
449            let left_val = eval(ctx, left)?;
450            let right_val = eval(ctx, right)?;
451            Ok(XPathValue::Number(
452                left_val.as_number() % right_val.as_number(),
453            ))
454        }
455        BinaryOp::Union => {
456            // Union is handled at the Expr level, not BinaryOp level
457            unreachable!("Union operator should be handled by Expr::Union")
458        }
459    }
460}
461
462/// Evaluate a union expression: `left | right`.
463fn eval_union(ctx: &mut XPathContext, left: &Expr, right: &Expr) -> Result<XPathValue, String> {
464    let left_val = eval(ctx, left)?;
465    let right_val = eval(ctx, right)?;
466
467    let mut result = left_val.as_node_set().clone();
468    result.extend(right_val.as_node_set());
469    result.sort();
470
471    Ok(XPathValue::NodeSet(result))
472}
473
474// ═══════════════════════════════════════════════════════════════════════════════
475// Comparison Semantics
476// ═══════════════════════════════════════════════════════════════════════════════
477
478/// Compare two XPath values for equality (XPath 1.0 §3.4).
479fn compare_equal(_ctx: &mut XPathContext, a: &XPathValue, b: &XPathValue) -> bool {
480    match (a, b) {
481        // If both are node-sets, compare by set intersection
482        (XPathValue::NodeSet(ns_a), XPathValue::NodeSet(ns_b)) => {
483            for node_a in ns_a.iter() {
484                let val_a = node_string_value(node_a);
485                for node_b in ns_b.iter() {
486                    let val_b = node_string_value(node_b);
487                    if val_a == val_b {
488                        return true;
489                    }
490                }
491            }
492            false
493        }
494        // If one is a node-set and the other is not
495        (XPathValue::NodeSet(ns), other) | (other, XPathValue::NodeSet(ns)) => {
496            for node in ns.iter() {
497                let node_str = node_string_value(node);
498                match other {
499                    XPathValue::Boolean(_) => {
500                        // Compare boolean(node-set) == other
501                        return (!ns.is_empty()) == other.as_boolean();
502                    }
503                    XPathValue::Number(_) => {
504                        let node_num = string_to_number(&node_str);
505                        if (node_num - other.as_number()).abs() < f64::EPSILON {
506                            return true;
507                        }
508                        continue;
509                    }
510                    XPathValue::String(_) => {
511                        if node_str == other.as_string() {
512                            return true;
513                        }
514                        continue;
515                    }
516                    _ => continue,
517                };
518            }
519            false
520        }
521        // Neither is a node-set
522        _ => match (a, b) {
523            (XPathValue::Boolean(_), _) | (_, XPathValue::Boolean(_)) => {
524                a.as_boolean() == b.as_boolean()
525            }
526            (XPathValue::Number(_), _) | (_, XPathValue::Number(_)) => {
527                let na = a.as_number();
528                let nb = b.as_number();
529                if na.is_nan() || nb.is_nan() {
530                    false
531                } else {
532                    (na - nb).abs() < f64::EPSILON || na == nb
533                }
534            }
535            _ => a.as_string() == b.as_string(),
536        },
537    }
538}
539
540/// Compare two XPath values for ordering (XPath 1.0 §3.4).
541fn compare_ordered(_ctx: &mut XPathContext, a: &XPathValue, b: &XPathValue) -> std::cmp::Ordering {
542    match (a, b) {
543        // If both are node-sets, compare pairwise
544        (XPathValue::NodeSet(ns_a), XPathValue::NodeSet(ns_b)) => {
545            for node_a in ns_a.iter() {
546                let num_a = string_to_number(&node_string_value(node_a));
547                for node_b in ns_b.iter() {
548                    let num_b = string_to_number(&node_string_value(node_b));
549                    if num_a < num_b {
550                        return std::cmp::Ordering::Less;
551                    }
552                    if num_a > num_b {
553                        return std::cmp::Ordering::Greater;
554                    }
555                }
556            }
557            std::cmp::Ordering::Equal
558        }
559        // If one is a node-set
560        (XPathValue::NodeSet(ns), other) | (other, XPathValue::NodeSet(ns)) => {
561            let other_num = other.as_number();
562            for node in ns.iter() {
563                let node_num = string_to_number(&node_string_value(node));
564                if node_num < other_num {
565                    return std::cmp::Ordering::Less;
566                }
567                if node_num > other_num {
568                    return std::cmp::Ordering::Greater;
569                }
570            }
571            std::cmp::Ordering::Equal
572        }
573        // Neither is a node-set: compare as numbers
574        _ => {
575            let na = a.as_number();
576            let nb = b.as_number();
577            if na.is_nan() || nb.is_nan() {
578                std::cmp::Ordering::Equal // NaN comparisons return false, so equal for ordering
579            } else if na < nb {
580                std::cmp::Ordering::Less
581            } else if na > nb {
582                std::cmp::Ordering::Greater
583            } else {
584                std::cmp::Ordering::Equal
585            }
586        }
587    }
588}
589
590// ═══════════════════════════════════════════════════════════════════════════════
591// Top-level evaluation API
592// ═══════════════════════════════════════════════════════════════════════════════
593
594/// Evaluate an XPath expression string against a document.
595///
596/// This is the main entry point for XPath evaluation.
597pub fn eval_xpath(ctx: &mut XPathContext, expression: &str) -> Result<XPathValue, String> {
598    let expr = crate::xml::xpath::parser::parse_xpath(expression).map_err(|e| e.message)?;
599    eval(ctx, &expr)
600}
601
602// ═══════════════════════════════════════════════════════════════════════════════
603// Tests
604// ═══════════════════════════════════════════════════════════════════════════════
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609    use crate::xml::xpath::context::XPathContext;
610    use crate::xml::xpath::functions;
611
612    fn setup_context() -> XPathContext {
613        let mut ctx = XPathContext::new(std::ptr::null_mut());
614        // Register core functions
615        let funcs = functions::core_functions();
616        for (name, func) in funcs {
617            ctx.register_function(&name, func);
618        }
619        ctx
620    }
621
622    #[test]
623    fn test_eval_string_literal() {
624        let mut ctx = setup_context();
625        let result = eval_xpath(&mut ctx, "'hello'").unwrap();
626        assert_eq!(result.as_string(), "hello");
627    }
628
629    #[test]
630    fn test_eval_number_literal() {
631        let mut ctx = setup_context();
632        let result = eval_xpath(&mut ctx, "42").unwrap();
633        assert_eq!(result.as_number(), 42.0);
634    }
635
636    #[test]
637    fn test_eval_addition() {
638        let mut ctx = setup_context();
639        let result = eval_xpath(&mut ctx, "1 + 2").unwrap();
640        assert_eq!(result.as_number(), 3.0);
641    }
642
643    #[test]
644    fn test_eval_subtraction() {
645        let mut ctx = setup_context();
646        let result = eval_xpath(&mut ctx, "5 - 3").unwrap();
647        assert_eq!(result.as_number(), 2.0);
648    }
649
650    #[test]
651    fn test_eval_multiplication() {
652        let mut ctx = setup_context();
653        let result = eval_xpath(&mut ctx, "3 * 4").unwrap();
654        assert_eq!(result.as_number(), 12.0);
655    }
656
657    #[test]
658    fn test_eval_division() {
659        let mut ctx = setup_context();
660        let result = eval_xpath(&mut ctx, "10 div 3").unwrap();
661        assert!((result.as_number() - 3.3333333333333335).abs() < 1e-10);
662    }
663
664    #[test]
665    fn test_eval_modulo() {
666        let mut ctx = setup_context();
667        let result = eval_xpath(&mut ctx, "10 mod 3").unwrap();
668        assert_eq!(result.as_number(), 1.0);
669    }
670
671    #[test]
672    fn test_eval_equality() {
673        let mut ctx = setup_context();
674        assert!(eval_xpath(&mut ctx, "1 = 1").unwrap().as_boolean());
675        assert!(!eval_xpath(&mut ctx, "1 = 2").unwrap().as_boolean());
676        assert!(eval_xpath(&mut ctx, "1 != 2").unwrap().as_boolean());
677    }
678
679    #[test]
680    fn test_eval_comparison() {
681        let mut ctx = setup_context();
682        assert!(eval_xpath(&mut ctx, "1 < 2").unwrap().as_boolean());
683        assert!(eval_xpath(&mut ctx, "2 > 1").unwrap().as_boolean());
684        assert!(eval_xpath(&mut ctx, "1 <= 1").unwrap().as_boolean());
685        assert!(eval_xpath(&mut ctx, "2 >= 2").unwrap().as_boolean());
686    }
687
688    #[test]
689    fn test_eval_and_or() {
690        let mut ctx = setup_context();
691        assert!(eval_xpath(&mut ctx, "true() and true()")
692            .unwrap()
693            .as_boolean());
694        assert!(!eval_xpath(&mut ctx, "true() and false()")
695            .unwrap()
696            .as_boolean());
697        assert!(eval_xpath(&mut ctx, "true() or false()")
698            .unwrap()
699            .as_boolean());
700        assert!(!eval_xpath(&mut ctx, "false() or false()")
701            .unwrap()
702            .as_boolean());
703    }
704
705    #[test]
706    fn test_eval_not() {
707        let mut ctx = setup_context();
708        assert!(!eval_xpath(&mut ctx, "not(true())").unwrap().as_boolean());
709        assert!(eval_xpath(&mut ctx, "not(false())").unwrap().as_boolean());
710    }
711
712    #[test]
713    fn test_eval_boolean() {
714        let mut ctx = setup_context();
715        assert!(eval_xpath(&mut ctx, "boolean('hello')")
716            .unwrap()
717            .as_boolean());
718        assert!(!eval_xpath(&mut ctx, "boolean('')").unwrap().as_boolean());
719        assert!(!eval_xpath(&mut ctx, "boolean(0)").unwrap().as_boolean());
720        assert!(eval_xpath(&mut ctx, "boolean(1)").unwrap().as_boolean());
721    }
722
723    #[test]
724    fn test_eval_number() {
725        let mut ctx = setup_context();
726        assert_eq!(
727            eval_xpath(&mut ctx, "number('42')").unwrap().as_number(),
728            42.0
729        );
730    }
731
732    #[test]
733    fn test_eval_string() {
734        let mut ctx = setup_context();
735        assert_eq!(
736            eval_xpath(&mut ctx, "string(42)").unwrap().as_string(),
737            "42"
738        );
739    }
740
741    #[test]
742    fn test_eval_concat() {
743        let mut ctx = setup_context();
744        assert_eq!(
745            eval_xpath(&mut ctx, "concat('a', 'b', 'c')")
746                .unwrap()
747                .as_string(),
748            "abc"
749        );
750    }
751
752    #[test]
753    fn test_eval_starts_with() {
754        let mut ctx = setup_context();
755        assert!(eval_xpath(&mut ctx, "starts-with('hello', 'he')")
756            .unwrap()
757            .as_boolean());
758    }
759
760    #[test]
761    fn test_eval_contains() {
762        let mut ctx = setup_context();
763        assert!(eval_xpath(&mut ctx, "contains('hello', 'ell')")
764            .unwrap()
765            .as_boolean());
766    }
767
768    #[test]
769    fn test_eval_substring() {
770        let mut ctx = setup_context();
771        assert_eq!(
772            eval_xpath(&mut ctx, "substring('12345', 1, 3)")
773                .unwrap()
774                .as_string(),
775            "123"
776        );
777        assert_eq!(
778            eval_xpath(&mut ctx, "substring('12345', 2)")
779                .unwrap()
780                .as_string(),
781            "2345"
782        );
783    }
784
785    #[test]
786    fn test_eval_string_length() {
787        let mut ctx = setup_context();
788        assert_eq!(
789            eval_xpath(&mut ctx, "string-length('hello')")
790                .unwrap()
791                .as_number(),
792            5.0
793        );
794    }
795
796    #[test]
797    fn test_eval_normalize_space() {
798        let mut ctx = setup_context();
799        assert_eq!(
800            eval_xpath(&mut ctx, "normalize-space('  hello   world  ')")
801                .unwrap()
802                .as_string(),
803            "hello world"
804        );
805    }
806
807    #[test]
808    fn test_eval_floor_ceiling_round() {
809        let mut ctx = setup_context();
810        assert_eq!(eval_xpath(&mut ctx, "floor(3.7)").unwrap().as_number(), 3.0);
811        assert_eq!(
812            eval_xpath(&mut ctx, "ceiling(3.2)").unwrap().as_number(),
813            4.0
814        );
815        assert_eq!(eval_xpath(&mut ctx, "round(3.5)").unwrap().as_number(), 4.0);
816    }
817
818    #[test]
819    fn test_eval_sum() {
820        // sum() on an empty node-set should return 0
821        let mut ctx = setup_context();
822        ctx.document = std::ptr::null_mut();
823        // Can't test sum directly without a node-set, but we can test it
824        // with literal values when we add node-set construction
825    }
826
827    #[test]
828    fn test_eval_variable_not_found() {
829        let mut ctx = setup_context();
830        let result = eval_xpath(&mut ctx, "$undefined_var");
831        assert!(result.is_err());
832    }
833
834    #[test]
835    fn test_eval_variable_found() {
836        let mut ctx = setup_context();
837        ctx.register_variable("x", XPathValue::Number(42.0));
838        let result = eval_xpath(&mut ctx, "$x").unwrap();
839        assert_eq!(result.as_number(), 42.0);
840    }
841
842    #[test]
843    fn test_eval_union() {
844        // Union requires node-sets, which need a document.
845        // Test basic union of numbers (should error since numbers aren't node-sets).
846        // Actually, union on non-node-sets would panic at as_node_set().
847        // For now, this is a placeholder for when we have document support.
848    }
849
850    #[test]
851    fn test_eval_operator_precedence() {
852        let mut ctx = setup_context();
853        // 1 + 2 * 3 should be 7 (multiplication before addition)
854        let result = eval_xpath(&mut ctx, "1 + 2 * 3").unwrap();
855        assert_eq!(result.as_number(), 7.0);
856
857        // (1 + 2) * 3 should be 9
858        let result = eval_xpath(&mut ctx, "(1 + 2) * 3").unwrap();
859        assert_eq!(result.as_number(), 9.0);
860    }
861
862    #[test]
863    fn test_eval_unary_minus() {
864        let mut ctx = setup_context();
865        let result = eval_xpath(&mut ctx, "-5").unwrap();
866        assert_eq!(result.as_number(), -5.0);
867
868        let result = eval_xpath(&mut ctx, "--5").unwrap();
869        assert_eq!(result.as_number(), 5.0);
870    }
871
872    #[test]
873    fn test_eval_true_false() {
874        let mut ctx = setup_context();
875        assert!(eval_xpath(&mut ctx, "true()").unwrap().as_boolean());
876        assert!(!eval_xpath(&mut ctx, "false()").unwrap().as_boolean());
877    }
878
879    #[test]
880    fn test_eval_translate() {
881        let mut ctx = setup_context();
882        assert_eq!(
883            eval_xpath(
884                &mut ctx,
885                "translate('hello', 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')"
886            )
887            .unwrap()
888            .as_string(),
889            "HELLO"
890        );
891    }
892
893    #[test]
894    fn test_eval_empty_expression() {
895        let mut ctx = setup_context();
896        let result = eval_xpath(&mut ctx, "");
897        assert!(result.is_err());
898    }
899}