Skip to main content

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}