Skip to main content

libxml_rs/xml/xpath/
ast.rs

1//! XPath 1.0 Expression AST (§25).
2//!
3//! Internal Rust representation of XPath expressions, separate from the C ABI.
4//! The parser builds this AST; the evaluator walks it.
5//!
6//! # UPSTREAM-PARITY
7//!
8//! Covers the full XPath 1.0 grammar:
9//! - Location paths (relative/absolute, steps, axes, node tests, predicates)
10//! - Operators (union, comparison, boolean, arithmetic)
11//! - Functions, variables, literals
12//! - Filter expressions (primary with predicates)
13//!
14//! # Upstream contract
15//!
16//! Mirrors the compiled-expression tree of upstream `xpath.c`
17//! (`SRC-LIBXML2-2.15.0-XPATH-C`, parity target libxml2 2.15.3 oracle):
18//! where upstream lowers an expression to an `xmlXPathCompExpr` op tree
19//! (the XPATH_OP_* nodes), this module is the internal Rust AST that the
20//! C ABI surface keeps behind a compiled-expression registry
21//! (exports.rs `xmlXPathCtxtCompile` / `xmlXPathCompiledEval`).
22//!
23//! # Conceptual behavior
24//!
25//! The parser builds this AST; the evaluator walks it. The model covers
26//! the full XPath 1.0 grammar: location paths (relative/absolute, steps,
27//! axes, node tests, predicates), operators, function calls, variables,
28//! literals and filter expressions — including the union type and the
29//! step-level attribute/namespace flags that mirror the axis semantics.
30//!
31//! # Ownership & safety invariants
32//!
33//! AST nodes are owned by `CompiledExpr` (a single owning tree, no shared
34//! subnodes); the evaluator borrows it. No raw pointers cross the AST
35//! boundary — node-sets hold borrowed `_xmlNode` pointers defined in
36//! types.rs, so the AST itself is Send-safe for concurrent compilation.
37//!
38//! # Historical quirks & epochs
39//!
40//! R-000105: node tests like `node()` / `text()` must parse as node
41//! tests, not as function calls — a parser-epoch bug fixed during the
42//! XPath closure. The step model matches the 2.15.3 oracle, which is the
43//! E-001 epoch for node-set output semantics.
44//!
45//! # Deliberate oddities
46//!
47//! The internal AST deliberately does NOT reproduce the XPATH_OP_* byte
48//! layout of upstream `xmlXPathCompExpr`: the op tree is opaque to C
49//! callers, so the divergence is invisible at the ABI and only the
50//! observable evaluation semantics must match.
51//!
52//! # Proving courts
53//!
54//! XPATH / XPOINTER / XINCLUDE court families exercise compiled
55//! expressions end-to-end (byte-identical against the oracle DSO); cargo
56//! test covers the AST round-trip unit suites.
57//!
58//! # Tempting simplifications that would break parity
59//!
60//! Do not flatten steps/predicates into a linear list: predicate
61//! evaluation order and context position/size depend on the step tree.
62//! Do not share subnodes (e.g. via Rc): expression ownership is exclusive
63//! and the compiled-expression registry frees whole trees.
64
65use std::fmt;
66
67// ═══════════════════════════════════════════════════════════════════════════════
68// Axes
69// ═══════════════════════════════════════════════════════════════════════════════
70
71/// XPath 1.0 axis.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73pub enum Axis {
74    /// `ancestor` — all ancestors of the context node (parent, grandparent, …)
75    Ancestor,
76    /// `ancestor-or-self` — all ancestors plus the context node itself
77    AncestorOrSelf,
78    /// `attribute` — the attributes of the context node
79    Attribute,
80    /// `child` — the children of the context node
81    Child,
82    /// `descendant` — all descendants of the context node (children, grandchildren, …)
83    Descendant,
84    /// `descendant-or-self` — all descendants plus the context node itself
85    DescendantOrSelf,
86    /// `following` — every node after the context node in document order, excluding descendants
87    Following,
88    /// `following-sibling` — the siblings that follow the context node
89    FollowingSibling,
90    /// `namespace` — the namespace nodes of the context node
91    Namespace,
92    /// `parent` — the parent of the context node (at most one node)
93    Parent,
94    /// `preceding` — every node before the context node in document order, excluding ancestors
95    Preceding,
96    /// `preceding-sibling` — the siblings that precede the context node
97    PrecedingSibling,
98    /// `self` — the context node itself
99    Self_,
100}
101
102impl Axis {
103    /// All 13 XPath 1.0 axes.
104    pub const ALL: &'static [Axis] = &[
105        Axis::Ancestor,
106        Axis::AncestorOrSelf,
107        Axis::Attribute,
108        Axis::Child,
109        Axis::Descendant,
110        Axis::DescendantOrSelf,
111        Axis::Following,
112        Axis::FollowingSibling,
113        Axis::Namespace,
114        Axis::Parent,
115        Axis::Preceding,
116        Axis::PrecedingSibling,
117        Axis::Self_,
118    ];
119
120    /// Return the axis name as written in an XPath expression
121    /// (e.g. `"ancestor-or-self"`).
122    pub const fn as_str(&self) -> &'static str {
123        match self {
124            Axis::Ancestor => "ancestor",
125            Axis::AncestorOrSelf => "ancestor-or-self",
126            Axis::Attribute => "attribute",
127            Axis::Child => "child",
128            Axis::Descendant => "descendant",
129            Axis::DescendantOrSelf => "descendant-or-self",
130            Axis::Following => "following",
131            Axis::FollowingSibling => "following-sibling",
132            Axis::Namespace => "namespace",
133            Axis::Parent => "parent",
134            Axis::Preceding => "preceding",
135            Axis::PrecedingSibling => "preceding-sibling",
136            Axis::Self_ => "self",
137        }
138    }
139}
140
141impl fmt::Display for Axis {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        write!(f, "{}", self.as_str())
144    }
145}
146
147// ═══════════════════════════════════════════════════════════════════════════════
148// Node Tests
149// ═══════════════════════════════════════════════════════════════════════════════
150
151/// XPath node test.
152#[derive(Debug, Clone, PartialEq)]
153pub enum NodeTest {
154    /// name() — matches any node of principal node type
155    NameTest(NameTest),
156    /// comment()
157    Comment,
158    /// text()
159    Text,
160    /// processing-instruction() or processing-instruction("target")
161    ProcessingInstruction(Option<String>),
162    /// node()
163    Node,
164    /// * — matches any node of principal node type
165    Wildcard,
166    /// prefix:* — namespace wildcard
167    NsWildcard(String),
168    /// {uri}* — namespace wildcard with the prefix already resolved to a
169    /// URI at evaluation time (upstream resolves QName prefixes at compile)
170    NsWildcardUri(String),
171}
172
173/// A name test (QName or wildcard).
174#[derive(Debug, Clone, PartialEq)]
175pub enum NameTest {
176    /// Just a local name: "para"
177    LocalName(String),
178    /// Qualified name: "xslt:template"
179    QName {
180        /// Namespace prefix, e.g. `xslt` in `xslt:template`
181        prefix: String,
182        /// Local part, e.g. `template` in `xslt:template`
183        local: String,
184    },
185    /// Qualified name with the prefix already resolved to a URI
186    /// (upstream xmlXPathCompQName resolves via the context nsHash).
187    QNameUri {
188        /// Namespace URI
189        uri: String,
190        /// Local part
191        local: String,
192    },
193    /// Wildcard name: *
194    Any,
195}
196
197impl fmt::Display for NodeTest {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        match self {
200            NodeTest::NameTest(n) => match n {
201                NameTest::LocalName(s) => write!(f, "{}", s),
202                NameTest::QName { prefix, local } => write!(f, "{}:{}", prefix, local),
203                NameTest::QNameUri { uri, local } => write!(f, "{{{}}}{}", uri, local),
204                NameTest::Any => write!(f, "*"),
205            },
206            NodeTest::Comment => write!(f, "comment()"),
207            NodeTest::Text => write!(f, "text()"),
208            NodeTest::ProcessingInstruction(None) => write!(f, "processing-instruction()"),
209            NodeTest::ProcessingInstruction(Some(t)) => {
210                write!(f, "processing-instruction('{}')", t)
211            }
212            NodeTest::Node => write!(f, "node()"),
213            NodeTest::Wildcard => write!(f, "*"),
214            NodeTest::NsWildcard(prefix) => write!(f, "{}:*", prefix),
215            NodeTest::NsWildcardUri(uri) => write!(f, "{{{}}}:*", uri),
216        }
217    }
218}
219
220// ═══════════════════════════════════════════════════════════════════════════════
221// Step
222// ═══════════════════════════════════════════════════════════════════════════════
223
224/// A single location step: axis::node-test\[predicates\]
225#[derive(Debug, Clone, PartialEq)]
226pub struct Step {
227    /// Axis the step traverses (defaults to `child` for abbreviated steps)
228    pub axis: Axis,
229    /// Node test selecting which nodes the step matches
230    pub node_test: NodeTest,
231    /// Predicates filtering the selected nodes, applied left to right
232    pub predicates: Vec<Expr>,
233}
234
235// ═══════════════════════════════════════════════════════════════════════════════
236// Binary Operators
237// ═══════════════════════════════════════════════════════════════════════════════
238
239/// XPath 1.0 binary operator.
240#[derive(Debug, Clone, Copy, PartialEq)]
241pub enum BinaryOp {
242    /// `|` — union
243    Union,
244    /// `and`
245    And,
246    /// `or`
247    Or,
248    /// `=`
249    Eq,
250    /// `!=`
251    Ne,
252    /// `<`
253    Lt,
254    /// `>`
255    Gt,
256    /// `<=`
257    Le,
258    /// `>=`
259    Ge,
260    /// `+`
261    Add,
262    /// `-`
263    Sub,
264    /// `*` (multiplication)
265    Mul,
266    /// `div`
267    Div,
268    /// `mod`
269    Mod,
270}
271
272// ═══════════════════════════════════════════════════════════════════════════════
273// Expressions
274// ═══════════════════════════════════════════════════════════════════════════════
275
276/// XPath expression AST node.
277#[derive(Debug, Clone, PartialEq)]
278pub enum Expr {
279    /// Absolute location path: `/expr`
280    AbsolutePath(Box<Expr>),
281    /// Relative location path: `step1/step2`
282    RelativePath(Box<Expr>, Box<Expr>),
283    /// A single step
284    Step(Step),
285    /// Filter expression: `primary[pred1][pred2]`
286    Filter(Box<Expr>, Vec<Expr>),
287    /// Variable reference: `$name`
288    Variable(String),
289    /// String literal: `'hello'`
290    StringLiteral(String),
291    /// Numeric literal: `42` or `3.14`
292    NumberLiteral(f64),
293    /// Boolean literal
294    BooleanLiteral(bool),
295    /// Function call: `name(arg1, arg2)`
296    FunctionCall {
297        /// Function name (a QName, possibly with a namespace prefix)
298        name: String,
299        /// Argument expressions, evaluated in order
300        args: Vec<Expr>,
301    },
302    /// Binary operation: `left op right`
303    BinaryOp {
304        /// The operator applied to the two operands
305        op: BinaryOp,
306        /// Left operand
307        left: Box<Expr>,
308        /// Right operand
309        right: Box<Expr>,
310    },
311    /// Unary minus: `-expr`
312    UnaryMinus(Box<Expr>),
313    /// Union expression: `left | right`
314    Union(Box<Expr>, Box<Expr>),
315}
316
317impl Expr {
318    /// Check if this expression is a location path (returns nodeset).
319    pub const fn is_location_path(&self) -> bool {
320        matches!(
321            self,
322            Expr::AbsolutePath(_) | Expr::RelativePath(_, _) | Expr::Step(_) | Expr::Filter(_, _)
323        )
324    }
325
326    /// Check if this is a constant value (no evaluation needed).
327    pub const fn is_constant(&self) -> bool {
328        matches!(
329            self,
330            Expr::StringLiteral(_) | Expr::NumberLiteral(_) | Expr::BooleanLiteral(_)
331        )
332    }
333}
334
335// ═══════════════════════════════════════════════════════════════════════════════
336// Compiled Expression
337// ═══════════════════════════════════════════════════════════════════════════════
338
339/// A compiled XPath expression.
340///
341/// Internal representation, not the C ABI `xmlXPathCompExprPtr`.
342#[derive(Debug, Clone)]
343pub struct CompiledExpr {
344    /// The original XPath expression source text
345    pub original: String,
346    /// The parsed expression tree
347    pub expr: Expr,
348}
349
350impl CompiledExpr {
351    /// Create a compiled expression from its source text and parsed AST.
352    pub const fn new(original: String, expr: Expr) -> Self {
353        Self { original, expr }
354    }
355}