Skip to main content

xsd_schema/xpath/
eval.rs

1//! XPath AST evaluation phase.
2//!
3//! This module provides the `eval_node()` function which evaluates a bound
4//! XPath AST at runtime. The AST must be bound using `bind_node()` before
5//! evaluation.
6//!
7//! ## Supported Node Types
8//!
9//! Currently implemented:
10//! - `Value` - Literal values (string, integer, double, boolean, empty)
11//! - `ContextItem` - Context item reference (`.`)
12//! - `VarRef` - Variable references
13//! - `Expr` - Sequence expressions
14//! - `If` - Conditional expressions
15//! - `FunctionCall` - Function calls (dispatched via `eval_function`)
16//!
17//! Other node types return `not_implemented` errors for now.
18
19use crate::types::{ItemType, NameTest as RuntimeNameTest, SequenceType, XmlTypeCode};
20use crate::xpath::arena::{AstArena, AstNodeId};
21use crate::xpath::ast::{
22    AstNode, Axis, BinaryOpKind, FilterExprNode, ForBinding, ForNode, ItemTypeNode, KindTest,
23    NodeTest as AstNodeTest, OccurrenceIndicator, PathExprNode, PathStepNode, QuantifiedNode,
24    QuantifierKind, TypeExprKind, TypeExprNode, ValueNode,
25};
26use crate::xpath::axis_iterators::{
27    AncestorAxis, AttributeAxis, ChildAxis, DescendantNodeIterator, FollowingNodeIterator,
28    FollowingSiblingAxis, NamespaceAxis, ParentAxis, PrecedingNodeIterator, PrecedingSiblingAxis,
29    SelfAxis, SequentialAxisNodeIterator,
30};
31use crate::xpath::cast::{cast_to, castable, occurrence_allows_count, resolved_type_to_type_code};
32use crate::xpath::context::{DynamicContext, XPathContext};
33use crate::xpath::error::XPathError;
34use crate::xpath::functions::{
35    atomize_to_single_opt, effective_boolean_value, effective_boolean_value_10, XPathValue,
36};
37use crate::xpath::iterator::{
38    DocumentOrderNodeIterator, VecNodeIterator, XmlItem, XmlNodeIterator,
39};
40use crate::xpath::node_ops::{following_node, get_root, preceding_node, same_node};
41use crate::xpath::node_test::{matches_item_type_node, NodeTest};
42use crate::xpath::operators::cast_to_qname_with_context;
43use crate::xpath::operators::{
44    eval_binary, eval_numeric_binary_10, eval_range, eval_unary, general_eq_iter,
45    general_eq_iter_10, general_ge_iter, general_ge_iter_10, general_gt_iter, general_gt_iter_10,
46    general_le_iter, general_le_iter_10, general_lt_iter, general_lt_iter_10, general_ne_iter,
47    general_ne_iter_10,
48};
49use crate::xpath::sequence_ops::{except_nodes, intersect_nodes, union_nodes};
50use crate::xpath::{DomNavigator, XPathMode};
51
52/// Evaluate an AST node and return the result.
53///
54/// This function recursively evaluates the AST, dispatching to appropriate
55/// handlers based on node type. The AST must have been bound using `bind_node()`
56/// before evaluation.
57///
58/// # Arguments
59/// * `arena` - The AST arena containing all nodes
60/// * `id` - The ID of the node to evaluate
61/// * `ctx` - The dynamic context for evaluation
62///
63/// # Returns
64/// * `Ok(XPathValue)` containing the evaluation result
65/// * `Err(XPathError)` if evaluation fails
66///
67/// # Errors
68/// * `XPDY0002` - Context item is undefined when required
69/// * `XPST0008` - Variable is not bound
70/// * Various function-specific errors
71pub fn eval_node<N: DomNavigator>(
72    arena: &AstArena,
73    id: AstNodeId,
74    ctx: &mut DynamicContext<'_, N>,
75) -> Result<XPathValue<N>, XPathError> {
76    let node = arena.get(id);
77
78    match node {
79        AstNode::Expr(expr) => {
80            // Evaluate all items and concatenate results
81            if expr.items.is_empty() {
82                return Ok(XPathValue::empty());
83            }
84
85            if expr.items.len() == 1 {
86                // Single item - no concatenation needed
87                return eval_node(arena, expr.items[0], ctx);
88            }
89
90            // XPath 1.0: comma-separated sequences are not allowed
91            // (the comma operator only appears in function args, which use a separate grammar production)
92            if ctx.static_context.mode() == XPathMode::XPath10 {
93                return Err(XPathError::XPST0003 {
94                    message: "Sequence expressions (comma operator) are not available in XPath 1.0"
95                        .to_string(),
96                });
97            }
98
99            // Multiple items - collect all results
100            let mut results: Vec<XmlItem<N>> = Vec::new();
101            for item_id in &expr.items {
102                let value = eval_node(arena, *item_id, ctx)?;
103                results.extend(value.into_vec());
104            }
105            Ok(XPathValue::from_sequence(results))
106        }
107
108        AstNode::Value(value_node) => {
109            // XPath 1.0: reject constructs that slipped past the lexer
110            if ctx.static_context.mode() == XPathMode::XPath10 {
111                if matches!(value_node, ValueNode::Empty) {
112                    return Err(XPathError::XPST0003 {
113                        message: "Empty sequence () is not available in XPath 1.0".to_string(),
114                    });
115                }
116                if matches!(value_node, ValueNode::Double(_)) {
117                    return Err(XPathError::XPST0003 {
118                        message: "Double literals (e.g. 1e10) are not available in XPath 1.0"
119                            .to_string(),
120                    });
121                }
122            }
123            // Convert ValueNode to XPathValue
124            eval_value(value_node, ctx.static_context.mode())
125        }
126
127        AstNode::ContextItem(_) => {
128            // Return the context item, or error if undefined
129            match &ctx.context_item {
130                Some(item) => Ok(XPathValue::from_item(item.clone())),
131                None => Err(XPathError::XPDY0002 {
132                    message: "Context item is undefined".to_string(),
133                }),
134            }
135        }
136
137        AstNode::VarRef(var_ref) => {
138            // Get the variable value from the context
139            let slot = var_ref
140                .slot
141                .ok_or_else(|| XPathError::Internal("Variable reference not bound".to_string()))?;
142
143            ctx.get_variable(slot)
144                .cloned()
145                .ok_or_else(|| XPathError::XPDY0002 {
146                    message: format!("Variable ${} is not set", var_ref.local_name),
147                })
148        }
149
150        AstNode::If(if_node) => {
151            // Evaluate condition and return appropriate branch
152            let test_value = eval_node(arena, if_node.test, ctx)?;
153            let condition = effective_boolean_value(&test_value)?;
154
155            if condition {
156                eval_node(arena, if_node.then_branch, ctx)
157            } else {
158                eval_node(arena, if_node.else_branch, ctx)
159            }
160        }
161
162        AstNode::FunctionCall(func_call) => {
163            // Get the resolved function handle
164            let handle = func_call
165                .function_handle
166                .ok_or_else(|| XPathError::Internal("Function call not bound".to_string()))?;
167
168            // Evaluate all arguments
169            let mut args: Vec<XPathValue<N>> = Vec::with_capacity(func_call.args.len());
170            for arg_id in &func_call.args {
171                args.push(eval_node(arena, *arg_id, ctx)?);
172            }
173
174            // Dispatch via the context's eval_function method (supports custom functions)
175            ctx.eval_function(handle, args)
176        }
177
178        AstNode::For(for_node) => eval_for_expression(arena, for_node, ctx),
179
180        AstNode::Quantified(quant_node) => eval_quantified_expression(arena, quant_node, ctx),
181
182        AstNode::PathExpr(path_expr) => eval_path_expr(arena, path_expr, ctx),
183
184        AstNode::FilterExpr(filter_expr) => eval_filter_expr(arena, filter_expr, ctx),
185
186        AstNode::Range(range) => {
187            let start_val = eval_node(arena, range.start, ctx)?;
188            let end_val = eval_node(arena, range.end, ctx)?;
189
190            let start_opt = atomize_to_single_opt(start_val)?;
191            let end_opt = atomize_to_single_opt(end_val)?;
192
193            match (start_opt, end_opt) {
194                (None, _) | (_, None) => Ok(XPathValue::empty()),
195                (Some(start), Some(end)) => {
196                    let values = eval_range(&start, &end)?;
197                    let items: Vec<XmlItem<N>> = values.into_iter().map(XmlItem::Atomic).collect();
198                    Ok(XPathValue::from_sequence(items))
199                }
200            }
201        }
202
203        AstNode::UnaryOp(unary_op) => {
204            let operand_val = eval_node(arena, unary_op.operand, ctx)?;
205            let opt = atomize_to_single_opt(operand_val)?;
206
207            match opt {
208                None => Ok(XPathValue::empty()),
209                Some(operand) => {
210                    let result = eval_unary(unary_op.kind, &operand)?;
211                    Ok(XPathValue::from_atomic(result))
212                }
213            }
214        }
215
216        AstNode::BinaryOp(bin_op) => {
217            match bin_op.kind {
218                // Logical operators - short-circuit evaluation
219                BinaryOpKind::And => {
220                    let left_val = eval_node(arena, bin_op.left, ctx)?;
221                    let left_bool = if ctx.static_context.mode() == XPathMode::XPath10 {
222                        effective_boolean_value_10(&left_val)?
223                    } else {
224                        effective_boolean_value(&left_val)?
225                    };
226                    if !left_bool {
227                        return Ok(XPathValue::boolean(false));
228                    }
229                    let right_val = eval_node(arena, bin_op.right, ctx)?;
230                    let right_bool = if ctx.static_context.mode() == XPathMode::XPath10 {
231                        effective_boolean_value_10(&right_val)?
232                    } else {
233                        effective_boolean_value(&right_val)?
234                    };
235                    Ok(XPathValue::boolean(right_bool))
236                }
237                BinaryOpKind::Or => {
238                    let left_val = eval_node(arena, bin_op.left, ctx)?;
239                    let left_bool = if ctx.static_context.mode() == XPathMode::XPath10 {
240                        effective_boolean_value_10(&left_val)?
241                    } else {
242                        effective_boolean_value(&left_val)?
243                    };
244                    if left_bool {
245                        return Ok(XPathValue::boolean(true));
246                    }
247                    let right_val = eval_node(arena, bin_op.right, ctx)?;
248                    let right_bool = if ctx.static_context.mode() == XPathMode::XPath10 {
249                        effective_boolean_value_10(&right_val)?
250                    } else {
251                        effective_boolean_value(&right_val)?
252                    };
253                    Ok(XPathValue::boolean(right_bool))
254                }
255
256                // Arithmetic and value comparison operators - atomize to single values
257                BinaryOpKind::Add
258                | BinaryOpKind::Sub
259                | BinaryOpKind::Mul
260                | BinaryOpKind::Div
261                | BinaryOpKind::IDiv
262                | BinaryOpKind::Mod
263                | BinaryOpKind::ValueEq
264                | BinaryOpKind::ValueNe
265                | BinaryOpKind::ValueLt
266                | BinaryOpKind::ValueLe
267                | BinaryOpKind::ValueGt
268                | BinaryOpKind::ValueGe => {
269                    let left_val = eval_node(arena, bin_op.left, ctx)?;
270                    let right_val = eval_node(arena, bin_op.right, ctx)?;
271
272                    let left_opt = atomize_to_single_opt(left_val)?;
273                    let right_opt = atomize_to_single_opt(right_val)?;
274
275                    match (left_opt, right_opt) {
276                        (None, _) | (_, None) => Ok(XPathValue::empty()),
277                        (Some(left), Some(right)) => {
278                            let is_arithmetic = matches!(
279                                bin_op.kind,
280                                BinaryOpKind::Add
281                                    | BinaryOpKind::Sub
282                                    | BinaryOpKind::Mul
283                                    | BinaryOpKind::Div
284                                    | BinaryOpKind::Mod
285                            );
286                            let result = if is_arithmetic
287                                && ctx.static_context.mode() == XPathMode::XPath10
288                            {
289                                eval_numeric_binary_10(bin_op.kind, &left, &right)?
290                            } else {
291                                eval_binary(bin_op.kind, &left, &right)?
292                            };
293                            Ok(XPathValue::from_atomic(result))
294                        }
295                    }
296                }
297
298                // General comparisons - use Cartesian product semantics
299                BinaryOpKind::GeneralEq
300                | BinaryOpKind::GeneralNe
301                | BinaryOpKind::GeneralLt
302                | BinaryOpKind::GeneralLe
303                | BinaryOpKind::GeneralGt
304                | BinaryOpKind::GeneralGe => {
305                    let left_val = eval_node(arena, bin_op.left, ctx)?;
306                    let right_val = eval_node(arena, bin_op.right, ctx)?;
307
308                    // XPath 1.0 §3.4: node-set vs boolean → convert node-set to boolean as a whole
309                    if ctx.static_context.mode() == XPathMode::XPath10 {
310                        let left_is_bool = is_boolean_value(&left_val);
311                        let right_is_bool = is_boolean_value(&right_val);
312                        let left_has_nodes = has_nodes_or_empty(&left_val);
313                        let right_has_nodes = has_nodes_or_empty(&right_val);
314
315                        if (left_is_bool && right_has_nodes) || (right_is_bool && left_has_nodes) {
316                            let l = effective_boolean_value_10(&left_val)?;
317                            let r = effective_boolean_value_10(&right_val)?;
318                            let result = match bin_op.kind {
319                                BinaryOpKind::GeneralEq => l == r,
320                                BinaryOpKind::GeneralNe => l != r,
321                                BinaryOpKind::GeneralLt
322                                | BinaryOpKind::GeneralLe
323                                | BinaryOpKind::GeneralGt
324                                | BinaryOpKind::GeneralGe => {
325                                    let ln = if l { 1.0_f64 } else { 0.0 };
326                                    let rn = if r { 1.0_f64 } else { 0.0 };
327                                    match bin_op.kind {
328                                        BinaryOpKind::GeneralLt => ln < rn,
329                                        BinaryOpKind::GeneralLe => ln <= rn,
330                                        BinaryOpKind::GeneralGt => ln > rn,
331                                        BinaryOpKind::GeneralGe => ln >= rn,
332                                        _ => unreachable!(),
333                                    }
334                                }
335                                _ => unreachable!(),
336                            };
337                            return Ok(XPathValue::boolean(result));
338                        }
339                    }
340
341                    let left_iter = VecNodeIterator::new(left_val.into_vec());
342                    let right_iter = VecNodeIterator::new(right_val.into_vec());
343
344                    let result = if ctx.static_context.mode() == XPathMode::XPath10 {
345                        match bin_op.kind {
346                            BinaryOpKind::GeneralEq => general_eq_iter_10(&left_iter, &right_iter)?,
347                            BinaryOpKind::GeneralNe => general_ne_iter_10(&left_iter, &right_iter)?,
348                            BinaryOpKind::GeneralLt => general_lt_iter_10(&left_iter, &right_iter)?,
349                            BinaryOpKind::GeneralLe => general_le_iter_10(&left_iter, &right_iter)?,
350                            BinaryOpKind::GeneralGt => general_gt_iter_10(&left_iter, &right_iter)?,
351                            BinaryOpKind::GeneralGe => general_ge_iter_10(&left_iter, &right_iter)?,
352                            _ => unreachable!(),
353                        }
354                    } else {
355                        match bin_op.kind {
356                            BinaryOpKind::GeneralEq => {
357                                general_eq_iter(ctx.static_context, &left_iter, &right_iter)?
358                            }
359                            BinaryOpKind::GeneralNe => {
360                                general_ne_iter(ctx.static_context, &left_iter, &right_iter)?
361                            }
362                            BinaryOpKind::GeneralLt => {
363                                general_lt_iter(ctx.static_context, &left_iter, &right_iter)?
364                            }
365                            BinaryOpKind::GeneralLe => {
366                                general_le_iter(ctx.static_context, &left_iter, &right_iter)?
367                            }
368                            BinaryOpKind::GeneralGt => {
369                                general_gt_iter(ctx.static_context, &left_iter, &right_iter)?
370                            }
371                            BinaryOpKind::GeneralGe => {
372                                general_ge_iter(ctx.static_context, &left_iter, &right_iter)?
373                            }
374                            _ => unreachable!(),
375                        }
376                    };
377                    Ok(XPathValue::boolean(result))
378                }
379
380                // Node comparisons - use node identity/document order
381                BinaryOpKind::Is | BinaryOpKind::Before | BinaryOpKind::After => {
382                    let left_val = eval_node(arena, bin_op.left, ctx)?;
383                    let right_val = eval_node(arena, bin_op.right, ctx)?;
384
385                    let left_node = extract_single_node(left_val)?;
386                    let right_node = extract_single_node(right_val)?;
387
388                    // Per XPath 2.0 spec: if either operand is empty, result is empty sequence
389                    match (left_node, right_node) {
390                        (Some(left), Some(right)) => {
391                            let result = match bin_op.kind {
392                                BinaryOpKind::Is => same_node(&left, &right),
393                                BinaryOpKind::Before => preceding_node(&left, &right),
394                                BinaryOpKind::After => following_node(&left, &right),
395                                _ => unreachable!(),
396                            };
397                            Ok(XPathValue::boolean(result))
398                        }
399                        _ => Ok(XPathValue::empty()),
400                    }
401                }
402
403                // Sequence operators - node-only, return document order with duplicates removed
404                BinaryOpKind::Union | BinaryOpKind::Intersect | BinaryOpKind::Except => {
405                    let left_val = eval_node(arena, bin_op.left, ctx)?;
406                    let right_val = eval_node(arena, bin_op.right, ctx)?;
407
408                    let left_vec = left_val.into_vec();
409                    let right_vec = right_val.into_vec();
410
411                    let result = match bin_op.kind {
412                        BinaryOpKind::Union => union_nodes(left_vec, right_vec)?,
413                        BinaryOpKind::Intersect => intersect_nodes(left_vec, right_vec)?,
414                        BinaryOpKind::Except => except_nodes(left_vec, right_vec)?,
415                        _ => unreachable!(),
416                    };
417                    Ok(XPathValue::from_sequence(result))
418                }
419            }
420        }
421
422        AstNode::PathStep(_) => {
423            // PathStep should not be evaluated directly - it's processed via eval_path_step
424            Err(XPathError::Internal(
425                "PathStep should not be evaluated directly".to_string(),
426            ))
427        }
428
429        AstNode::TypeExpr(type_expr) => eval_type_expr(arena, type_expr, ctx),
430    }
431}
432
433// ============================================================================
434// Type Expression Evaluation
435// ============================================================================
436
437/// Evaluate a type expression (`instance of`, `treat as`, `cast as`, `castable as`).
438fn eval_type_expr<N: DomNavigator>(
439    arena: &AstArena,
440    type_expr: &TypeExprNode,
441    ctx: &mut DynamicContext<'_, N>,
442) -> Result<XPathValue<N>, XPathError> {
443    // Evaluate the operand
444    let operand = eval_node(arena, type_expr.operand, ctx)?;
445
446    match type_expr.kind {
447        TypeExprKind::InstanceOf => eval_instance_of(operand, type_expr, ctx),
448        TypeExprKind::TreatAs => eval_treat_as(operand, type_expr, ctx),
449        TypeExprKind::CastAs => eval_cast_as(operand, type_expr, ctx),
450        TypeExprKind::CastableAs => eval_castable_as(operand, type_expr, ctx),
451    }
452}
453
454/// Evaluate `expr instance of type`.
455///
456/// Returns true if the value matches the sequence type (cardinality + item type).
457fn eval_instance_of<N: DomNavigator>(
458    operand: XPathValue<N>,
459    type_expr: &TypeExprNode,
460    ctx: &DynamicContext<'_, N>,
461) -> Result<XPathValue<N>, XPathError> {
462    let items = operand.into_vec();
463    let count = items.len();
464
465    // Handle empty-sequence() first (special case - only matches empty)
466    if type_expr.target_type.item_type.is_none() {
467        return Ok(XPathValue::boolean(count == 0));
468    }
469
470    // Check cardinality
471    if !occurrence_allows_count(type_expr.target_type.occurrence, count) {
472        return Ok(XPathValue::boolean(false));
473    }
474
475    // Get item type (we know it's Some from the check above)
476    let item_type = type_expr.target_type.item_type.as_ref().unwrap();
477
478    // Check each item matches the item type
479    for item in &items {
480        if !matches_item_type_node(
481            item,
482            item_type,
483            type_expr.resolved_atomic_type.as_ref(),
484            ctx.static_context,
485        ) {
486            return Ok(XPathValue::boolean(false));
487        }
488    }
489
490    Ok(XPathValue::boolean(true))
491}
492
493/// Evaluate `expr treat as type`.
494///
495/// Returns the value unchanged if it matches the type, otherwise raises XPTY0004.
496fn eval_treat_as<N: DomNavigator>(
497    operand: XPathValue<N>,
498    type_expr: &TypeExprNode,
499    ctx: &DynamicContext<'_, N>,
500) -> Result<XPathValue<N>, XPathError> {
501    let items = operand.into_vec();
502    let count = items.len();
503
504    // Check cardinality
505    if !occurrence_allows_count(type_expr.target_type.occurrence, count) {
506        return Err(XPathError::XPTY0004 {
507            expected: format_sequence_type(&type_expr.target_type, ctx),
508            found: format!("sequence of {} items", count),
509        });
510    }
511
512    // Handle empty-sequence()
513    let item_type = match &type_expr.target_type.item_type {
514        None => {
515            // empty-sequence() - only accepts empty
516            if count == 0 {
517                return Ok(XPathValue::empty());
518            } else {
519                return Err(XPathError::XPTY0004 {
520                    expected: "empty-sequence()".to_string(),
521                    found: format!("sequence of {} items", count),
522                });
523            }
524        }
525        Some(it) => it,
526    };
527
528    // Check each item matches the item type
529    for item in &items {
530        if !matches_item_type_node(
531            item,
532            item_type,
533            type_expr.resolved_atomic_type.as_ref(),
534            ctx.static_context,
535        ) {
536            return Err(XPathError::XPTY0004 {
537                expected: format_sequence_type(&type_expr.target_type, ctx),
538                found: format_item_type(item),
539            });
540        }
541    }
542
543    // Return the original value
544    Ok(XPathValue::from_sequence(items))
545}
546
547/// Evaluate `expr cast as type`.
548///
549/// Atomizes the operand and casts to the target atomic type.
550fn eval_cast_as<N: DomNavigator>(
551    operand: XPathValue<N>,
552    type_expr: &TypeExprNode,
553    ctx: &DynamicContext<'_, N>,
554) -> Result<XPathValue<N>, XPathError> {
555    // Cast only works with atomic types
556    let item_type =
557        type_expr
558            .target_type
559            .item_type
560            .as_ref()
561            .ok_or_else(|| XPathError::XPTY0004 {
562                expected: "atomic type".to_string(),
563                found: "empty-sequence()".to_string(),
564            })?;
565
566    // The item type must be Atomic for cast
567    if !matches!(item_type, ItemTypeNode::Atomic(_)) {
568        return Err(XPathError::XPTY0004 {
569            expected: "atomic type".to_string(),
570            found: "non-atomic type".to_string(),
571        });
572    }
573
574    // Atomize the operand to get at most one atomic value
575    let atomic_opt = atomize_to_single_opt(operand)?;
576
577    // Check cardinality
578    let allows_empty = matches!(
579        type_expr.target_type.occurrence,
580        OccurrenceIndicator::ZeroOrOne | OccurrenceIndicator::ZeroOrMore
581    );
582
583    match atomic_opt {
584        None => {
585            if allows_empty {
586                Ok(XPathValue::empty())
587            } else {
588                Err(XPathError::XPTY0004 {
589                    expected: format_sequence_type(&type_expr.target_type, ctx),
590                    found: "empty-sequence()".to_string(),
591                })
592            }
593        }
594        Some(value) => {
595            // Get target type code from resolved QName
596            let qname = type_expr
597                .resolved_atomic_type
598                .as_ref()
599                .ok_or_else(|| XPathError::Internal("Cast target type not resolved".to_string()))?;
600            let target_type = resolved_type_to_type_code(qname, ctx.static_context.names)?;
601
602            // QName/NOTATION require namespace resolution from static context
603            let result = if matches!(target_type, XmlTypeCode::QName | XmlTypeCode::Notation) {
604                cast_to_qname_with_context(ctx.static_context, &value, target_type)?
605            } else {
606                cast_to(&value, target_type)?
607            };
608            Ok(XPathValue::from_atomic(result))
609        }
610    }
611}
612
613/// Evaluate `expr castable as type`.
614///
615/// Returns true if the cast would succeed, false otherwise.
616fn eval_castable_as<N: DomNavigator>(
617    operand: XPathValue<N>,
618    type_expr: &TypeExprNode,
619    ctx: &DynamicContext<'_, N>,
620) -> Result<XPathValue<N>, XPathError> {
621    // Cast only works with atomic types
622    let item_type = type_expr.target_type.item_type.as_ref();
623    if !matches!(item_type, Some(ItemTypeNode::Atomic(_))) {
624        return Ok(XPathValue::boolean(false));
625    }
626
627    // Atomize the operand
628    let atomic_opt = match atomize_to_single_opt(operand) {
629        Ok(opt) => opt,
630        Err(_) => return Ok(XPathValue::boolean(false)), // More than one item
631    };
632
633    // Check cardinality
634    let allows_empty = matches!(
635        type_expr.target_type.occurrence,
636        OccurrenceIndicator::ZeroOrOne | OccurrenceIndicator::ZeroOrMore
637    );
638
639    match atomic_opt {
640        None => {
641            // Empty sequence - allowed if occurrence allows it
642            Ok(XPathValue::boolean(allows_empty))
643        }
644        Some(value) => {
645            // Get target type code from resolved QName
646            let qname = match type_expr.resolved_atomic_type.as_ref() {
647                Some(q) => q,
648                None => return Ok(XPathValue::boolean(false)),
649            };
650            let target_type = match resolved_type_to_type_code(qname, ctx.static_context.names) {
651                Ok(tc) => tc,
652                Err(_) => return Ok(XPathValue::boolean(false)),
653            };
654
655            // QName/NOTATION require namespace resolution from static context
656            let is_castable = if matches!(target_type, XmlTypeCode::QName | XmlTypeCode::Notation) {
657                cast_to_qname_with_context(ctx.static_context, &value, target_type).is_ok()
658            } else {
659                castable(&value, target_type)
660            };
661            Ok(XPathValue::boolean(is_castable))
662        }
663    }
664}
665
666/// Format a sequence type for error messages.
667fn format_sequence_type<N: DomNavigator>(
668    seq_type: &crate::xpath::ast::SequenceTypeNode,
669    _ctx: &DynamicContext<'_, N>,
670) -> String {
671    let item_str = match &seq_type.item_type {
672        None => "empty-sequence()".to_string(),
673        Some(ItemTypeNode::Item) => "item()".to_string(),
674        Some(ItemTypeNode::Atomic(qname)) => {
675            if qname.prefix.is_empty() {
676                qname.local.clone()
677            } else {
678                format!("{}:{}", qname.prefix, qname.local)
679            }
680        }
681        Some(ItemTypeNode::Kind(kind)) => format_kind_test(kind),
682    };
683
684    let occ_str = match seq_type.occurrence {
685        OccurrenceIndicator::One => "",
686        OccurrenceIndicator::ZeroOrOne => "?",
687        OccurrenceIndicator::ZeroOrMore => "*",
688        OccurrenceIndicator::OneOrMore => "+",
689    };
690
691    format!("{}{}", item_str, occ_str)
692}
693
694/// Format a kind test for error messages.
695fn format_kind_test(kind: &crate::xpath::ast::KindTest) -> String {
696    use crate::xpath::ast::KindTest;
697    match kind {
698        KindTest::AnyKind => "node()".to_string(),
699        KindTest::Text => "text()".to_string(),
700        KindTest::Comment => "comment()".to_string(),
701        KindTest::ProcessingInstruction(None) => "processing-instruction()".to_string(),
702        KindTest::ProcessingInstruction(Some(name)) => {
703            format!("processing-instruction('{}')", name)
704        }
705        KindTest::Document(None) => "document-node()".to_string(),
706        KindTest::Document(Some(inner)) => {
707            format!("document-node({})", format_kind_test(inner))
708        }
709        KindTest::Element(test) => {
710            if let Some(ref qname) = test.name {
711                if qname.prefix.is_empty() {
712                    format!("element({})", qname.local)
713                } else {
714                    format!("element({}:{})", qname.prefix, qname.local)
715                }
716            } else {
717                "element()".to_string()
718            }
719        }
720        KindTest::Attribute(test) => {
721            if let Some(ref qname) = test.name {
722                if qname.prefix.is_empty() {
723                    format!("attribute({})", qname.local)
724                } else {
725                    format!("attribute({}:{})", qname.prefix, qname.local)
726                }
727            } else {
728                "attribute()".to_string()
729            }
730        }
731        KindTest::SchemaElement(name) => format!("schema-element({})", name),
732        KindTest::SchemaAttribute(name) => format!("schema-attribute({})", name),
733    }
734}
735
736/// Format an XmlItem type for error messages.
737fn format_item_type<N: DomNavigator>(item: &XmlItem<N>) -> String {
738    match item {
739        XmlItem::Node(nav) => {
740            use crate::xpath::DomNodeType;
741            match nav.node_type() {
742                DomNodeType::Root => "document-node()".to_string(),
743                DomNodeType::Element => format!("element({})", nav.local_name()),
744                DomNodeType::Attribute => format!("attribute({})", nav.local_name()),
745                DomNodeType::Text
746                | DomNodeType::Whitespace
747                | DomNodeType::SignificantWhitespace => "text()".to_string(),
748                DomNodeType::Comment => "comment()".to_string(),
749                DomNodeType::ProcessingInstruction => "processing-instruction()".to_string(),
750                DomNodeType::Namespace => "namespace-node()".to_string(),
751                DomNodeType::All => "node()".to_string(),
752            }
753        }
754        XmlItem::Atomic(value) => {
755            format!("{:?}", value.type_code)
756        }
757    }
758}
759
760// ============================================================================
761// Path Expression Evaluation
762// ============================================================================
763
764/// Evaluate a path expression.
765///
766/// Implements XPath 2.0 path expression semantics:
767/// - Root-only path (`/`): Returns the document root
768/// - Absolute paths (`/a/b`): Start from document root
769/// - Relative paths (`a/b`): Start from context node
770/// - Paths are evaluated left-to-right, chaining steps
771fn eval_path_expr<N: DomNavigator>(
772    arena: &AstArena,
773    path_expr: &PathExprNode,
774    ctx: &mut DynamicContext<'_, N>,
775) -> Result<XPathValue<N>, XPathError> {
776    // Handle root-only path: "/"
777    if path_expr.is_absolute && path_expr.steps.is_empty() {
778        let context_node = ctx.require_context_node()?;
779        let root = get_root(context_node);
780        return Ok(XPathValue::from_node(root));
781    }
782
783    // Check if the first step is a "primary expression" that doesn't require context nodes.
784    // This includes function calls, literals, parenthesized expressions, variable references,
785    // type expressions (constructor functions like xs:int(...) that the binder
786    // converts from FunctionCall to TypeExpr/CastAs in-place), and ContextItem.
787    // ContextItem (`.`) accesses the dynamic context item directly (which may be atomic),
788    // so it must not go through require_context_node() which demands a node.
789    // Only PathStep (axis steps) truly require a node context.
790    let first_is_primary = path_expr.steps.first().is_some_and(|&step_id| {
791        matches!(
792            arena.get(step_id),
793            AstNode::FilterExpr(_)
794                | AstNode::FunctionCall(_)
795                | AstNode::Value(_)
796                | AstNode::Expr(_)
797                | AstNode::VarRef(_)
798                | AstNode::TypeExpr(_)
799                | AstNode::ContextItem(_)
800        )
801    });
802
803    // Determine the starting nodes based on path type
804    let starting_nodes: Vec<N> = if path_expr.is_absolute {
805        // Absolute path: start from document root
806        let context_node = ctx.require_context_node()?;
807        vec![get_root(context_node)]
808    } else if first_is_primary {
809        // First step is a primary expression - no initial context nodes needed
810        Vec::new()
811    } else {
812        // Relative path: start from context node
813        let context_node = ctx.require_context_node()?;
814        vec![context_node.clone()]
815    };
816
817    // Check if any step is a FilterExpr as the first step (special case)
818    // or if this path might need document order sorting
819    let needs_doc_order = path_needs_document_order(arena, path_expr);
820
821    // Process steps sequentially
822    let mut current_nodes: Vec<XmlItem<N>> =
823        starting_nodes.into_iter().map(XmlItem::Node).collect();
824
825    for (step_idx, &step_id) in path_expr.steps.iter().enumerate() {
826        let step_node = arena.get(step_id);
827
828        current_nodes = match step_node {
829            AstNode::PathStep(path_step) => {
830                eval_path_step(arena, path_step, current_nodes, ctx, step_idx == 0)?
831            }
832            AstNode::FilterExpr(filter_expr) => {
833                // FilterExpr as a step - evaluate it and use its result
834                if step_idx == 0 {
835                    // First step is a FilterExpr - evaluate it directly
836                    let result = eval_filter_expr(arena, filter_expr, ctx)?;
837                    result.into_vec()
838                } else {
839                    // FilterExpr in a later position - this is applied to each node in sequence
840                    let mut results = Vec::new();
841                    for item in current_nodes {
842                        // Set context to this item and evaluate the filter expression
843                        let saved_context = ctx.context_item.take();
844                        let saved_pos = ctx.context_position;
845                        let saved_size = ctx.context_size;
846
847                        ctx.context_item = Some(item);
848                        ctx.context_position = 1;
849                        ctx.context_size = 1;
850
851                        let step_result = eval_filter_expr(arena, filter_expr, ctx)?;
852                        results.extend(step_result.into_vec());
853
854                        ctx.context_item = saved_context;
855                        ctx.context_position = saved_pos;
856                        ctx.context_size = saved_size;
857                    }
858                    results
859                }
860            }
861            _ => {
862                // Other expression types (like function calls, parenthesized exprs) as steps
863                if step_idx == 0 && current_nodes.is_empty() {
864                    // First step is a primary expression (function call, etc.) with no initial context
865                    // Evaluate it directly
866                    let result = eval_node(arena, step_id, ctx)?;
867                    result.into_vec()
868                } else {
869                    // Evaluate for each node in the current sequence
870                    let mut results = Vec::new();
871                    for item in current_nodes {
872                        let saved_context = ctx.context_item.take();
873                        let saved_pos = ctx.context_position;
874                        let saved_size = ctx.context_size;
875
876                        ctx.context_item = Some(item);
877                        ctx.context_position = 1;
878                        ctx.context_size = 1;
879
880                        let step_result = eval_node(arena, step_id, ctx)?;
881                        results.extend(step_result.into_vec());
882
883                        ctx.context_item = saved_context;
884                        ctx.context_position = saved_pos;
885                        ctx.context_size = saved_size;
886                    }
887                    results
888                }
889            }
890        };
891
892        // Early exit if sequence becomes empty
893        if current_nodes.is_empty() {
894            return Ok(XPathValue::empty());
895        }
896    }
897
898    // Apply document order if needed (for paths with reverse axes)
899    if needs_doc_order && !current_nodes.is_empty() {
900        let iter = VecNodeIterator::new(current_nodes);
901        let doc_order_iter = DocumentOrderNodeIterator::new(iter)?;
902        let mut doc_order_iter = doc_order_iter;
903        current_nodes = collect_iterator(&mut doc_order_iter)?;
904    }
905
906    Ok(XPathValue::from_sequence(current_nodes))
907}
908
909/// Check if a path expression needs document order sorting.
910///
911/// Returns true if the path contains:
912/// - Any reverse axis (parent, ancestor, preceding, preceding-sibling, ancestor-or-self)
913/// - FilterExpr at non-first position
914/// - Descendant/DescendantOrSelf/Following axis followed by non-Attribute/non-Namespace steps
915///   (these can produce duplicates when input nodes have overlapping descendants)
916fn path_needs_document_order(arena: &AstArena, path_expr: &PathExprNode) -> bool {
917    let len = path_expr.steps.len();
918    for (idx, &step_id) in path_expr.steps.iter().enumerate() {
919        match arena.get(step_id) {
920            AstNode::PathStep(step) => {
921                // Reverse axes always need sorting
922                if step.axis.is_reverse() {
923                    return true;
924                }
925                // Descendant/DescendantOrSelf/Following axes need sorting if followed
926                // by non-attribute/non-namespace steps (can produce duplicates)
927                if matches!(
928                    step.axis,
929                    Axis::Descendant | Axis::DescendantOrSelf | Axis::Following
930                ) {
931                    for s in (idx + 1)..len {
932                        if let AstNode::PathStep(next_step) = arena.get(path_expr.steps[s]) {
933                            if !matches!(next_step.axis, Axis::Attribute | Axis::Namespace) {
934                                return true;
935                            }
936                        }
937                    }
938                }
939            }
940            AstNode::FilterExpr(_) if idx > 0 => {
941                return true;
942            }
943            _ => {}
944        }
945    }
946    false
947}
948
949/// Evaluate a single path step against a sequence of nodes.
950fn eval_path_step<N: DomNavigator>(
951    arena: &AstArena,
952    step: &PathStepNode,
953    input_nodes: Vec<XmlItem<N>>,
954    ctx: &mut DynamicContext<'_, N>,
955    _is_first_step: bool,
956) -> Result<Vec<XmlItem<N>>, XPathError> {
957    // Convert input to nodes only (XPTY0019 if atomic values present)
958    let nodes: Vec<N> = input_nodes
959        .into_iter()
960        .map(|item| match item {
961            XmlItem::Node(n) => Ok(n),
962            XmlItem::Atomic(_) => Err(XPathError::XPTY0019),
963        })
964        .collect::<Result<Vec<_>, _>>()?;
965
966    if nodes.is_empty() {
967        return Ok(Vec::new());
968    }
969
970    // Convert the step's node test to runtime NodeTest
971    let node_test = step_to_node_test(step, ctx.static_context);
972
973    // Create the base iterator from input nodes
974    let base_iter = VecNodeIterator::new(nodes.into_iter().map(XmlItem::Node).collect());
975
976    // Apply axis iterator
977    let xpath_ctx = ctx.static_context.clone();
978    let stepped_items = apply_axis_iterator(step.axis, node_test, xpath_ctx, base_iter)?;
979
980    // Apply predicates if any
981    if step.predicates.is_empty() {
982        Ok(stepped_items)
983    } else {
984        eval_predicates(arena, &step.predicates, ctx, stepped_items)
985    }
986}
987
988/// Convert a PathStepNode to a runtime NodeTest.
989fn step_to_node_test(step: &PathStepNode, ctx: &XPathContext<'_>) -> Option<NodeTest> {
990    // If we have a resolved_test from binding, use it
991    if let Some(ref resolved) = step.resolved_test {
992        return Some(NodeTest::Name(resolved.clone()));
993    }
994
995    // Otherwise, convert from AST node test
996    match &step.test {
997        AstNodeTest::Name(name_test) => {
998            // Convert AST NameTest to runtime NameTest
999            match (&name_test.prefix, &name_test.local_name) {
1000                (None, None) => {
1001                    // * - wildcard
1002                    Some(NodeTest::Name(RuntimeNameTest::Wildcard))
1003                }
1004                (None, Some(local)) => {
1005                    // *:local - namespace wildcard
1006                    let local_id = ctx.names.add(local);
1007                    Some(NodeTest::Name(RuntimeNameTest::NamespaceWildcard(local_id)))
1008                }
1009                (Some(prefix), None) => {
1010                    // prefix:* - local wildcard
1011                    if let Some(ns_uri) = ctx.resolve_prefix(prefix) {
1012                        let ns_id = ctx.names.add(&ns_uri);
1013                        Some(NodeTest::Name(RuntimeNameTest::LocalWildcard(ns_id)))
1014                    } else {
1015                        None // Unknown prefix
1016                    }
1017                }
1018                (Some(prefix), Some(local)) => {
1019                    // prefix:local - specific QName
1020                    let local_id = ctx.names.add(local);
1021                    let ns_uri = if prefix.is_empty() {
1022                        ctx.default_element_ns
1023                    } else {
1024                        ctx.resolve_prefix(prefix).map(|s| ctx.names.add(&s))
1025                    };
1026                    let qname = crate::namespace::qname::QualifiedName::new(ns_uri, local_id, None);
1027                    Some(NodeTest::Name(RuntimeNameTest::QName(qname)))
1028                }
1029            }
1030        }
1031        AstNodeTest::Kind(kind_test) => {
1032            // Convert AST KindTest to SequenceType
1033            let seq_type = kind_test_to_sequence_type(kind_test);
1034            Some(NodeTest::Type(seq_type))
1035        }
1036    }
1037}
1038
1039/// Convert an AST KindTest to a SequenceType.
1040fn kind_test_to_sequence_type(kind: &KindTest) -> SequenceType {
1041    match kind {
1042        KindTest::AnyKind => SequenceType::node(),
1043        KindTest::Text => SequenceType::one(ItemType::Text),
1044        KindTest::Comment => SequenceType::one(ItemType::Comment),
1045        KindTest::ProcessingInstruction(target) => {
1046            SequenceType::one(ItemType::ProcessingInstruction(target.clone()))
1047        }
1048        KindTest::Document(inner) => {
1049            let inner_type = inner.as_ref().map(|k| Box::new(kind_test_to_item_type(k)));
1050            SequenceType::one(ItemType::Document(inner_type))
1051        }
1052        KindTest::Element(_) => {
1053            // For simplicity, treat as element() without name/type constraints
1054            // The actual name test is handled separately
1055            SequenceType::one(ItemType::Element(None, None))
1056        }
1057        KindTest::Attribute(_) => {
1058            // For simplicity, treat as attribute() without name/type constraints
1059            SequenceType::one(ItemType::Attribute(None, None))
1060        }
1061        KindTest::SchemaElement(_) | KindTest::SchemaAttribute(_) => {
1062            // Schema-aware types - treat as generic element/attribute for now
1063            SequenceType::node()
1064        }
1065    }
1066}
1067
1068/// Convert an AST KindTest to an ItemType (for nested tests like document-node(element(...))).
1069fn kind_test_to_item_type(kind: &KindTest) -> ItemType {
1070    match kind {
1071        KindTest::AnyKind => ItemType::AnyNode,
1072        KindTest::Text => ItemType::Text,
1073        KindTest::Comment => ItemType::Comment,
1074        KindTest::ProcessingInstruction(target) => ItemType::ProcessingInstruction(target.clone()),
1075        KindTest::Document(inner) => {
1076            let inner_type = inner.as_ref().map(|k| Box::new(kind_test_to_item_type(k)));
1077            ItemType::Document(inner_type)
1078        }
1079        KindTest::Element(_) => ItemType::Element(None, None),
1080        KindTest::Attribute(_) => ItemType::Attribute(None, None),
1081        KindTest::SchemaElement(_) | KindTest::SchemaAttribute(_) => ItemType::AnyNode,
1082    }
1083}
1084
1085/// Apply an axis iterator to a base iterator.
1086fn apply_axis_iterator<N: DomNavigator>(
1087    axis: Axis,
1088    node_test: Option<NodeTest>,
1089    ctx: XPathContext<'_>,
1090    base_iter: VecNodeIterator<N>,
1091) -> Result<Vec<XmlItem<N>>, XPathError> {
1092    match axis {
1093        Axis::Child => {
1094            let mut iter =
1095                SequentialAxisNodeIterator::new(ctx, node_test, false, base_iter, ChildAxis);
1096            collect_iterator(&mut iter)
1097        }
1098        Axis::Descendant => {
1099            let mut iter = DescendantNodeIterator::new(ctx, node_test, false, base_iter);
1100            collect_iterator(&mut iter)
1101        }
1102        Axis::DescendantOrSelf => {
1103            let mut iter = DescendantNodeIterator::new(ctx, node_test, true, base_iter);
1104            collect_iterator(&mut iter)
1105        }
1106        Axis::Attribute => {
1107            let mut iter =
1108                SequentialAxisNodeIterator::new(ctx, node_test, false, base_iter, AttributeAxis);
1109            collect_iterator(&mut iter)
1110        }
1111        Axis::SelfAxis => {
1112            // SelfAxis returns current node via move_to_first, so match_self=false
1113            let mut iter =
1114                SequentialAxisNodeIterator::new(ctx, node_test, false, base_iter, SelfAxis);
1115            collect_iterator(&mut iter)
1116        }
1117        Axis::Parent => {
1118            let mut iter =
1119                SequentialAxisNodeIterator::new(ctx, node_test, false, base_iter, ParentAxis);
1120            collect_iterator(&mut iter)
1121        }
1122        Axis::Ancestor => {
1123            let mut iter =
1124                SequentialAxisNodeIterator::new(ctx, node_test, false, base_iter, AncestorAxis);
1125            collect_iterator(&mut iter)
1126        }
1127        Axis::AncestorOrSelf => {
1128            let mut iter =
1129                SequentialAxisNodeIterator::new(ctx, node_test, true, base_iter, AncestorAxis);
1130            collect_iterator(&mut iter)
1131        }
1132        Axis::FollowingSibling => {
1133            let mut iter = SequentialAxisNodeIterator::new(
1134                ctx,
1135                node_test,
1136                false,
1137                base_iter,
1138                FollowingSiblingAxis,
1139            );
1140            collect_iterator(&mut iter)
1141        }
1142        Axis::PrecedingSibling => {
1143            let mut iter = SequentialAxisNodeIterator::new(
1144                ctx,
1145                node_test,
1146                false,
1147                base_iter,
1148                PrecedingSiblingAxis,
1149            );
1150            collect_iterator(&mut iter)
1151        }
1152        Axis::Following => {
1153            let mut iter = FollowingNodeIterator::new(ctx, node_test, base_iter);
1154            collect_iterator(&mut iter)
1155        }
1156        Axis::Preceding => {
1157            let mut iter = PrecedingNodeIterator::new(ctx, node_test, base_iter);
1158            collect_iterator(&mut iter)
1159        }
1160        Axis::Namespace => {
1161            let mut iter = SequentialAxisNodeIterator::new(
1162                ctx,
1163                node_test,
1164                false,
1165                base_iter,
1166                NamespaceAxis::default(),
1167            );
1168            collect_iterator(&mut iter)
1169        }
1170    }
1171}
1172
1173/// Collect iterator results into a Vec.
1174fn collect_iterator<I: XmlNodeIterator>(
1175    iter: &mut I,
1176) -> Result<Vec<XmlItem<I::Navigator>>, XPathError> {
1177    let mut results = Vec::new();
1178    while iter.move_next()? {
1179        if let Some(item_ref) = iter.current() {
1180            let item = match item_ref {
1181                crate::xpath::iterator::XmlItemRef::Node(n) => XmlItem::Node(n.clone()),
1182                crate::xpath::iterator::XmlItemRef::Atomic(v) => XmlItem::Atomic(v.clone()),
1183            };
1184            results.push(item);
1185        }
1186    }
1187    Ok(results)
1188}
1189
1190// ============================================================================
1191// Filter Expression Evaluation
1192// ============================================================================
1193
1194/// Evaluate a filter expression (`expr[predicate][predicate]...`).
1195fn eval_filter_expr<N: DomNavigator>(
1196    arena: &AstArena,
1197    filter_expr: &FilterExprNode,
1198    ctx: &mut DynamicContext<'_, N>,
1199) -> Result<XPathValue<N>, XPathError> {
1200    // Evaluate the base expression
1201    let base_value = eval_node(arena, filter_expr.base, ctx)?;
1202
1203    // If no predicates, return base value directly
1204    if filter_expr.predicates.is_empty() {
1205        return Ok(base_value);
1206    }
1207
1208    // Apply predicates
1209    let items = base_value.into_vec();
1210    let filtered = eval_predicates(arena, &filter_expr.predicates, ctx, items)?;
1211    Ok(XPathValue::from_sequence(filtered))
1212}
1213
1214/// Evaluate predicates on a sequence of items.
1215///
1216/// Each predicate is evaluated in order. For each predicate:
1217/// - If the predicate evaluates to a number, select the item at that position
1218/// - Otherwise, use effective boolean value to filter
1219fn eval_predicates<N: DomNavigator>(
1220    arena: &AstArena,
1221    predicates: &[AstNodeId],
1222    ctx: &mut DynamicContext<'_, N>,
1223    mut items: Vec<XmlItem<N>>,
1224) -> Result<Vec<XmlItem<N>>, XPathError> {
1225    for &pred_id in predicates {
1226        if items.is_empty() {
1227            break;
1228        }
1229
1230        let size = items.len();
1231        let mut filtered = Vec::new();
1232
1233        for (idx, item) in items.into_iter().enumerate() {
1234            let position = idx + 1; // 1-based position
1235
1236            // Save current context
1237            let saved_item = ctx.context_item.take();
1238            let saved_pos = ctx.context_position;
1239            let saved_size = ctx.context_size;
1240
1241            // Set predicate context
1242            ctx.context_item = Some(item.clone());
1243            ctx.context_position = position;
1244            ctx.context_size = size;
1245
1246            // Evaluate predicate
1247            let pred_result = eval_node(arena, pred_id, ctx)?;
1248
1249            // Restore context
1250            ctx.context_item = saved_item;
1251            ctx.context_position = saved_pos;
1252            ctx.context_size = saved_size;
1253
1254            // Check if item should be included
1255            let is_10 = ctx.static_context.mode() == XPathMode::XPath10;
1256            let include = match &pred_result {
1257                XPathValue::Item(XmlItem::Atomic(value)) if value.type_code.is_numeric() => {
1258                    // XPath 1.0 §2.4 and 2.0: exact comparison, no rounding
1259                    let num = crate::xpath::atomize::to_number(value);
1260                    if num.is_nan() {
1261                        false
1262                    } else {
1263                        (position as f64) == num
1264                    }
1265                }
1266                _ => {
1267                    if is_10 {
1268                        effective_boolean_value_10(&pred_result)?
1269                    } else {
1270                        effective_boolean_value(&pred_result)?
1271                    }
1272                }
1273            };
1274
1275            if include {
1276                filtered.push(item);
1277            }
1278        }
1279
1280        items = filtered;
1281    }
1282
1283    Ok(items)
1284}
1285
1286use std::ops::ControlFlow;
1287
1288// ============================================================================
1289// For Expression Evaluation
1290// ============================================================================
1291
1292/// Evaluate a for expression (`for $x in X, $y in Y return expr`).
1293///
1294/// Semantics per XPath 2.0 spec:
1295/// - Evaluate each binding's `in_expr` to produce a sequence
1296/// - Iterate through all combinations (Cartesian product for multiple bindings)
1297/// - For each combination, bind variables and evaluate `return_expr`
1298/// - Concatenate all results into a single sequence
1299fn eval_for_expression<N: DomNavigator>(
1300    arena: &AstArena,
1301    for_node: &ForNode,
1302    ctx: &mut DynamicContext<'_, N>,
1303) -> Result<XPathValue<N>, XPathError> {
1304    // Collect results from iterating over Cartesian product
1305    let mut results: Vec<XmlItem<N>> = Vec::new();
1306
1307    // For the for expression, we use XPathError as the break type since we only
1308    // break on errors (never short-circuit for other reasons)
1309    match eval_for_bindings(
1310        arena,
1311        &for_node.bindings,
1312        0,
1313        for_node.return_expr,
1314        ctx,
1315        &mut |result| match result {
1316            Ok(value) => {
1317                results.extend(value.into_vec());
1318                ControlFlow::Continue(())
1319            }
1320            Err(e) => ControlFlow::Break(e),
1321        },
1322    ) {
1323        ControlFlow::Continue(()) => {}
1324        ControlFlow::Break(e) => return Err(e),
1325    }
1326
1327    Ok(XPathValue::from_sequence(results))
1328}
1329
1330/// Recursively iterate over Cartesian product of bindings with lazy evaluation.
1331///
1332/// This helper handles the recursive iteration for for/quantified expressions.
1333/// For each binding, it evaluates the `in_expr` (allowing dependent bindings),
1334/// iterates over its sequence, and recursively processes remaining bindings.
1335/// When all bindings are processed, it evaluates the body.
1336///
1337/// IMPORTANT: Each binding's `in_expr` is evaluated lazily, AFTER all previous
1338/// binding variables have been bound. This allows dependent bindings like:
1339/// `for $x in 1 to 3, $y in $x+1 return $y`
1340///
1341/// The function is generic over the break type `B`, allowing callers to use
1342/// different types for different control flow needs:
1343/// - For expressions use `B = XPathError` (only break on errors)
1344/// - Quantified expressions use `B = QuantifiedExit` (break on short-circuit or error)
1345fn eval_for_bindings<N: DomNavigator, B>(
1346    arena: &AstArena,
1347    bindings: &[ForBinding],
1348    binding_index: usize,
1349    body_id: AstNodeId,
1350    ctx: &mut DynamicContext<'_, N>,
1351    collector: &mut impl FnMut(Result<XPathValue<N>, XPathError>) -> ControlFlow<B>,
1352) -> ControlFlow<B> {
1353    if binding_index >= bindings.len() {
1354        // All bindings processed, evaluate the body
1355        let result = eval_node(arena, body_id, ctx);
1356        return collector(result);
1357    }
1358
1359    let binding = &bindings[binding_index];
1360    let slot = match binding.slot {
1361        Some(s) => s,
1362        None => {
1363            return collector(Err(XPathError::Internal(
1364                "For binding slot not assigned".to_string(),
1365            )))
1366        }
1367    };
1368
1369    // LAZY EVALUATION: Evaluate in_expr NOW (previous variables are already bound)
1370    let seq_value = match eval_node(arena, binding.in_expr, ctx) {
1371        Ok(v) => v,
1372        Err(e) => return collector(Err(e)),
1373    };
1374    let items = seq_value.into_vec();
1375
1376    // If binding sequence is empty, we simply don't iterate (produces empty result)
1377    if items.is_empty() {
1378        return ControlFlow::Continue(());
1379    }
1380
1381    // Iterate over each item in the current binding's sequence
1382    for item in items {
1383        // Set the variable for this binding
1384        ctx.set_variable(slot, XPathValue::from_item(item));
1385
1386        // Recursively process remaining bindings
1387        if let cf @ ControlFlow::Break(_) =
1388            eval_for_bindings(arena, bindings, binding_index + 1, body_id, ctx, collector)
1389        {
1390            return cf;
1391        }
1392    }
1393
1394    ControlFlow::Continue(())
1395}
1396
1397// ============================================================================
1398// Quantified Expression Evaluation
1399// ============================================================================
1400
1401/// Exit type for quantified expression short-circuit evaluation.
1402///
1403/// This enum cleanly distinguishes between a legitimate short-circuit exit
1404/// (when the quantified expression's answer is determined) and an actual error.
1405enum QuantifiedExit {
1406    /// Short-circuit: the quantified expression's answer is determined.
1407    ShortCircuit,
1408    /// A real error occurred during evaluation.
1409    Error(XPathError),
1410}
1411
1412/// Evaluate a quantified expression (`some/every $x in X satisfies expr`).
1413///
1414/// Semantics per XPath 2.0 spec:
1415/// - `some`: Returns true if at least one combination satisfies the expression
1416/// - `every`: Returns true if all combinations satisfy (including empty - vacuous truth)
1417/// - Short-circuit evaluation when result is determined
1418///
1419/// NOTE: Bindings are evaluated lazily, allowing dependent bindings like:
1420/// `some $x in (1,2), $y in ($x*2) satisfies $y > 3`
1421fn eval_quantified_expression<N: DomNavigator>(
1422    arena: &AstArena,
1423    quant_node: &QuantifiedNode,
1424    ctx: &mut DynamicContext<'_, N>,
1425) -> Result<XPathValue<N>, XPathError> {
1426    // Track whether we had any iterations (for vacuous truth handling)
1427    let mut had_any_iteration = false;
1428    let mut found_some = false;
1429    let mut all_satisfied = true;
1430
1431    // Use QuantifiedExit as the break type to cleanly distinguish between
1432    // short-circuit (answer found) and real errors
1433    match eval_for_bindings(
1434        arena,
1435        &quant_node.bindings,
1436        0,
1437        quant_node.satisfies,
1438        ctx,
1439        &mut |result| {
1440            had_any_iteration = true;
1441            match result {
1442                Ok(value) => match effective_boolean_value(&value) {
1443                    Ok(satisfied) => {
1444                        match quant_node.kind {
1445                            QuantifierKind::Some => {
1446                                if satisfied {
1447                                    found_some = true;
1448                                    return ControlFlow::Break(QuantifiedExit::ShortCircuit);
1449                                }
1450                            }
1451                            QuantifierKind::Every => {
1452                                if !satisfied {
1453                                    all_satisfied = false;
1454                                    return ControlFlow::Break(QuantifiedExit::ShortCircuit);
1455                                }
1456                            }
1457                        }
1458                        ControlFlow::Continue(())
1459                    }
1460                    Err(e) => ControlFlow::Break(QuantifiedExit::Error(e)),
1461                },
1462                Err(e) => ControlFlow::Break(QuantifiedExit::Error(e)),
1463            }
1464        },
1465    ) {
1466        ControlFlow::Continue(()) => {} // Completed all iterations
1467        ControlFlow::Break(QuantifiedExit::ShortCircuit) => {} // Found answer early
1468        ControlFlow::Break(QuantifiedExit::Error(e)) => return Err(e),
1469    }
1470
1471    // Handle vacuous truth: if no iterations occurred (empty binding),
1472    // "every" is vacuously true, "some" is false
1473    if !had_any_iteration {
1474        return match quant_node.kind {
1475            QuantifierKind::Some => Ok(XPathValue::boolean(false)),
1476            QuantifierKind::Every => Ok(XPathValue::boolean(true)),
1477        };
1478    }
1479
1480    match quant_node.kind {
1481        QuantifierKind::Some => Ok(XPathValue::boolean(found_some)),
1482        QuantifierKind::Every => Ok(XPathValue::boolean(all_satisfied)),
1483    }
1484}
1485
1486/// Evaluate a ValueNode to an XPathValue.
1487fn eval_value<N: DomNavigator>(
1488    value: &ValueNode,
1489    mode: XPathMode,
1490) -> Result<XPathValue<N>, XPathError> {
1491    match value {
1492        ValueNode::Empty => Ok(XPathValue::empty()),
1493
1494        ValueNode::String(s) => Ok(XPathValue::string(s.clone())),
1495
1496        ValueNode::Boolean(b) => Ok(XPathValue::boolean(*b)),
1497
1498        ValueNode::Integer(s) => {
1499            // Parse integer string to BigInt
1500            let i: num_bigint::BigInt = s.parse().map_err(|_| XPathError::FORG0001 {
1501                value: s.clone(),
1502                target_type: "xs:integer".to_string(),
1503            })?;
1504            Ok(XPathValue::integer(i))
1505        }
1506
1507        ValueNode::Decimal(s) => {
1508            if mode == XPathMode::XPath10 {
1509                // XPath 1.0: all numbers are doubles
1510                let d: f64 = s.parse().unwrap_or(f64::NAN);
1511                Ok(XPathValue::double(d))
1512            } else {
1513                let d: rust_decimal::Decimal = s.parse().map_err(|_| XPathError::FORG0001 {
1514                    value: s.clone(),
1515                    target_type: "xs:decimal".to_string(),
1516                })?;
1517                Ok(XPathValue::decimal(d))
1518            }
1519        }
1520
1521        ValueNode::Double(s) => {
1522            let d: f64 = s.parse().unwrap_or(f64::NAN);
1523            Ok(XPathValue::double(d))
1524        }
1525
1526        ValueNode::Typed(xml_value) => Ok(XPathValue::from_atomic(xml_value.clone())),
1527    }
1528}
1529
1530/// Extract a single node from an XPathValue for node comparison operators.
1531/// Returns Ok(None) for empty sequence, Ok(Some(node)) for single node,
1532/// or Err for type errors (non-node or multiple items).
1533fn extract_single_node<N: DomNavigator>(value: XPathValue<N>) -> Result<Option<N>, XPathError> {
1534    match value {
1535        XPathValue::Empty => Ok(None),
1536        XPathValue::Item(XmlItem::Node(node)) => Ok(Some(node)),
1537        XPathValue::Item(XmlItem::Atomic(_)) => Err(XPathError::XPTY0004 {
1538            expected: "node()".to_string(),
1539            found: "atomic value".to_string(),
1540        }),
1541        XPathValue::Sequence(items) => {
1542            if items.len() == 1 {
1543                match items.into_iter().next().unwrap() {
1544                    XmlItem::Node(node) => Ok(Some(node)),
1545                    XmlItem::Atomic(_) => Err(XPathError::XPTY0004 {
1546                        expected: "node()".to_string(),
1547                        found: "atomic value".to_string(),
1548                    }),
1549                }
1550            } else if items.is_empty() {
1551                Ok(None)
1552            } else {
1553                Err(XPathError::more_than_one_item())
1554            }
1555        }
1556    }
1557}
1558
1559/// Check if an XPathValue is a single boolean value (XPath 1.0 §3.4 helper).
1560fn is_boolean_value<N: DomNavigator>(val: &XPathValue<N>) -> bool {
1561    matches!(val, XPathValue::Item(XmlItem::Atomic(v)) if v.type_code == crate::types::XmlTypeCode::Boolean)
1562}
1563
1564/// Check if an XPathValue contains nodes or is empty (i.e., is a node-set in XPath 1.0 terms).
1565///
1566/// In XPath 1.0, the empty result of a path expression is an empty node-set,
1567/// so `XPathValue::Empty` is treated as a node-set (boolean false).
1568fn has_nodes_or_empty<N: DomNavigator>(val: &XPathValue<N>) -> bool {
1569    match val {
1570        XPathValue::Empty => true,
1571        XPathValue::Item(XmlItem::Node(_)) => true,
1572        XPathValue::Sequence(items) => items.iter().any(|i| matches!(i, XmlItem::Node(_))),
1573        _ => false,
1574    }
1575}
1576
1577#[cfg(test)]
1578#[path = "eval_tests.rs"]
1579mod eval_tests;