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