jetro_core/ast.rs
1//! Abstract syntax tree for Jetro v2 expressions.
2//!
3//! The AST is the contract between the parser and every other v2 layer —
4//! the compiler lowers it to opcodes, analyses inspect it for ident-use
5//! / purity, and tests build it directly. Because every component
6//! observes `Expr`, its variants are kept deliberately orthogonal:
7//! each is a language concept, not an implementation shortcut.
8//!
9//! `Arc<str>` is used for every identifier so that cloning a name into
10//! an opcode is a refcount bump rather than a byte copy. Recursive
11//! sub-expressions are `Box<Expr>` — owned, not shared — since the
12//! compiler rewrites the AST in-place (see `reorder_and_operands`).
13
14use std::sync::Arc;
15
16// ── AST nodes ─────────────────────────────────────────────────────────────────
17
18#[derive(Debug, Clone)]
19pub enum Expr {
20 // Literals
21 Null,
22 Bool(bool),
23 Int(i64),
24 Float(f64),
25 Str(String),
26 FString(Vec<FStringPart>),
27
28 // References
29 Root, // $
30 Current, // @
31 Ident(String), // variable or implicit current-item field
32
33 // Navigation chain: base followed by postfix steps
34 Chain(Box<Expr>, Vec<Step>),
35
36 // Binary operations
37 BinOp(Box<Expr>, BinOp, Box<Expr>),
38 UnaryNeg(Box<Expr>),
39 Not(Box<Expr>),
40
41 // Kind check: expr kind [not] type
42 Kind {
43 expr: Box<Expr>,
44 ty: KindType,
45 negate: bool,
46 },
47
48 // Null-coalesce: lhs ?| rhs
49 Coalesce(Box<Expr>, Box<Expr>),
50
51 // Object / array construction
52 Object(Vec<ObjField>),
53 Array(Vec<ArrayElem>),
54
55 // Pipeline: base | step1 | step2 or base -> name | ...
56 Pipeline {
57 base: Box<Expr>,
58 steps: Vec<PipeStep>,
59 },
60
61 // Comprehensions
62 ListComp {
63 expr: Box<Expr>,
64 vars: Vec<String>,
65 iter: Box<Expr>,
66 cond: Option<Box<Expr>>,
67 },
68 DictComp {
69 key: Box<Expr>,
70 val: Box<Expr>,
71 vars: Vec<String>,
72 iter: Box<Expr>,
73 cond: Option<Box<Expr>>,
74 },
75 SetComp {
76 expr: Box<Expr>,
77 vars: Vec<String>,
78 iter: Box<Expr>,
79 cond: Option<Box<Expr>>,
80 },
81 GenComp {
82 expr: Box<Expr>,
83 vars: Vec<String>,
84 iter: Box<Expr>,
85 cond: Option<Box<Expr>>,
86 },
87
88 // Lambda: lambda x, y: body
89 Lambda {
90 params: Vec<String>,
91 body: Box<Expr>,
92 },
93
94 // Let binding: let x = init in body
95 Let {
96 name: String,
97 init: Box<Expr>,
98 body: Box<Expr>,
99 },
100
101 // Python-style conditional: `then_ if cond else else_`.
102 // Short-circuits — only the taken branch is evaluated.
103 IfElse {
104 cond: Box<Expr>,
105 then_: Box<Expr>,
106 else_: Box<Expr>,
107 },
108
109 // Global function calls
110 GlobalCall {
111 name: String,
112 args: Vec<Arg>,
113 },
114
115 // Type cast: `expr as int` / `expr as string` etc. Add-on sugar on top
116 // of existing `.to_int()` / `.to_string()` methods — semantics identical.
117 Cast {
118 expr: Box<Expr>,
119 ty: CastType,
120 },
121
122 // Declarative patch block: `patch root { path1: val1, path2: val2, ... }`.
123 // Returns a new document with the listed paths rewritten in order.
124 // COW semantics — unchanged subtrees are shared.
125 Patch {
126 root: Box<Expr>,
127 ops: Vec<PatchOp>,
128 },
129
130 // Sentinel: `DELETE` inside a patch-field value removes the key.
131 // Only meaningful at the leaf of a patch path; outside patch it errors.
132 DeleteMark,
133}
134
135// ── Patch ─────────────────────────────────────────────────────────────────────
136
137#[derive(Debug, Clone)]
138pub struct PatchOp {
139 pub path: Vec<PathStep>,
140 pub val: Expr,
141 pub cond: Option<Expr>,
142}
143
144#[derive(Debug, Clone)]
145pub enum PathStep {
146 Field(String),
147 Index(i64),
148 DynIndex(Expr),
149 Wildcard,
150 WildcardFilter(Box<Expr>),
151 Descendant(String),
152}
153
154// ── Cast type ─────────────────────────────────────────────────────────────────
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum CastType {
158 Int, Float, Number, Str, Bool, Array, Object, Null,
159}
160
161// ── Pipeline step ─────────────────────────────────────────────────────────────
162
163#[derive(Debug, Clone)]
164pub enum PipeStep {
165 /// `| expr` — forward current value as new context
166 Forward(Expr),
167 /// `-> target` — label current value, pass through unchanged
168 Bind(BindTarget),
169}
170
171#[derive(Debug, Clone)]
172pub enum BindTarget {
173 Name(String),
174 Obj { fields: Vec<String>, rest: Option<String> },
175 Arr(Vec<String>),
176}
177
178// ── F-string parts ────────────────────────────────────────────────────────────
179
180#[derive(Debug, Clone)]
181pub enum FStringPart {
182 Lit(String),
183 Interp { expr: Expr, fmt: Option<FmtSpec> },
184}
185
186#[derive(Debug, Clone)]
187pub enum FmtSpec {
188 Spec(String), // :.2f :>10 etc.
189 Pipe(String), // |upper |trim etc.
190}
191
192// ── Array element ─────────────────────────────────────────────────────────────
193
194#[derive(Debug, Clone)]
195pub enum ArrayElem {
196 Expr(Expr),
197 Spread(Expr),
198}
199
200// ── Postfix steps ─────────────────────────────────────────────────────────────
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub enum QuantifierKind {
204 /// `?` — first element or null
205 First,
206 /// `!` — exactly one element (error if 0 or >1)
207 One,
208}
209
210#[derive(Debug, Clone)]
211pub enum Step {
212 Field(String), // .field
213 OptField(String), // ?.field
214 Descendant(String), // ..field
215 DescendAll, // .. (all descendants)
216 Index(i64), // [n]
217 DynIndex(Box<Expr>), // [expr]
218 Slice(Option<i64>, Option<i64>), // [n:m]
219 Method(String, Vec<Arg>), // .method(args)
220 OptMethod(String, Vec<Arg>), // ?.method(args)
221 InlineFilter(Box<Expr>), // {pred}
222 Quantifier(QuantifierKind), // ? or !
223}
224
225// ── Function arguments ────────────────────────────────────────────────────────
226
227#[derive(Debug, Clone)]
228pub enum Arg {
229 Pos(Expr),
230 Named(String, Expr),
231}
232
233// ── Object field ─────────────────────────────────────────────────────────────
234
235#[derive(Debug, Clone)]
236pub enum ObjField {
237 /// `key: expr` (optionally `key: expr when cond`)
238 Kv { key: String, val: Expr, optional: bool, cond: Option<Expr> },
239 /// `key` — shorthand for `key: key`
240 Short(String),
241 /// `[expr]: expr` — dynamic key
242 Dynamic { key: Expr, val: Expr },
243 /// `...expr` — spread object (shallow)
244 Spread(Expr),
245 /// `...**expr` — deep-merge spread: recurse into nested objects,
246 /// concatenate arrays, rhs wins for scalars
247 SpreadDeep(Expr),
248}
249
250// ── Binary operators ──────────────────────────────────────────────────────────
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub enum BinOp {
254 Add, Sub, Mul, Div, Mod,
255 Eq, Neq, Lt, Lte, Gt, Gte,
256 Fuzzy,
257 And, Or,
258}
259
260// ── Kind types ────────────────────────────────────────────────────────────────
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub enum KindType {
264 Null, Bool, Number, Str, Array, Object,
265}
266
267// ── Sort key ──────────────────────────────────────────────────────────────────
268
269#[derive(Debug, Clone)]
270pub struct SortKey {
271 pub expr: Expr,
272 pub desc: bool,
273}
274
275impl Expr {
276 /// Wrap in a Chain if steps is non-empty, otherwise return self.
277 pub fn maybe_chain(self, steps: Vec<Step>) -> Self {
278 if steps.is_empty() { self } else { Expr::Chain(Box::new(self), steps) }
279 }
280}
281
282/// Shared reference to an expression (for lambdas stored in closures).
283pub type ExprRef = Arc<Expr>;