Skip to main content

libxml_rs/xml/xpath/
parser.rs

1//! XPath 1.0 Expression Parser (§25).
2//!
3//! Parses token streams from the lexer into the AST defined in `ast.rs`.
4//! Implements the full XPath 1.0 grammar.
5//!
6//! # UPSTREAM-PARITY
7//!
8//! Grammar (from XPath 1.0 spec §3.7):
9//!
10//! ```text
11//! Expr        ::= OrExpr
12//! OrExpr      ::= AndExpr ('or' AndExpr)*
13//! AndExpr     ::= EqualityExpr ('and' EqualityExpr)*
14//! EqualityExpr ::= RelationalExpr (('=' | '!=') RelationalExpr)*
15//! RelationalExpr ::= AdditiveExpr (('<' | '>' | '<=' | '>=') AdditiveExpr)*
16//! AdditiveExpr ::= MultiplicativeExpr (('+' | '-') MultiplicativeExpr)*
17//! MultiplicativeExpr ::= UnaryExpr (('*' | 'div' | 'mod') UnaryExpr)*
18//! UnaryExpr   ::= '-'* UnionExpr
19//! UnionExpr   ::= PathExpr ('|' PathExpr)*
20//! PathExpr    ::= LocationPath | FilterExpr (('/' | '//') RelativeLocationPath)?
21//! LocationPath ::= AbsoluteLocationPath | RelativeLocationPath
22//! AbsoluteLocationPath ::= '/' RelativeLocationPath? | '//' RelativeLocationPath
23//! RelativeLocationPath ::= Step (('/' | '//') Step)*
24//! Step        ::= AxisSpecifier NodeTest Predicate*
25//!              |  AbbreviatedStep
26//! AxisSpecifier ::= AxisName '::' | '@'?
27//! AbbreviatedStep ::= '.' | '..'
28//! Predicate   ::= '[' Expr ']'
29//! FilterExpr  ::= PrimaryExpr Predicate*
30//! PrimaryExpr ::= VariableReference | '(' Expr ')' | Literal | Number | FunctionCall
31//! ```
32//!
33//! # Courts
34//!
35//! XPATH-PARSER-*
36//!
37//! # Upstream contract
38//!
39//! Mirrors the compilation half of upstream `xpath.c`
40//! (`SRC-LIBXML2-2.15.0-XPATH-C`, parity target libxml2 2.15.3 oracle):
41//! xmlXPathCompile builds an xmlXPathCompExpr from the same grammar this
42//! recursive-descent parser implements (XPath 1.0 §3.7).
43//!
44//! # Conceptual behavior
45//!
46//! Implements the full XPath 1.0 grammar from the lexer token stream into
47//! the ast.rs expression tree, including precedence (or → and → equality
48//! → relational → additive → multiplicative → unary → union → path),
49//! predicates on steps and filter expressions, abbreviated steps and the
50//! axis-specifier forms.
51//!
52//! # Ownership & safety invariants
53//!
54//! The parser owns the token stream for the duration of the parse and
55//! produces an owned AST; `ParseError` carries an owned message and
56//! position. No C pointers cross the parser boundary — compilation is
57//! safe to run on any thread.
58//!
59//! # Historical quirks & epochs
60//!
61//! R-000105: node tests (`node()`, `text()`, `comment()`, `processing-
62//! instruction()`) were originally parsed as function calls; the fix
63//! distinguishes them at the node-test production, matching the 2.15.3
64//! oracle. The grammar itself is stable across the oracle matrix (the
65//! E-001 epoch changed xmllint node-set output, not expression parsing).
66//!
67//! # Deliberate oddities
68//!
69//! The parser accepts the upstream-lenient forms (e.g. whitespace
70//! handling around abbreviated axes) that a strict grammar would reject,
71//! because compile errors are observable through xmlXPathCompile return
72//! values.
73//!
74//! # Proving courts
75//!
76//! XPATH-PARSER-* differential probes compile expressions against the
77//! oracle and compare success/error byte-identical; the XSLT pattern
78//! courts compile match patterns through this parser.
79//!
80//! # Tempting simplifications that would break parity
81//!
82//! Do not treat node-test names as generic function calls: R-000105
83//! proved that breaks `//text()` style paths. Do not normalize or reject
84//! lenient whitespace forms — xmlXPathCompile error parity is part of the
85//! C ABI.
86
87use crate::xml::xpath::ast::*;
88use crate::xml::xpath::lexer::Token;
89
90/// Errors that can occur during parsing.
91#[derive(Debug, Clone, PartialEq)]
92pub struct ParseError {
93    /// Human-readable description of what went wrong
94    pub message: String,
95    /// Token index at which the error was detected
96    pub pos: usize,
97}
98
99impl std::fmt::Display for ParseError {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        write!(
102            f,
103            "XPath parse error at position {}: {}",
104            self.pos, self.message
105        )
106    }
107}
108
109// ═══════════════════════════════════════════════════════════════════════════════
110// Parser
111// ═══════════════════════════════════════════════════════════════════════════════
112
113/// UPSTREAM-PARITY (xpath.c `XPATH_MAX_RECURSION_DEPTH`): upstream budgets
114/// recursive expression parsing at 10 depth units per nested expression
115/// ("Parsing a single '(' pushes about 10 functions on the call stack before
116/// recursing!") and raises XPATH_RECURSION_LIMIT_EXCEEDED once the budget is
117/// exhausted. Normal builds use 5000 units (~499 nested '(' groups — the
118/// 2.15.3 oracle accepts 499 and rejects 500, verified with xmllint);
119/// oss-fuzz builds define FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION and drop to
120/// 500 units so the fuzzer cannot overflow the ASan stack. cargo-fuzz sets
121/// `--cfg fuzzing`, so the candidate applies the same reduced budget under
122/// the fuzz harness (Phase 16 ASan fuzz finding: a ~3000-'(' expression
123/// overflowed the stack in parse_unary_expr).
124#[cfg(fuzzing)]
125const XPATH_MAX_RECURSION_DEPTH: u32 = 500;
126#[cfg(not(fuzzing))]
127const XPATH_MAX_RECURSION_DEPTH: u32 = 5000;
128
129/// Recursive-descent parser for XPath 1.0 expressions.
130///
131/// Consumes the token stream produced by the lexer and builds the
132/// expression AST defined in `crate::xml::xpath::ast`.
133#[derive(Debug)]
134pub struct Parser {
135    tokens: Vec<Token>,
136    pos: usize,
137    /// Nested-expression depth budget (upstream `ctxt->context->depth`),
138    /// incremented by 10 per `Expr` production entered.
139    depth: u32,
140}
141
142impl Parser {
143    /// Create a parser over a token stream produced by the lexer.
144    pub const fn new(tokens: Vec<Token>) -> Self {
145        Self {
146            tokens,
147            pos: 0,
148            depth: 0,
149        }
150    }
151
152    /// Parse a complete XPath expression.
153    pub fn parse(&mut self) -> Result<Expr, ParseError> {
154        let expr = self.parse_or_expr()?;
155        if !self.is_eof() {
156            Err(self.error(format!("Unexpected token: {}", self.current())))?;
157        }
158        Ok(expr)
159    }
160
161    // ── Current token helpers ────────────────────────────────────────────
162
163    fn current(&self) -> Token {
164        if self.pos < self.tokens.len() {
165            self.tokens[self.pos].clone()
166        } else {
167            Token::Eof
168        }
169    }
170
171    fn peek(&self) -> Token {
172        if self.pos + 1 < self.tokens.len() {
173            self.tokens[self.pos + 1].clone()
174        } else {
175            Token::Eof
176        }
177    }
178
179    const fn advance(&mut self) {
180        if self.pos < self.tokens.len() {
181            self.pos += 1;
182        }
183    }
184
185    fn is_eof(&self) -> bool {
186        matches!(self.current(), Token::Eof)
187    }
188
189    const fn error(&self, msg: String) -> ParseError {
190        ParseError {
191            message: msg,
192            pos: self.pos,
193        }
194    }
195
196    /// Check if the current token matches the given token.
197    fn at(&self, token: &Token) -> bool {
198        std::mem::discriminant(&self.current()) == std::mem::discriminant(token)
199    }
200
201    /// Expect and consume a specific token.
202    fn expect(&mut self, expected: &Token) -> Result<(), ParseError> {
203        if self.at(expected) {
204            self.advance();
205            Ok(())
206        } else {
207            Err(self.error(format!("Expected {}, got {}", expected, self.current())))
208        }
209    }
210
211    // ── Grammar productions ──────────────────────────────────────────────
212
213    /// OrExpr ::= AndExpr ('or' AndExpr)*
214    fn parse_or_expr(&mut self) -> Result<Expr, ParseError> {
215        // UPSTREAM-PARITY (xpath.c xmlXPathCompileExpr): each nested
216        // expression (a '(' group, function argument, predicate, ...)
217        // consumes 10 depth units; exceeding the budget fails compilation
218        // with XPATH_RECURSION_LIMIT_EXCEEDED ("Recursion limit exceeded") —
219        // exactly the oracle's observable boundary, never a stack overflow.
220        if self.depth >= XPATH_MAX_RECURSION_DEPTH {
221            return Err(self.error("Recursion limit exceeded".to_string()));
222        }
223        self.depth += 10;
224        let result = self.parse_or_expr_body();
225        self.depth -= 10;
226        result
227    }
228
229    fn parse_or_expr_body(&mut self) -> Result<Expr, ParseError> {
230        let mut left = self.parse_and_expr()?;
231        while matches!(self.current(), Token::Or) {
232            self.advance();
233            let right = self.parse_and_expr()?;
234            left = Expr::BinaryOp {
235                op: BinaryOp::Or,
236                left: Box::new(left),
237                right: Box::new(right),
238            };
239        }
240        Ok(left)
241    }
242
243    /// AndExpr ::= EqualityExpr ('and' EqualityExpr)*
244    fn parse_and_expr(&mut self) -> Result<Expr, ParseError> {
245        let mut left = self.parse_equality_expr()?;
246        while matches!(self.current(), Token::And) {
247            self.advance();
248            let right = self.parse_equality_expr()?;
249            left = Expr::BinaryOp {
250                op: BinaryOp::And,
251                left: Box::new(left),
252                right: Box::new(right),
253            };
254        }
255        Ok(left)
256    }
257
258    /// EqualityExpr ::= RelationalExpr (('=' | '!=') RelationalExpr)*
259    fn parse_equality_expr(&mut self) -> Result<Expr, ParseError> {
260        let mut left = self.parse_relational_expr()?;
261        loop {
262            let op = match self.current() {
263                Token::Eq => BinaryOp::Eq,
264                Token::Ne => BinaryOp::Ne,
265                _ => break,
266            };
267            self.advance();
268            let right = self.parse_relational_expr()?;
269            left = Expr::BinaryOp {
270                op,
271                left: Box::new(left),
272                right: Box::new(right),
273            };
274        }
275        Ok(left)
276    }
277
278    /// RelationalExpr ::= AdditiveExpr (('<' | '>' | '<=' | '>=') AdditiveExpr)*
279    fn parse_relational_expr(&mut self) -> Result<Expr, ParseError> {
280        let mut left = self.parse_additive_expr()?;
281        loop {
282            let op = match self.current() {
283                Token::Lt => BinaryOp::Lt,
284                Token::Gt => BinaryOp::Gt,
285                Token::Le => BinaryOp::Le,
286                Token::Ge => BinaryOp::Ge,
287                _ => break,
288            };
289            self.advance();
290            let right = self.parse_additive_expr()?;
291            left = Expr::BinaryOp {
292                op,
293                left: Box::new(left),
294                right: Box::new(right),
295            };
296        }
297        Ok(left)
298    }
299
300    /// AdditiveExpr ::= MultiplicativeExpr (('+' | '-') MultiplicativeExpr)*
301    fn parse_additive_expr(&mut self) -> Result<Expr, ParseError> {
302        let mut left = self.parse_multiplicative_expr()?;
303        loop {
304            let op = match self.current() {
305                Token::Plus => BinaryOp::Add,
306                Token::Minus => BinaryOp::Sub,
307                _ => break,
308            };
309            self.advance();
310            let right = self.parse_multiplicative_expr()?;
311            left = Expr::BinaryOp {
312                op,
313                left: Box::new(left),
314                right: Box::new(right),
315            };
316        }
317        Ok(left)
318    }
319
320    /// MultiplicativeExpr ::= UnaryExpr (('*' | 'div' | 'mod') UnaryExpr)*
321    fn parse_multiplicative_expr(&mut self) -> Result<Expr, ParseError> {
322        let mut left = self.parse_unary_expr()?;
323        loop {
324            let op = match self.current() {
325                // '*' after an expression is multiply, not wildcard
326                Token::Star => BinaryOp::Mul,
327                Token::Div => BinaryOp::Div,
328                Token::Mod => BinaryOp::Mod,
329                _ => break,
330            };
331            self.advance();
332            let right = self.parse_unary_expr()?;
333            left = Expr::BinaryOp {
334                op,
335                left: Box::new(left),
336                right: Box::new(right),
337            };
338        }
339        Ok(left)
340    }
341
342    /// UnaryExpr ::= '-'* UnionExpr
343    fn parse_unary_expr(&mut self) -> Result<Expr, ParseError> {
344        let mut minus_count = 0;
345        while matches!(self.current(), Token::Minus) {
346            self.advance();
347            minus_count += 1;
348        }
349        let mut expr = self.parse_union_expr()?;
350        if minus_count % 2 == 1 {
351            expr = Expr::UnaryMinus(Box::new(expr));
352        }
353        Ok(expr)
354    }
355
356    /// UnionExpr ::= PathExpr ('|' PathExpr)*
357    fn parse_union_expr(&mut self) -> Result<Expr, ParseError> {
358        let mut left = self.parse_path_expr()?;
359        while matches!(self.current(), Token::Pipe) {
360            self.advance();
361            let right = self.parse_path_expr()?;
362            left = Expr::Union(Box::new(left), Box::new(right));
363        }
364        Ok(left)
365    }
366
367    /// PathExpr ::= LocationPath | FilterExpr (('/' | '//') RelativeLocationPath)?
368    fn parse_path_expr(&mut self) -> Result<Expr, ParseError> {
369        // Check if it starts with a location path
370        if self.is_location_path_start() {
371            return self.parse_location_path();
372        }
373
374        // Otherwise it's a FilterExpr (primary with optional predicates and path)
375        let mut expr = self.parse_filter_expr()?;
376
377        // Optional / or // followed by relative location path
378        loop {
379            match self.current() {
380                Token::Slash => {
381                    self.advance();
382                    let step = self.parse_relative_location_path()?;
383                    expr = Expr::RelativePath(Box::new(expr), Box::new(step));
384                }
385                Token::DoubleSlash => {
386                    self.advance();
387                    let step = self.parse_relative_location_path()?;
388                    // // is shorthand for /descendant-or-self::node()/
389                    let descendant = Expr::Step(Step {
390                        axis: Axis::DescendantOrSelf,
391                        node_test: NodeTest::Node,
392                        predicates: vec![],
393                    });
394                    let path = Expr::RelativePath(Box::new(descendant), Box::new(step));
395                    expr = Expr::RelativePath(Box::new(expr), Box::new(path));
396                }
397                _ => break,
398            }
399        }
400
401        Ok(expr)
402    }
403
404    /// Check if the current position starts a location path.
405    fn is_location_path_start(&self) -> bool {
406        match self.current() {
407            Token::Slash | Token::DoubleSlash => true,
408            Token::Dot | Token::DotDot => true,
409            Token::At => true,
410            Token::Star => true,
411            // Name that could be a step (not followed by '(' which means function call)
412            Token::Name(ref n) => {
413                // Check if this is followed by :: (axis), /, //, [, or is a simple step
414                let next = self.peek();
415                if matches!(next, Token::LParen) {
416                    // UPSTREAM-PARITY (xpath.c xmlXPathCompPathExpr): a name
417                    // followed by '(' is only a function call for
418                    // non-node-type names; node/text/comment/
419                    // processing-instruction are node-type tests that BEGIN a
420                    // location path (an expression-initial `node()` was
421                    // previously misparsed as a function call, making
422                    // apply-templates select="node()|@*" fail with
423                    // "Unregistered function: node").
424                    !n.contains(':')
425                        && matches!(
426                            n.as_str(),
427                            "node" | "text" | "comment" | "processing-instruction"
428                        )
429                } else {
430                    true
431                }
432            }
433            // Axis keywords
434            Token::Child
435            | Token::Descendant
436            | Token::DescendantOrSelf
437            | Token::Ancestor
438            | Token::AncestorOrSelf
439            | Token::Attribute
440            | Token::Following
441            | Token::FollowingSibling
442            | Token::Namespace
443            | Token::Parent
444            | Token::Preceding
445            | Token::PrecedingSibling
446            | Token::Self_ => true,
447            _ => false,
448        }
449    }
450
451    /// LocationPath ::= AbsoluteLocationPath | RelativeLocationPath
452    fn parse_location_path(&mut self) -> Result<Expr, ParseError> {
453        match self.current() {
454            Token::Slash => {
455                self.advance();
456                if self.is_location_path_start() {
457                    let path = self.parse_relative_location_path()?;
458                    Ok(Expr::AbsolutePath(Box::new(path)))
459                } else {
460                    // Just "/" - root node
461                    Ok(Expr::Step(Step {
462                        axis: Axis::Self_,
463                        node_test: NodeTest::Node,
464                        predicates: vec![],
465                    }))
466                }
467            }
468            Token::DoubleSlash => {
469                self.advance();
470                let path = self.parse_relative_location_path()?;
471                let descendant = Expr::Step(Step {
472                    axis: Axis::DescendantOrSelf,
473                    node_test: NodeTest::Node,
474                    predicates: vec![],
475                });
476                Ok(Expr::AbsolutePath(Box::new(Expr::RelativePath(
477                    Box::new(descendant),
478                    Box::new(path),
479                ))))
480            }
481            _ => self.parse_relative_location_path(),
482        }
483    }
484
485    /// RelativeLocationPath ::= Step (('/' | '//') Step)*
486    fn parse_relative_location_path(&mut self) -> Result<Expr, ParseError> {
487        let mut expr = self.parse_step()?;
488        loop {
489            match self.current() {
490                Token::Slash => {
491                    self.advance();
492                    let step = self.parse_step()?;
493                    expr = Expr::RelativePath(Box::new(expr), Box::new(step));
494                }
495                Token::DoubleSlash => {
496                    self.advance();
497                    let step = self.parse_step()?;
498                    let descendant = Expr::Step(Step {
499                        axis: Axis::DescendantOrSelf,
500                        node_test: NodeTest::Node,
501                        predicates: vec![],
502                    });
503                    let path = Expr::RelativePath(Box::new(descendant), Box::new(step));
504                    expr = Expr::RelativePath(Box::new(expr), Box::new(path));
505                }
506                _ => break,
507            }
508        }
509        Ok(expr)
510    }
511
512    /// Step ::= AxisSpecifier NodeTest Predicate*
513    ///        | AbbreviatedStep
514    fn parse_step(&mut self) -> Result<Expr, ParseError> {
515        // AbbreviatedStep ::= '.' | '..'
516        match self.current() {
517            Token::Dot => {
518                self.advance();
519                return Ok(Expr::Step(Step {
520                    axis: Axis::Self_,
521                    node_test: NodeTest::Node,
522                    predicates: vec![],
523                }));
524            }
525            Token::DotDot => {
526                self.advance();
527                return Ok(Expr::Step(Step {
528                    axis: Axis::Parent,
529                    node_test: NodeTest::Node,
530                    predicates: vec![],
531                }));
532            }
533            _ => {}
534        }
535
536        // Determine axis
537        let axis = self.parse_axis_specifier();
538
539        // Parse node test
540        let node_test = self.parse_node_test()?;
541
542        // Parse predicates
543        let mut predicates = Vec::new();
544        while matches!(self.current(), Token::LBracket) {
545            self.advance(); // consume '['
546            let pred = self.parse_or_expr()?;
547            self.expect(&Token::RBracket)?;
548            predicates.push(pred);
549        }
550
551        Ok(Expr::Step(Step {
552            axis,
553            node_test,
554            predicates,
555        }))
556    }
557
558    /// AxisSpecifier ::= AxisName '::' | '@'?
559    fn parse_axis_specifier(&mut self) -> Axis {
560        // Check for @ (attribute axis shorthand)
561        if matches!(self.current(), Token::At) {
562            self.advance();
563            return Axis::Attribute;
564        }
565
566        // Check for axis keyword followed by ::
567        // We check if the NEXT token is DoubleColon to decide whether this
568        // is an axis specifier or just a name being used as a node test.
569        let is_axis = match self.current() {
570            Token::Ancestor
571            | Token::AncestorOrSelf
572            | Token::Attribute
573            | Token::Child
574            | Token::Descendant
575            | Token::DescendantOrSelf
576            | Token::Following
577            | Token::FollowingSibling
578            | Token::Namespace
579            | Token::Parent
580            | Token::Preceding
581            | Token::PrecedingSibling
582            | Token::Self_ => matches!(self.peek(), Token::DoubleColon),
583            _ => false,
584        };
585
586        if is_axis {
587            let axis = match self.current() {
588                Token::Ancestor => Axis::Ancestor,
589                Token::AncestorOrSelf => Axis::AncestorOrSelf,
590                Token::Attribute => Axis::Attribute,
591                Token::Child => Axis::Child,
592                Token::Descendant => Axis::Descendant,
593                Token::DescendantOrSelf => Axis::DescendantOrSelf,
594                Token::Following => Axis::Following,
595                Token::FollowingSibling => Axis::FollowingSibling,
596                Token::Namespace => Axis::Namespace,
597                Token::Parent => Axis::Parent,
598                Token::Preceding => Axis::Preceding,
599                Token::PrecedingSibling => Axis::PrecedingSibling,
600                Token::Self_ => Axis::Self_,
601                _ => unreachable!(),
602            };
603            self.advance(); // consume axis keyword
604            self.advance(); // consume ::
605            return axis;
606        }
607
608        // Default axis is "child" for everything except attribute
609        Axis::Child
610    }
611
612    /// Convert a keyword token back to its string name for use as a node test.
613    fn token_to_name(&self, token: &Token) -> Option<String> {
614        match token {
615            Token::Name(ref s) => Some(s.clone()),
616            Token::Div => Some("div".to_string()),
617            Token::Mod => Some("mod".to_string()),
618            Token::And => Some("and".to_string()),
619            Token::Or => Some("or".to_string()),
620            Token::Ancestor => Some("ancestor".to_string()),
621            Token::AncestorOrSelf => Some("ancestor-or-self".to_string()),
622            Token::Attribute => Some("attribute".to_string()),
623            Token::Child => Some("child".to_string()),
624            Token::Descendant => Some("descendant".to_string()),
625            Token::DescendantOrSelf => Some("descendant-or-self".to_string()),
626            Token::Following => Some("following".to_string()),
627            Token::FollowingSibling => Some("following-sibling".to_string()),
628            Token::Namespace => Some("namespace".to_string()),
629            Token::Parent => Some("parent".to_string()),
630            Token::Preceding => Some("preceding".to_string()),
631            Token::PrecedingSibling => Some("preceding-sibling".to_string()),
632            Token::Self_ => Some("self".to_string()),
633            _ => None,
634        }
635    }
636
637    /// NodeTest ::= NameTest | 'comment()' | 'text()' | 'processing-instruction()' | 'node()'
638    /// NameTest ::= '*' | NCName ':' '*' | QName
639    fn parse_node_test(&mut self) -> Result<NodeTest, ParseError> {
640        // Try to get the current token as a potential name
641        let name_opt = self.token_to_name(&self.current());
642
643        match self.current() {
644            Token::Star => {
645                self.advance();
646                Ok(NodeTest::NameTest(NameTest::Any))
647            }
648            _ if name_opt.is_some() => {
649                let name = name_opt.unwrap();
650
651                // Check for function-style node tests: node(), text(), comment(), processing-instruction()
652                if matches!(self.peek(), Token::LParen) {
653                    match name.as_str() {
654                        "node" => {
655                            self.advance();
656                            self.advance();
657                            self.advance(); // name, (, )
658                            Ok(NodeTest::Node)
659                        }
660                        "text" => {
661                            self.advance();
662                            self.advance();
663                            self.advance();
664                            Ok(NodeTest::Text)
665                        }
666                        "comment" => {
667                            self.advance();
668                            self.advance();
669                            self.advance();
670                            Ok(NodeTest::Comment)
671                        }
672                        "processing-instruction" => {
673                            self.advance(); // name
674                            self.advance(); // (
675                                            // Check for optional string argument
676                            let target = if matches!(self.current(), Token::StringLiteral(_)) {
677                                if let Token::StringLiteral(s) = self.current() {
678                                    self.advance();
679                                    Some(s)
680                                } else {
681                                    None
682                                }
683                            } else {
684                                None
685                            };
686                            self.expect(&Token::RParen)?;
687                            Ok(NodeTest::ProcessingInstruction(target))
688                        }
689                        _ => {
690                            // Regular function call, not a node test
691                            self.advance(); // function name
692                            self.advance(); // (
693                            let mut args = Vec::new();
694                            if !matches!(self.current(), Token::RParen) {
695                                args.push(self.parse_or_expr()?);
696                                while matches!(self.current(), Token::Comma) {
697                                    self.advance();
698                                    args.push(self.parse_or_expr()?);
699                                }
700                            }
701                            self.expect(&Token::RParen)?;
702                            // Wrap in a step with a name test
703                            Ok(NodeTest::NameTest(NameTest::LocalName(name)))
704                        }
705                    }
706                } else {
707                    self.advance();
708                    // Check for prefix:*
709                    if let Some(rest) = name.strip_suffix(":*") {
710                        Ok(NodeTest::NsWildcard(rest.to_string()))
711                    } else if let Some((prefix, local)) = name.split_once(':') {
712                        Ok(NodeTest::NameTest(NameTest::QName {
713                            prefix: prefix.to_string(),
714                            local: local.to_string(),
715                        }))
716                    } else {
717                        Ok(NodeTest::NameTest(NameTest::LocalName(name)))
718                    }
719                }
720            }
721            _ => Err(self.error(format!("Expected node test, got {}", self.current()))),
722        }
723    }
724
725    /// FilterExpr ::= PrimaryExpr Predicate*
726    fn parse_filter_expr(&mut self) -> Result<Expr, ParseError> {
727        let primary = self.parse_primary_expr()?;
728
729        // Predicates after primary
730        let mut predicates = Vec::new();
731        while matches!(self.current(), Token::LBracket) {
732            self.advance(); // consume '['
733            let pred = self.parse_or_expr()?;
734            self.expect(&Token::RBracket)?;
735            predicates.push(pred);
736        }
737
738        if predicates.is_empty() {
739            Ok(primary)
740        } else {
741            Ok(Expr::Filter(Box::new(primary), predicates))
742        }
743    }
744
745    /// PrimaryExpr ::= VariableReference | '(' Expr ')' | Literal | Number | FunctionCall
746    fn parse_primary_expr(&mut self) -> Result<Expr, ParseError> {
747        match self.current() {
748            Token::Dollar => {
749                self.advance();
750                if let Token::Name(name) = self.current() {
751                    let name = name.clone();
752                    self.advance();
753                    Ok(Expr::Variable(name))
754                } else {
755                    Err(self.error("Expected variable name after $".to_string()))
756                }
757            }
758            Token::LParen => {
759                self.advance();
760                let expr = self.parse_or_expr()?;
761                self.expect(&Token::RParen)?;
762                Ok(expr)
763            }
764            Token::StringLiteral(ref s) => {
765                let s = s.clone();
766                self.advance();
767                Ok(Expr::StringLiteral(s))
768            }
769            Token::NumberLiteral(n) => {
770                self.advance();
771                Ok(Expr::NumberLiteral(n))
772            }
773            Token::Name(ref name) => {
774                let name = name.clone();
775                if matches!(self.peek(), Token::LParen) {
776                    // Function call
777                    self.advance(); // function name
778                    self.advance(); // (
779                    let mut args = Vec::new();
780                    if !matches!(self.current(), Token::RParen) {
781                        args.push(self.parse_or_expr()?);
782                        while matches!(self.current(), Token::Comma) {
783                            self.advance();
784                            args.push(self.parse_or_expr()?);
785                        }
786                    }
787                    self.expect(&Token::RParen)?;
788                    Ok(Expr::FunctionCall { name, args })
789                } else {
790                    // Standalone name - could be a step or something else
791                    // But at the primary level, this shouldn't happen
792                    Err(self.error(format!("Unexpected name '{}' in primary expression", name)))
793                }
794            }
795            _ => Err(self.error(format!(
796                "Expected primary expression, got {}",
797                self.current()
798            ))),
799        }
800    }
801}
802
803// ═══════════════════════════════════════════════════════════════════════════════
804// Convenience function
805// ═══════════════════════════════════════════════════════════════════════════════
806
807/// Parse an XPath expression string into an AST.
808pub fn parse_xpath(input: &str) -> Result<Expr, ParseError> {
809    let mut lexer = crate::xml::xpath::lexer::Lexer::new(input);
810    let mut tokens = Vec::new();
811    loop {
812        let tok = lexer.next_token();
813        let is_eof = matches!(tok, Token::Eof);
814        tokens.push(tok);
815        if is_eof {
816            break;
817        }
818    }
819    let starts = lexer.token_starts();
820    let mut parser = Parser::new(tokens);
821    parser.parse().map_err(|e| {
822        // UPSTREAM-PARITY (xpath.c xmlXPathErrFmt): the recorded error
823        // position is the BYTE offset into the expression (`int1 =
824        // ctxt->cur - ctxt->base`), not the token index — it drives the
825        // caret of the "XPath error : Invalid expression" diagnostic
826        // (HOSTILE-FAILURE F3).
827        let byte_off = starts
828            .get(e.pos)
829            .copied()
830            .unwrap_or(input.len())
831            .min(input.len());
832        ParseError {
833            message: e.message,
834            pos: byte_off,
835        }
836    })
837}
838
839// ═══════════════════════════════════════════════════════════════════════════════
840// Tests
841// ═══════════════════════════════════════════════════════════════════════════════
842
843#[cfg(test)]
844mod tests {
845    use super::*;
846
847    #[test]
848    fn test_parse_simple_name() {
849        let expr = parse_xpath("para").unwrap();
850        assert!(matches!(expr, Expr::Step(_)));
851    }
852
853    #[test]
854    fn test_parse_absolute_path() {
855        let expr = parse_xpath("/child::para").unwrap();
856        assert!(matches!(expr, Expr::AbsolutePath(_)));
857    }
858
859    #[test]
860    fn test_parse_attribute() {
861        let expr = parse_xpath("@attr").unwrap();
862        assert!(matches!(expr, Expr::Step(_)));
863    }
864
865    #[test]
866    fn test_parse_predicate() {
867        let expr = parse_xpath("para[1]").unwrap();
868        assert!(matches!(expr, Expr::Step(_)));
869    }
870
871    #[test]
872    fn test_parse_function_call() {
873        let expr = parse_xpath("position()").unwrap();
874        assert!(matches!(expr, Expr::FunctionCall { .. }));
875    }
876
877    #[test]
878    fn test_parse_binary_op() {
879        let expr = parse_xpath("a = b").unwrap();
880        assert!(matches!(
881            expr,
882            Expr::BinaryOp {
883                op: BinaryOp::Eq,
884                ..
885            }
886        ));
887    }
888
889    #[test]
890    fn test_parse_union() {
891        let expr = parse_xpath("a | b").unwrap();
892        assert!(matches!(expr, Expr::Union(_, _)));
893    }
894
895    #[test]
896    fn test_parse_variable() {
897        let expr = parse_xpath("$var").unwrap();
898        assert!(matches!(expr, Expr::Variable(_)));
899    }
900
901    #[test]
902    fn test_parse_string_literal() {
903        let expr = parse_xpath("'hello'").unwrap();
904        assert_eq!(expr, Expr::StringLiteral("hello".to_string()));
905    }
906
907    #[test]
908    fn test_parse_number() {
909        let expr = parse_xpath("42").unwrap();
910        assert_eq!(expr, Expr::NumberLiteral(42.0));
911    }
912
913    #[test]
914    fn test_parse_nested_expression() {
915        let expr = parse_xpath("(1 + 2) * 3").unwrap();
916        assert!(matches!(
917            expr,
918            Expr::BinaryOp {
919                op: BinaryOp::Mul,
920                ..
921            }
922        ));
923    }
924
925    #[test]
926    fn test_parse_chained_path() {
927        let expr = parse_xpath("a/b/c").unwrap();
928        assert!(matches!(expr, Expr::RelativePath(_, _)));
929    }
930
931    #[test]
932    fn test_parse_double_slash() {
933        let expr = parse_xpath("//para").unwrap();
934        assert!(matches!(expr, Expr::AbsolutePath(_)));
935    }
936
937    #[test]
938    fn test_parse_dot() {
939        let expr = parse_xpath(".").unwrap();
940        assert!(matches!(expr, Expr::Step(_)));
941    }
942
943    #[test]
944    fn test_parse_dot_dot() {
945        let expr = parse_xpath("..").unwrap();
946        assert!(matches!(expr, Expr::Step(_)));
947    }
948
949    #[test]
950    fn test_parse_unary_minus() {
951        let expr = parse_xpath("-5").unwrap();
952        assert!(matches!(expr, Expr::UnaryMinus(_)));
953    }
954
955    #[test]
956    fn test_parse_double_unary_minus() {
957        let expr = parse_xpath("--5").unwrap();
958        // Should cancel out
959        assert!(!matches!(expr, Expr::UnaryMinus(_)));
960    }
961
962    #[test]
963    fn test_parse_complex_expression() {
964        let expr = parse_xpath("/html/body//div[@class='main']/p[1]").unwrap();
965        assert!(matches!(expr, Expr::AbsolutePath(_)));
966    }
967
968    #[test]
969    fn test_parse_error() {
970        let result = parse_xpath("(");
971        assert!(result.is_err());
972    }
973
974    #[test]
975    fn test_parse_empty() {
976        let result = parse_xpath("");
977        assert!(result.is_err());
978    }
979
980    #[test]
981    fn test_parse_and_or() {
982        let expr = parse_xpath("a = 1 and b = 2 or c = 3").unwrap();
983        assert!(matches!(expr, Expr::BinaryOp { .. }));
984    }
985
986    #[test]
987    fn test_parse_comparison_chain() {
988        let expr = parse_xpath("a < b <= c > d >= e").unwrap();
989        // Should parse as: ((((a < b) <= c) > d) >= e)
990        assert!(matches!(
991            expr,
992            Expr::BinaryOp {
993                op: BinaryOp::Ge,
994                ..
995            }
996        ));
997    }
998
999    #[test]
1000    fn test_parse_arithmetic() {
1001        let expr = parse_xpath("1 + 2 * 3").unwrap();
1002        // 2 * 3 should bind tighter: 1 + (2 * 3)
1003        match expr {
1004            Expr::BinaryOp {
1005                op: BinaryOp::Add,
1006                left,
1007                right,
1008            } => {
1009                assert!(matches!(*left, Expr::NumberLiteral(1.0)));
1010                assert!(matches!(
1011                    *right,
1012                    Expr::BinaryOp {
1013                        op: BinaryOp::Mul,
1014                        ..
1015                    }
1016                ));
1017            }
1018            _ => panic!("Expected Add expression"),
1019        }
1020    }
1021
1022    #[test]
1023    fn test_parse_filter_path() {
1024        let expr = parse_xpath("//div/span").unwrap();
1025        assert!(matches!(expr, Expr::AbsolutePath(_)));
1026    }
1027
1028    #[test]
1029    fn test_parse_node_test_functions() {
1030        let expr = parse_xpath("child::node()").unwrap();
1031        assert!(matches!(expr, Expr::Step(_)));
1032        if let Expr::Step(step) = expr {
1033            assert_eq!(step.node_test, NodeTest::Node);
1034        }
1035    }
1036}