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}