jsonata_core/ast_transform.rs
1// Post-parse AST transformation pass.
2// Mirrors parser.js's processAST/seekParent/pushAncestry/resolveAncestry
3// (tests/jsonata-js/src/parser.js ~L937-1235), adapted to Rust's ownership
4// model: instead of mutating tree nodes in place, this consumes the raw
5// tree and rebuilds an enriched one with ancestor/tuple metadata resolved.
6
7// Recursion-depth safety (added: see docs/superpowers/plans/2026-07-07-parser-depth-and-u16-truncation-fixes-plan.md):
8// `parser::parse()` (src/parser.rs:1730) unconditionally pipes every parse
9// through `resolve_ancestry` below, so a deeply-nested input expression can
10// overflow the native stack here even though the raw Pratt parse itself
11// completed successfully -- confirmed empirically: a 200,000-term
12// left-nested arithmetic chain (`1+1+1+...`) SIGABRTs ("stack overflow")
13// via the full `parser::parse()` entry point, in this file, not the parser.
14//
15// There are THREE recursive pieces in this file, but only TWO independent
16// stack budgets: data flows one-way from resolve_ancestry into
17// transform_node/transform_children/transform_path_steps/
18// migrate_binding_markers ("cycle 1"), and separately into substitute_labels
19// ("cycle 2", a second full-tree walk that only starts after cycle 1 has
20// fully unwound -- see resolve_ancestry). walk_backward/seek_parent_step/
21// seek_parent_wrapped ("cycle 3") is reached FROM cycle 1 (transform_path_
22// steps's predicate/own-pending resolution, and transform_children's Sort
23// arm) while cycle 1's frames are still LIVE on the native stack -- it nests
24// ON TOP of cycle 1's depth rather than running after it -- so cycle 1 and
25// cycle 3 share one stack budget and their depths ADD, not two independent
26// caps. A depth guard that gives cycle 1 and cycle 3 each their own
27// independent counter capped at the native-safe limit would still allow
28// cap1 + cap3 frames live simultaneously and overflow; Task 2 needs ONE
29// counter threaded through cycle 1 AND cycle 3 together, and a SEPARATE
30// counter (reset to 0) for cycle 2 (substitute_labels), which only runs
31// after cycle 1/3's frames are gone. Guard all of the functions listed
32// below regardless of which cycle they're in -- checking depth in only one
33// cycle's functions is the exact "Task 5 pattern" (a check added at only
34// one of several recursive entry points) this task exists to avoid.
35//
36// (1) Main tree-transform mutual recursion -- depth scales with the general
37// AST's nesting depth (binary op chains, block/array/function-arg
38// nesting, parenthesized sub-paths used as a path step's node, etc.):
39// - transform_node (:558) -- recurses directly (Path -> transform_path_steps;
40// Block -> transform_node per element, a loop, but each iteration's call
41// itself recurses; Binary{FocusBind/IndexBind} -> transform_node(lhs));
42// for every other node kind, delegates to transform_children (still the
43// same cycle). This is the actual site hit by the confirmed arithmetic-
44// chain repro (`1+1+1+...` has no Path/`%` at all -- it's pure nested
45// Binary, handled by transform_node's `other => transform_children(...)`
46// fallback).
47// - transform_children (:653) -- recurses via transform_node on every
48// child of every composite node type (Binary lhs/rhs, Unary operand,
49// Array/Function/Call-args/Object/ObjectTransform/Sort/Transform/
50// ArrayGroup elements, Conditional branches, Lambda body, Predicate/
51// FunctionApplication inner). This is the other function actually hit
52// by the arithmetic-chain repro (Binary's lhs/rhs recursion).
53// - transform_path_steps (:933) -- does NOT recurse on the flat
54// `Vec<PathStep>` itself (that's a `for` loop over the steps -- bounded
55// iteration, not stack depth; confirmed empirically in Step 3 below: a
56// 50,000-step flat dot-path parses fine). It DOES feed back into the
57// cycle per-step: calls `migrate_binding_markers(step, ...)` for every
58// step, and separately calls `transform_node` on each filter-stage
59// expression. Depth here scales with how deeply a single step's OWN
60// node is nested (e.g. a parenthesized sub-path `(Order.Product)` used
61// as one step, itself containing another Path), not with the number of
62// steps in the flat list.
63// - migrate_binding_markers (:1224) -- not itself self-recursive (one
64// match, each arm calls transform_node/splice_marker_steps once), but
65// it's the edge that closes the transform_path_steps -> transform_node
66// cycle, so it needs to participate in whatever depth-counter scheme
67// Task 2 uses (thread it through, even if it never increments/checks
68// independently of the transform_node call it makes).
69//
70// (2) substitute_labels (:273) -- self-recursive only (never calls
71// transform_node/transform_children/transform_path_steps), structurally
72// mirroring transform_children's per-node-type dispatch (every
73// composite node type recurses into every child). Runs as a SECOND,
74// separate full-tree walk after transform_node returns (see
75// resolve_ancestry), so it needs its own depth counter/reset -- reusing
76// a counter left over (at whatever depth) from pass (1) would be wrong.
77//
78// (3) Ancestor-seek recursion, reached from pass (1) (transform_path_steps's
79// predicate/own-pending resolution loop calling resolve_predicate_slot/
80// walk_backward, and transform_children's Sort arm calling
81// walk_backward directly) while pass (1)'s own frames are still live --
82// it never calls back into transform_node/transform_children/
83// transform_path_steps (a one-way bridge, not a mutual cycle with (1)),
84// but because it nests ON TOP of (1)'s live stack rather than running
85// after it unwinds, (1) and (3) share ONE stack budget (see the note
86// above the fold -- their depths add). Depth here scales with how many
87// levels of parenthesized sub-path nesting (`(...)` wrapping another
88// `(...)`) a `%` reference has to walk through, not with path step
89// count or general AST depth:
90// - walk_backward (:1056) -- its own "while level > 0" loop walking
91// backward through one `&mut [PathStep]` is bounded iteration (not a
92// stack risk regardless of the slice's length), but it calls
93// seek_parent_step per candidate step, which can call back into
94// walk_backward (via seek_parent_wrapped's Path case) -- indirect
95// recursion.
96// - seek_parent_step (:1121) -- recurses via seek_parent_wrapped for the
97// FunctionApplication and Block step-node cases (a parenthesized
98// sub-path used as a step).
99// - seek_parent_wrapped (:1191) -- recurses via walk_backward (Path case)
100// AND directly calls itself (Block case, recursing into the block's
101// last expression) -- e.g. doubly (or N-ly) nested parens.
102// - resolve_predicate_slot (:1028) -- NOT part of this cycle itself (no
103// self-loop; called once per predicate slot from transform_path_steps's
104// loop over a bounded number of stages), but forwards into it
105// (seek_parent_step / walk_backward), so its own frame sits at the
106// base of chain (3) each time -- no guard needed in this function
107// itself, but Task 2 should not assume the chain "starts" at
108// walk_backward/seek_parent_step without going through here first in
109// the predicate case.
110//
111// Functions confirmed NOT to need guarding (either non-recursive, or their
112// only "recursion" is bounded iteration over a Vec/HashMap-chain, not stack
113// depth):
114// - coded (:161), AncestryState::new (:207), AncestryState::fresh_label
115// (:214), Transformed::leaf (:243) -- trivial constructors/helpers, no
116// recursive or child-node-walking calls at all.
117// - AncestryState::canonical (:224) -- a `while let Some(...)` loop
118// following an alias chain in a HashMap; iteration, not recursion, and
119// the doc comment right above it already notes chains longer than one
120// hop shouldn't arise in practice regardless.
121// - apply_marker_to_step (:419), check_focus_bind_target (:454) -- single
122// match/if-chain over already-computed values, no calls back into any
123// tree-walking function.
124// - splice_marker_steps (:486) -- loops over a `Vec<PathStep>` produced by
125// an already-fully-transformed `Transformed` (its `steps`/`pending`
126// inputs were recursed into by the CALLER before this runs), and over a
127// small fixed-shape `while` popping trailing `Predicate` pseudo-steps;
128// calls only check_focus_bind_target/apply_marker_to_step, never
129// transform_node or itself.
130// - wrap_marker_as_path (:545) -- calls splice_marker_steps once; no
131// recursion, no self-loop.
132// - resolve_ancestry (:252) -- the pass's entry point: calls
133// transform_node exactly once, then substitute_labels exactly once.
134// Not itself part of either cycle (never re-entered from within the
135// tree walk it kicks off), so it doesn't need a depth CHECK, but Task 2
136// should initialize/reset each of the three counters above here (one
137// for cycle (1)+(shared edge into (3)), one for substitute_labels).
138//
139// Step 3 sanity check performed (throwaway test, not committed): a
140// 200,000-step flat dot-path (`a.a.a...a`) -- same N as the crashing
141// arithmetic chain, for a clean apples-to-apples Ok-vs-crash comparison --
142// parsed via the FULL `parser::parse()` entry point returns `Ok`
143// immediately (iteration in transform_path_steps's `for step in steps`
144// loop, not recursion), while the 200,000-term arithmetic chain (`1+1+1+
145// ...`) still SIGABRTs ("stack overflow") via the same full `parser::
146// parse()` entry point in the same run -- confirming the root cause
147// identified in the prior session is still live in current code, and that
148// it's specifically recursion-on-nesting-depth (transform_children's
149// Binary arm), not merely "large input," that triggers it.
150
151use crate::ast::{AstNode, BinaryOp, PathStep, Stage};
152use std::collections::HashMap;
153use thiserror::Error;
154
155#[derive(Error, Debug)]
156pub enum AstTransformError {
157 #[error("{code}: {message}")]
158 Coded { code: &'static str, message: String },
159}
160
161fn coded(code: &'static str, message: impl Into<String>) -> AstTransformError {
162 AstTransformError::Coded {
163 code,
164 message: message.into(),
165 }
166}
167
168// --- Recursion-depth safety (Task 2 of the plan referenced in the module
169// doc comment above) ---
170//
171// Two INDEPENDENT depth counters, per the module doc comment's analysis:
172// one shared by cycle 1 (transform_node/transform_children/
173// transform_path_steps/migrate_binding_markers) AND cycle 3
174// (walk_backward/seek_parent_step/seek_parent_wrapped) -- because cycle 3 is
175// reached from WITHIN live cycle-1 stack frames, their depths add and must
176// share one counter -- and a second, wholly separate counter for
177// substitute_labels (cycle 2), which only ever runs after cycle 1/3 have
178// fully unwound (see `resolve_ancestry`). Do NOT let these two counters
179// influence each other.
180//
181// A LATER guard was added directly in `parser.rs` (`MAX_PARSE_DEPTH`, also
182// 1000) that bounds the parser's OWN recursion/loop-iteration counter --
183// this is NOT the same thing as "real AST tree depth is always <=1000".
184// Calibrated empirically (throwaway harness, this session) across several
185// shapes: for simple ones (flat Binary/Unary/Array/Object chains) the
186// parser's counter tracks real tree depth 1:1, so `ast_transform`'s own
187// ceiling below fires first, well before the parser's. But for COMPOUND
188// shapes where one parser-guarded call/iteration corresponds to more than
189// one real `AstNode`-nesting hop (e.g. `a.(a.(a.(...)))`, where each level
190// costs one recursive `parse_expression` call for the `FunctionApplication`
191// body PLUS the `Path` wrapper it returns into), real tree depth reachable
192// via a parser-accepted expression can run up to ~2x the parser's own
193// counter value (observed: parser counter 999 -> real tree depth 2000).
194// This means DO NOT assume "the parser already bounds real depth to
195// <=1000" as a reason to raise this file's ceiling to just above 1000 --
196// that reasoning is unsound for compound shapes and was corrected before
197// shipping (see PR discussion). This file's own ceiling stays genuinely
198// load-bearing for those shapes, not just cosmetic defense-in-depth.
199//
200// Ceiling rationale: chosen empirically (see
201// `test_deeply_nested_arithmetic_does_not_overflow_native_stack_at_parse_time`
202// and `test_reasonable_nesting_still_parses_successfully` in
203// tests/integration_test.rs) to be comfortably safe on a 1MB-stack thread
204// (matching Windows' default, the same constraint `evaluate_internal`'s
205// analogous guard in evaluator.rs was built for). `stacker::maybe_grow`
206// below helps DURING this file's own guarded traversal, but it is NOT the
207// only thing keeping this ceiling load-bearing:
208//
209// A tree that successfully passes this guard (returns `Ok` all the way out
210// of `parser::parse()`) is handed to whatever caller holds it next (the
211// evaluator, the Python `JsonataExpression`, a test's local variable) and
212// is eventually dropped there via Rust's ORDINARY recursive `Drop` glue --
213// NOT the iterative teardown this file uses on its own bail-out path (see
214// `push_ast_node_children`'s doc comment below). Confirmed empirically this
215// session: simply constructing then normally-dropping a `Box<AstNode>`
216// chain somewhere between ~5,000 and ~20,000 levels deep overflows a
217// 1MB-stack thread, with ZERO ast_transform code involved. This ceiling
218// (1000, i.e. at most ~1000 levels of ACTUAL AST nesting, since cycle 1
219// costs >=1 depth unit per level) stays far enough below that downstream
220// threshold that any successfully-returned tree is safe for a caller to
221// drop normally. RAISING this ceiling without separately re-verifying the
222// downstream-drop threshold would silently reintroduce a native-stack
223// crash on the SUCCESS path -- a different crash than the one this task
224// fixes, and not exercised by either test above (one only exercises the
225// bail path at n=200,000; the other only exercises a SHALLOW successful
226// tree, not a near-ceiling one).
227const MAX_TRANSFORM_DEPTH: usize = 1000;
228// Same ceiling as MAX_TRANSFORM_DEPTH, for a DIFFERENT reason than "2x
229// headroom" might suggest: cycle 1 costs >=1 depth unit per level of ACTUAL
230// AST nesting (2 units for a Binary/Unary/etc. level via the
231// transform_node+transform_children pair, but exactly 1 unit for a level
232// that's pure nested blocks/parens, e.g. `((((...))))`, which only ever
233// enters transform_node's Block arm -- no transform_children hop). For
234// THAT shape, this counter and MAX_TRANSFORM_DEPTH are checking the exact
235// same per-level cost, so a tree that just barely passes cycle 1 (depth
236// ~1000) can arrive at substitute_labels already ~1000 deep too -- zero
237// margin, not 2x. It's still safe (substitute_labels's own bail-out lets
238// its `node` drop normally, unlike cycle 1/3's iterative-teardown bail
239// path, but by the time cycle 2 runs, cycle 1 already guaranteed the tree
240// is <= this same ceiling deep, well below the ~5,000-20,000-level
241// downstream-drop threshold noted above) -- just not defended by a margin,
242// so don't raise this independently of MAX_TRANSFORM_DEPTH without
243// re-checking this reasoning.
244const MAX_LABEL_SUBSTITUTION_DEPTH: usize = 1000;
245
246// Same constants `evaluate_internal` (src/evaluator.rs) uses for its
247// analogous native-stack safety net -- see that function's doc comment for
248// the full rationale. Kept as separate constants (rather than reused from
249// evaluator.rs) since this module has no dependency on evaluator.rs and the
250// two guards are conceptually independent safety nets.
251const AST_TRANSFORM_RED_ZONE: usize = 128 * 1024;
252const AST_TRANSFORM_GROW_STACK_SIZE: usize = 8 * 1024 * 1024;
253
254/// New error code `U1002` (no jsonata-js equivalent, like its sibling
255/// `U1001` in evaluator.rs -- JS has no comparable native-stack-overflow
256/// failure mode): a `U`-prefixed, not `S`-prefixed, code since this is a
257/// Rust-implementation-specific resource guard on an otherwise syntactically
258/// valid expression, not a JSONata syntax error. Using an `S0`-numbered slot
259/// (the next unused being S0218) would risk colliding with a future upstream
260/// jsonata-js `S0218` that means something else entirely; `U1001` is already
261/// taken by evaluator.rs's analogous stack-depth guard, so this is `U1002`.
262fn check_transform_depth(depth: usize) -> Result<(), AstTransformError> {
263 if depth > MAX_TRANSFORM_DEPTH {
264 Err(coded(
265 "U1002",
266 format!(
267 "Stack overflow - maximum expression nesting depth ({}) exceeded while post-processing the parsed expression",
268 MAX_TRANSFORM_DEPTH
269 ),
270 ))
271 } else {
272 Ok(())
273 }
274}
275
276/// See `check_transform_depth` -- same error code, separate counter/ceiling
277/// for `substitute_labels`'s own independent recursion (cycle 2).
278fn check_label_substitution_depth(depth: usize) -> Result<(), AstTransformError> {
279 if depth > MAX_LABEL_SUBSTITUTION_DEPTH {
280 Err(coded(
281 "U1002",
282 format!(
283 "Stack overflow - maximum expression nesting depth ({}) exceeded while finalizing the parsed expression",
284 MAX_LABEL_SUBSTITUTION_DEPTH
285 ),
286 ))
287 } else {
288 Ok(())
289 }
290}
291
292// --- Iterative teardown for the depth guard's bail-out path ---
293//
294// A SECOND, independent stack-overflow vector from the traversal recursion
295// the depth guard above protects against, found empirically while
296// validating this task's fix: when `check_transform_depth`/
297// `check_label_substitution_depth` trips inside `transform_node`/
298// `transform_children`/`transform_path_steps`, that function's OWN
299// `node`/`steps` parameter can still hold an ENORMOUS unprocessed remainder
300// (we bail out before ever destructuring it -- e.g. a `1+1+1+...` chain 200
301// levels past the ceiling still has ~199,800 more nested `Binary` levels
302// hanging off the node we're about to return `Err` for). If we just let
303// that `node`/`steps` value drop normally as the function returns, Rust's
304// compiler-generated recursive `Drop` glue walks the WHOLE remaining chain
305// one native stack frame per nesting level -- completely bypassing the
306// depth counter above, since that glue isn't a function call this file
307// controls. Confirmed empirically, independent of any ast_transform code:
308// simply constructing then normally-dropping a ~20,000-deep `Box<AstNode>`
309// chain overflows a 1MB-stack thread outright.
310//
311// The fix is an explicit heap work-list (`Vec<AstNode>`) instead of the
312// call stack: pop one node, move its `Box<AstNode>`/`Vec<AstNode>`/
313// `Vec<PathStep>` children onto the list (dropping only that one node's own
314// shallow, non-recursive fields for free as the match arm's temporaries go
315// out of scope), repeat. Stack usage is O(1) regardless of how deep the
316// abandoned remainder is, because no recursive function call (guarded or
317// not) is ever made -- only a `while` loop over a heap-allocated `Vec`.
318//
319// Every function that OWNS an `AstNode`/`Vec<PathStep>`/`PathStep`
320// parameter and can bail out with that parameter's traversal still
321// incomplete (`transform_node`, `transform_children`, `transform_path_steps`,
322// and -- for the `step.stages` a step still carries when its OWN `.node`
323// fails to transform -- `migrate_binding_markers`) must route the abandoned
324// value through here instead of letting it drop implicitly. Anything that
325// already came back out of a SUCCESSFUL (`Ok`) call is guaranteed
326// depth-bounded (that call would have hit the guard above otherwise) and is
327// always safe to drop normally.
328fn push_ast_node_children(node: AstNode, stack: &mut Vec<AstNode>) {
329 match node {
330 AstNode::Path { steps } => {
331 for step in steps {
332 push_path_step_children(step, stack);
333 }
334 }
335 AstNode::Binary { lhs, rhs, .. } => {
336 stack.push(*lhs);
337 stack.push(*rhs);
338 }
339 AstNode::Unary { operand, .. } => stack.push(*operand),
340 AstNode::Function { args, .. } => stack.extend(args),
341 AstNode::Call { procedure, args } => {
342 stack.push(*procedure);
343 stack.extend(args);
344 }
345 AstNode::Lambda { body, .. } => stack.push(*body),
346 AstNode::Array(elements) | AstNode::Block(elements) | AstNode::ArrayGroup(elements) => {
347 stack.extend(elements);
348 }
349 AstNode::Object(pairs) => {
350 for (k, v) in pairs {
351 stack.push(k);
352 stack.push(v);
353 }
354 }
355 AstNode::ObjectTransform { input, pattern } => {
356 stack.push(*input);
357 for (k, v) in pattern {
358 stack.push(k);
359 stack.push(v);
360 }
361 }
362 AstNode::Conditional {
363 condition,
364 then_branch,
365 else_branch,
366 } => {
367 stack.push(*condition);
368 stack.push(*then_branch);
369 if let Some(e) = else_branch {
370 stack.push(*e);
371 }
372 }
373 AstNode::Sort { input, terms } => {
374 stack.push(*input);
375 for (e, _asc) in terms {
376 stack.push(e);
377 }
378 }
379 AstNode::Transform {
380 location,
381 update,
382 delete,
383 } => {
384 stack.push(*location);
385 stack.push(*update);
386 if let Some(d) = delete {
387 stack.push(*d);
388 }
389 }
390 AstNode::FunctionApplication(inner) | AstNode::Predicate(inner) => stack.push(*inner),
391 // Leaf nodes (String/Name/Number/Boolean/Null/Undefined/Placeholder/
392 // Regex/Variable/ParentVariable/Wildcard/Descendant/Parent): no
393 // nested AstNode -- whatever's left of `node` (a String, an f64,
394 // ...) is dropped here for free, in O(1), as this match arm ends.
395 _ => {}
396 }
397}
398
399/// See `push_ast_node_children` -- the `PathStep`-flavored equivalent
400/// (a step's `.node` plus any `Stage::Filter` predicate expressions its
401/// `.stages` carries; the step's other fields -- `focus`/`index_var`/
402/// `ancestor_label`/`is_tuple` -- are never recursive, so they drop for
403/// free here too).
404fn push_path_step_children(step: PathStep, stack: &mut Vec<AstNode>) {
405 stack.push(step.node);
406 push_stage_children(step.stages, stack);
407}
408
409/// See `push_ast_node_children` -- pulls the `Box<AstNode>` out of every
410/// `Stage::Filter` (a step's predicate expressions) onto the work-list;
411/// `Stage::Index` carries only a variable name, nothing recursive.
412fn push_stage_children(stages: Vec<Stage>, stack: &mut Vec<AstNode>) {
413 for stage in stages {
414 if let Stage::Filter(e) = stage {
415 stack.push(*e);
416 }
417 }
418}
419
420/// Entry point: drop `node` iteratively rather than via the ordinary
421/// recursive `Drop` glue. See the doc comment above `push_ast_node_children`
422/// for why this exists.
423fn drop_ast_node_iteratively(node: AstNode) {
424 let mut stack = vec![node];
425 while let Some(n) = stack.pop() {
426 push_ast_node_children(n, &mut stack);
427 }
428}
429
430/// See `drop_ast_node_iteratively` -- the `Vec<PathStep>`-flavored entry
431/// point, used by `transform_path_steps`'s own bail-out (its parameter is a
432/// `Vec<PathStep>`, not a single `AstNode`).
433fn drop_path_steps_iteratively(steps: Vec<PathStep>) {
434 let mut stack = Vec::new();
435 for step in steps {
436 push_path_step_children(step, &mut stack);
437 }
438 while let Some(n) = stack.pop() {
439 push_ast_node_children(n, &mut stack);
440 }
441}
442
443/// See `drop_ast_node_iteratively` -- the `Vec<Stage>`-flavored entry point,
444/// used by `migrate_binding_markers`'s bail-out (a step's OWN `.node` failed
445/// to transform, but its `.stages` -- filter predicates -- are still owned
446/// and untouched).
447fn drop_stages_iteratively(stages: Vec<Stage>) {
448 let mut stack = Vec::new();
449 push_stage_children(stages, &mut stack);
450 while let Some(n) = stack.pop() {
451 push_ast_node_children(n, &mut stack);
452 }
453}
454
455/// Builds the `U1002` error without needing ownership of the abandoned
456/// node/steps -- kept separate from `drop_ast_node_iteratively`/
457/// `drop_path_steps_iteratively` so callers can drop first, then construct
458/// the error, in either order convenient at the call site.
459fn max_transform_depth_error() -> AstTransformError {
460 coded(
461 "U1002",
462 format!(
463 "Stack overflow - maximum expression nesting depth ({}) exceeded while post-processing the parsed expression",
464 MAX_TRANSFORM_DEPTH
465 ),
466 )
467}
468
469/// A `%` reference still seeking its ancestor step, mirroring jsonata-js's
470/// `slot` object (`{label, level, index}` -- we don't need `index`, since
471/// that's only used by jsonata-js to index into its global mutable
472/// `ancestry` array for the in-place relabeling trick; see `AncestryState`
473/// for how we get the same "reuse an existing label" behavior without it).
474#[derive(Debug, Clone)]
475struct PendingAncestor {
476 label: String,
477 /// Remaining backward steps needed before this reference resolves.
478 /// A fresh `%` starts at level 1 (its own immediately-preceding step);
479 /// walking backward over ANOTHER not-yet-resolved `%` step increments
480 /// this (mirrors seekParent's `case 'parent': slot.level++`).
481 level: usize,
482}
483
484/// Threaded through the whole pass: generates fresh synthetic ancestor
485/// labels ("!0", "!1", ...) and records label aliases.
486///
487/// Rust's immutable-rebuild model can't replicate jsonata-js's in-place
488/// mutation `ancestry[slot.index].slot.label = node.ancestor.label` (used
489/// when a *second* `%` resolves to a step some *earlier* `%` already
490/// tagged -- jsonata-js renames the second slot's label to match the first,
491/// by mutating a shared JS object referenced from both the `ancestry` array
492/// and the corresponding `AstNode::Parent` node already sitting in the
493/// tree). Since our tree nodes are owned values already moved by the time a
494/// later reuse is discovered, we can't reach back in and rewrite an
495/// already-built `Parent(label)` node in place. Instead: record the alias
496/// (`new_label -> canonical_label`) here as resolution proceeds, then run
497/// one final substitution pass (`substitute_labels`, called from
498/// `resolve_ancestry` after the whole tree is built) that rewrites every
499/// `AstNode::Parent(label)` to its canonical form. `PathStep.ancestor_label`
500/// itself never needs substitution: it's set at most once per step (the
501/// first `%` to resolve there), so it's always already canonical.
502struct AncestryState {
503 next_label: usize,
504 aliases: HashMap<String, String>,
505}
506
507impl AncestryState {
508 fn new() -> Self {
509 AncestryState {
510 next_label: 0,
511 aliases: HashMap::new(),
512 }
513 }
514
515 fn fresh_label(&mut self) -> String {
516 let label = format!("!{}", self.next_label);
517 self.next_label += 1;
518 label
519 }
520
521 /// Follow the alias chain to a label's canonical form. Chains longer
522 /// than one hop shouldn't arise (a step's `ancestor_label`, once set, is
523 /// never itself replaced -- only newcomers get aliased to it) but this
524 /// still follows the chain defensively rather than assuming depth 1.
525 fn canonical(&self, label: &str) -> String {
526 let mut cur = label;
527 while let Some(next) = self.aliases.get(cur) {
528 cur = next;
529 }
530 cur.to_string()
531 }
532}
533
534/// The result of transforming a node: the rebuilt node, plus any `%`
535/// references within it that are still seeking an ancestor step, bubbling
536/// up to whatever contains this node -- mirrors jsonata-js's `seekingParent`
537/// array property, attached to whatever node `pushAncestry` was called on.
538struct Transformed {
539 node: AstNode,
540 pending: Vec<PendingAncestor>,
541}
542
543impl Transformed {
544 fn leaf(node: AstNode) -> Self {
545 Transformed {
546 node,
547 pending: Vec::new(),
548 }
549 }
550}
551
552/// Entry point: resolve all ancestor references in a freshly-parsed AST.
553pub fn resolve_ancestry(ast: AstNode) -> Result<AstNode, AstTransformError> {
554 let mut state = AncestryState::new();
555 // Depth 0: the root of cycle 1+3's shared counter.
556 let transformed = transform_node(ast, &mut state, 0)?;
557 // Mirrors jsonata-js's final check (parser.js ~L1404): a bare `%` as the
558 // WHOLE expression, or any dangling (never-resolved) pending ancestor
559 // reference that bubbled all the way to the top, means there was no
560 // enclosing path to derive an ancestor from.
561 if !transformed.pending.is_empty() || matches!(transformed.node, AstNode::Parent(_)) {
562 return Err(coded(
563 "S0217",
564 "The parent operator % cannot be used at this point in the expression",
565 ));
566 }
567 // Depth 0 again: substitute_labels is a SEPARATE, independent counter --
568 // cycle 1+3's frames are gone by now (transform_node above already
569 // returned), so this is not a continuation of the depth above.
570 substitute_labels(transformed.node, &state, 0)
571}
572
573/// Final pass: rewrite every `AstNode::Parent(label)` in the tree to its
574/// canonical (alias-resolved) label. See `AncestryState` for why this is a
575/// separate pass rather than done inline. Mirrors `transform_children`'s
576/// traversal shape exactly (every composite node type), since by this point
577/// there's no error case left to handle -- the tree is already fully valid.
578fn substitute_labels(
579 node: AstNode,
580 state: &AncestryState,
581 depth: usize,
582) -> Result<AstNode, AstTransformError> {
583 check_label_substitution_depth(depth)?;
584 stacker::maybe_grow(
585 AST_TRANSFORM_RED_ZONE,
586 AST_TRANSFORM_GROW_STACK_SIZE,
587 || substitute_labels_impl(node, state, depth),
588 )
589}
590
591fn substitute_labels_impl(
592 node: AstNode,
593 state: &AncestryState,
594 depth: usize,
595) -> Result<AstNode, AstTransformError> {
596 let depth = depth + 1;
597 Ok(match node {
598 AstNode::Parent(label) => AstNode::Parent(state.canonical(&label)),
599 AstNode::Path { steps } => AstNode::Path {
600 steps: steps
601 .into_iter()
602 .map(|s| -> Result<PathStep, AstTransformError> {
603 Ok(PathStep {
604 node: substitute_labels(s.node, state, depth)?,
605 // Stages (predicates) can contain `%` references whose
606 // labels were aliased during resolution (e.g. a second
607 // predicate reusing a step an earlier one already tagged),
608 // so they must be substituted too -- otherwise the
609 // pre-alias label survives and evaluates against the wrong
610 // tuple key.
611 stages: s
612 .stages
613 .into_iter()
614 .map(|st| -> Result<Stage, AstTransformError> {
615 Ok(match st {
616 Stage::Filter(e) => Stage::Filter(Box::new(substitute_labels(
617 *e, state, depth,
618 )?)),
619 Stage::Index(v) => Stage::Index(v),
620 })
621 })
622 .collect::<Result<Vec<_>, _>>()?,
623 ..s
624 })
625 })
626 .collect::<Result<Vec<_>, _>>()?,
627 },
628 AstNode::Block(exprs) => AstNode::Block(
629 exprs
630 .into_iter()
631 .map(|e| substitute_labels(e, state, depth))
632 .collect::<Result<Vec<_>, _>>()?,
633 ),
634 AstNode::Binary { op, lhs, rhs } => AstNode::Binary {
635 op,
636 lhs: Box::new(substitute_labels(*lhs, state, depth)?),
637 rhs: Box::new(substitute_labels(*rhs, state, depth)?),
638 },
639 AstNode::Unary { op, operand } => AstNode::Unary {
640 op,
641 operand: Box::new(substitute_labels(*operand, state, depth)?),
642 },
643 AstNode::Array(elements) => AstNode::Array(
644 elements
645 .into_iter()
646 .map(|e| substitute_labels(e, state, depth))
647 .collect::<Result<Vec<_>, _>>()?,
648 ),
649 AstNode::Function {
650 name,
651 args,
652 is_builtin,
653 } => AstNode::Function {
654 name,
655 args: args
656 .into_iter()
657 .map(|a| substitute_labels(a, state, depth))
658 .collect::<Result<Vec<_>, _>>()?,
659 is_builtin,
660 },
661 AstNode::Call { procedure, args } => AstNode::Call {
662 procedure: Box::new(substitute_labels(*procedure, state, depth)?),
663 args: args
664 .into_iter()
665 .map(|a| substitute_labels(a, state, depth))
666 .collect::<Result<Vec<_>, _>>()?,
667 },
668 AstNode::Lambda {
669 params,
670 body,
671 signature,
672 thunk,
673 } => AstNode::Lambda {
674 params,
675 body: Box::new(substitute_labels(*body, state, depth)?),
676 signature,
677 thunk,
678 },
679 AstNode::Object(pairs) => AstNode::Object(
680 pairs
681 .into_iter()
682 .map(|(k, v)| -> Result<(AstNode, AstNode), AstTransformError> {
683 Ok((
684 substitute_labels(k, state, depth)?,
685 substitute_labels(v, state, depth)?,
686 ))
687 })
688 .collect::<Result<Vec<_>, _>>()?,
689 ),
690 AstNode::ObjectTransform { input, pattern } => AstNode::ObjectTransform {
691 input: Box::new(substitute_labels(*input, state, depth)?),
692 pattern: pattern
693 .into_iter()
694 .map(|(k, v)| -> Result<(AstNode, AstNode), AstTransformError> {
695 Ok((
696 substitute_labels(k, state, depth)?,
697 substitute_labels(v, state, depth)?,
698 ))
699 })
700 .collect::<Result<Vec<_>, _>>()?,
701 },
702 AstNode::Conditional {
703 condition,
704 then_branch,
705 else_branch,
706 } => AstNode::Conditional {
707 condition: Box::new(substitute_labels(*condition, state, depth)?),
708 then_branch: Box::new(substitute_labels(*then_branch, state, depth)?),
709 else_branch: match else_branch {
710 Some(e) => Some(Box::new(substitute_labels(*e, state, depth)?)),
711 None => None,
712 },
713 },
714 AstNode::Sort { input, terms } => AstNode::Sort {
715 input: Box::new(substitute_labels(*input, state, depth)?),
716 terms: terms
717 .into_iter()
718 .map(|(e, asc)| -> Result<(AstNode, bool), AstTransformError> {
719 Ok((substitute_labels(e, state, depth)?, asc))
720 })
721 .collect::<Result<Vec<_>, _>>()?,
722 },
723 AstNode::Transform {
724 location,
725 update,
726 delete,
727 } => AstNode::Transform {
728 location: Box::new(substitute_labels(*location, state, depth)?),
729 update: Box::new(substitute_labels(*update, state, depth)?),
730 delete: match delete {
731 Some(d) => Some(Box::new(substitute_labels(*d, state, depth)?)),
732 None => None,
733 },
734 },
735 AstNode::FunctionApplication(inner) => {
736 AstNode::FunctionApplication(Box::new(substitute_labels(*inner, state, depth)?))
737 }
738 AstNode::ArrayGroup(elements) => AstNode::ArrayGroup(
739 elements
740 .into_iter()
741 .map(|e| substitute_labels(e, state, depth))
742 .collect::<Result<Vec<_>, _>>()?,
743 ),
744 AstNode::Predicate(inner) => {
745 AstNode::Predicate(Box::new(substitute_labels(*inner, state, depth)?))
746 }
747 // Leaf nodes and everything else pass through unchanged.
748 other => other,
749 })
750}
751
752/// A raw parse-time binding marker (`@$var` or `#$var`) that still needs to
753/// be migrated into `PathStep.focus`/`PathStep.index_var` + `is_tuple`.
754/// Shared between the "marker nested inside an existing `PathStep`" case
755/// (`migrate_binding_markers`) and the "marker is the top-level/raw node
756/// itself" case (`wrap_marker_as_path`), so the stamping logic itself lives
757/// in exactly one place: `apply_marker_to_step`.
758enum BindingMarker {
759 Focus(String),
760 Index(String),
761}
762
763/// Stamp a binding marker onto a step: sets `focus` or `index_var` (per the
764/// marker kind) and `is_tuple = true`. The single place that knows how a
765/// marker maps onto `PathStep` fields.
766fn apply_marker_to_step(step: &mut PathStep, marker: BindingMarker) {
767 match marker {
768 BindingMarker::Focus(var_name) => step.focus = Some(var_name),
769 BindingMarker::Index(var_name) => step.index_var = Some(var_name),
770 }
771 step.is_tuple = true;
772}
773
774/// Core shared logic for both call sites that need to migrate a `@$var`/
775/// `#$var` marker: given the already-`transform_node`-recursed `lhs`/`input`
776/// that the marker was parsed against, produce the flat sequence of
777/// `PathStep`s the marker should resolve to, plus whatever pending ancestor
778/// references bubbled up from transforming that `lhs`/`input`.
779///
780/// Mirrors jsonata-js's `processAST` `case '@'`/`case '#'`:
781/// `result = processAST(expr.lhs); step = result; if (result.type ===
782/// 'path') { step = result.steps[result.steps.length - 1]; }` -- `result`
783/// (the possibly-multi-step path) is always what gets kept/spliced in, and
784/// only `step` (the thing that gets the marker's flags stamped onto it) is
785/// reassigned to the LAST step of that path when `result` is itself a path.
786/// Note jsonata-js's `@`/`#` cases do NOT call `pushAncestry` on the lhs --
787/// we deviate slightly (forwarding the lhs's pending through as this
788/// marker's own pending) since dropping it silently seems more surprising
789/// than propagating it, and no test data combines `%` with `@`/`#` closely
790/// enough to distinguish the two choices.
791///
792/// - If `transformed` is a multi-step `Path`, the marker's flags land on its
793/// LAST step, and ALL of its steps are returned to be spliced into the
794/// caller's flat steps list (never wrapped in a new outer step).
795/// - Otherwise (e.g. a bare `Name` with no `.` at all), wrap it into a new
796/// single-step `Path` and stamp the marker onto that one step.
797///
798/// S0215/S0216 validation for `@` (focus binding only -- `#`/index binding
799/// has no such restriction in jsonata-js): the target must not already have
800/// predicates/stages attached, and must not itself be a `Sort` node.
801fn check_focus_bind_target(
802 marker: &BindingMarker,
803 target_stages: &[crate::ast::Stage],
804 target_node: &AstNode,
805) -> Result<(), AstTransformError> {
806 if !matches!(marker, BindingMarker::Focus(_)) {
807 return Ok(());
808 }
809 if !target_stages.is_empty() {
810 return Err(coded(
811 "S0215",
812 "A context variable binding must precede any predicates on a step",
813 ));
814 }
815 if matches!(target_node, AstNode::Sort { .. }) {
816 return Err(coded(
817 "S0216",
818 "A context variable binding must precede the 'order-by' clause on a step",
819 ));
820 }
821 Ok(())
822}
823///
824/// Fallible because `@` (focus binding) specifically -- not `#` -- rejects
825/// being applied to a step that already has predicates/stages (S0215) or
826/// that is itself a sort step (S0216), mirroring jsonata-js's `case '@'`
827/// checks (parser.js ~L1183-1199): `step = result; if (result.type ===
828/// 'path') { step = result.steps[...length-1]; }` -- note `step` can be the
829/// bare (non-Path) `result` itself, e.g. `Account.Order^(...)@$o.Product`
830/// parses `Account.Order^(...)` into a bare top-level `Sort` node (not
831/// wrapped in a Path) *before* `@$o` wraps around it, so the S0216 check
832/// must inspect the raw `other` node too, not just a `Path`'s last step.
833fn splice_marker_steps(
834 transformed: Transformed,
835 marker: BindingMarker,
836) -> Result<(Vec<PathStep>, Vec<PendingAncestor>), AstTransformError> {
837 let Transformed { node, pending } = transformed;
838 let steps = match node {
839 AstNode::Path { mut steps } => {
840 // Our parser encodes `$[[1..4]]` (and any `expr[pred]`) as a separate
841 // trailing `Predicate` step rather than a step carrying the predicate
842 // as a `stage` (as jsonata-js does). For an index marker, mirror
843 // jsonata's `#` case (parser.js ~L1206-1223: when the target step
844 // already has stages, PUSH an index stage) by folding those trailing
845 // predicate pseudo-steps into the preceding real step's stages, then
846 // stamping the index on that step. This makes `$[[1..4]]#$pos[$pos>=2]`
847 // apply the `[[1..4]]` filter, then number the survivors, then filter
848 // by `$pos` -- rather than crashing on a `Predicate` step node in
849 // create_tuple_stream.
850 if matches!(marker, BindingMarker::Index(_)) {
851 while steps.len() >= 2
852 && matches!(steps.last().map(|s| &s.node), Some(AstNode::Predicate(_)))
853 {
854 let pred = steps.pop().unwrap();
855 if let AstNode::Predicate(inner) = pred.node {
856 steps.last_mut().unwrap().stages.push(Stage::Filter(inner));
857 }
858 }
859 }
860 if let Some(last) = steps.last_mut() {
861 check_focus_bind_target(&marker, &last.stages, &last.node)?;
862 // A SECOND index binding on the same step (e.g. `books#$ib[...]#$ib2`)
863 // must not overwrite the first: append it as an ordered index
864 // stage so it numbers the post-filter positions (jsonata's `#`
865 // case pushing an index stage when the step already has one).
866 if let (BindingMarker::Index(var), true) = (&marker, last.index_var.is_some()) {
867 last.stages.push(Stage::Index(var.clone()));
868 last.is_tuple = true;
869 } else {
870 apply_marker_to_step(last, marker);
871 }
872 }
873 steps
874 }
875 other => {
876 check_focus_bind_target(&marker, &[], &other)?;
877 let mut step = PathStep::new(other);
878 apply_marker_to_step(&mut step, marker);
879 vec![step]
880 }
881 };
882 Ok((steps, pending))
883}
884
885/// Handle a `@$var`/`#$var` marker reaching `transform_node` as the raw node
886/// itself (not already nested inside a `PathStep`) -- e.g. `Order@$o` or
887/// `Account.Order@$o` where the parser's flat infix loop has already merged
888/// any preceding `.` steps into a `Path` (or, for a single bare name, left a
889/// non-Path leaf) *before* wrapping the whole thing in the marker node. At
890/// this (top-level) call site there's no outer steps list to splice into, so
891/// the spliced steps become the whole resulting `Path`.
892fn wrap_marker_as_path(
893 transformed: Transformed,
894 marker: BindingMarker,
895) -> Result<Transformed, AstTransformError> {
896 let (steps, pending) = splice_marker_steps(transformed, marker)?;
897 Ok(Transformed {
898 node: AstNode::Path { steps },
899 pending,
900 })
901}
902
903/// Recursively rebuild `node`, resolving any `%`/`@`/`#` found within.
904/// Mirrors jsonata-js's processAST's generic per-node-type dispatch.
905///
906/// `depth` is the shared cycle-1+cycle-3 counter (see the module doc comment
907/// and the constants above `coded`) -- every recursive call anywhere in
908/// cycle 1 OR cycle 3 must pass `depth + 1`, never a fresh `0`.
909fn transform_node(
910 node: AstNode,
911 state: &mut AncestryState,
912 depth: usize,
913) -> Result<Transformed, AstTransformError> {
914 if depth > MAX_TRANSFORM_DEPTH {
915 // `node` may still hold an enormous unprocessed remainder here --
916 // see the doc comment above `push_ast_node_children` for why this
917 // can't just be allowed to drop normally.
918 drop_ast_node_iteratively(node);
919 return Err(max_transform_depth_error());
920 }
921 stacker::maybe_grow(
922 AST_TRANSFORM_RED_ZONE,
923 AST_TRANSFORM_GROW_STACK_SIZE,
924 || transform_node_impl(node, state, depth),
925 )
926}
927
928fn transform_node_impl(
929 node: AstNode,
930 state: &mut AncestryState,
931 depth: usize,
932) -> Result<Transformed, AstTransformError> {
933 let depth = depth + 1;
934 match node {
935 AstNode::Path { steps } => {
936 let (transformed_steps, pending) = transform_path_steps(steps, state, depth)?;
937 Ok(Transformed {
938 node: AstNode::Path {
939 steps: transformed_steps,
940 },
941 pending,
942 })
943 }
944 AstNode::Block(exprs) => {
945 let mut pending = Vec::new();
946 let mut transformed_exprs = Vec::with_capacity(exprs.len());
947 for e in exprs {
948 let t = transform_node(e, state, depth)?;
949 pending.extend(t.pending);
950 transformed_exprs.push(t.node);
951 }
952 Ok(Transformed {
953 node: AstNode::Block(transformed_exprs),
954 pending,
955 })
956 }
957 // A bare `%` -- mirrors jsonata-js's `case 'parent'`, which assigns
958 // a fresh slot the MOMENT any recursive processAST call first sees a
959 // 'parent'-type node (not just at the top of transform_node), i.e.
960 // eagerly, before any backward walk starts. The one pending
961 // reference this produces starts at level 1 (its own immediately
962 // preceding step); `%.%` chains extend the level as the backward
963 // walk crosses further `%` steps (see `seek_parent_step`).
964 AstNode::Parent(_) => {
965 let label = state.fresh_label();
966 Ok(Transformed {
967 node: AstNode::Parent(label.clone()),
968 pending: vec![PendingAncestor { label, level: 1 }],
969 })
970 }
971 // `@$var` reaching transform_node as the raw top-level node itself
972 // (not nested inside an existing PathStep) -- e.g. `Order@$o` or
973 // `Account.Order@$o`, where the parser's flat infix loop applies `@`
974 // to the already-built lhs (a Path, or a bare leaf if there was no
975 // `.` at all) rather than to a single step. See `wrap_marker_as_path`.
976 AstNode::Binary {
977 op: BinaryOp::FocusBind,
978 lhs,
979 rhs,
980 } => {
981 let var_name = match *rhs {
982 AstNode::Variable(name) => name,
983 _ => unreachable!("parser guarantees FocusBind's rhs is always Variable"),
984 };
985 let transformed_lhs = transform_node(*lhs, state, depth)?;
986 wrap_marker_as_path(transformed_lhs, BindingMarker::Focus(var_name))
987 }
988 // Same story as FocusBind above, but for bare top-level `#$var`
989 // (now represented the same generic way as FocusBind -- see
990 // BinaryOp::IndexBind's doc comment in ast.rs).
991 AstNode::Binary {
992 op: BinaryOp::IndexBind,
993 lhs,
994 rhs,
995 } => {
996 let var_name = match *rhs {
997 AstNode::Variable(name) => name,
998 _ => unreachable!("parser guarantees IndexBind's rhs is always Variable"),
999 };
1000 let transformed_lhs = transform_node(*lhs, state, depth)?;
1001 wrap_marker_as_path(transformed_lhs, BindingMarker::Index(var_name))
1002 }
1003 // Recurse into every other node's children unchanged (no ancestor
1004 // resolution needed for nodes that aren't paths/blocks/parent refs).
1005 other => transform_children(other, state, depth),
1006 }
1007}
1008
1009/// Recurse into a node's child expressions without any path-specific
1010/// ancestor logic (used for node types that can't themselves be paths),
1011/// aggregating pending ancestor references from every child -- mirrors
1012/// jsonata-js's per-case `pushAncestry` calls in processAST.
1013///
1014/// Two deliberate asymmetries with the generic "bubble everything" rule,
1015/// both matching jsonata-js exactly:
1016/// - `Call`'s `procedure` does NOT bubble (only `args` do) -- jsonata-js's
1017/// function/partial case never calls `pushAncestry` on `result.procedure`.
1018/// - `Lambda`'s `body` does NOT bubble at all -- jsonata-js's lambda case
1019/// has no `pushAncestry` call for the body. A `%` inside a lambda body
1020/// refers to that lambda's OWN invocation-time ancestry chain (irrelevant
1021/// at definition/parse time), so it's correctly not resolved here; it
1022/// simply remains an inert `AstNode::Parent(label)` in the body until the
1023/// lambda is invoked (matching jsonata-js: `function(){%}` parses fine,
1024/// with the raw `%` untouched inside the body).
1025///
1026/// `depth` follows the same shared cycle-1+cycle-3 convention as
1027/// `transform_node` (see its doc comment) -- `transform_children` is a full
1028/// participant of cycle 1, so it checks depth itself rather than relying
1029/// solely on `transform_node`'s check.
1030fn transform_children(
1031 node: AstNode,
1032 state: &mut AncestryState,
1033 depth: usize,
1034) -> Result<Transformed, AstTransformError> {
1035 if depth > MAX_TRANSFORM_DEPTH {
1036 // See transform_node's identical check: `node` may still hold an
1037 // enormous unprocessed remainder here.
1038 drop_ast_node_iteratively(node);
1039 return Err(max_transform_depth_error());
1040 }
1041 stacker::maybe_grow(
1042 AST_TRANSFORM_RED_ZONE,
1043 AST_TRANSFORM_GROW_STACK_SIZE,
1044 || transform_children_impl(node, state, depth),
1045 )
1046}
1047
1048fn transform_children_impl(
1049 node: AstNode,
1050 state: &mut AncestryState,
1051 depth: usize,
1052) -> Result<Transformed, AstTransformError> {
1053 let depth = depth + 1;
1054 match node {
1055 AstNode::Binary { op, lhs, rhs } => {
1056 let lhs_t = transform_node(*lhs, state, depth)?;
1057 let rhs_t = transform_node(*rhs, state, depth)?;
1058 let mut pending = lhs_t.pending;
1059 pending.extend(rhs_t.pending);
1060 Ok(Transformed {
1061 node: AstNode::Binary {
1062 op,
1063 lhs: Box::new(lhs_t.node),
1064 rhs: Box::new(rhs_t.node),
1065 },
1066 pending,
1067 })
1068 }
1069 AstNode::Unary { op, operand } => {
1070 let t = transform_node(*operand, state, depth)?;
1071 Ok(Transformed {
1072 node: AstNode::Unary {
1073 op,
1074 operand: Box::new(t.node),
1075 },
1076 pending: t.pending,
1077 })
1078 }
1079 AstNode::Array(elements) => {
1080 let mut pending = Vec::new();
1081 let mut transformed = Vec::with_capacity(elements.len());
1082 for e in elements {
1083 let t = transform_node(e, state, depth)?;
1084 pending.extend(t.pending);
1085 transformed.push(t.node);
1086 }
1087 Ok(Transformed {
1088 node: AstNode::Array(transformed),
1089 pending,
1090 })
1091 }
1092 AstNode::Function {
1093 name,
1094 args,
1095 is_builtin,
1096 } => {
1097 let mut pending = Vec::new();
1098 let mut transformed = Vec::with_capacity(args.len());
1099 for a in args {
1100 let t = transform_node(a, state, depth)?;
1101 pending.extend(t.pending);
1102 transformed.push(t.node);
1103 }
1104 Ok(Transformed {
1105 node: AstNode::Function {
1106 name,
1107 args: transformed,
1108 is_builtin,
1109 },
1110 pending,
1111 })
1112 }
1113 AstNode::Call { procedure, args } => {
1114 // Only args bubble (see doc comment above) -- procedure is
1115 // still structurally transformed, just doesn't contribute to
1116 // this Call's own pending.
1117 let procedure_t = transform_node(*procedure, state, depth)?;
1118 let mut pending = Vec::new();
1119 let mut transformed_args = Vec::with_capacity(args.len());
1120 for a in args {
1121 let t = transform_node(a, state, depth)?;
1122 pending.extend(t.pending);
1123 transformed_args.push(t.node);
1124 }
1125 Ok(Transformed {
1126 node: AstNode::Call {
1127 procedure: Box::new(procedure_t.node),
1128 args: transformed_args,
1129 },
1130 pending,
1131 })
1132 }
1133 AstNode::Lambda {
1134 params,
1135 body,
1136 signature,
1137 thunk,
1138 } => {
1139 // body's pending is deliberately dropped -- see doc comment above.
1140 let body_t = transform_node(*body, state, depth)?;
1141 Ok(Transformed::leaf(AstNode::Lambda {
1142 params,
1143 body: Box::new(body_t.node),
1144 signature,
1145 thunk,
1146 }))
1147 }
1148 AstNode::Object(pairs) => {
1149 let mut pending = Vec::new();
1150 let mut transformed = Vec::with_capacity(pairs.len());
1151 for (k, v) in pairs {
1152 let k_t = transform_node(k, state, depth)?;
1153 pending.extend(k_t.pending);
1154 let v_t = transform_node(v, state, depth)?;
1155 pending.extend(v_t.pending);
1156 transformed.push((k_t.node, v_t.node));
1157 }
1158 Ok(Transformed {
1159 node: AstNode::Object(transformed),
1160 pending,
1161 })
1162 }
1163 AstNode::ObjectTransform { input, pattern } => {
1164 let input_t = transform_node(*input, state, depth)?;
1165 let mut pending = input_t.pending;
1166 let mut transformed_pattern = Vec::with_capacity(pattern.len());
1167 for (k, v) in pattern {
1168 let k_t = transform_node(k, state, depth)?;
1169 pending.extend(k_t.pending);
1170 let v_t = transform_node(v, state, depth)?;
1171 pending.extend(v_t.pending);
1172 transformed_pattern.push((k_t.node, v_t.node));
1173 }
1174 Ok(Transformed {
1175 node: AstNode::ObjectTransform {
1176 input: Box::new(input_t.node),
1177 pattern: transformed_pattern,
1178 },
1179 pending,
1180 })
1181 }
1182 AstNode::Conditional {
1183 condition,
1184 then_branch,
1185 else_branch,
1186 } => {
1187 let condition_t = transform_node(*condition, state, depth)?;
1188 let then_t = transform_node(*then_branch, state, depth)?;
1189 let mut pending = condition_t.pending;
1190 pending.extend(then_t.pending);
1191 let else_t = match else_branch {
1192 Some(e) => Some(transform_node(*e, state, depth)?),
1193 None => None,
1194 };
1195 let else_node = else_t.map(|t| {
1196 pending.extend(t.pending);
1197 Box::new(t.node)
1198 });
1199 Ok(Transformed {
1200 node: AstNode::Conditional {
1201 condition: Box::new(condition_t.node),
1202 then_branch: Box::new(then_t.node),
1203 else_branch: else_node,
1204 },
1205 pending,
1206 })
1207 }
1208 AstNode::Sort { input, terms } => {
1209 // Mirrors jsonata-js's `case '^'` (parser.js ~L1151-1170): the
1210 // sort is modeled as a synthetic `sort` step APPENDED to the
1211 // input path, each term's own seeking `%` slots are bubbled onto
1212 // it, then resolveAncestry walks them backward. Because the sort
1213 // step sits after every input step, resolveAncestry starts at the
1214 // step BEFORE it -- i.e. the LAST real input step -- so a level-1
1215 // term slot resolves against the last input step (no predicate-
1216 // style "resolve against the step itself" special case is needed;
1217 // it's a plain uniform backward walk over the input steps).
1218 let input_t = transform_node(*input, state, depth)?;
1219 let was_path = matches!(input_t.node, AstNode::Path { .. });
1220 // jsonata wraps a non-path input into a single-step path so the
1221 // sort step has something to walk back through. We do the same for
1222 // the walk, then unwrap again if nothing tagged the wrapped step.
1223 let mut steps = match input_t.node {
1224 AstNode::Path { steps } => steps,
1225 other => vec![PathStep::new(other)],
1226 };
1227 let mut pending = input_t.pending;
1228 let mut transformed_terms = Vec::with_capacity(terms.len());
1229 for (expr, asc) in terms {
1230 let t = transform_node(expr, state, depth)?;
1231 for slot in t.pending {
1232 // Cycle 3 entry point: this walk_backward call nests ON
1233 // TOP of transform_children's still-live frame, so it
1234 // gets `depth + 1` from the SAME shared counter, not a
1235 // fresh one (see the module doc comment's cycle-1/cycle-3
1236 // analysis).
1237 let remaining =
1238 walk_backward(&mut steps, &slot.label, slot.level, state, depth + 1)?;
1239 if remaining > 0 {
1240 pending.push(PendingAncestor {
1241 label: slot.label,
1242 level: remaining,
1243 });
1244 }
1245 }
1246 transformed_terms.push((t.node, asc));
1247 }
1248 let input_node = if was_path {
1249 AstNode::Path { steps }
1250 } else {
1251 // Single-node input: keep it wrapped only if a sort term
1252 // actually tagged it (so the ancestor label survives on a
1253 // PathStep); otherwise restore the bare node unchanged.
1254 let s = steps.pop().expect("single wrapped step");
1255 if s.is_tuple || s.ancestor_label.is_some() {
1256 AstNode::Path { steps: vec![s] }
1257 } else {
1258 s.node
1259 }
1260 };
1261 Ok(Transformed {
1262 node: AstNode::Sort {
1263 input: Box::new(input_node),
1264 terms: transformed_terms,
1265 },
1266 pending,
1267 })
1268 }
1269 AstNode::Transform {
1270 location,
1271 update,
1272 delete,
1273 } => {
1274 let location_t = transform_node(*location, state, depth)?;
1275 let update_t = transform_node(*update, state, depth)?;
1276 let mut pending = location_t.pending;
1277 pending.extend(update_t.pending);
1278 let delete_t = match delete {
1279 Some(d) => Some(transform_node(*d, state, depth)?),
1280 None => None,
1281 };
1282 let delete_node = delete_t.map(|t| {
1283 pending.extend(t.pending);
1284 Box::new(t.node)
1285 });
1286 Ok(Transformed {
1287 node: AstNode::Transform {
1288 location: Box::new(location_t.node),
1289 update: Box::new(update_t.node),
1290 delete: delete_node,
1291 },
1292 pending,
1293 })
1294 }
1295 AstNode::FunctionApplication(inner) => {
1296 let t = transform_node(*inner, state, depth)?;
1297 Ok(Transformed {
1298 node: AstNode::FunctionApplication(Box::new(t.node)),
1299 pending: t.pending,
1300 })
1301 }
1302 AstNode::ArrayGroup(elements) => {
1303 let mut pending = Vec::new();
1304 let mut transformed = Vec::with_capacity(elements.len());
1305 for e in elements {
1306 let t = transform_node(e, state, depth)?;
1307 pending.extend(t.pending);
1308 transformed.push(t.node);
1309 }
1310 Ok(Transformed {
1311 node: AstNode::ArrayGroup(transformed),
1312 pending,
1313 })
1314 }
1315 AstNode::Predicate(inner) => {
1316 let t = transform_node(*inner, state, depth)?;
1317 Ok(Transformed {
1318 node: AstNode::Predicate(Box::new(t.node)),
1319 pending: t.pending,
1320 })
1321 }
1322 // Leaf nodes and everything else pass through unchanged.
1323 other => Ok(Transformed::leaf(other)),
1324 }
1325}
1326
1327/// Resolve a path's steps: migrate `#`/`@` markers into step-level flags,
1328/// then walk backward resolving any `%`/`%.%` references, left to right in
1329/// step-encounter order. Mirrors resolveAncestry (parser.js ~L1002-1030),
1330/// collapsed from jsonata-js's incremental per-'.' invocation into a single
1331/// pass: our parser already flattens an entire dotted chain into one flat
1332/// `steps` list up front (unlike jsonata-js's nested binary '.' AST nodes,
1333/// processed one dot at a time), so resolving every step's own pending
1334/// reference against the FULL flattened list-so-far in left-to-right order
1335/// produces the same result as jsonata-js's incremental resolution.
1336///
1337/// `depth` follows the shared cycle-1+cycle-3 convention (see
1338/// `transform_node`'s doc comment) -- this function is a full cycle-1
1339/// participant (its own frame sits between `transform_node`'s Path arm and
1340/// the `migrate_binding_markers`/`transform_node`/`resolve_predicate_slot`/
1341/// `walk_backward` calls it makes), so it checks depth itself.
1342fn transform_path_steps(
1343 steps: Vec<PathStep>,
1344 state: &mut AncestryState,
1345 depth: usize,
1346) -> Result<(Vec<PathStep>, Vec<PendingAncestor>), AstTransformError> {
1347 if depth > MAX_TRANSFORM_DEPTH {
1348 // `steps` may still hold unprocessed steps whose own `.node`/
1349 // `.stages` are deeply nested -- see transform_node's identical
1350 // check and the doc comment above `push_ast_node_children`.
1351 drop_path_steps_iteratively(steps);
1352 return Err(max_transform_depth_error());
1353 }
1354 stacker::maybe_grow(
1355 AST_TRANSFORM_RED_ZONE,
1356 AST_TRANSFORM_GROW_STACK_SIZE,
1357 || transform_path_steps_impl(steps, state, depth),
1358 )
1359}
1360
1361fn transform_path_steps_impl(
1362 steps: Vec<PathStep>,
1363 state: &mut AncestryState,
1364 depth: usize,
1365) -> Result<(Vec<PathStep>, Vec<PendingAncestor>), AstTransformError> {
1366 let depth = depth + 1;
1367 // Pass 1: migrate #/@ into step flags, recursing into nested content
1368 // (which may itself bubble up pending `%` references, e.g. an object
1369 // constructor or array containing a `%`). `own_pending[i]` is whatever
1370 // pending arose from producing `resolved[i]` -- attached to the LAST
1371 // step of a marker's splice, since that's the step position pending
1372 // ancestor resolution should walk backward from.
1373 let mut resolved: Vec<PathStep> = Vec::with_capacity(steps.len());
1374 let mut own_pending: Vec<Vec<PendingAncestor>> = Vec::with_capacity(steps.len());
1375 // `pred_pending[i]` holds the seeking `%` slots bubbled up from step i's
1376 // own filter predicate(s) (`Stage::Filter`), transformed here so a `%`
1377 // inside `Product[%.OrderID=...]` is resolved (was previously left
1378 // untouched, since stages weren't recursed into). Transformed AFTER the
1379 // step's node so the step's OWN `%`-ness (if any) claims a label first,
1380 // matching jsonata-js's slot-creation order.
1381 let mut pred_pending: Vec<Vec<PendingAncestor>> = Vec::with_capacity(steps.len());
1382 for step in steps {
1383 // migrate_binding_markers itself never checks/increments (per the
1384 // module doc comment), but it forwards into transform_node, so it
1385 // still needs `depth + 1` to account for its own stack frame.
1386 let (spliced, pending) = migrate_binding_markers(step, state, depth)?;
1387 let last_idx = spliced.len().saturating_sub(1);
1388 let mut pending_opt = Some(pending);
1389 for (i, mut s) in spliced.into_iter().enumerate() {
1390 let mut pp: Vec<PendingAncestor> = Vec::new();
1391 let stages = std::mem::take(&mut s.stages);
1392 let mut new_stages = Vec::with_capacity(stages.len());
1393 for stage in stages {
1394 match stage {
1395 Stage::Filter(expr) => {
1396 let t = transform_node(*expr, state, depth)?;
1397 pp.extend(t.pending);
1398 new_stages.push(Stage::Filter(Box::new(t.node)));
1399 }
1400 // Index stages carry only a variable name -- nothing to
1401 // resolve/transform.
1402 Stage::Index(v) => new_stages.push(Stage::Index(v)),
1403 }
1404 }
1405 s.stages = new_stages;
1406 resolved.push(s);
1407 pred_pending.push(pp);
1408 if i == last_idx {
1409 own_pending.push(pending_opt.take().unwrap_or_default());
1410 } else {
1411 own_pending.push(Vec::new());
1412 }
1413 }
1414 }
1415
1416 // Pass 2: for each step (in ascending/encounter order), resolve first its
1417 // predicate slots (mirroring jsonata-js pushing predicate slots onto the
1418 // step's seekingParent BEFORE the step's own slot), then its own pending.
1419 // Any reference that runs off the front of this path (never finding a
1420 // target) bubbles up as this whole Path's own pending.
1421 //
1422 // Both resolve_predicate_slot and walk_backward here are cycle-3 entry
1423 // points nesting ON TOP of this still-live transform_path_steps frame --
1424 // `depth + 1` from the SAME shared counter, not a fresh one.
1425 let mut path_pending: Vec<PendingAncestor> = Vec::new();
1426 for i in 0..resolved.len() {
1427 for pending in std::mem::take(&mut pred_pending[i]) {
1428 let remaining = resolve_predicate_slot(
1429 &mut resolved,
1430 i,
1431 &pending.label,
1432 pending.level,
1433 state,
1434 depth + 1,
1435 )?;
1436 if remaining > 0 {
1437 path_pending.push(PendingAncestor {
1438 label: pending.label,
1439 level: remaining,
1440 });
1441 }
1442 }
1443 let pending_here = std::mem::take(&mut own_pending[i]);
1444 for pending in pending_here {
1445 let remaining = walk_backward(
1446 &mut resolved[..i],
1447 &pending.label,
1448 pending.level,
1449 state,
1450 depth + 1,
1451 )?;
1452 if remaining > 0 {
1453 path_pending.push(PendingAncestor {
1454 label: pending.label,
1455 level: remaining,
1456 });
1457 }
1458 }
1459 }
1460
1461 Ok((resolved, path_pending))
1462}
1463
1464/// Resolve one seeking `%` slot that bubbled up out of a filter predicate
1465/// attached to step `i`. Mirrors jsonata-js's `case '['` slot handling
1466/// (parser.js ~L1119-1128):
1467/// - a `level == 1` slot resolves against the attached step ITSELF first
1468/// (`seekParent(step, slot)`): a `name`/`wildcard` step gets tagged; a `%`
1469/// (parent) step instead bumps the level and the walk continues backward;
1470/// - a `level > 1` slot is decremented (the attached step is skipped, never
1471/// tagged) and resolved by walking backward through the steps BEFORE it.
1472///
1473/// Either way, whatever level remains unresolved is walked backward through
1474/// `resolved[..i]`; the leftover (if the reference runs off the path front)
1475/// is returned to bubble up as the enclosing path's own pending.
1476///
1477/// No depth CHECK of its own (per the module doc comment: this frame sits at
1478/// the base of cycle 3 each time but isn't itself part of a self-loop), but
1479/// it still must forward `depth + 1` -- accounting for its own stack frame --
1480/// into `seek_parent_step`/`walk_backward`, both shared-counter cycle-3
1481/// entry points.
1482fn resolve_predicate_slot(
1483 resolved: &mut [PathStep],
1484 i: usize,
1485 label: &str,
1486 level: usize,
1487 state: &mut AncestryState,
1488 depth: usize,
1489) -> Result<usize, AstTransformError> {
1490 // Split so the attached step (`rest[0]`) and the steps before it
1491 // (`prefix`) can be borrowed mutably at the same time.
1492 let (prefix, rest) = resolved.split_at_mut(i);
1493 let remaining = if level == 1 {
1494 seek_parent_step(&mut rest[0], label, 1, state, depth + 1)?
1495 } else {
1496 level - 1
1497 };
1498 if remaining == 0 {
1499 Ok(0)
1500 } else {
1501 walk_backward(prefix, label, remaining, state, depth + 1)
1502 }
1503}
1504
1505/// Walk backward through `steps` (from its last element) trying to resolve
1506/// a single pending ancestor reference at `level`. Returns the remaining
1507/// level: 0 means fully resolved (some step in `steps` was tagged); >0 means
1508/// `steps` ran out before the reference resolved, so the caller must keep
1509/// walking further back through whatever contains `steps` (or, if there is
1510/// nothing further back, treat it as still-pending / bubble it up).
1511///
1512/// `depth` is the SAME shared cycle-1+cycle-3 counter used by
1513/// `transform_node`/`transform_children`/`transform_path_steps` (see the
1514/// module doc comment) -- `walk_backward` is reached FROM live cycle-1
1515/// frames, so it must never reset to a fresh counter here.
1516fn walk_backward(
1517 steps: &mut [PathStep],
1518 label: &str,
1519 level: usize,
1520 state: &mut AncestryState,
1521 depth: usize,
1522) -> Result<usize, AstTransformError> {
1523 check_transform_depth(depth)?;
1524 stacker::maybe_grow(
1525 AST_TRANSFORM_RED_ZONE,
1526 AST_TRANSFORM_GROW_STACK_SIZE,
1527 || walk_backward_impl(steps, label, level, state, depth),
1528 )
1529}
1530
1531fn walk_backward_impl(
1532 steps: &mut [PathStep],
1533 label: &str,
1534 mut level: usize,
1535 state: &mut AncestryState,
1536 depth: usize,
1537) -> Result<usize, AstTransformError> {
1538 // Computed once, not per-iteration: the `while` loop below is bounded
1539 // iteration (each seek_parent_step call fully returns before the next
1540 // iteration starts, so their stack usage never overlaps) -- only the
1541 // *nesting* into seek_parent_step/seek_parent_wrapped's own recursion
1542 // adds a real stack frame, which is what `depth + 1` accounts for.
1543 let depth = depth + 1;
1544 let mut index = steps.len();
1545 while level > 0 {
1546 if index == 0 {
1547 return Ok(level);
1548 }
1549 index -= 1;
1550 // Skip filter-predicate pseudo-steps: our parser encodes `@$v[pred]`
1551 // and standalone `foo[pred]` chained after a marker as a separate
1552 // `Predicate` step, whereas jsonata-js carries the predicate as a
1553 // `stage` on the owning step (so it never appears as a distinct step in
1554 // resolveAncestry). A predicate is a filter, never an ancestor target,
1555 // so the backward ancestry walk steps over it -- without this, a `%`
1556 // after `books@$B[$L.isbn=$B.isbn]` hits the predicate step and wrongly
1557 // reports S0217.
1558 //
1559 // Then skip over a run of contiguous focus-bound (`@$var`) steps,
1560 // treating them as a SINGLE ancestor hop -- mirrors jsonata-js
1561 // resolveAncestry (parser.js ~L1023-1025): `while(index >= 0 &&
1562 // step.focus && path.steps[index].focus) { step = path.steps[index--] }`.
1563 // Because our extra `Predicate` steps sit between the focus steps (which
1564 // in jsonata are adjacent, the predicates being stages), the
1565 // focus-contiguity test must look through those predicate steps to the
1566 // previous REAL navigation step. So in
1567 // `library.loans@$L.books@$B[...].customers@$C[...].{ $keys(%.%) }` all
1568 // three focus steps collapse into one hop and `%.%` reaches the root.
1569 loop {
1570 while index > 0 && matches!(steps[index].node, AstNode::Predicate(_)) {
1571 index -= 1;
1572 }
1573 // Locate the previous non-predicate step (if any) to test contiguity.
1574 let mut prev = None;
1575 if index > 0 {
1576 let mut j = index - 1;
1577 loop {
1578 if !matches!(steps[j].node, AstNode::Predicate(_)) {
1579 prev = Some(j);
1580 break;
1581 }
1582 if j == 0 {
1583 break;
1584 }
1585 j -= 1;
1586 }
1587 }
1588 match prev {
1589 Some(p) if steps[index].focus.is_some() && steps[p].focus.is_some() => {
1590 index = p;
1591 }
1592 _ => break,
1593 }
1594 }
1595 level = seek_parent_step(&mut steps[index], label, level, state, depth)?;
1596 }
1597 Ok(0)
1598}
1599
1600/// Try to resolve one level of a pending ancestor reference against a
1601/// single candidate step. Returns the remaining level (0 = tagged here).
1602/// Mirrors jsonata-js's seekParent (parser.js ~L941-986).
1603///
1604/// `depth` is the same shared cycle-1+cycle-3 counter (see the module doc
1605/// comment).
1606fn seek_parent_step(
1607 step: &mut PathStep,
1608 label: &str,
1609 level: usize,
1610 state: &mut AncestryState,
1611 depth: usize,
1612) -> Result<usize, AstTransformError> {
1613 check_transform_depth(depth)?;
1614 stacker::maybe_grow(
1615 AST_TRANSFORM_RED_ZONE,
1616 AST_TRANSFORM_GROW_STACK_SIZE,
1617 || seek_parent_step_impl(step, label, level, state, depth),
1618 )
1619}
1620
1621fn seek_parent_step_impl(
1622 step: &mut PathStep,
1623 label: &str,
1624 level: usize,
1625 state: &mut AncestryState,
1626 depth: usize,
1627) -> Result<usize, AstTransformError> {
1628 let depth = depth + 1;
1629 match &mut step.node {
1630 AstNode::Name(_) | AstNode::Wildcard => {
1631 let remaining = level - 1;
1632 if remaining == 0 {
1633 match &step.ancestor_label {
1634 // Reuse: an earlier `%` already tagged this exact step.
1635 // Record the alias instead of overwriting (see
1636 // AncestryState's doc comment).
1637 Some(existing) => {
1638 state.aliases.insert(label.to_string(), existing.clone());
1639 }
1640 None => {
1641 step.ancestor_label = Some(label.to_string());
1642 }
1643 }
1644 step.is_tuple = true;
1645 }
1646 Ok(remaining)
1647 }
1648 // Chained %.%: this step is itself another (already independently
1649 // resolved-or-pending) `%` -- extend the level and keep walking
1650 // further back, exactly mirroring seekParent's `case 'parent':
1651 // slot.level++` (which notably does NOT set `.tuple` here).
1652 AstNode::Parent(_) => Ok(level + 1),
1653 // Parenthesized sub-path as a path step (e.g. `Account.(Order.Product).%`
1654 // parses `(Order.Product)` as `FunctionApplication(Path{...})`) --
1655 // mirrors seekParent's 'block'/'path' cases layered together: this
1656 // outer step becomes tuple-producing regardless of where inside the
1657 // parens the actual ancestor tag lands, and we recurse inward to
1658 // find it.
1659 AstNode::FunctionApplication(inner) => {
1660 step.is_tuple = true;
1661 seek_parent_wrapped(inner.as_mut(), label, level, state, depth)
1662 }
1663 // A parenthesized block reached directly as a path step (e.g. a
1664 // leading `(Account.Order)` with no `.` before it, or a multi-
1665 // statement `(...)`) -- mirrors seekParent's 'block' case: recurse
1666 // into the LAST expression.
1667 AstNode::Block(exprs) => match exprs.last_mut() {
1668 Some(last) => {
1669 step.is_tuple = true;
1670 seek_parent_wrapped(last, label, level, state, depth)
1671 }
1672 // An empty block `()` produces no ancestor and no tuple; the walk
1673 // simply steps over it with the level unchanged (mirrors jsonata-js
1674 // seekParent's `if(node.expressions.length > 0)` guard, which leaves
1675 // the slot untouched for an empty block). Lets `Account.Order.().%`
1676 // resolve `%` against `Order` rather than raising S0217.
1677 None => Ok(level),
1678 },
1679 _ => Err(coded(
1680 "S0217",
1681 "The parent operator % cannot derive an ancestor from this kind of path step",
1682 )),
1683 }
1684}
1685
1686/// Recurse into a "wrapped" target (a `FunctionApplication`'s sole inner
1687/// expression, or a `Block`'s last expression) that must itself resolve to
1688/// a nested `Path` for us to walk backward through it -- mirrors how
1689/// jsonata-js's block/path seekParent cases can be layered on top of each
1690/// other for doubly-nested parens (e.g. `Account.(Order.(Product)).%`).
1691/// Anything else (a literal, a function call, ...) can't derive an
1692/// ancestor: S0217.
1693///
1694/// `depth` is the same shared cycle-1+cycle-3 counter (see the module doc
1695/// comment).
1696fn seek_parent_wrapped(
1697 node: &mut AstNode,
1698 label: &str,
1699 level: usize,
1700 state: &mut AncestryState,
1701 depth: usize,
1702) -> Result<usize, AstTransformError> {
1703 check_transform_depth(depth)?;
1704 stacker::maybe_grow(
1705 AST_TRANSFORM_RED_ZONE,
1706 AST_TRANSFORM_GROW_STACK_SIZE,
1707 || seek_parent_wrapped_impl(node, label, level, state, depth),
1708 )
1709}
1710
1711fn seek_parent_wrapped_impl(
1712 node: &mut AstNode,
1713 label: &str,
1714 level: usize,
1715 state: &mut AncestryState,
1716 depth: usize,
1717) -> Result<usize, AstTransformError> {
1718 let depth = depth + 1;
1719 match node {
1720 AstNode::Path { steps } => walk_backward(steps, label, level, state, depth),
1721 // A nested block (e.g. the inner `()` of `.()`, or `(a; b)`): recurse
1722 // into its last expression, or -- for an empty block -- step over it
1723 // leaving the level unchanged (jsonata-js seekParent's block guard).
1724 AstNode::Block(exprs) => match exprs.last_mut() {
1725 Some(last) => seek_parent_wrapped(last, label, level, state, depth),
1726 None => Ok(level),
1727 },
1728 _ => Err(coded(
1729 "S0217",
1730 "The parent operator % cannot derive an ancestor from this kind of expression",
1731 )),
1732 }
1733}
1734
1735/// Convert a step's raw-parse-time binding marker (if any) into the unified
1736/// PathStep flags, recursing into the step's own node first (a step's node
1737/// can itself be a Block/nested Path containing `%`/`@`/`#`).
1738///
1739/// Returns a `Vec` (not a single `PathStep`) because a marker's `lhs`/`input`
1740/// can itself turn out to be a multi-step `Path` -- see `splice_marker_steps`
1741/// -- in which case ALL of those steps must be spliced into the caller's
1742/// flat list in place of this one input step, with the marker's flags
1743/// stamped onto the LAST of them (not onto a step wrapping the whole thing).
1744/// Also returns whatever pending ancestor references bubbled up from
1745/// transforming this step's content.
1746///
1747/// No depth CHECK of its own (per the module doc comment: not itself
1748/// self-recursive), but it closes the transform_path_steps -> transform_node
1749/// cycle, so `depth + 1` (accounting for its own stack frame) must still be
1750/// forwarded into `transform_node`.
1751fn migrate_binding_markers(
1752 mut step: PathStep,
1753 state: &mut AncestryState,
1754 depth: usize,
1755) -> Result<(Vec<PathStep>, Vec<PendingAncestor>), AstTransformError> {
1756 match step.node {
1757 AstNode::Binary {
1758 op: BinaryOp::FocusBind,
1759 lhs,
1760 rhs,
1761 } => {
1762 let var_name = match *rhs {
1763 AstNode::Variable(name) => name,
1764 _ => unreachable!("parser guarantees FocusBind's rhs is always Variable"),
1765 };
1766 match transform_node(*lhs, state, depth + 1) {
1767 Ok(transformed_lhs) => {
1768 splice_marker_steps(transformed_lhs, BindingMarker::Focus(var_name))
1769 }
1770 // `step.node` (the `Binary{FocusBind, lhs, rhs}`) was already
1771 // consumed by the match above, but `step.stages` (this
1772 // step's own predicate expressions, untouched so far) is
1773 // still owned here -- see push_ast_node_children's doc
1774 // comment for why it can't just be allowed to drop normally.
1775 Err(e) => {
1776 drop_stages_iteratively(std::mem::take(&mut step.stages));
1777 Err(e)
1778 }
1779 }
1780 }
1781 AstNode::Binary {
1782 op: BinaryOp::IndexBind,
1783 lhs,
1784 rhs,
1785 } => {
1786 let var_name = match *rhs {
1787 AstNode::Variable(name) => name,
1788 _ => unreachable!("parser guarantees IndexBind's rhs is always Variable"),
1789 };
1790 match transform_node(*lhs, state, depth + 1) {
1791 Ok(transformed_lhs) => {
1792 splice_marker_steps(transformed_lhs, BindingMarker::Index(var_name))
1793 }
1794 Err(e) => {
1795 drop_stages_iteratively(std::mem::take(&mut step.stages));
1796 Err(e)
1797 }
1798 }
1799 }
1800 other => match transform_node(other, state, depth + 1) {
1801 Ok(t) => {
1802 step.node = t.node;
1803 Ok((vec![step], t.pending))
1804 }
1805 Err(e) => {
1806 drop_stages_iteratively(std::mem::take(&mut step.stages));
1807 Err(e)
1808 }
1809 },
1810 }
1811}
1812
1813#[cfg(test)]
1814mod tests {
1815 use super::*;
1816
1817 // --- Task 6: `%` inside filter predicates and sort terms ---
1818 //
1819 // Mechanism ported from jsonata-js processAST (parser.js). Ground truth
1820 // for every tag target below was dumped from jsonata-js's own `.ast()`
1821 // (via `node -e 'jsonata(expr).ast()'` in tests/jsonata-js).
1822 //
1823 // PREDICATE (`case '['`, parser.js ~L1097-1130): each slot the predicate
1824 // is still seeking is examined -- a level-1 slot resolves against the
1825 // STEP the predicate is attached to (`seekParent(step, slot)`, which tags
1826 // that step, or bumps the level if the step is itself a `%`); a level>N>1
1827 // slot is decremented and then resolved by walking backward through the
1828 // steps BEFORE the attached step. In our flat-path model this is: for a
1829 // predicate slot on step i, level==1 -> seek_parent_step(resolved[i]);
1830 // level>1 -> walk_backward(resolved[..i], level-1).
1831 //
1832 // SORT (`case '^'`, parser.js ~L1151-1170): jsonata appends a synthetic
1833 // `sort` step to the input path, bubbles every term's own seeking slots
1834 // onto it, then runs resolveAncestry -- which walks backward starting at
1835 // the step BEFORE the sort step, i.e. the LAST real input step. So a
1836 // level-1 sort-term slot resolves against the last input step (no
1837 // predicate-style "attach to the step itself" special case is needed;
1838 // it's a uniform backward walk over the input steps).
1839
1840 // Helper: locate the ancestor_label a resolved path assigns to a given
1841 // step index, panicking with context if the shape is wrong.
1842 fn resolve_path(expr: &str) -> Vec<PathStep> {
1843 let ast = crate::parser::Parser::new(expr.to_string())
1844 .unwrap()
1845 .parse()
1846 .unwrap();
1847 match resolve_ancestry(ast).unwrap() {
1848 AstNode::Path { steps } => steps,
1849 other => panic!("expected Path, got {:?}", other),
1850 }
1851 }
1852
1853 #[test]
1854 fn test_parent_inside_predicate_resolves_against_enclosing_step() {
1855 // Account.Order.Product[%.OrderID='order104'].SKU
1856 // Ground truth (jsonata-js .ast()): the `%` inside the predicate
1857 // tags the Product step (steps[2]) -- i.e. `%` resolves to Product's
1858 // own input (Order), and Product itself carries the ancestor label.
1859 let steps = resolve_path("Account.Order.Product[%.OrderID='order104'].SKU");
1860 assert_eq!(steps.len(), 4);
1861 assert!(matches!(steps[2].node, AstNode::Name(ref n) if n == "Product"));
1862 let product_label = steps[2].ancestor_label.clone();
1863 assert!(product_label.is_some(), "Product must be tagged");
1864 assert!(steps[2].is_tuple);
1865 assert!(
1866 steps[1].ancestor_label.is_none(),
1867 "Order must NOT be tagged"
1868 );
1869 // The `%` inside the predicate must carry Product's label.
1870 match &steps[2].stages[0] {
1871 Stage::Index(_) => unreachable!("no index stage in this test"),
1872 Stage::Filter(expr) => match expr.as_ref() {
1873 AstNode::Binary { lhs, .. } => match lhs.as_ref() {
1874 AstNode::Path { steps: inner } => match &inner[0].node {
1875 AstNode::Parent(label) => {
1876 assert_eq!(Some(label.clone()), product_label)
1877 }
1878 other => panic!("expected Parent, got {:?}", other),
1879 },
1880 other => panic!("expected inner Path, got {:?}", other),
1881 },
1882 other => panic!("expected Binary, got {:?}", other),
1883 },
1884 }
1885 }
1886
1887 #[test]
1888 fn test_parent_chain_inside_predicate_resolves_two_levels() {
1889 // Account.Order.Product[%.%.`Account Name`='Firefly'].SKU
1890 // Ground truth: first `%` tags Product (steps[2]), second `%` tags
1891 // Order (steps[1]).
1892 let steps = resolve_path("Account.Order.Product[%.%.`Account Name`='Firefly'].SKU");
1893 assert_eq!(steps.len(), 4);
1894 let product_label = steps[2].ancestor_label.clone();
1895 let order_label = steps[1].ancestor_label.clone();
1896 assert!(product_label.is_some(), "Product must be tagged");
1897 assert!(order_label.is_some(), "Order must be tagged");
1898 assert_ne!(product_label, order_label);
1899 match &steps[2].stages[0] {
1900 Stage::Index(_) => unreachable!("no index stage in this test"),
1901 Stage::Filter(expr) => match expr.as_ref() {
1902 AstNode::Binary { lhs, .. } => match lhs.as_ref() {
1903 AstNode::Path { steps: inner } => {
1904 // inner = [Parent, Parent, Name("Account Name")]
1905 match &inner[0].node {
1906 AstNode::Parent(l) => assert_eq!(Some(l.clone()), product_label),
1907 other => panic!("expected Parent, got {:?}", other),
1908 }
1909 match &inner[1].node {
1910 AstNode::Parent(l) => assert_eq!(Some(l.clone()), order_label),
1911 other => panic!("expected Parent, got {:?}", other),
1912 }
1913 }
1914 other => panic!("expected inner Path, got {:?}", other),
1915 },
1916 other => panic!("expected Binary, got {:?}", other),
1917 },
1918 }
1919 }
1920
1921 #[test]
1922 fn test_parent_predicate_on_parent_step_itself() {
1923 // Account.Order.Product.Price.%[%.OrderID='order103'].SKU
1924 // Ground truth: the trailing `.%` step's own reference tags Price
1925 // (steps[3]); the predicate's `%` (attached to a `%` step, so bumped
1926 // one level) tags Product (steps[2]).
1927 let steps = resolve_path("Account.Order.Product.Price.%[%.OrderID='order103'].SKU");
1928 // [Account, Order, Product, Price, %(stages), SKU]
1929 assert_eq!(steps.len(), 6);
1930 assert!(matches!(steps[4].node, AstNode::Parent(_)));
1931 let price_label = steps[3].ancestor_label.clone();
1932 let product_label = steps[2].ancestor_label.clone();
1933 assert!(
1934 price_label.is_some(),
1935 "Price must be tagged (by the % step)"
1936 );
1937 assert!(
1938 product_label.is_some(),
1939 "Product must be tagged (by the predicate %)"
1940 );
1941 assert_ne!(price_label, product_label);
1942 }
1943
1944 #[test]
1945 fn test_two_predicates_share_and_differ_labels() {
1946 // Account.Order.Product[%.OrderID='order104'][%.%.`Account Name`='Firefly'].SKU
1947 // Ground truth: first predicate's `%` -> Product; second predicate's
1948 // first `%` -> Product (REUSE same label); second `%` -> Order.
1949 let steps = resolve_path(
1950 "Account.Order.Product[%.OrderID='order104'][%.%.`Account Name`='Firefly'].SKU",
1951 );
1952 assert_eq!(steps.len(), 4);
1953 assert_eq!(steps[2].stages.len(), 2);
1954 let product_label = steps[2].ancestor_label.clone();
1955 let order_label = steps[1].ancestor_label.clone();
1956 assert!(product_label.is_some());
1957 assert!(order_label.is_some());
1958 assert_ne!(product_label, order_label);
1959 // first predicate: % -> Product
1960 match &steps[2].stages[0] {
1961 Stage::Index(_) => unreachable!("no index stage in this test"),
1962 Stage::Filter(expr) => match expr.as_ref() {
1963 AstNode::Binary { lhs, .. } => match lhs.as_ref() {
1964 AstNode::Path { steps: inner } => match &inner[0].node {
1965 AstNode::Parent(l) => assert_eq!(Some(l.clone()), product_label),
1966 other => panic!("{:?}", other),
1967 },
1968 other => panic!("{:?}", other),
1969 },
1970 other => panic!("{:?}", other),
1971 },
1972 }
1973 // second predicate: %.% -> Product (reuse), Order
1974 match &steps[2].stages[1] {
1975 Stage::Index(_) => unreachable!("no index stage in this test"),
1976 Stage::Filter(expr) => match expr.as_ref() {
1977 AstNode::Binary { lhs, .. } => match lhs.as_ref() {
1978 AstNode::Path { steps: inner } => {
1979 match &inner[0].node {
1980 AstNode::Parent(l) => assert_eq!(Some(l.clone()), product_label),
1981 other => panic!("{:?}", other),
1982 }
1983 match &inner[1].node {
1984 AstNode::Parent(l) => assert_eq!(Some(l.clone()), order_label),
1985 other => panic!("{:?}", other),
1986 }
1987 }
1988 other => panic!("{:?}", other),
1989 },
1990 other => panic!("{:?}", other),
1991 },
1992 }
1993 }
1994
1995 #[test]
1996 fn test_parent_inside_sort_term_resolves_to_last_input_step() {
1997 // Account.Order.Product.SKU^(%.Price)
1998 // Ground truth: the sort term's `%` tags SKU (the last input step).
1999 let ast = crate::parser::Parser::new("Account.Order.Product.SKU^(%.Price)".to_string())
2000 .unwrap()
2001 .parse()
2002 .unwrap();
2003 match resolve_ancestry(ast).unwrap() {
2004 AstNode::Sort { input, terms } => {
2005 let steps = match input.as_ref() {
2006 AstNode::Path { steps } => steps,
2007 other => panic!("expected Path input, got {:?}", other),
2008 };
2009 assert_eq!(steps.len(), 4);
2010 assert!(matches!(steps[3].node, AstNode::Name(ref n) if n == "SKU"));
2011 let sku_label = steps[3].ancestor_label.clone();
2012 assert!(sku_label.is_some(), "SKU must be tagged");
2013 // term = (Path[Parent, Name("Price")], asc)
2014 match &terms[0].0 {
2015 AstNode::Path { steps: inner } => match &inner[0].node {
2016 AstNode::Parent(l) => assert_eq!(Some(l.clone()), sku_label),
2017 other => panic!("{:?}", other),
2018 },
2019 other => panic!("{:?}", other),
2020 }
2021 }
2022 other => panic!("expected Sort, got {:?}", other),
2023 }
2024 }
2025
2026 #[test]
2027 fn test_two_sort_terms_share_and_differ_labels() {
2028 // Account.Order.Product.SKU^(%.Price, >%.%.OrderID)
2029 // Ground truth: term1 `%` -> SKU; term2 `%.%` -> SKU (reuse), Product.
2030 let ast = crate::parser::Parser::new(
2031 "Account.Order.Product.SKU^(%.Price, >%.%.OrderID)".to_string(),
2032 )
2033 .unwrap()
2034 .parse()
2035 .unwrap();
2036 match resolve_ancestry(ast).unwrap() {
2037 AstNode::Sort { input, terms } => {
2038 let steps = match input.as_ref() {
2039 AstNode::Path { steps } => steps,
2040 other => panic!("{:?}", other),
2041 };
2042 let sku_label = steps[3].ancestor_label.clone();
2043 let product_label = steps[2].ancestor_label.clone();
2044 assert!(sku_label.is_some());
2045 assert!(product_label.is_some());
2046 assert_ne!(sku_label, product_label);
2047 assert_eq!(terms.len(), 2);
2048 // term2 = %.%.OrderID
2049 match &terms[1].0 {
2050 AstNode::Path { steps: inner } => {
2051 match &inner[0].node {
2052 AstNode::Parent(l) => assert_eq!(Some(l.clone()), sku_label),
2053 other => panic!("{:?}", other),
2054 }
2055 match &inner[1].node {
2056 AstNode::Parent(l) => assert_eq!(Some(l.clone()), product_label),
2057 other => panic!("{:?}", other),
2058 }
2059 }
2060 other => panic!("{:?}", other),
2061 }
2062 }
2063 other => panic!("expected Sort, got {:?}", other),
2064 }
2065 }
2066
2067 #[test]
2068 fn test_focus_bind_becomes_step_flag() {
2069 // Order@$o --> Path{steps: [Name("Order") with focus=Some("o"), is_tuple=true]}
2070 let ast = AstNode::Path {
2071 steps: vec![PathStep::new(AstNode::Binary {
2072 op: BinaryOp::FocusBind,
2073 lhs: Box::new(AstNode::Name("Order".to_string())),
2074 rhs: Box::new(AstNode::Variable("o".to_string())),
2075 })],
2076 };
2077 let result = resolve_ancestry(ast).unwrap();
2078 match result {
2079 AstNode::Path { steps } => {
2080 assert_eq!(steps.len(), 1);
2081 assert!(matches!(steps[0].node, AstNode::Name(ref n) if n == "Order"));
2082 assert_eq!(steps[0].focus, Some("o".to_string()));
2083 assert!(steps[0].is_tuple);
2084 }
2085 other => panic!("expected Path, got {:?}", other),
2086 }
2087 }
2088
2089 #[test]
2090 fn test_index_bind_becomes_step_flag() {
2091 // arr#$i --> Path{steps: [Name("arr") with index_var=Some("i"), is_tuple=true]}
2092 let ast = AstNode::Path {
2093 steps: vec![PathStep::new(AstNode::Binary {
2094 op: BinaryOp::IndexBind,
2095 lhs: Box::new(AstNode::Name("arr".to_string())),
2096 rhs: Box::new(AstNode::Variable("i".to_string())),
2097 })],
2098 };
2099 let result = resolve_ancestry(ast).unwrap();
2100 match result {
2101 AstNode::Path { steps } => {
2102 assert_eq!(steps.len(), 1);
2103 assert!(matches!(steps[0].node, AstNode::Name(ref n) if n == "arr"));
2104 assert_eq!(steps[0].index_var, Some("i".to_string()));
2105 assert!(steps[0].is_tuple);
2106 }
2107 other => panic!("expected Path, got {:?}", other),
2108 }
2109 }
2110
2111 #[test]
2112 fn test_bare_parent_at_top_level_is_s0217() {
2113 let err = resolve_ancestry(AstNode::Parent(String::new())).unwrap_err();
2114 assert!(err.to_string().starts_with("S0217"));
2115 }
2116
2117 #[test]
2118 fn test_path_step_with_stages_preserved() {
2119 // Ensure stages (predicates) survive the transform unchanged when
2120 // there's no binding marker involved.
2121 let ast = AstNode::Path {
2122 steps: vec![PathStep::with_stages(
2123 AstNode::Name("Order".to_string()),
2124 vec![Stage::Filter(Box::new(AstNode::Boolean(true)))],
2125 )],
2126 };
2127 let result = resolve_ancestry(ast).unwrap();
2128 match result {
2129 AstNode::Path { steps } => {
2130 assert_eq!(steps[0].stages.len(), 1);
2131 }
2132 other => panic!("expected Path, got {:?}", other),
2133 }
2134 }
2135
2136 // --- Regression tests using the REAL parser (Task 3 review findings) ---
2137 //
2138 // Hand-built synthetic ASTs only exercise the shapes that happen to
2139 // already work. These tests go through `crate::parser::parse()` on real
2140 // source text, which is what surfaced two root-cause bugs in Task 3:
2141 // (1) transform_children not recursing into most composite node types,
2142 // and (2) `@$var`/`#$var` never being migrated when the marker is the
2143 // TOP-LEVEL node reaching transform_node (only when already nested
2144 // inside a PathStep). The same discipline applies to Task 4's `%`
2145 // resolution below: expected label/level assertions are checked for
2146 // internal consistency (same target step -> same label; different
2147 // targets -> different labels) rather than against jsonata-js's exact
2148 // "!0"/"!1"/... strings, since those are implementation-internal and
2149 // arbitrary -- but the STEPS that get tagged are cross-checked against
2150 // jsonata-js's actual `.ast()` output (see comments below).
2151
2152 #[test]
2153 fn test_real_parser_bare_focus_bind_no_dot() {
2154 // "Order@$o" -- bare single-step, no dot anywhere. The parser
2155 // produces Binary{FocusBind, lhs: Name("Order"), rhs: Variable("o")}
2156 // at the top level (no Path at all, since there's no `.`).
2157 let ast = crate::parser::Parser::new("Order@$o".to_string())
2158 .unwrap()
2159 .parse()
2160 .unwrap();
2161 let result = resolve_ancestry(ast).unwrap();
2162 match result {
2163 AstNode::Path { steps } => {
2164 assert_eq!(steps.len(), 1);
2165 assert!(matches!(steps[0].node, AstNode::Name(ref n) if n == "Order"));
2166 assert_eq!(steps[0].focus, Some("o".to_string()));
2167 assert!(steps[0].is_tuple);
2168 }
2169 other => panic!("expected Path, got {:?}", other),
2170 }
2171 }
2172
2173 #[test]
2174 fn test_real_parser_focus_bind_on_final_step_of_multistep_path() {
2175 // "Account.Order@$o" -- 2-step path, marker on the final step, no
2176 // trailing dot. Previously: `@` wrapped the whole 2-step Path in a
2177 // top-level Binary{FocusBind,...} that was never migrated (Bug 2).
2178 let ast = crate::parser::Parser::new("Account.Order@$o".to_string())
2179 .unwrap()
2180 .parse()
2181 .unwrap();
2182 let result = resolve_ancestry(ast).unwrap();
2183 match result {
2184 AstNode::Path { steps } => {
2185 assert_eq!(steps.len(), 2);
2186 assert!(matches!(steps[0].node, AstNode::Name(ref n) if n == "Account"));
2187 assert!(steps[0].focus.is_none());
2188 assert!(!steps[0].is_tuple);
2189 assert!(matches!(steps[1].node, AstNode::Name(ref n) if n == "Order"));
2190 assert_eq!(steps[1].focus, Some("o".to_string()));
2191 assert!(steps[1].is_tuple);
2192 }
2193 other => panic!("expected Path, got {:?}", other),
2194 }
2195 }
2196
2197 #[test]
2198 fn test_real_parser_bare_index_bind() {
2199 // "arr#$i" -- bare index bind, no dot. Previously never migrated
2200 // when reaching transform_node as the raw top-level IndexBind node.
2201 let ast = crate::parser::Parser::new("arr#$i".to_string())
2202 .unwrap()
2203 .parse()
2204 .unwrap();
2205 let result = resolve_ancestry(ast).unwrap();
2206 match result {
2207 AstNode::Path { steps } => {
2208 assert_eq!(steps.len(), 1);
2209 assert!(matches!(steps[0].node, AstNode::Name(ref n) if n == "arr"));
2210 assert_eq!(steps[0].index_var, Some("i".to_string()));
2211 assert!(steps[0].is_tuple);
2212 }
2213 other => panic!("expected Path, got {:?}", other),
2214 }
2215 }
2216
2217 #[test]
2218 fn test_real_parser_bare_parent_inside_function_args_is_s0217() {
2219 // "$count(%)" -- a bare `%` nested inside a Function call's args.
2220 // Previously transform_children didn't recurse into Function args
2221 // at all (Bug 1), so this silently returned Ok(unchanged) instead
2222 // of raising S0217.
2223 let ast = crate::parser::Parser::new("$count(%)".to_string())
2224 .unwrap()
2225 .parse()
2226 .unwrap();
2227 let err = resolve_ancestry(ast).unwrap_err();
2228 assert!(err.to_string().starts_with("S0217"));
2229 }
2230
2231 // --- Regression tests for the "nested Path from multi-step @/# marker"
2232 // finding (Task 3, second review round) ---
2233
2234 #[test]
2235 fn test_real_parser_focus_bind_multistep_prefix_and_suffix_is_flat() {
2236 // "Account.Order@$o.Product" must produce a FLAT 3-step path, not a
2237 // 2-step path whose first step's node is itself a nested 2-step Path.
2238 let ast = crate::parser::Parser::new("Account.Order@$o.Product".to_string())
2239 .unwrap()
2240 .parse()
2241 .unwrap();
2242 let result = resolve_ancestry(ast).unwrap();
2243 match result {
2244 AstNode::Path { steps } => {
2245 assert_eq!(steps.len(), 3, "expected a flat 3-step path");
2246 assert!(matches!(steps[0].node, AstNode::Name(ref n) if n == "Account"));
2247 assert!(steps[0].focus.is_none());
2248 assert!(!steps[0].is_tuple);
2249 assert!(matches!(steps[1].node, AstNode::Name(ref n) if n == "Order"));
2250 assert_eq!(steps[1].focus, Some("o".to_string()));
2251 assert!(steps[1].is_tuple);
2252 assert!(matches!(steps[2].node, AstNode::Name(ref n) if n == "Product"));
2253 assert!(steps[2].focus.is_none());
2254 }
2255 other => panic!("expected flat Path, got {:?}", other),
2256 }
2257 }
2258
2259 #[test]
2260 fn test_real_parser_index_bind_multistep_prefix_and_suffix_is_flat() {
2261 // Same shape as above but for `#$i` (IndexBind) instead of `@$o`.
2262 let ast = crate::parser::Parser::new("Account.Order#$i.Product".to_string())
2263 .unwrap()
2264 .parse()
2265 .unwrap();
2266 let result = resolve_ancestry(ast).unwrap();
2267 match result {
2268 AstNode::Path { steps } => {
2269 assert_eq!(steps.len(), 3, "expected a flat 3-step path");
2270 assert!(matches!(steps[0].node, AstNode::Name(ref n) if n == "Account"));
2271 assert!(steps[0].index_var.is_none());
2272 assert!(!steps[0].is_tuple);
2273 assert!(matches!(steps[1].node, AstNode::Name(ref n) if n == "Order"));
2274 assert_eq!(steps[1].index_var, Some("i".to_string()));
2275 assert!(steps[1].is_tuple);
2276 assert!(matches!(steps[2].node, AstNode::Name(ref n) if n == "Product"));
2277 assert!(steps[2].index_var.is_none());
2278 }
2279 other => panic!("expected flat Path, got {:?}", other),
2280 }
2281 }
2282
2283 // --- Task 4: `%`/`%.%` ancestor resolution, real-parser-based ---
2284 //
2285 // Ground truth for every test below was independently verified against
2286 // jsonata-js's OWN `.ast()` output (`node -e 'jsonata(expr).ast()'` in
2287 // tests/jsonata-js), not derived by hand. This is what caught the task
2288 // brief's off-by-one (it asserted the wrong target steps for a `%.%`
2289 // chain) before any code was written against it.
2290
2291 #[test]
2292 fn test_real_parser_single_level_parent_resolves_to_previous_step() {
2293 // "Account.Order.%" -- jsonata-js tags `Order` (steps[1]), and the
2294 // trailing `%` step (steps[2]) carries the matching label. `%`
2295 // refers to Order's own INPUT (i.e. what produced it, Account) --
2296 // confirmed by live evaluation: Account.Order.% evaluates to the
2297 // Account object, not the Order object.
2298 let ast = crate::parser::Parser::new("Account.Order.%".to_string())
2299 .unwrap()
2300 .parse()
2301 .unwrap();
2302 let result = resolve_ancestry(ast).unwrap();
2303 match result {
2304 AstNode::Path { steps } => {
2305 assert_eq!(steps.len(), 3);
2306 assert!(matches!(steps[0].node, AstNode::Name(ref n) if n == "Account"));
2307 assert!(steps[0].ancestor_label.is_none());
2308 assert!(matches!(steps[1].node, AstNode::Name(ref n) if n == "Order"));
2309 assert!(steps[1].ancestor_label.is_some());
2310 assert!(steps[1].is_tuple);
2311 match &steps[2].node {
2312 AstNode::Parent(label) => {
2313 assert_eq!(Some(label.clone()), steps[1].ancestor_label);
2314 }
2315 other => panic!("expected Parent(label), got {:?}", other),
2316 }
2317 }
2318 other => panic!("expected Path, got {:?}", other),
2319 }
2320 }
2321
2322 #[test]
2323 fn test_real_parser_chained_parent_resolves_two_levels_back() {
2324 // "Account.Order.Product.%.%" (mirrors parent002.jsonata's shape).
2325 // Ground truth from jsonata-js: the FIRST `%` tags Product
2326 // (steps[2]), the SECOND `%` tags Order (steps[1]) -- NOT Order and
2327 // Account as a naive reading might suggest. Each `%` targets the
2328 // step whose INPUT it refers to: the first `%`'s target is Product
2329 // (whose input is Order), the second `%` walks one step further
2330 // back to Order (whose input is Account).
2331 let ast = crate::parser::Parser::new("Account.Order.Product.%.%".to_string())
2332 .unwrap()
2333 .parse()
2334 .unwrap();
2335 let result = resolve_ancestry(ast).unwrap();
2336 match result {
2337 AstNode::Path { steps } => {
2338 assert_eq!(steps.len(), 5);
2339 assert!(steps[0].ancestor_label.is_none(), "Account untagged");
2340 let order_label = steps[1].ancestor_label.clone();
2341 let product_label = steps[2].ancestor_label.clone();
2342 assert!(order_label.is_some(), "Order must be tagged");
2343 assert!(product_label.is_some(), "Product must be tagged");
2344 assert_ne!(
2345 order_label, product_label,
2346 "two distinct % chains must get distinct labels"
2347 );
2348 match &steps[3].node {
2349 AstNode::Parent(label) => assert_eq!(Some(label.clone()), product_label),
2350 other => panic!("expected Parent(label), got {:?}", other),
2351 }
2352 match &steps[4].node {
2353 AstNode::Parent(label) => assert_eq!(Some(label.clone()), order_label),
2354 other => panic!("expected Parent(label), got {:?}", other),
2355 }
2356 }
2357 other => panic!("expected Path, got {:?}", other),
2358 }
2359 }
2360
2361 #[test]
2362 fn test_real_parser_object_constructor_percent_tags_preceding_step() {
2363 // parent000.jsonata's shape: "Account.Order.Product.{'order': %.OrderID}"
2364 // -- % lives INSIDE the object constructor's value, not as its own
2365 // trailing path step. Ground truth (jsonata-js): Product (steps[2])
2366 // gets tagged, and the nested %'s label matches.
2367 let ast =
2368 crate::parser::Parser::new("Account.Order.Product.{'order': %.OrderID}".to_string())
2369 .unwrap()
2370 .parse()
2371 .unwrap();
2372 let result = resolve_ancestry(ast).unwrap();
2373 match result {
2374 AstNode::Path { steps } => {
2375 assert_eq!(steps.len(), 4);
2376 assert!(matches!(steps[2].node, AstNode::Name(ref n) if n == "Product"));
2377 let product_label = steps[2].ancestor_label.clone();
2378 assert!(product_label.is_some());
2379 assert!(steps[2].is_tuple);
2380 match &steps[3].node {
2381 AstNode::Object(pairs) => {
2382 assert_eq!(pairs.len(), 1);
2383 match &pairs[0].1 {
2384 AstNode::Path { steps: inner } => {
2385 assert_eq!(inner.len(), 2);
2386 match &inner[0].node {
2387 AstNode::Parent(label) => {
2388 assert_eq!(Some(label.clone()), product_label)
2389 }
2390 other => panic!("expected Parent(label), got {:?}", other),
2391 }
2392 }
2393 other => panic!("expected inner Path, got {:?}", other),
2394 }
2395 }
2396 other => panic!("expected Object, got {:?}", other),
2397 }
2398 }
2399 other => panic!("expected Path, got {:?}", other),
2400 }
2401 }
2402
2403 #[test]
2404 fn test_real_parser_object_constructor_two_percent_chains_share_and_differ() {
2405 // parent002.jsonata's actual shape:
2406 // "Account.Order.Product.{'Product':`Product Name`,'Order':%.OrderID,'Account':%.%.`Account Name`}"
2407 // Ground truth (jsonata-js, verified via live .ast() dump): the
2408 // 'Order' value's single `%` and the 'Account' value's FIRST `%`
2409 // (of its `%.%` chain) both resolve to Product -- i.e. they share
2410 // ONE label (the "reuse an existing label" mechanic) -- while the
2411 // 'Account' value's SECOND `%` resolves to Order, getting a
2412 // DIFFERENT label.
2413 let ast = crate::parser::Parser::new(
2414 "Account.Order.Product.{'Product':`Product Name`,'Order':%.OrderID,'Account':%.%.`Account Name`}"
2415 .to_string(),
2416 )
2417 .unwrap()
2418 .parse()
2419 .unwrap();
2420 let result = resolve_ancestry(ast).unwrap();
2421 match result {
2422 AstNode::Path { steps } => {
2423 assert_eq!(steps.len(), 4);
2424 let product_label = steps[2].ancestor_label.clone();
2425 let order_label = steps[1].ancestor_label.clone();
2426 assert!(product_label.is_some(), "Product must be tagged");
2427 assert!(order_label.is_some(), "Order must be tagged");
2428 assert_ne!(product_label, order_label);
2429
2430 match &steps[3].node {
2431 AstNode::Object(pairs) => {
2432 assert_eq!(pairs.len(), 3);
2433 // pairs[1] = 'Order': %.OrderID
2434 match &pairs[1].1 {
2435 AstNode::Path { steps: inner } => match &inner[0].node {
2436 AstNode::Parent(label) => {
2437 assert_eq!(Some(label.clone()), product_label)
2438 }
2439 other => panic!("expected Parent(label), got {:?}", other),
2440 },
2441 other => panic!("expected inner Path, got {:?}", other),
2442 }
2443 // pairs[2] = 'Account': %.%.`Account Name`
2444 match &pairs[2].1 {
2445 AstNode::Path { steps: inner } => {
2446 assert_eq!(inner.len(), 3);
2447 match &inner[0].node {
2448 AstNode::Parent(label) => {
2449 // Reuse: same label as the 'Order'
2450 // value's % (both target Product).
2451 assert_eq!(Some(label.clone()), product_label)
2452 }
2453 other => panic!("expected Parent(label), got {:?}", other),
2454 }
2455 match &inner[1].node {
2456 AstNode::Parent(label) => {
2457 assert_eq!(Some(label.clone()), order_label)
2458 }
2459 other => panic!("expected Parent(label), got {:?}", other),
2460 }
2461 }
2462 other => panic!("expected inner Path, got {:?}", other),
2463 }
2464 }
2465 other => panic!("expected Object, got {:?}", other),
2466 }
2467 }
2468 other => panic!("expected Path, got {:?}", other),
2469 }
2470 }
2471
2472 #[test]
2473 fn test_real_parser_percent_through_parenthesized_step_function_application() {
2474 // parent001.jsonata's shape: "Account.(Order.Product).%" -- parens
2475 // around a multi-step sub-path parse as a FunctionApplication step
2476 // wrapping a nested Path. `%` must walk INTO that nested path to
2477 // find Product (its last step) as the target, exactly as if the
2478 // parens weren't there.
2479 let ast = crate::parser::Parser::new("Account.(Order.Product).%".to_string())
2480 .unwrap()
2481 .parse()
2482 .unwrap();
2483 let result = resolve_ancestry(ast).unwrap();
2484 match result {
2485 AstNode::Path { steps } => {
2486 assert_eq!(steps.len(), 3);
2487 assert!(steps[1].is_tuple, "the wrapping step must be flagged tuple");
2488 match &steps[1].node {
2489 AstNode::FunctionApplication(inner) => match inner.as_ref() {
2490 AstNode::Path { steps: inner_steps } => {
2491 assert_eq!(inner_steps.len(), 2);
2492 assert!(
2493 matches!(inner_steps[0].node, AstNode::Name(ref n) if n == "Order")
2494 );
2495 assert!(
2496 matches!(inner_steps[1].node, AstNode::Name(ref n) if n == "Product")
2497 );
2498 let product_label = inner_steps[1].ancestor_label.clone();
2499 assert!(product_label.is_some(), "Product must be tagged");
2500 match &steps[2].node {
2501 AstNode::Parent(label) => {
2502 assert_eq!(Some(label.clone()), product_label)
2503 }
2504 other => panic!("expected Parent(label), got {:?}", other),
2505 }
2506 }
2507 other => panic!("expected inner Path, got {:?}", other),
2508 },
2509 other => panic!("expected FunctionApplication, got {:?}", other),
2510 }
2511 }
2512 other => panic!("expected Path, got {:?}", other),
2513 }
2514 }
2515
2516 #[test]
2517 fn test_real_parser_percent_through_leading_paren_block() {
2518 // parent006.jsonata's shape: "(Account.Order).(Product).{...}" -- a
2519 // LEADING bare paren (not preceded by `.`) parses as a generic
2520 // `Block` (not FunctionApplication), then becomes the first step of
2521 // the outer path via the normal-dot fallback; `.(Product)` becomes a
2522 // second, `FunctionApplication`-wrapped step.
2523 //
2524 // Ground truth from jsonata-js (verified via live `.ast()` dump,
2525 // NOT hand-derived -- an earlier draft of this test wrongly assumed
2526 // % must walk past the `(Product)` step into the `(Account.Order)`
2527 // step to find Order; jsonata-js instead resolves it in ONE level,
2528 // same as the un-parenthesized `Account.Order.Product.%` case):
2529 // `%` (level 1) resolves entirely WITHIN the immediately preceding
2530 // step -- the `(Product)` FunctionApplication -- tagging Product
2531 // itself. The `(Account.Order)` block is never even visited.
2532 let ast =
2533 crate::parser::Parser::new("(Account.Order).(Product).{'x': %.OrderID}".to_string())
2534 .unwrap()
2535 .parse()
2536 .unwrap();
2537 let result = resolve_ancestry(ast).unwrap();
2538 match result {
2539 AstNode::Path { steps } => {
2540 assert_eq!(steps.len(), 3);
2541 // steps[0]: the untouched (Account.Order) block.
2542 match &steps[0].node {
2543 AstNode::Block(exprs) => {
2544 assert_eq!(exprs.len(), 1);
2545 match &exprs[0] {
2546 AstNode::Path { steps: inner } => {
2547 assert_eq!(inner.len(), 2);
2548 assert!(
2549 matches!(inner[0].node, AstNode::Name(ref n) if n == "Account")
2550 );
2551 assert!(
2552 matches!(inner[1].node, AstNode::Name(ref n) if n == "Order")
2553 );
2554 assert!(
2555 inner[1].ancestor_label.is_none(),
2556 "Order must NOT be tagged -- % resolves one level back, at Product"
2557 );
2558 }
2559 other => panic!("expected inner Path, got {:?}", other),
2560 }
2561 }
2562 other => panic!("expected Block, got {:?}", other),
2563 }
2564 assert!(!steps[0].is_tuple);
2565 // steps[1]: the (Product) FunctionApplication -- this is
2566 // where % actually resolves.
2567 assert!(steps[1].is_tuple, "the wrapping step must be flagged tuple");
2568 match &steps[1].node {
2569 AstNode::FunctionApplication(inner) => match inner.as_ref() {
2570 AstNode::Path { steps: inner_steps } => {
2571 assert_eq!(inner_steps.len(), 1);
2572 assert!(
2573 matches!(inner_steps[0].node, AstNode::Name(ref n) if n == "Product")
2574 );
2575 let product_label = inner_steps[0].ancestor_label.clone();
2576 assert!(product_label.is_some(), "Product must be tagged");
2577 match &steps[2].node {
2578 AstNode::Object(pairs) => match &pairs[0].1 {
2579 AstNode::Path { steps: value_steps } => {
2580 match &value_steps[0].node {
2581 AstNode::Parent(label) => {
2582 assert_eq!(Some(label.clone()), product_label)
2583 }
2584 other => {
2585 panic!("expected Parent(label), got {:?}", other)
2586 }
2587 }
2588 }
2589 other => panic!("expected value Path, got {:?}", other),
2590 },
2591 other => panic!("expected Object, got {:?}", other),
2592 }
2593 }
2594 other => panic!("expected inner Path, got {:?}", other),
2595 },
2596 other => panic!("expected FunctionApplication, got {:?}", other),
2597 }
2598 }
2599 other => panic!("expected Path, got {:?}", other),
2600 }
2601 }
2602
2603 #[test]
2604 fn test_real_parser_percent_cannot_derive_ancestor_from_literal() {
2605 // A `%` immediately after a step that isn't name/wildcard/block/path
2606 // (here, a string literal step is folded to a Name by the parser's
2607 // own S0213-adjacent handling, so use a case that stays non-
2608 // resolvable: % with nothing at all before it in an enclosing path).
2609 let ast = crate::parser::Parser::new("%.OrderID".to_string())
2610 .unwrap()
2611 .parse()
2612 .unwrap();
2613 let err = resolve_ancestry(ast).unwrap_err();
2614 assert!(err.to_string().starts_with("S0217"));
2615 }
2616
2617 #[test]
2618 fn test_real_parser_lambda_body_percent_does_not_bubble_or_error() {
2619 // "function(){ % }" -- jsonata-js parses this successfully (the raw
2620 // `%` is left untouched inside the lambda body, only failing at
2621 // runtime when/if the lambda is invoked). Confirms Lambda bodies
2622 // don't bubble their pending to the enclosing scope.
2623 let ast = crate::parser::Parser::new("function(){ % }".to_string())
2624 .unwrap()
2625 .parse()
2626 .unwrap();
2627 let result = resolve_ancestry(ast).unwrap();
2628 match result {
2629 AstNode::Lambda { body, .. } => {
2630 assert!(matches!(*body, AstNode::Parent(_)));
2631 }
2632 other => panic!("expected Lambda, got {:?}", other),
2633 }
2634 }
2635
2636 // --- S0215/S0216: `@` (focus binding) rejects a step that already has
2637 // predicates or is a sort step. These checks were a pre-existing gap
2638 // (never implemented, not even by Task 3) that only surfaced once
2639 // ast_transform started running unconditionally via parser::parse():
2640 // previously an unresolved `@`/`#` reaching the evaluator always threw
2641 // (for the WRONG reason -- "must be resolved by ast_transform pass"),
2642 // and the reference-suite harness lenient-accepts any error without an
2643 // extractable code, masking the missing S0215/S0216 checks entirely.
2644 // Ground truth: tests/jsonata-js/test/test-suite/groups/joins/errors.json.
2645
2646 #[test]
2647 fn test_real_parser_focus_bind_after_predicate_is_s0215() {
2648 let ast = crate::parser::Parser::new("Account.Order[1]@$o.Product".to_string())
2649 .unwrap()
2650 .parse()
2651 .unwrap();
2652 let err = resolve_ancestry(ast).unwrap_err();
2653 assert!(err.to_string().starts_with("S0215"), "got: {}", err);
2654 }
2655
2656 #[test]
2657 fn test_real_parser_focus_bind_after_sort_is_s0216() {
2658 let ast = crate::parser::Parser::new(
2659 "Account.Order^(>OrderID)@$o.Product.{ 'name':`Product Name`, 'orderid':$o.OrderID }"
2660 .to_string(),
2661 )
2662 .unwrap()
2663 .parse()
2664 .unwrap();
2665 let err = resolve_ancestry(ast).unwrap_err();
2666 assert!(err.to_string().starts_with("S0216"), "got: {}", err);
2667 }
2668
2669 #[test]
2670 fn test_real_parser_index_bind_after_predicate_is_not_an_error() {
2671 // Unlike `@`, `#` has NO S0215-equivalent restriction in jsonata-js
2672 // -- it's allowed after predicates (it just appends an index stage
2673 // rather than setting a plain `index` field when stages already
2674 // exist). Confirms `check_focus_bind_target`'s marker-kind guard
2675 // correctly only fires for Focus, not Index.
2676 let ast = crate::parser::Parser::new("Account.Order[1]#$o.Product".to_string())
2677 .unwrap()
2678 .parse()
2679 .unwrap();
2680 assert!(resolve_ancestry(ast).is_ok());
2681 }
2682}