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}
169
170/// A name test (QName or wildcard).
171#[derive(Debug, Clone, PartialEq)]
172pub enum NameTest {
173    /// Just a local name: "para"
174    LocalName(String),
175    /// Qualified name: "xslt:template"
176    QName {
177        /// Namespace prefix, e.g. `xslt` in `xslt:template`
178        prefix: String,
179        /// Local part, e.g. `template` in `xslt:template`
180        local: String,
181    },
182    /// Wildcard name: *
183    Any,
184}
185
186impl fmt::Display for NodeTest {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        match self {
189            NodeTest::NameTest(n) => match n {
190                NameTest::LocalName(s) => write!(f, "{}", s),
191                NameTest::QName { prefix, local } => write!(f, "{}:{}", prefix, local),
192                NameTest::Any => write!(f, "*"),
193            },
194            NodeTest::Comment => write!(f, "comment()"),
195            NodeTest::Text => write!(f, "text()"),
196            NodeTest::ProcessingInstruction(None) => write!(f, "processing-instruction()"),
197            NodeTest::ProcessingInstruction(Some(t)) => {
198                write!(f, "processing-instruction('{}')", t)
199            }
200            NodeTest::Node => write!(f, "node()"),
201            NodeTest::Wildcard => write!(f, "*"),
202            NodeTest::NsWildcard(prefix) => write!(f, "{}:*", prefix),
203        }
204    }
205}
206
207// ═══════════════════════════════════════════════════════════════════════════════
208// Step
209// ═══════════════════════════════════════════════════════════════════════════════
210
211/// A single location step: axis::node-test\[predicates\]
212#[derive(Debug, Clone, PartialEq)]
213pub struct Step {
214    /// Axis the step traverses (defaults to `child` for abbreviated steps)
215    pub axis: Axis,
216    /// Node test selecting which nodes the step matches
217    pub node_test: NodeTest,
218    /// Predicates filtering the selected nodes, applied left to right
219    pub predicates: Vec<Expr>,
220}
221
222// ═══════════════════════════════════════════════════════════════════════════════
223// Binary Operators
224// ═══════════════════════════════════════════════════════════════════════════════
225
226/// XPath 1.0 binary operator.
227#[derive(Debug, Clone, Copy, PartialEq)]
228pub enum BinaryOp {
229    /// `|` — union
230    Union,
231    /// `and`
232    And,
233    /// `or`
234    Or,
235    /// `=`
236    Eq,
237    /// `!=`
238    Ne,
239    /// `<`
240    Lt,
241    /// `>`
242    Gt,
243    /// `<=`
244    Le,
245    /// `>=`
246    Ge,
247    /// `+`
248    Add,
249    /// `-`
250    Sub,
251    /// `*` (multiplication)
252    Mul,
253    /// `div`
254    Div,
255    /// `mod`
256    Mod,
257}
258
259// ═══════════════════════════════════════════════════════════════════════════════
260// Expressions
261// ═══════════════════════════════════════════════════════════════════════════════
262
263/// XPath expression AST node.
264#[derive(Debug, Clone, PartialEq)]
265pub enum Expr {
266    /// Absolute location path: `/expr`
267    AbsolutePath(Box<Expr>),
268    /// Relative location path: `step1/step2`
269    RelativePath(Box<Expr>, Box<Expr>),
270    /// A single step
271    Step(Step),
272    /// Filter expression: `primary[pred1][pred2]`
273    Filter(Box<Expr>, Vec<Expr>),
274    /// Variable reference: `$name`
275    Variable(String),
276    /// String literal: `'hello'`
277    StringLiteral(String),
278    /// Numeric literal: `42` or `3.14`
279    NumberLiteral(f64),
280    /// Boolean literal
281    BooleanLiteral(bool),
282    /// Function call: `name(arg1, arg2)`
283    FunctionCall {
284        /// Function name (a QName, possibly with a namespace prefix)
285        name: String,
286        /// Argument expressions, evaluated in order
287        args: Vec<Expr>,
288    },
289    /// Binary operation: `left op right`
290    BinaryOp {
291        /// The operator applied to the two operands
292        op: BinaryOp,
293        /// Left operand
294        left: Box<Expr>,
295        /// Right operand
296        right: Box<Expr>,
297    },
298    /// Unary minus: `-expr`
299    UnaryMinus(Box<Expr>),
300    /// Union expression: `left | right`
301    Union(Box<Expr>, Box<Expr>),
302}
303
304impl Expr {
305    /// Check if this expression is a location path (returns nodeset).
306    pub const fn is_location_path(&self) -> bool {
307        matches!(
308            self,
309            Expr::AbsolutePath(_) | Expr::RelativePath(_, _) | Expr::Step(_) | Expr::Filter(_, _)
310        )
311    }
312
313    /// Check if this is a constant value (no evaluation needed).
314    pub const fn is_constant(&self) -> bool {
315        matches!(
316            self,
317            Expr::StringLiteral(_) | Expr::NumberLiteral(_) | Expr::BooleanLiteral(_)
318        )
319    }
320}
321
322// ═══════════════════════════════════════════════════════════════════════════════
323// Compiled Expression
324// ═══════════════════════════════════════════════════════════════════════════════
325
326/// A compiled XPath expression.
327///
328/// Internal representation, not the C ABI `xmlXPathCompExprPtr`.
329#[derive(Debug, Clone)]
330pub struct CompiledExpr {
331    /// The original XPath expression source text
332    pub original: String,
333    /// The parsed expression tree
334    pub expr: Expr,
335}
336
337impl CompiledExpr {
338    /// Create a compiled expression from its source text and parsed AST.
339    pub const fn new(original: String, expr: Expr) -> Self {
340        Self { original, expr }
341    }
342}