Skip to main content

xsd_schema/xpath/
context.rs

1//! XPath static and dynamic context definitions.
2//!
3//! This module provides:
4//! - `XPathContext` - Static context for expression binding and evaluation
5//! - `DynamicContext` - Runtime context for XPath evaluation
6//! - `VarStore` - Variable storage (Vec-based arena indexed by VarSlotId)
7//! - `NameBinder` - Compile-time variable slot allocation
8
9use crate::ids::NameId;
10use crate::namespace::context::NamespaceContextSnapshot;
11use crate::namespace::qname::QualifiedName;
12use crate::namespace::table::NameTable;
13use crate::schema::SchemaSet;
14use crate::types::value::{DateTimeValue, TimezoneOffset};
15
16use super::functions::{BuiltinCatalog, BuiltinEvaluator, FunctionCatalog, FunctionEvaluator};
17use super::iterator::XmlItem;
18use super::DomNavigator;
19use super::XPathMode;
20
21// ============================================================================
22// XPathContext (static context for bind-time and eval-time)
23// ============================================================================
24
25/// XPath 2.0 static context for expression binding and evaluation.
26///
27/// The static context provides information needed during expression compilation
28/// and evaluation:
29/// - Namespace prefix resolution
30/// - Function registry access
31/// - Default namespaces
32/// - Schema type information
33#[derive(Debug, Clone)]
34pub struct XPathContext<'a> {
35    /// Name table for string interning
36    pub names: &'a NameTable,
37    /// Schema set for type information
38    pub schema_set: Option<&'a SchemaSet>,
39    /// Namespace bindings for prefix resolution
40    pub namespaces: NamespaceContextSnapshot,
41    /// Default namespace for unprefixed element names
42    pub default_element_ns: Option<NameId>,
43    /// Default namespace for unprefixed function names (fn: namespace)
44    pub default_function_ns: Option<&'static str>,
45    /// Implicit timezone
46    pub implicit_timezone: Option<TimezoneOffset>,
47    /// Base URI for relative URI resolution
48    pub base_uri: Option<String>,
49    /// XPath language mode (1.0 or 2.0)
50    pub mode: XPathMode,
51    /// Enable fn:trace() output to stderr (disabled by default)
52    pub trace_enabled: bool,
53    /// Function catalog for bind-time lookup (None = use builtins).
54    function_catalog: Option<&'a dyn FunctionCatalog>,
55}
56
57impl<'a> XPathContext<'a> {
58    /// Create a new static context with the given name table.
59    pub fn new(names: &'a NameTable) -> Self {
60        Self {
61            names,
62            schema_set: None,
63            namespaces: NamespaceContextSnapshot::default(),
64            default_element_ns: None,
65            default_function_ns: Some(super::functions::FN_NAMESPACE),
66            implicit_timezone: None,
67            base_uri: None,
68            mode: XPathMode::XPath20,
69            trace_enabled: false,
70            function_catalog: None,
71        }
72    }
73
74    /// Set the schema set
75    pub fn with_schema_set(mut self, schema_set: &'a SchemaSet) -> Self {
76        self.schema_set = Some(schema_set);
77        self
78    }
79
80    /// Set the namespace bindings
81    pub fn with_namespaces(mut self, namespaces: NamespaceContextSnapshot) -> Self {
82        self.namespaces = namespaces;
83        self
84    }
85
86    /// Set the default element namespace
87    pub fn with_default_element_ns(mut self, ns: NameId) -> Self {
88        self.default_element_ns = Some(ns);
89        self
90    }
91
92    /// Set the default function namespace
93    pub fn with_default_function_ns(mut self, ns: &'static str) -> Self {
94        self.default_function_ns = Some(ns);
95        self
96    }
97
98    /// Set the implicit timezone
99    pub fn with_implicit_timezone(mut self, tz: TimezoneOffset) -> Self {
100        self.implicit_timezone = Some(tz);
101        self
102    }
103
104    /// Set the base URI
105    pub fn with_base_uri(mut self, base_uri: impl Into<String>) -> Self {
106        self.base_uri = Some(base_uri.into());
107        self
108    }
109
110    /// Set the XPath language mode.
111    pub fn with_mode(mut self, mode: XPathMode) -> Self {
112        self.mode = mode;
113        self
114    }
115
116    /// Enable fn:trace() output to stderr.
117    pub fn with_trace_enabled(mut self, enabled: bool) -> Self {
118        self.trace_enabled = enabled;
119        self
120    }
121
122    /// Get the XPath language mode.
123    pub fn mode(&self) -> XPathMode {
124        self.mode
125    }
126
127    /// Set the function catalog for custom function support.
128    pub fn with_function_catalog(mut self, catalog: &'a dyn FunctionCatalog) -> Self {
129        self.function_catalog = Some(catalog);
130        self
131    }
132
133    /// Get the function catalog, using built-in functions as default.
134    ///
135    /// Returns a reference to the configured catalog, or `BuiltinCatalog` if none set.
136    pub fn function_catalog(&self) -> &dyn FunctionCatalog {
137        static BUILTIN: BuiltinCatalog = BuiltinCatalog;
138        self.function_catalog.unwrap_or(&BUILTIN)
139    }
140
141    /// Resolve a prefix to a namespace URI.
142    ///
143    /// Returns the namespace URI for the given prefix, or None if not found.
144    pub fn resolve_prefix(&self, prefix: &str) -> Option<String> {
145        if prefix.is_empty() {
146            // Empty prefix: use default element namespace
147            self.default_element_ns
148                .and_then(|id| self.names.try_resolve(id))
149        } else if let Some(prefix_id) = self.names.get(prefix) {
150            self.namespaces
151                .resolve_prefix(prefix_id)
152                .and_then(|ns_id| self.names.try_resolve(ns_id))
153        } else {
154            None
155        }
156    }
157
158    /// Resolve a prefix to a namespace URI using NameId.
159    pub fn resolve_prefix_id(&self, prefix_id: NameId) -> Option<NameId> {
160        self.namespaces.resolve_prefix(prefix_id)
161    }
162
163    /// Get the default function namespace.
164    ///
165    /// In XPath 1.0 mode, core functions live in no namespace (empty string).
166    /// In XPath 2.0 mode, the default is the fn: namespace.
167    pub fn default_function_namespace(&self) -> &str {
168        match self.mode {
169            XPathMode::XPath10 => "",
170            XPathMode::XPath20 => self
171                .default_function_ns
172                .unwrap_or(super::functions::FN_NAMESPACE),
173        }
174    }
175
176    /// Resolve a name from the name table.
177    pub fn resolve_name(&self, id: NameId) -> Option<String> {
178        self.names.try_resolve(id)
179    }
180}
181
182// ============================================================================
183// NameBinder (compile-time variable slot allocation)
184// ============================================================================
185
186/// Variable slot identifier for indexing into VarStore.
187pub type VarSlotId = u32;
188
189/// Reference to a variable slot.
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub struct VarRef {
192    pub slot: VarSlotId,
193}
194
195/// Entry in the NameBinder stack.
196#[derive(Debug, Clone)]
197pub struct NameSlot {
198    pub name: QualifiedName,
199    pub slot: VarSlotId,
200}
201
202/// Compile-time variable binder for slot allocation.
203///
204/// Provides stack-based scoping with slot IDs into a data pool.
205/// Used during expression binding to assign variable slots.
206///
207/// External variables (those declared before `mark_external_boundary()` is called)
208/// are tracked separately and can be retrieved via `external_vars()`.
209#[derive(Debug, Default)]
210pub struct NameBinder {
211    next_slot: VarSlotId,
212    stack: Vec<NameSlot>,
213    /// Count of external variables (pushed before mark_external_boundary)
214    external_var_count: usize,
215}
216
217impl NameBinder {
218    /// Create a new empty name binder.
219    pub fn new() -> Self {
220        Self {
221            next_slot: 0,
222            stack: Vec::new(),
223            external_var_count: 0,
224        }
225    }
226
227    /// Get the total number of slots allocated.
228    ///
229    /// Use this after bind() to determine VarStore size.
230    pub fn len(&self) -> usize {
231        self.next_slot as usize
232    }
233
234    /// Check if any slots have been allocated.
235    pub fn is_empty(&self) -> bool {
236        self.next_slot == 0
237    }
238
239    /// Mark the current stack position as the boundary between external variables
240    /// and internally-bound variables.
241    ///
242    /// Call this after pushing all external variables (those provided by the API user)
243    /// and before binding the expression (which may introduce for/let/quantified variables).
244    pub fn mark_external_boundary(&mut self) {
245        self.external_var_count = self.stack.len();
246    }
247
248    /// Iterate over external variables (those pushed before `mark_external_boundary()`).
249    ///
250    /// Returns an iterator of (name, slot) pairs for all external variables.
251    pub fn external_vars(&self) -> impl Iterator<Item = (&QualifiedName, VarSlotId)> {
252        self.stack
253            .iter()
254            .take(self.external_var_count)
255            .map(|slot| (&slot.name, slot.slot))
256    }
257
258    /// Get the number of external variables.
259    pub fn external_var_count(&self) -> usize {
260        self.external_var_count
261    }
262
263    /// Push a new variable binding onto the stack.
264    ///
265    /// Allocates a new slot and returns a VarRef to it.
266    pub fn push_var(&mut self, name: QualifiedName) -> VarRef {
267        let slot = self.next_slot;
268        self.next_slot += 1;
269        self.stack.push(NameSlot { name, slot });
270        VarRef { slot }
271    }
272
273    /// Pop the most recent variable binding.
274    ///
275    /// Used by for/some/every expressions after binding their body.
276    pub fn pop_var(&mut self) {
277        self.stack.pop();
278    }
279
280    /// Resolve a variable name to its slot.
281    ///
282    /// Searches from the top of the stack (most recent binding first).
283    /// Returns XPST0008 if the variable is not bound.
284    pub fn resolve(&self, name: &QualifiedName) -> Result<VarRef, super::error::XPathError> {
285        // Walk stack from end to beginning (last-in, first-out scoping)
286        for entry in self.stack.iter().rev() {
287            if entry.name == *name {
288                return Ok(VarRef { slot: entry.slot });
289            }
290        }
291        // Format QName for error - we don't have access to NameTable here,
292        // so we use the raw NameId values in the message
293        Err(super::error::XPathError::XPST0008 {
294            qname: format!("$var(local={})", name.local_name.0),
295        })
296    }
297
298    /// Resolve a variable name to its slot, with NameTable for error messages.
299    ///
300    /// Same as `resolve()` but provides better error messages.
301    pub fn resolve_with_names(
302        &self,
303        name: &QualifiedName,
304        names: &NameTable,
305    ) -> Result<VarRef, super::error::XPathError> {
306        // Walk stack from end to beginning (last-in, first-out scoping)
307        for entry in self.stack.iter().rev() {
308            if entry.name == *name {
309                return Ok(VarRef { slot: entry.slot });
310            }
311        }
312        // Format QName for error using NameTable
313        let local = names
314            .try_resolve(name.local_name)
315            .unwrap_or_else(|| "<unknown>".to_string());
316        let qname_str = if let Some(prefix_id) = name.prefix {
317            let prefix = names
318                .try_resolve(prefix_id)
319                .unwrap_or_else(|| "<unknown>".to_string());
320            format!("{}:{}", prefix, local)
321        } else {
322            local.to_string()
323        };
324        Err(super::error::XPathError::XPST0008 { qname: qname_str })
325    }
326}
327
328// ============================================================================
329// VarStore (variable storage - Vec-based arena)
330// ============================================================================
331
332/// Variable storage for XPath evaluation.
333///
334/// Stores variable values indexed by VarSlotId.
335/// Size is determined by NameBinder::len() after binding.
336#[derive(Debug, Clone)]
337pub struct VarStore<V> {
338    /// Variable values indexed by slot ID
339    values: Vec<Option<V>>,
340}
341
342impl<V> VarStore<V> {
343    /// Create a new variable store with the given size.
344    ///
345    /// The size should be NameBinder::len() after binding.
346    pub fn new(size: usize) -> Self {
347        let mut values = Vec::with_capacity(size);
348        values.resize_with(size, || None);
349        Self { values }
350    }
351
352    /// Get a variable value by slot ID.
353    pub fn get(&self, slot: VarSlotId) -> Option<&V> {
354        self.values.get(slot as usize).and_then(|v| v.as_ref())
355    }
356
357    /// Set a variable value.
358    pub fn set(&mut self, slot: VarSlotId, value: V) {
359        if let Some(cell) = self.values.get_mut(slot as usize) {
360            *cell = Some(value);
361        }
362    }
363
364    /// Clear a variable slot.
365    pub fn clear_slot(&mut self, slot: VarSlotId) {
366        if let Some(cell) = self.values.get_mut(slot as usize) {
367            *cell = None;
368        }
369    }
370
371    /// Clear all variable values.
372    pub fn clear(&mut self) {
373        for cell in &mut self.values {
374            *cell = None;
375        }
376    }
377
378    /// Get the number of slots.
379    pub fn len(&self) -> usize {
380        self.values.len()
381    }
382
383    /// Check if the store is empty.
384    pub fn is_empty(&self) -> bool {
385        self.values.is_empty()
386    }
387}
388
389impl<V> Default for VarStore<V> {
390    fn default() -> Self {
391        Self::new(0)
392    }
393}
394
395// ============================================================================
396// DynamicContext (for eval-time)
397// ============================================================================
398
399/// XPath 2.0 dynamic context for expression evaluation.
400///
401/// The dynamic context provides runtime information:
402/// - Current context item (node or atomic value)
403/// - Context position and size (for predicates)
404/// - Variable bindings (indexed by VarSlotId)
405/// - Current date/time (stable for duration of query)
406/// - Implicit timezone
407pub struct DynamicContext<'a, N: DomNavigator> {
408    /// Reference to the static context
409    pub static_context: &'a XPathContext<'a>,
410    /// Current context item (if any)
411    pub context_item: Option<XmlItem<N>>,
412    /// Current context position (1-based)
413    pub context_position: usize,
414    /// Current context size
415    pub context_size: usize,
416    /// Current date/time (stable for entire query evaluation)
417    pub current_datetime: Option<DateTimeValue>,
418    /// Implicit timezone
419    pub implicit_timezone: Option<TimezoneOffset>,
420    /// Base URI for resolving relative URIs
421    pub base_uri: Option<String>,
422    /// Variable bindings (indexed by VarSlotId from NameBinder)
423    pub variables: VarStore<super::functions::XPathValue<N>>,
424    /// Function evaluator for eval-time dispatch (None = use builtins).
425    function_evaluator: Option<&'a dyn FunctionEvaluator<N>>,
426}
427
428impl<'a, N: DomNavigator> DynamicContext<'a, N> {
429    /// Create a new dynamic context with the given static context.
430    ///
431    /// The var_count should be NameBinder::len() after binding.
432    pub fn new(static_context: &'a XPathContext<'a>, var_count: usize) -> Self {
433        Self {
434            static_context,
435            context_item: None,
436            context_position: 0,
437            context_size: 0,
438            current_datetime: None,
439            implicit_timezone: static_context.implicit_timezone,
440            base_uri: static_context.base_uri.clone(),
441            variables: VarStore::new(var_count),
442            function_evaluator: None,
443        }
444    }
445
446    /// Set the context item.
447    pub fn with_context_item(mut self, item: XmlItem<N>) -> Self {
448        self.context_item = Some(item);
449        self.context_position = 1;
450        self.context_size = 1;
451        self
452    }
453
454    /// Set the context node.
455    pub fn with_context_node(self, node: N) -> Self {
456        self.with_context_item(XmlItem::Node(node))
457    }
458
459    /// Set context position and size (for predicate evaluation).
460    pub fn with_position(mut self, position: usize, size: usize) -> Self {
461        self.context_position = position;
462        self.context_size = size;
463        self
464    }
465
466    /// Set the current date/time.
467    pub fn with_current_datetime(mut self, dt: DateTimeValue) -> Self {
468        self.current_datetime = Some(dt);
469        self
470    }
471
472    /// Set the implicit timezone.
473    pub fn with_implicit_timezone(mut self, tz: TimezoneOffset) -> Self {
474        self.implicit_timezone = Some(tz);
475        self
476    }
477
478    /// Get the context item, returning an error if undefined.
479    pub fn require_context_item(&self) -> Result<&XmlItem<N>, super::error::XPathError> {
480        self.context_item
481            .as_ref()
482            .ok_or_else(|| super::error::XPathError::XPDY0002 {
483                message: "Context item is undefined".to_string(),
484            })
485    }
486
487    /// Get the context node, returning an error if undefined or not a node.
488    ///
489    /// Returns XPTY0020 if the context item is not a node (per XPath 2.0 spec for axis steps).
490    pub fn require_context_node(&self) -> Result<&N, super::error::XPathError> {
491        match self.context_item.as_ref() {
492            Some(XmlItem::Node(node)) => Ok(node),
493            Some(XmlItem::Atomic(_)) => Err(super::error::XPathError::XPTY0020),
494            None => Err(super::error::XPathError::XPDY0002 {
495                message: "Context item is undefined".to_string(),
496            }),
497        }
498    }
499
500    /// Get a variable value by slot ID.
501    pub fn get_variable(&self, slot: VarSlotId) -> Option<&super::functions::XPathValue<N>> {
502        self.variables.get(slot)
503    }
504
505    /// Set a variable value.
506    pub fn set_variable(&mut self, slot: VarSlotId, value: super::functions::XPathValue<N>) {
507        self.variables.set(slot, value);
508    }
509
510    /// Set the function evaluator for custom function support.
511    pub fn with_function_evaluator(mut self, evaluator: &'a dyn FunctionEvaluator<N>) -> Self {
512        self.function_evaluator = Some(evaluator);
513        self
514    }
515
516    /// Get the function evaluator, using built-in functions as default.
517    ///
518    /// Returns a reference to the configured evaluator, or `BuiltinEvaluator` if none set.
519    pub fn function_evaluator(&self) -> &dyn FunctionEvaluator<N> {
520        static BUILTIN: BuiltinEvaluator = BuiltinEvaluator;
521        self.function_evaluator.unwrap_or(&BUILTIN)
522    }
523
524    /// Check if a custom function evaluator is configured.
525    pub fn has_custom_evaluator(&self) -> bool {
526        self.function_evaluator.is_some()
527    }
528
529    /// Evaluate a function using the configured evaluator.
530    ///
531    /// This method exists to work around borrow checker issues with calling
532    /// `self.function_evaluator().eval(handle, self, args)` where the evaluator
533    /// borrow conflicts with the mutable self borrow.
534    pub fn eval_function(
535        &mut self,
536        handle: super::functions::FunctionHandle,
537        args: Vec<super::functions::XPathValue<N>>,
538    ) -> Result<super::functions::XPathValue<N>, super::error::XPathError> {
539        // Fast path: built-in handles with no custom evaluator go directly to BuiltinEvaluator
540        if handle.is_builtin() && self.function_evaluator.is_none() {
541            return BuiltinEvaluator.eval(handle, self, args);
542        }
543
544        // Route through the custom evaluator (e.g. XPath10Evaluator intercepts builtins).
545        // For custom handles, we need to call through the configured evaluator.
546        // Get the evaluator pointer before borrowing self mutably.
547        match self.function_evaluator {
548            Some(evaluator) => {
549                // SAFETY: The evaluator reference has lifetime 'a which is valid
550                // for the duration of this DynamicContext. We're converting to a
551                // raw pointer and back to work around the borrow checker, but the
552                // reference is valid for this call.
553                let evaluator_ptr = evaluator as *const dyn FunctionEvaluator<N>;
554                // Re-borrow as shared reference for the call
555                let evaluator_ref = unsafe { &*evaluator_ptr };
556                evaluator_ref.eval(handle, self, args)
557            }
558            None => {
559                // Fallback for custom handles without a configured evaluator
560                BuiltinEvaluator.eval(handle, self, args)
561            }
562        }
563    }
564}
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569
570    #[test]
571    fn test_var_store() {
572        let mut store: VarStore<i32> = VarStore::new(3);
573
574        assert!(store.get(0).is_none());
575        store.set(0, 42);
576        assert_eq!(store.get(0), Some(&42));
577
578        store.set(1, 100);
579        assert_eq!(store.get(1), Some(&100));
580
581        store.clear_slot(0);
582        assert!(store.get(0).is_none());
583
584        store.clear();
585        assert!(store.get(1).is_none());
586    }
587
588    #[test]
589    fn test_xpath_context_default_function_ns() {
590        let names = NameTable::new();
591        let ctx = XPathContext::new(&names);
592        assert_eq!(
593            ctx.default_function_namespace(),
594            super::super::functions::FN_NAMESPACE
595        );
596    }
597
598    #[test]
599    fn test_name_binder_push_pop() {
600        let names = NameTable::new();
601        let mut binder = NameBinder::new();
602        assert!(binder.is_empty());
603        assert_eq!(binder.len(), 0);
604
605        let x_id = names.add("x");
606        let y_id = names.add("y");
607
608        let name1 = QualifiedName::local(x_id);
609        let ref1 = binder.push_var(name1.clone());
610        assert_eq!(ref1.slot, 0);
611        assert_eq!(binder.len(), 1);
612
613        let name2 = QualifiedName::local(y_id);
614        let ref2 = binder.push_var(name2.clone());
615        assert_eq!(ref2.slot, 1);
616        assert_eq!(binder.len(), 2);
617
618        // Resolve should find the variables
619        let resolved1 = binder.resolve(&name1).unwrap();
620        assert_eq!(resolved1.slot, 0);
621
622        let resolved2 = binder.resolve(&name2).unwrap();
623        assert_eq!(resolved2.slot, 1);
624
625        // Pop y, x should still be resolvable
626        binder.pop_var();
627        let resolved1_again = binder.resolve(&name1).unwrap();
628        assert_eq!(resolved1_again.slot, 0);
629
630        // y should not be resolvable after pop
631        let err = binder.resolve(&name2);
632        assert!(err.is_err());
633    }
634
635    #[test]
636    fn test_name_binder_shadowing() {
637        let names = NameTable::new();
638        let mut binder = NameBinder::new();
639
640        let x_id = names.add("x");
641        let name = QualifiedName::local(x_id);
642
643        // Push x (slot 0)
644        let ref1 = binder.push_var(name.clone());
645        assert_eq!(ref1.slot, 0);
646
647        // Push x again (slot 1, shadows slot 0)
648        let ref2 = binder.push_var(name.clone());
649        assert_eq!(ref2.slot, 1);
650
651        // Resolve should find the shadowing slot
652        let resolved = binder.resolve(&name).unwrap();
653        assert_eq!(resolved.slot, 1);
654
655        // Pop the shadow, should now resolve to original
656        binder.pop_var();
657        let resolved_after_pop = binder.resolve(&name).unwrap();
658        assert_eq!(resolved_after_pop.slot, 0);
659    }
660
661    #[test]
662    fn test_name_binder_unbound_error() {
663        let names = NameTable::new();
664        let binder = NameBinder::new();
665        let undefined_id = names.add("undefined");
666        let name = QualifiedName::local(undefined_id);
667        let result = binder.resolve(&name);
668        assert!(matches!(
669            result,
670            Err(super::super::error::XPathError::XPST0008 { .. })
671        ));
672    }
673}