Skip to main content

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}