jsonata_core/ast.rs
1// Abstract Syntax Tree definitions
2// Mirrors the AST structure from jsonata.js
3
4use serde::{Deserialize, Serialize};
5
6/// Stage types that can be attached to path steps
7///
8/// In JSONata, predicates following path segments become "stages" that are applied
9/// during the extraction process, not as separate steps.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub enum Stage {
12 /// Filter/predicate stage [expr]
13 Filter(Box<AstNode>),
14 /// Positional index stage `#$var` that lands on a step already carrying an
15 /// index/stages (jsonata-js's `{type: 'index', value}` stage). Binds the
16 /// variable to each surviving tuple's position in the CURRENT (post-earlier-
17 /// stages) result, so e.g. `books@$b#$ib[$l.isbn=$b.isbn]#$ib2` gives `$ib`
18 /// the pre-filter book position and `$ib2` the post-filter position.
19 Index(String),
20}
21
22/// A step in a path expression with optional stages
23///
24/// Stages are operations (like predicates) that apply during the step evaluation,
25/// not after all steps are complete.
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27pub struct PathStep {
28 /// The main step node (field name, wildcard, etc.)
29 pub node: AstNode,
30 /// Stages to apply during this step (e.g., predicates)
31 pub stages: Vec<Stage>,
32 /// Set by `@$var` (focus binding): binds the step's per-element value to
33 /// this variable name (without the `$` prefix) during tuple-stream
34 /// evaluation. Mirrors jsonata-js's `step.focus`.
35 pub focus: Option<String>,
36 /// Set by `#$var` (index binding): binds the step's per-element index to
37 /// this variable name (without the `$` prefix). Mirrors jsonata-js's
38 /// `step.index`. Replaces the retired `AstNode::IndexBind` wrapping node.
39 pub index_var: Option<String>,
40 /// Set by ast_transform when a later `%` reference needs this step's
41 /// *input* value preserved. The label is synthetic (e.g. "!0", "!1", ...)
42 /// and used as a tuple-dict key at runtime. Mirrors jsonata-js's
43 /// `step.ancestor.label`.
44 pub ancestor_label: Option<String>,
45 /// True when this step must participate in tuple-stream evaluation
46 /// (because of its own `focus`/`index_var`/`ancestor_label`, or because
47 /// an earlier step in the same path already entered tuple-stream mode).
48 /// Mirrors jsonata-js's `step.tuple`.
49 pub is_tuple: bool,
50}
51
52/// AST Node types
53///
54/// This enum represents all possible node types in a JSONata expression AST.
55/// The structure closely mirrors the JavaScript implementation to facilitate
56/// maintenance and upstream synchronization.
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58pub enum AstNode {
59 /// String literal (e.g., "hello", 'world')
60 String(String),
61
62 /// Field/property name in path expressions (e.g., foo in foo.bar)
63 /// This is distinct from String: Name is a field access, String is a literal value
64 Name(String),
65
66 /// Number literal
67 Number(f64),
68
69 /// Boolean literal
70 Boolean(bool),
71
72 /// Null literal
73 Null,
74
75 /// Undefined literal (distinct from null in JavaScript semantics)
76 /// In JSONata, undefined represents "no value" and propagates through expressions
77 Undefined,
78
79 /// Placeholder for partial application (?)
80 /// When used as a function argument, creates a partially applied function
81 Placeholder,
82
83 /// Regex literal (e.g., /pattern/flags)
84 Regex { pattern: String, flags: String },
85
86 /// Variable reference (e.g., $var)
87 Variable(String),
88
89 /// Parent variable reference (e.g., $$)
90 ParentVariable(String),
91
92 /// Path expression (e.g., foo.bar)
93 /// Each step can have stages (like predicates) attached
94 Path { steps: Vec<PathStep> },
95
96 /// Binary operation
97 Binary {
98 op: BinaryOp,
99 lhs: Box<AstNode>,
100 rhs: Box<AstNode>,
101 },
102
103 /// Unary operation
104 Unary { op: UnaryOp, operand: Box<AstNode> },
105
106 /// Function call by name
107 Function {
108 name: String,
109 args: Vec<AstNode>,
110 /// Whether this was called with $ prefix (built-in function)
111 /// True for $string(x), false for string(x)
112 is_builtin: bool,
113 },
114
115 /// Call an arbitrary expression as a function
116 /// Used for IIFE patterns like `(function($x){...})(5)` or chained calls
117 /// The procedure can be any expression that evaluates to a function
118 Call {
119 procedure: Box<AstNode>,
120 args: Vec<AstNode>,
121 },
122
123 /// Lambda function definition
124 Lambda {
125 params: Vec<String>,
126 body: Box<AstNode>,
127 /// Optional signature for type checking (e.g., "<n-n:n>")
128 signature: Option<String>,
129 /// Whether this lambda's body is a thunk (contains tail call that should be optimized)
130 /// A thunk wraps a tail-position function call for TCO
131 #[serde(default)]
132 thunk: bool,
133 },
134
135 /// Array constructor
136 Array(Vec<AstNode>),
137
138 /// Object constructor
139 Object(Vec<(AstNode, AstNode)>),
140
141 /// Object transform (postfix object constructor): expr{key: value}
142 /// Transforms the input using the object pattern
143 ObjectTransform {
144 input: Box<AstNode>,
145 pattern: Vec<(AstNode, AstNode)>,
146 },
147
148 /// Block expression
149 Block(Vec<AstNode>),
150
151 /// Conditional expression (? :)
152 Conditional {
153 condition: Box<AstNode>,
154 then_branch: Box<AstNode>,
155 else_branch: Option<Box<AstNode>>,
156 },
157
158 /// Wildcard operator (*) in path expressions
159 Wildcard,
160
161 /// Descendant operator (**) in path expressions
162 Descendant,
163
164 /// Parent-reference operator (%) in path expressions, resolved.
165 /// Carries the synthetic ancestor label ("!0", "!1", ...) assigned by
166 /// ast_transform -- looked up at runtime via the same tuple/scope
167 /// mechanism as $-variables. The parser produces `Parent(String::new())`
168 /// (an empty placeholder never observed by the evaluator); ast_transform
169 /// fills every occurrence with a real label or raises S0217.
170 Parent(String),
171
172 /// Array filter/predicate [condition]
173 /// Can be an index (number) or a predicate (boolean expression)
174 Predicate(Box<AstNode>),
175
176 /// Array grouping in path expression .[expr]
177 /// Like Array but doesn't flatten when used in paths
178 ArrayGroup(Vec<AstNode>),
179
180 /// Function application in path expression .(expr)
181 /// Maps expr over the current value, with $ referring to each element
182 FunctionApplication(Box<AstNode>),
183
184 /// Sort operator in path expression ^(expr)
185 /// Sorts the current value by evaluating expr for each element
186 /// expr can be prefixed with < (ascending, default) or > (descending)
187 Sort {
188 /// The input expression to sort
189 input: Box<AstNode>,
190 /// Sort terms - list of (expression, ascending) tuples
191 terms: Vec<(AstNode, bool)>,
192 },
193
194 /// Transform operator |location|update[,delete]|
195 /// Creates a function that transforms objects by:
196 /// 1. Evaluating location to find objects to modify
197 /// 2. Applying update (object constructor) to each matched object
198 /// 3. Optionally deleting fields specified in delete array
199 Transform {
200 /// Expression to locate objects to transform
201 location: Box<AstNode>,
202 /// Object constructor expression for updates
203 update: Box<AstNode>,
204 /// Optional array of field names to delete
205 delete: Option<Box<AstNode>>,
206 },
207}
208
209/// Binary operators
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
211pub enum BinaryOp {
212 // Arithmetic
213 Add,
214 Subtract,
215 Multiply,
216 Divide,
217 Modulo,
218
219 // Comparison
220 Equal,
221 NotEqual,
222 LessThan,
223 LessThanOrEqual,
224 GreaterThan,
225 GreaterThanOrEqual,
226
227 // Logical
228 And,
229 Or,
230
231 // String
232 Concatenate,
233
234 // Range
235 Range,
236
237 // Other
238 In,
239
240 // Variable binding
241 ColonEqual, // :=
242
243 // Focus binding (raw parse-time marker for @$var; resolved into a
244 // PathStep.focus flag by ast_transform, matching jsonata-js's own
245 // parser.js:834-847, which also produces a generic binary node here)
246 FocusBind,
247
248 // Index binding (raw parse-time marker for #$var; resolved into a
249 // PathStep.index_var flag by ast_transform). Reuses the same generic
250 // Binary-node representation as FocusBind (rather than a dedicated
251 // `AstNode::IndexBind` struct variant) so the retired `IndexBind`
252 // variant has zero remaining construction sites once removed from
253 // ast.rs -- see Task 4's ast_transform.rs migrate_binding_markers.
254 IndexBind,
255
256 // Coalescing
257 Coalesce, // ??
258
259 // Default
260 Default, // ?:
261
262 // Function chaining/piping
263 ChainPipe, // ~>
264}
265
266/// Unary operators
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
268pub enum UnaryOp {
269 /// Negation (-)
270 Negate,
271
272 /// Logical NOT
273 Not,
274}
275
276impl PathStep {
277 /// Create a path step from a node without stages
278 pub fn new(node: AstNode) -> Self {
279 PathStep {
280 node,
281 stages: Vec::new(),
282 focus: None,
283 index_var: None,
284 ancestor_label: None,
285 is_tuple: false,
286 }
287 }
288
289 /// Create a path step with stages
290 pub fn with_stages(node: AstNode, stages: Vec<Stage>) -> Self {
291 PathStep {
292 node,
293 stages,
294 focus: None,
295 index_var: None,
296 ancestor_label: None,
297 is_tuple: false,
298 }
299 }
300}
301
302impl AstNode {
303 /// Create a string literal node
304 pub fn string(s: impl Into<String>) -> Self {
305 AstNode::String(s.into())
306 }
307
308 /// Create a number literal node
309 pub fn number(n: f64) -> Self {
310 AstNode::Number(n)
311 }
312
313 /// Create a boolean literal node
314 pub fn boolean(b: bool) -> Self {
315 AstNode::Boolean(b)
316 }
317
318 /// Create a null literal node
319 pub fn null() -> Self {
320 AstNode::Null
321 }
322
323 /// Create an undefined literal node
324 pub fn undefined() -> Self {
325 AstNode::Undefined
326 }
327
328 /// Create a variable reference node
329 pub fn variable(name: impl Into<String>) -> Self {
330 AstNode::Variable(name.into())
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_ast_node_creation() {
340 let str_node = AstNode::string("hello");
341 assert!(matches!(str_node, AstNode::String(_)));
342
343 let num_node = AstNode::number(42.0);
344 assert!(matches!(num_node, AstNode::Number(_)));
345
346 let bool_node = AstNode::boolean(true);
347 assert!(matches!(bool_node, AstNode::Boolean(_)));
348
349 let null_node = AstNode::null();
350 assert!(matches!(null_node, AstNode::Null));
351 }
352
353 #[test]
354 fn test_binary_op() {
355 let node = AstNode::Binary {
356 op: BinaryOp::Add,
357 lhs: Box::new(AstNode::number(1.0)),
358 rhs: Box::new(AstNode::number(2.0)),
359 };
360 assert!(matches!(node, AstNode::Binary { .. }));
361 }
362
363 #[test]
364 fn test_path_step_new_defaults_new_fields_to_none() {
365 let step = PathStep::new(AstNode::Name("foo".to_string()));
366 assert_eq!(step.focus, None);
367 assert_eq!(step.index_var, None);
368 assert_eq!(step.ancestor_label, None);
369 assert!(!step.is_tuple);
370 }
371}