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