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