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