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}