Skip to main content

xsd_schema/xpath/
api.rs

1//! High-level, ergonomic XPath evaluation API.
2//!
3//! This module provides a user-friendly interface for compiling and evaluating
4//! XPath expressions with Rust-idiomatic patterns:
5//!
6//! - **Separation of compilation and execution**: Compile once, evaluate many times
7//! - **Builder pattern**: Fluent API for setting variables and options
8//! - **`From` traits**: Ergonomic value conversion (use `42` instead of `XPathValue::integer(42)`)
9//! - **No lifetimes in compiled expressions**: Store `XPathExpr` anywhere
10//!
11//! # Example
12//!
13//! ```no_run
14//! use xsd_schema::xpath::api::{XPathExpr, EvalValue};
15//! use xsd_schema::xpath::{XPathContext, RoXmlNavigator};
16//! use xsd_schema::namespace::table::NameTable;
17//!
18//! // Setup context
19//! let names = NameTable::new();
20//! let ctx = XPathContext::new(&names);
21//!
22//! // Compile once with external variables
23//! let expr = XPathExpr::compile_with_vars("$x + $y", &ctx, &["x", "y"]).unwrap();
24//!
25//! // Evaluate with fluent builder
26//! let result = expr.evaluator(&ctx)
27//!     .with_variable("x", 10).unwrap()
28//!     .with_variable("y", 32).unwrap()
29//!     .run::<RoXmlNavigator<'static>>().unwrap();
30//!
31//! // Convenience methods for common return types
32//! let sum = expr.evaluator(&ctx)
33//!     .with_variable("x", 20).unwrap()
34//!     .with_variable("y", 22).unwrap()
35//!     .run_number::<RoXmlNavigator<'static>>().unwrap();
36//! assert_eq!(sum, 42.0);
37//! ```
38
39use num_bigint::BigInt;
40
41use crate::namespace::qname::QualifiedName;
42
43use super::arena::{AstArena, AstNodeId, SourceSpan};
44use super::bind::bind_node;
45use super::context::{DynamicContext, NameBinder, VarSlotId, XPathContext};
46use super::error::XPathError;
47use super::eval::eval_node;
48use super::functions::{effective_boolean_value, XPathValue};
49use super::iterator::XmlItem;
50use super::parser::{parse, parse_with_mode};
51use super::DomNavigator;
52
53// ============================================================================
54// ExternalVar - Information about a declared external variable
55// ============================================================================
56
57/// Information about an external variable declared at compile time.
58///
59/// External variables are those declared by the user (via `compile_with_vars`)
60/// as opposed to variables introduced by the expression itself (like `for $x in ...`).
61#[derive(Debug, Clone)]
62pub struct ExternalVar {
63    /// The qualified name of the variable
64    pub name: QualifiedName,
65    /// The slot ID for storing the variable's value
66    pub slot: VarSlotId,
67}
68
69// ============================================================================
70// XPathExpr - Compiled XPath expression (owns its AST)
71// ============================================================================
72
73/// A compiled XPath expression that can be evaluated multiple times.
74///
75/// `XPathExpr` owns its AST and contains no lifetimes, so it can be stored
76/// in structs, sent across threads (if using appropriate synchronization),
77/// or cached for repeated evaluation.
78///
79/// # Compilation vs Evaluation
80///
81/// Compilation (`compile()` or `compile_with_vars()`) parses and binds the expression,
82/// resolving function names, variable slots, and namespace prefixes. This is the
83/// expensive step.
84///
85/// Evaluation (`evaluator().run()`) executes the compiled AST, which is much faster.
86/// You can evaluate the same compiled expression many times with different variable
87/// values or context nodes.
88///
89/// # External Variables
90///
91/// Use `compile_with_vars()` to declare variables that will be provided at evaluation time:
92///
93/// ```no_run
94/// # use xsd_schema::xpath::api::XPathExpr;
95/// # use xsd_schema::xpath::XPathContext;
96/// # use xsd_schema::namespace::table::NameTable;
97/// let names = NameTable::new();
98/// let ctx = XPathContext::new(&names);
99///
100/// // Declare $x and $y as external variables
101/// let expr = XPathExpr::compile_with_vars("$x + $y", &ctx, &["x", "y"]).unwrap();
102/// ```
103#[derive(Debug, Clone)]
104pub struct XPathExpr {
105    /// Original source expression
106    source: String,
107    /// The AST arena containing all nodes
108    arena: AstArena,
109    /// Root node ID of the expression
110    root: AstNodeId,
111    /// Source span of the entire expression
112    span: SourceSpan,
113    /// Total number of variable slots needed
114    var_slots: usize,
115    /// External variables declared at compile time
116    external_vars: Vec<ExternalVar>,
117}
118
119impl XPathExpr {
120    /// Compile an XPath expression without external variables.
121    ///
122    /// Use this when your expression doesn't reference any variables, or when
123    /// all variables are provided by the expression itself (e.g., `for $x in 1 to 10`).
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if:
128    /// - The expression has syntax errors
129    /// - A function is not found
130    /// - A variable is referenced but not defined
131    /// - A namespace prefix is not bound
132    ///
133    /// # Example
134    ///
135    /// ```no_run
136    /// # use xsd_schema::xpath::api::XPathExpr;
137    /// # use xsd_schema::xpath::XPathContext;
138    /// # use xsd_schema::namespace::table::NameTable;
139    /// let names = NameTable::new();
140    /// let ctx = XPathContext::new(&names);
141    /// let expr = XPathExpr::compile("1 + 2 * 3", &ctx).unwrap();
142    /// ```
143    pub fn compile(expr: &str, ctx: &XPathContext<'_>) -> Result<Self, XPathError> {
144        Self::compile_with_vars(expr, ctx, &[])
145    }
146
147    /// Compile an XPath expression with declared external variables.
148    ///
149    /// External variables must be provided at evaluation time via `with_variable()`.
150    /// Variable names should be provided without the `$` prefix.
151    ///
152    /// # Errors
153    ///
154    /// Returns an error if:
155    /// - The expression has syntax errors
156    /// - A function is not found
157    /// - A variable is referenced that wasn't declared
158    /// - A namespace prefix is not bound
159    ///
160    /// # Example
161    ///
162    /// ```no_run
163    /// # use xsd_schema::xpath::api::XPathExpr;
164    /// # use xsd_schema::xpath::XPathContext;
165    /// # use xsd_schema::namespace::table::NameTable;
166    /// let names = NameTable::new();
167    /// let ctx = XPathContext::new(&names);
168    ///
169    /// // Compile expression that uses $x and $y
170    /// let expr = XPathExpr::compile_with_vars("$x + $y", &ctx, &["x", "y"]).unwrap();
171    /// ```
172    pub fn compile_with_vars(
173        expr: &str,
174        ctx: &XPathContext<'_>,
175        vars: &[&str],
176    ) -> Result<Self, XPathError> {
177        // Parse the expression using the mode from context (ParseError → XPathError via From)
178        let parsed = if ctx.mode() == super::XPathMode::XPath20 {
179            parse(expr)?
180        } else {
181            parse_with_mode(expr, ctx.mode())?
182        };
183
184        let mut arena = parsed.arena;
185        let root = parsed.root;
186        let span = parsed.span;
187
188        // Create name binder and push external variables
189        let mut binder = NameBinder::new();
190        let mut external_vars = Vec::with_capacity(vars.len());
191
192        for var_name in vars {
193            // Parse variable name - support both "local" and "prefix:local" formats
194            let qname = parse_variable_name(var_name, ctx)?;
195
196            // Check for duplicate variable declarations
197            if external_vars.iter().any(|v: &ExternalVar| v.name == qname) {
198                return Err(XPathError::XPST0003 {
199                    message: format!("Duplicate external variable declaration: ${}", var_name),
200                });
201            }
202
203            let var_ref = binder.push_var(qname.clone());
204            external_vars.push(ExternalVar {
205                name: qname,
206                slot: var_ref.slot,
207            });
208        }
209
210        // Mark boundary between external vars and expression-internal vars
211        binder.mark_external_boundary();
212
213        // Bind the expression (resolves functions, variables, namespaces)
214        bind_node(&mut arena, root, ctx, &mut binder)?;
215
216        let var_slots = binder.len();
217
218        Ok(Self {
219            source: expr.to_string(),
220            arena,
221            root,
222            span,
223            var_slots,
224            external_vars,
225        })
226    }
227
228    /// Get the original source expression.
229    pub fn source(&self) -> &str {
230        &self.source
231    }
232
233    /// Get the source span of the expression.
234    pub fn span(&self) -> SourceSpan {
235        self.span
236    }
237
238    /// Get the external variables declared for this expression.
239    pub fn external_vars(&self) -> &[ExternalVar] {
240        &self.external_vars
241    }
242
243    /// Borrow the bound AST arena of this expression.
244    ///
245    /// Useful for callers that want to inspect the compiled tree
246    /// (e.g. CTA schema-time validation walking type expressions).
247    pub fn arena(&self) -> &AstArena {
248        &self.arena
249    }
250
251    /// Create an evaluator for this expression.
252    ///
253    /// The evaluator uses a builder pattern to set variables and other options
254    /// before running the expression.
255    ///
256    /// # Example
257    ///
258    /// ```no_run
259    /// # use xsd_schema::xpath::api::XPathExpr;
260    /// # use xsd_schema::xpath::{XPathContext, RoXmlNavigator};
261    /// # use xsd_schema::namespace::table::NameTable;
262    /// let names = NameTable::new();
263    /// let ctx = XPathContext::new(&names);
264    /// let expr = XPathExpr::compile_with_vars("$x * 2", &ctx, &["x"]).unwrap();
265    ///
266    /// let result = expr.evaluator(&ctx)
267    ///     .with_variable("x", 21).unwrap()
268    ///     .run_number::<RoXmlNavigator<'static>>().unwrap();
269    /// assert_eq!(result, 42.0);
270    /// ```
271    pub fn evaluator<'a, 'ctx>(
272        &'a self,
273        ctx: &'ctx XPathContext<'ctx>,
274    ) -> XPathEvaluator<'a, 'ctx> {
275        XPathEvaluator::new(self, ctx)
276    }
277}
278
279// ============================================================================
280// EvalValue - Ergonomic value type for variable binding
281// ============================================================================
282
283/// A value that can be bound to an XPath variable at evaluation time.
284///
285/// This enum provides ergonomic conversion from Rust types to XPath values
286/// via the `From` trait implementations. You can use Rust literals directly:
287///
288/// ```no_run
289/// # use xsd_schema::xpath::api::{XPathExpr, EvalValue};
290/// # use xsd_schema::xpath::XPathContext;
291/// # use xsd_schema::namespace::table::NameTable;
292/// # let names = NameTable::new();
293/// # let ctx = XPathContext::new(&names);
294/// # let expr = XPathExpr::compile_with_vars("$x", &ctx, &["x"]).unwrap();
295/// // All of these work:
296/// expr.evaluator(&ctx).with_variable("x", 42);          // i32 -> Integer
297/// expr.evaluator(&ctx).with_variable("x", 3.14);        // f64 -> Double
298/// expr.evaluator(&ctx).with_variable("x", true);        // bool -> Bool
299/// expr.evaluator(&ctx).with_variable("x", "hello");     // &str -> String
300/// ```
301#[derive(Debug, Clone)]
302pub enum EvalValue {
303    /// Boolean value
304    Bool(bool),
305    /// Small integer (converted to BigInt internally)
306    Integer(i64),
307    /// Big integer
308    BigInteger(BigInt),
309    /// Double-precision floating point
310    Double(f64),
311    /// String value
312    String(String),
313}
314
315impl From<bool> for EvalValue {
316    fn from(b: bool) -> Self {
317        EvalValue::Bool(b)
318    }
319}
320
321impl From<i32> for EvalValue {
322    fn from(i: i32) -> Self {
323        EvalValue::Integer(i as i64)
324    }
325}
326
327impl From<i64> for EvalValue {
328    fn from(i: i64) -> Self {
329        EvalValue::Integer(i)
330    }
331}
332
333impl From<f32> for EvalValue {
334    fn from(f: f32) -> Self {
335        EvalValue::Double(f as f64)
336    }
337}
338
339impl From<f64> for EvalValue {
340    fn from(f: f64) -> Self {
341        EvalValue::Double(f)
342    }
343}
344
345impl From<String> for EvalValue {
346    fn from(s: String) -> Self {
347        EvalValue::String(s)
348    }
349}
350
351impl From<&str> for EvalValue {
352    fn from(s: &str) -> Self {
353        EvalValue::String(s.to_string())
354    }
355}
356
357impl From<BigInt> for EvalValue {
358    fn from(i: BigInt) -> Self {
359        EvalValue::BigInteger(i)
360    }
361}
362
363// ============================================================================
364// PendingValue - Internal type for deferred XPathValue construction
365// ============================================================================
366
367/// Internal representation of a value waiting to be converted to XPathValue<N>.
368///
369/// Since XPathValue is generic over the navigator type N, and we don't know N
370/// until `run()` is called, we store values in this intermediate form.
371#[derive(Debug, Clone)]
372enum PendingValue {
373    Bool(bool),
374    Integer(i64),
375    BigInteger(BigInt),
376    Double(f64),
377    String(String),
378}
379
380impl PendingValue {
381    /// Convert this pending value to an XPathValue for the given navigator type.
382    fn into_xpath_value<N: DomNavigator>(self) -> XPathValue<N> {
383        match self {
384            PendingValue::Bool(b) => XPathValue::boolean(b),
385            PendingValue::Integer(i) => XPathValue::integer(BigInt::from(i)),
386            PendingValue::BigInteger(i) => XPathValue::integer(i),
387            PendingValue::Double(d) => XPathValue::double(d),
388            PendingValue::String(s) => XPathValue::string(s),
389        }
390    }
391}
392
393impl From<EvalValue> for PendingValue {
394    fn from(v: EvalValue) -> Self {
395        match v {
396            EvalValue::Bool(b) => PendingValue::Bool(b),
397            EvalValue::Integer(i) => PendingValue::Integer(i),
398            EvalValue::BigInteger(i) => PendingValue::BigInteger(i),
399            EvalValue::Double(d) => PendingValue::Double(d),
400            EvalValue::String(s) => PendingValue::String(s),
401        }
402    }
403}
404
405// ============================================================================
406// XPathEvaluator - Builder for expression evaluation
407// ============================================================================
408
409/// Builder for evaluating a compiled XPath expression.
410///
411/// Use this to set variables, context nodes, and other evaluation options
412/// before running the expression. The builder pattern allows fluent API usage:
413///
414/// ```no_run
415/// # use xsd_schema::xpath::api::XPathExpr;
416/// # use xsd_schema::xpath::{XPathContext, RoXmlNavigator};
417/// # use xsd_schema::namespace::table::NameTable;
418/// # let names = NameTable::new();
419/// # let ctx = XPathContext::new(&names);
420/// # let expr = XPathExpr::compile_with_vars("$x + $y", &ctx, &["x", "y"]).unwrap();
421/// let result = expr.evaluator(&ctx)
422///     .with_variable("x", 10).unwrap()
423///     .with_variable("y", 32).unwrap()
424///     .run::<RoXmlNavigator<'static>>().unwrap();
425/// ```
426pub struct XPathEvaluator<'expr, 'ctx> {
427    /// The compiled expression to evaluate
428    expr: &'expr XPathExpr,
429    /// The static context
430    static_ctx: &'ctx XPathContext<'ctx>,
431    /// Variables to set before evaluation (slot -> value)
432    pending_vars: Vec<(VarSlotId, PendingValue)>,
433}
434
435impl<'expr, 'ctx> XPathEvaluator<'expr, 'ctx> {
436    /// Create a new evaluator for the given expression and context.
437    fn new(expr: &'expr XPathExpr, static_ctx: &'ctx XPathContext<'ctx>) -> Self {
438        Self {
439            expr,
440            static_ctx,
441            pending_vars: Vec::new(),
442        }
443    }
444
445    /// Set an external variable's value.
446    ///
447    /// The variable must have been declared when compiling the expression
448    /// (via `compile_with_vars()`). Variable names should not include the `$` prefix.
449    ///
450    /// # Errors
451    ///
452    /// Returns `XPST0008` if the variable was not declared at compile time.
453    ///
454    /// # Example
455    ///
456    /// ```no_run
457    /// # use xsd_schema::xpath::api::XPathExpr;
458    /// # use xsd_schema::xpath::XPathContext;
459    /// # use xsd_schema::namespace::table::NameTable;
460    /// # let names = NameTable::new();
461    /// # let ctx = XPathContext::new(&names);
462    /// let expr = XPathExpr::compile_with_vars("$price * $qty", &ctx, &["price", "qty"]).unwrap();
463    ///
464    /// let eval = expr.evaluator(&ctx)
465    ///     .with_variable("price", 19.99).unwrap()
466    ///     .with_variable("qty", 3).unwrap();
467    /// ```
468    pub fn with_variable(
469        mut self,
470        name: &str,
471        value: impl Into<EvalValue>,
472    ) -> Result<Self, XPathError> {
473        let slot = find_external_var(name, &self.expr.external_vars, self.static_ctx)?;
474        self.pending_vars.push((slot, value.into().into()));
475        Ok(self)
476    }
477
478    /// Evaluate the expression and return the full result.
479    ///
480    /// This is the most flexible method, returning the raw `XPathValue` which
481    /// can be empty, a single item, or a sequence.
482    ///
483    /// # Type Parameter
484    ///
485    /// - `N`: The navigator type (e.g., `RoXmlNavigator<'doc>`)
486    ///
487    /// # Errors
488    ///
489    /// Returns an error if evaluation fails (e.g., type errors, undefined context).
490    pub fn run<N: DomNavigator>(self) -> Result<XPathValue<N>, XPathError> {
491        self.run_internal(None)
492    }
493
494    /// Evaluate the expression with a context node.
495    ///
496    /// Sets the context item to the given node before evaluation. This is
497    /// necessary for expressions that use `.` or axis steps like `child::*`.
498    ///
499    /// # Example
500    ///
501    /// ```no_run
502    /// # use xsd_schema::xpath::api::XPathExpr;
503    /// # use xsd_schema::xpath::{XPathContext, RoXmlNavigator, DomNavigator};
504    /// # use xsd_schema::namespace::table::NameTable;
505    /// # let names = NameTable::new();
506    /// # let ctx = XPathContext::new(&names);
507    /// let expr = XPathExpr::compile("child::item", &ctx).unwrap();
508    ///
509    /// // Parse some XML and get a navigator
510    /// let doc = roxmltree::Document::parse("<root><item/></root>").unwrap();
511    /// let mut nav = RoXmlNavigator::new(&doc);
512    /// nav.move_to_first_child(); // move to <root>
513    ///
514    /// let result = expr.evaluator(&ctx)
515    ///     .run_with_node(nav).unwrap();
516    /// ```
517    pub fn run_with_node<N: DomNavigator>(self, node: N) -> Result<XPathValue<N>, XPathError> {
518        self.run_internal(Some(node))
519    }
520
521    /// Internal evaluation implementation.
522    fn run_internal<N: DomNavigator>(
523        self,
524        context_node: Option<N>,
525    ) -> Result<XPathValue<N>, XPathError> {
526        // Create dynamic context
527        let mut dyn_ctx = DynamicContext::new(self.static_ctx, self.expr.var_slots);
528
529        // Set context node if provided
530        if let Some(node) = context_node {
531            dyn_ctx = dyn_ctx.with_context_node(node);
532        }
533
534        // Set pending variables
535        for (slot, pending) in self.pending_vars {
536            let value: XPathValue<N> = pending.into_xpath_value();
537            dyn_ctx.set_variable(slot, value);
538        }
539
540        // Evaluate the expression
541        eval_node(&self.expr.arena, self.expr.root, &mut dyn_ctx)
542    }
543
544    /// Evaluate and return the result as a boolean.
545    ///
546    /// Uses the XPath effective boolean value rules:
547    /// - Empty sequence → `false`
548    /// - Boolean → its value
549    /// - String → `false` if empty, `true` otherwise
550    /// - Number → `false` if 0 or NaN, `true` otherwise
551    /// - Node sequence → `true` if non-empty
552    ///
553    /// # Errors
554    ///
555    /// Returns an error if evaluation fails or if effective boolean value
556    /// cannot be computed (e.g., sequence of multiple atomic values).
557    pub fn run_bool<N: DomNavigator>(self) -> Result<bool, XPathError> {
558        let value = self.run::<N>()?;
559        effective_boolean_value(&value)
560    }
561
562    /// Evaluate and return the result as a string.
563    ///
564    /// Atomizes the result and converts to string. For sequences, returns
565    /// the string value of the first item (or empty string for empty sequence).
566    ///
567    /// # Errors
568    ///
569    /// Returns an error if evaluation fails.
570    pub fn run_string<N: DomNavigator>(self) -> Result<String, XPathError> {
571        let value = self.run::<N>()?;
572        Ok(xpath_value_to_string(&value))
573    }
574
575    /// Evaluate and return the result as a number (f64).
576    ///
577    /// Atomizes the result and converts to double. Returns `NaN` for
578    /// values that cannot be converted to numbers.
579    ///
580    /// # Errors
581    ///
582    /// Returns an error if evaluation fails.
583    pub fn run_number<N: DomNavigator>(self) -> Result<f64, XPathError> {
584        let value = self.run::<N>()?;
585        Ok(xpath_value_to_number(&value))
586    }
587
588    /// Evaluate and return the result as a vector of nodes.
589    ///
590    /// Filters the result to include only nodes, discarding atomic values.
591    /// Useful for path expressions that return node sequences.
592    ///
593    /// # Errors
594    ///
595    /// Returns an error if evaluation fails.
596    pub fn run_nodes<N: DomNavigator>(self) -> Result<Vec<N>, XPathError> {
597        let value = self.run::<N>()?;
598        let items = value.into_vec();
599        let nodes = items
600            .into_iter()
601            .filter_map(|item| match item {
602                XmlItem::Node(n) => Some(n),
603                XmlItem::Atomic(_) => None,
604            })
605            .collect();
606        Ok(nodes)
607    }
608
609    /// Evaluate with a setup callback for advanced variable binding.
610    ///
611    /// This method allows binding variables that cannot be represented as `EvalValue`,
612    /// such as:
613    /// - Node values
614    /// - Sequences of items
615    /// - Empty sequences
616    ///
617    /// The setup callback receives a mutable reference to the `DynamicContext`
618    /// and can use `set_variable_by_name()` or `context.set_variable()` directly.
619    ///
620    /// # Example
621    ///
622    /// ```no_run
623    /// # use xsd_schema::xpath::api::XPathExpr;
624    /// # use xsd_schema::xpath::{XPathContext, RoXmlNavigator, XPathValue};
625    /// # use xsd_schema::namespace::table::NameTable;
626    /// # let names = NameTable::new();
627    /// # let ctx = XPathContext::new(&names);
628    /// let expr = XPathExpr::compile_with_vars("count($items)", &ctx, &["items"]).unwrap();
629    ///
630    /// // Bind a sequence of integers
631    /// let result = expr.evaluator(&ctx)
632    ///     .run_with::<RoXmlNavigator<'static>, _>(|eval| {
633    ///         // Create a sequence value
634    ///         let seq = XPathValue::from_sequence(vec![
635    ///             xsd_schema::xpath::XmlItem::Atomic(xsd_schema::types::XmlValue::integer(1.into())),
636    ///             xsd_schema::xpath::XmlItem::Atomic(xsd_schema::types::XmlValue::integer(2.into())),
637    ///             xsd_schema::xpath::XmlItem::Atomic(xsd_schema::types::XmlValue::integer(3.into())),
638    ///         ]);
639    ///         eval.set_variable_by_name("items", seq).unwrap();
640    ///     })
641    ///     .unwrap();
642    /// ```
643    pub fn run_with<N, F>(self, setup: F) -> Result<XPathValue<N>, XPathError>
644    where
645        N: DomNavigator,
646        F: for<'a> FnOnce(&mut TypedEvaluator<'_, '_, 'a, N>),
647    {
648        self.run_with_node_and_setup(None, setup)
649    }
650
651    /// Evaluate with a context node and setup callback for advanced variable binding.
652    ///
653    /// Combines `run_with_node` and `run_with` functionality.
654    pub fn run_with_node_and_setup<N, F>(
655        self,
656        context_node: Option<N>,
657        setup: F,
658    ) -> Result<XPathValue<N>, XPathError>
659    where
660        N: DomNavigator,
661        F: for<'a> FnOnce(&mut TypedEvaluator<'_, '_, 'a, N>),
662    {
663        // Create dynamic context
664        let mut dyn_ctx = DynamicContext::new(self.static_ctx, self.expr.var_slots);
665
666        // Set context node if provided
667        if let Some(node) = context_node {
668            dyn_ctx = dyn_ctx.with_context_node(node);
669        }
670
671        // Set pending variables (from with_variable calls)
672        for (slot, pending) in self.pending_vars {
673            let value: XPathValue<N> = pending.into_xpath_value();
674            dyn_ctx.set_variable(slot, value);
675        }
676
677        // Create typed evaluator and run setup callback
678        {
679            let mut typed_eval = TypedEvaluator {
680                expr: self.expr,
681                static_ctx: self.static_ctx,
682                dyn_ctx: &mut dyn_ctx,
683            };
684            setup(&mut typed_eval);
685        } // typed_eval dropped here, releasing the borrow
686
687        // Evaluate the expression
688        eval_node(&self.expr.arena, self.expr.root, &mut dyn_ctx)
689    }
690}
691
692// ============================================================================
693// TypedEvaluator - For advanced variable binding with known navigator type
694// ============================================================================
695
696/// A typed evaluator that allows binding arbitrary `XPathValue<N>` values.
697///
698/// This is used within `run_with` callbacks to set variables that cannot be
699/// represented as simple `EvalValue` (like nodes or sequences).
700pub struct TypedEvaluator<'expr, 'ctx, 'dyn_ctx, N: DomNavigator> {
701    expr: &'expr XPathExpr,
702    static_ctx: &'ctx XPathContext<'ctx>,
703    dyn_ctx: &'dyn_ctx mut DynamicContext<'ctx, N>,
704}
705
706impl<'expr, 'ctx, 'dyn_ctx, N: DomNavigator> TypedEvaluator<'expr, 'ctx, 'dyn_ctx, N> {
707    /// Set a variable by name to an arbitrary XPath value.
708    ///
709    /// This allows binding nodes, sequences, empty sequences, or any other
710    /// `XPathValue<N>` to an external variable.
711    ///
712    /// # Errors
713    ///
714    /// Returns `XPST0008` if the variable was not declared at compile time.
715    pub fn set_variable_by_name(
716        &mut self,
717        name: &str,
718        value: XPathValue<N>,
719    ) -> Result<(), XPathError> {
720        let slot = find_external_var(name, &self.expr.external_vars, self.static_ctx)?;
721        self.dyn_ctx.set_variable(slot, value);
722        Ok(())
723    }
724
725    /// Set a variable by slot ID directly.
726    ///
727    /// Use this when you already know the slot ID (e.g., from `ExternalVar::slot`).
728    pub fn set_variable(&mut self, slot: VarSlotId, value: XPathValue<N>) {
729        self.dyn_ctx.set_variable(slot, value);
730    }
731
732    /// Get a reference to the dynamic context for advanced manipulation.
733    pub fn context(&mut self) -> &mut DynamicContext<'ctx, N> {
734        &mut *self.dyn_ctx
735    }
736}
737
738// ============================================================================
739// Helper Functions
740// ============================================================================
741
742/// Parse a variable name string into a QualifiedName.
743///
744/// Supports both simple names ("x") and prefixed names ("prefix:local").
745/// The prefix must be bound in the static context.
746fn parse_variable_name(name: &str, ctx: &XPathContext<'_>) -> Result<QualifiedName, XPathError> {
747    if let Some(colon_pos) = name.find(':') {
748        // Prefixed name: "prefix:local"
749        let prefix = &name[..colon_pos];
750        let local = &name[colon_pos + 1..];
751
752        if prefix.is_empty() || local.is_empty() {
753            return Err(XPathError::XPST0003 {
754                message: format!("Invalid variable name: '{}'", name),
755            });
756        }
757
758        let prefix_id = ctx.names.add(prefix);
759        let local_id = ctx.names.add(local);
760
761        // Resolve prefix to namespace
762        let ns_id = ctx
763            .resolve_prefix_id(prefix_id)
764            .ok_or_else(|| XPathError::undefined_prefix(prefix))?;
765
766        Ok(QualifiedName::new(Some(ns_id), local_id, Some(prefix_id)))
767    } else {
768        // Simple name: "x"
769        let local_id = ctx.names.add(name);
770        Ok(QualifiedName::local(local_id))
771    }
772}
773
774/// Find an external variable by name, searching from the end to match binder resolution.
775///
776/// Returns the slot ID if found, or an error if not declared.
777fn find_external_var(
778    name: &str,
779    external_vars: &[ExternalVar],
780    ctx: &XPathContext<'_>,
781) -> Result<VarSlotId, XPathError> {
782    let qname = parse_variable_name(name, ctx)?;
783
784    // Search from the end to match the binder's last-in-first-out resolution
785    external_vars
786        .iter()
787        .rev()
788        .find(|v| v.name == qname)
789        .map(|v| v.slot)
790        .ok_or_else(|| XPathError::XPST0008 {
791            qname: format!("${}", name),
792        })
793}
794
795/// Convert an XPathValue to a string.
796fn xpath_value_to_string<N: DomNavigator>(value: &XPathValue<N>) -> String {
797    match value {
798        XPathValue::Empty => String::new(),
799        XPathValue::Item(item) => item_to_string(item),
800        XPathValue::Sequence(items) => {
801            if let Some(first) = items.first() {
802                item_to_string(first)
803            } else {
804                String::new()
805            }
806        }
807    }
808}
809
810/// Convert an XmlItem to a string.
811fn item_to_string<N: DomNavigator>(item: &XmlItem<N>) -> String {
812    match item {
813        XmlItem::Node(nav) => nav.value(),
814        XmlItem::Atomic(val) => val.to_string_value(),
815    }
816}
817
818/// Convert an XPathValue to a number.
819fn xpath_value_to_number<N: DomNavigator>(value: &XPathValue<N>) -> f64 {
820    match value {
821        XPathValue::Empty => f64::NAN,
822        XPathValue::Item(item) => item_to_number(item),
823        XPathValue::Sequence(items) => {
824            if let Some(first) = items.first() {
825                item_to_number(first)
826            } else {
827                f64::NAN
828            }
829        }
830    }
831}
832
833/// Convert an XmlItem to a number.
834fn item_to_number<N: DomNavigator>(item: &XmlItem<N>) -> f64 {
835    match item {
836        XmlItem::Node(nav) => nav.value().trim().parse().unwrap_or(f64::NAN),
837        XmlItem::Atomic(val) => {
838            if let Some(d) = val.as_double() {
839                d
840            } else if let Some(i) = val.as_integer() {
841                // Convert BigInt to f64 (may lose precision for very large numbers)
842                i.to_string().parse().unwrap_or(f64::NAN)
843            } else {
844                // Try string conversion
845                val.to_string_value().trim().parse().unwrap_or(f64::NAN)
846            }
847        }
848    }
849}
850
851// ============================================================================
852// Tests
853// ============================================================================
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858    use crate::namespace::table::NameTable;
859    use crate::xpath::RoXmlNavigator;
860
861    #[test]
862    fn test_compile_simple_expression() {
863        let names = NameTable::new();
864        let ctx = XPathContext::new(&names);
865
866        let expr = XPathExpr::compile("1 + 2", &ctx);
867        assert!(expr.is_ok());
868
869        let expr = expr.unwrap();
870        assert_eq!(expr.source(), "1 + 2");
871        assert!(expr.external_vars().is_empty());
872    }
873
874    #[test]
875    fn test_compile_with_variables() {
876        let names = NameTable::new();
877        let ctx = XPathContext::new(&names);
878
879        let expr = XPathExpr::compile_with_vars("$x + $y", &ctx, &["x", "y"]);
880        assert!(expr.is_ok());
881
882        let expr = expr.unwrap();
883        assert_eq!(expr.external_vars().len(), 2);
884    }
885
886    #[test]
887    fn test_eval_simple_arithmetic() {
888        let names = NameTable::new();
889        let ctx = XPathContext::new(&names);
890
891        let expr = XPathExpr::compile("1 + 2", &ctx).unwrap();
892        let result = expr
893            .evaluator(&ctx)
894            .run_number::<RoXmlNavigator<'static>>()
895            .unwrap();
896
897        assert_eq!(result, 3.0);
898    }
899
900    #[test]
901    fn test_eval_with_variable() {
902        let names = NameTable::new();
903        let ctx = XPathContext::new(&names);
904
905        let expr = XPathExpr::compile_with_vars("$x + 1", &ctx, &["x"]).unwrap();
906        let result = expr
907            .evaluator(&ctx)
908            .with_variable("x", 41)
909            .unwrap()
910            .run_number::<RoXmlNavigator<'static>>()
911            .unwrap();
912
913        assert_eq!(result, 42.0);
914    }
915
916    #[test]
917    fn test_eval_with_string_variable() {
918        let names = NameTable::new();
919        let ctx = XPathContext::new(&names);
920
921        let expr = XPathExpr::compile_with_vars(
922            "concat($greeting, ' ', $name)",
923            &ctx,
924            &["greeting", "name"],
925        )
926        .unwrap();
927        let result = expr
928            .evaluator(&ctx)
929            .with_variable("greeting", "Hello")
930            .unwrap()
931            .with_variable("name", "World")
932            .unwrap()
933            .run_string::<RoXmlNavigator<'static>>()
934            .unwrap();
935
936        assert_eq!(result, "Hello World");
937    }
938
939    #[test]
940    fn test_eval_run_bool() {
941        let names = NameTable::new();
942        let ctx = XPathContext::new(&names);
943
944        let expr = XPathExpr::compile("1 < 2", &ctx).unwrap();
945        let result = expr
946            .evaluator(&ctx)
947            .run_bool::<RoXmlNavigator<'static>>()
948            .unwrap();
949        assert!(result);
950
951        let expr = XPathExpr::compile("2 < 1", &ctx).unwrap();
952        let result = expr
953            .evaluator(&ctx)
954            .run_bool::<RoXmlNavigator<'static>>()
955            .unwrap();
956        assert!(!result);
957    }
958
959    #[test]
960    fn test_eval_run_number() {
961        let names = NameTable::new();
962        let ctx = XPathContext::new(&names);
963
964        let expr = XPathExpr::compile("2.5", &ctx).unwrap();
965        let result = expr
966            .evaluator(&ctx)
967            .run_number::<RoXmlNavigator<'static>>()
968            .unwrap();
969        assert!((result - 2.5).abs() < 0.001);
970    }
971
972    #[test]
973    fn test_undefined_variable_error() {
974        let names = NameTable::new();
975        let ctx = XPathContext::new(&names);
976
977        // Try to compile expression with undefined variable
978        let result = XPathExpr::compile("$x", &ctx);
979        assert!(result.is_err());
980
981        if let Err(XPathError::XPST0008 { qname }) = result {
982            assert!(qname.contains("x"));
983        } else {
984            panic!("Expected XPST0008 error");
985        }
986    }
987
988    #[test]
989    fn test_setting_undeclared_variable_error() {
990        let names = NameTable::new();
991        let ctx = XPathContext::new(&names);
992
993        // Compile with only $x declared
994        let expr = XPathExpr::compile_with_vars("$x", &ctx, &["x"]).unwrap();
995
996        // Try to set undeclared variable $y
997        let result = expr
998            .evaluator(&ctx)
999            .with_variable("x", 1)
1000            .unwrap()
1001            .with_variable("y", 2); // $y was not declared
1002
1003        assert!(result.is_err());
1004        if let Err(XPathError::XPST0008 { qname }) = result {
1005            assert!(qname.contains("y"));
1006        } else {
1007            panic!("Expected XPST0008 error");
1008        }
1009    }
1010
1011    #[test]
1012    fn test_eval_with_context_node() {
1013        let names = NameTable::new();
1014        let ctx = XPathContext::new(&names);
1015
1016        let expr = XPathExpr::compile("child::item", &ctx).unwrap();
1017
1018        let doc = roxmltree::Document::parse("<root><item>value</item></root>").unwrap();
1019        let mut nav = RoXmlNavigator::new(&doc);
1020        nav.move_to_first_child(); // move to <root>
1021
1022        let result = expr.evaluator(&ctx).run_with_node(nav).unwrap();
1023        assert_eq!(result.len(), 1);
1024    }
1025
1026    #[test]
1027    fn test_expr_is_clone() {
1028        let names = NameTable::new();
1029        let ctx = XPathContext::new(&names);
1030
1031        let expr1 = XPathExpr::compile("1 + 2", &ctx).unwrap();
1032        let expr2 = expr1.clone();
1033
1034        // Both should evaluate to the same result
1035        let result1 = expr1
1036            .evaluator(&ctx)
1037            .run_number::<RoXmlNavigator<'static>>()
1038            .unwrap();
1039        let result2 = expr2
1040            .evaluator(&ctx)
1041            .run_number::<RoXmlNavigator<'static>>()
1042            .unwrap();
1043
1044        assert_eq!(result1, result2);
1045    }
1046
1047    #[test]
1048    fn test_eval_value_conversions() {
1049        // Test that all From implementations work
1050        let _: EvalValue = true.into();
1051        let _: EvalValue = 42i32.into();
1052        let _: EvalValue = 42i64.into();
1053        let _: EvalValue = 2.5f32.into();
1054        let _: EvalValue = 2.5f64.into();
1055        let _: EvalValue = "hello".into();
1056        let _: EvalValue = String::from("hello").into();
1057        let _: EvalValue = BigInt::from(1000000000000i64).into();
1058    }
1059
1060    #[test]
1061    fn test_multiple_evaluations_same_expr() {
1062        let names = NameTable::new();
1063        let ctx = XPathContext::new(&names);
1064
1065        let expr = XPathExpr::compile_with_vars("$x * 2", &ctx, &["x"]).unwrap();
1066
1067        // Evaluate multiple times with different values
1068        let result1 = expr
1069            .evaluator(&ctx)
1070            .with_variable("x", 5)
1071            .unwrap()
1072            .run_number::<RoXmlNavigator<'static>>()
1073            .unwrap();
1074
1075        let result2 = expr
1076            .evaluator(&ctx)
1077            .with_variable("x", 10)
1078            .unwrap()
1079            .run_number::<RoXmlNavigator<'static>>()
1080            .unwrap();
1081
1082        let result3 = expr
1083            .evaluator(&ctx)
1084            .with_variable("x", 21)
1085            .unwrap()
1086            .run_number::<RoXmlNavigator<'static>>()
1087            .unwrap();
1088
1089        assert_eq!(result1, 10.0);
1090        assert_eq!(result2, 20.0);
1091        assert_eq!(result3, 42.0);
1092    }
1093
1094    #[test]
1095    fn test_duplicate_variable_error() {
1096        let names = NameTable::new();
1097        let ctx = XPathContext::new(&names);
1098
1099        // Duplicate variable names should cause an error
1100        let result = XPathExpr::compile_with_vars("$x + $x", &ctx, &["x", "x"]);
1101        assert!(result.is_err());
1102
1103        if let Err(XPathError::XPST0003 { message }) = result {
1104            assert!(message.contains("Duplicate"));
1105        } else {
1106            panic!("Expected XPST0003 error for duplicate variable");
1107        }
1108    }
1109
1110    #[test]
1111    fn test_prefixed_variable() {
1112        let names = NameTable::new();
1113
1114        // Create a context with a namespace binding
1115        let my_ns = names.add("http://example.com/my");
1116        let my_prefix = names.add("my");
1117
1118        let mut namespaces = crate::namespace::context::NamespaceContextSnapshot::default();
1119        namespaces.bindings.push((my_prefix, my_ns));
1120
1121        let ctx = XPathContext::new(&names).with_namespaces(namespaces);
1122
1123        // Compile with a prefixed variable
1124        let expr = XPathExpr::compile_with_vars("$my:value + 1", &ctx, &["my:value"]).unwrap();
1125
1126        // Set the prefixed variable
1127        let result = expr
1128            .evaluator(&ctx)
1129            .with_variable("my:value", 41)
1130            .unwrap()
1131            .run_number::<RoXmlNavigator<'static>>()
1132            .unwrap();
1133
1134        assert_eq!(result, 42.0);
1135    }
1136
1137    #[test]
1138    fn test_run_with_sequence() {
1139        use crate::types::XmlValue;
1140
1141        let names = NameTable::new();
1142        let ctx = XPathContext::new(&names);
1143
1144        let expr = XPathExpr::compile_with_vars("count($items)", &ctx, &["items"]).unwrap();
1145
1146        // Use run_with to bind a sequence
1147        let result = expr
1148            .evaluator(&ctx)
1149            .run_with::<RoXmlNavigator<'static>, _>(|eval| {
1150                let seq = XPathValue::from_sequence(vec![
1151                    XmlItem::Atomic(XmlValue::integer(1.into())),
1152                    XmlItem::Atomic(XmlValue::integer(2.into())),
1153                    XmlItem::Atomic(XmlValue::integer(3.into())),
1154                ]);
1155                eval.set_variable_by_name("items", seq).unwrap();
1156            })
1157            .unwrap();
1158
1159        // count() should return 3
1160        assert_eq!(
1161            result.as_integer().map(|i| i.to_string()),
1162            Some("3".to_string())
1163        );
1164    }
1165
1166    #[test]
1167    fn test_run_with_empty_sequence() {
1168        let names = NameTable::new();
1169        let ctx = XPathContext::new(&names);
1170
1171        let expr = XPathExpr::compile_with_vars("empty($items)", &ctx, &["items"]).unwrap();
1172
1173        // Use run_with to bind an empty sequence
1174        let result = expr
1175            .evaluator(&ctx)
1176            .run_with::<RoXmlNavigator<'static>, _>(|eval| {
1177                eval.set_variable_by_name("items", XPathValue::empty())
1178                    .unwrap();
1179            })
1180            .unwrap();
1181
1182        // empty() should return true for empty sequence
1183        assert_eq!(result.as_bool(), Some(true));
1184    }
1185}