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