Skip to main content

jetro_core/eval/
mod.rs

1//! Tree-walking evaluator — reference semantics for Jetro v2.
2//!
3//! This module is the *source of truth*: when the optimiser or VM
4//! behaviour diverges from it, the VM is wrong.  It is also the
5//! smallest path from AST to value and thus the easiest to reason
6//! about.  [`vm`](super::vm) is a faster path that caches compiled
7//! programs and resolved pointers, but every new feature lands here
8//! first.
9//!
10//! # Data flow
11//!
12//! ```text
13//! evaluate(expr, doc)
14//!     │
15//!     ▼
16//!   apply_expr(&Expr, &Env)
17//!     │
18//!     ├── literals / operators    (inline)
19//!     ├── chain navigation        (apply_chain → apply_step)
20//!     ├── method calls            (dispatch_method → func_*/methods.rs)
21//!     └── comprehensions / let    (recursive into new Env)
22//! ```
23//!
24//! # `Env`
25//!
26//! `Env` owns the root doc, the "current" value (`@`), and a
27//! `SmallVec<[(Arc<str>, Val); 4]>` of let-bound names plus an
28//! `Arc<MethodRegistry>` for user-registered methods.  Scopes are
29//! pushed by appending and popped by truncation — lookup is linear
30//! but `SmallVec` keeps the first four slots inline, which covers
31//! every realistic query.
32//!
33//! # Registry
34//!
35//! [`MethodRegistry`] holds user methods behind `Arc<dyn Method>`
36//! and is itself `Clone` (via derive) so threading it through
37//! recursive calls is free.
38
39use std::sync::Arc;
40use indexmap::IndexMap;
41use smallvec::SmallVec;
42
43use super::ast::*;
44
45pub mod value;
46pub mod util;
47pub mod methods;
48pub mod builtins;
49mod func_strings;
50mod func_arrays;
51mod func_objects;
52mod func_paths;
53mod func_aggregates;
54mod func_csv;
55mod func_search;
56
57pub use value::Val;
58pub use methods::{Method, MethodRegistry};
59use util::*;
60
61// ── Error ─────────────────────────────────────────────────────────────────────
62
63#[derive(Debug, Clone)]
64pub struct EvalError(pub String);
65
66impl std::fmt::Display for EvalError {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        write!(f, "eval error: {}", self.0)
69    }
70}
71impl std::error::Error for EvalError {}
72
73macro_rules! err {
74    ($($t:tt)*) => { Err(EvalError(format!($($t)*))) };
75}
76
77// ── Environment ───────────────────────────────────────────────────────────────
78// SmallVec<4> keeps ≤4 bindings on the stack — covers the vast majority of
79// queries without a heap allocation.  Linear scan is fine for n ≤ 4.
80
81/// Saved-state token returned by `Env::push_lam` and consumed by
82/// `Env::pop_lam`.  Lets hot loops bind a single lambda slot + swap
83/// `current` without cloning the whole Env per iteration.
84pub struct LamFrame {
85    prev_current: Val,
86    prev_var:     LamVarPrev,
87}
88
89enum LamVarPrev {
90    None,
91    Pushed,
92    Replaced(usize, Val),
93}
94
95#[derive(Clone)]
96pub struct Env {
97    vars:     SmallVec<[(Arc<str>, Val); 4]>,
98    pub root:    Val,
99    pub current: Val,
100    registry: Arc<MethodRegistry>,
101    /// Raw JSON bytes the `root` was parsed from, when available.  Enables
102    /// SIMD byte-scan fast paths for `$..key` and `$..find(key op lit)`
103    /// queries that start at the document root.
104    pub(crate) raw_bytes: Option<Arc<[u8]>>,
105}
106
107impl Env {
108    fn new(root: Val) -> Self {
109        Self {
110            vars: SmallVec::new(),
111            root: root.clone(),
112            current: root,
113            registry: Arc::new(MethodRegistry::new()),
114            raw_bytes: None,
115        }
116    }
117
118    pub fn new_with_registry(root: Val, registry: Arc<MethodRegistry>) -> Self {
119        Self { vars: SmallVec::new(), root: root.clone(), current: root, registry, raw_bytes: None }
120    }
121
122    /// Build an `Env` that carries the original JSON source bytes so that
123    /// SIMD byte-scan can short-circuit deep-descendant queries.
124    pub fn new_with_raw(
125        root: Val,
126        registry: Arc<MethodRegistry>,
127        raw_bytes: Arc<[u8]>,
128    ) -> Self {
129        Self {
130            vars: SmallVec::new(),
131            root: root.clone(),
132            current: root,
133            registry,
134            raw_bytes: Some(raw_bytes),
135        }
136    }
137
138    #[inline]
139    pub(super) fn registry_ref(&self) -> &MethodRegistry { &self.registry }
140
141    #[inline]
142    pub fn with_current(&self, current: Val) -> Self {
143        Self {
144            vars: self.vars.clone(),
145            root: self.root.clone(),
146            current,
147            registry: self.registry.clone(),
148            raw_bytes: self.raw_bytes.clone(),
149        }
150    }
151
152    /// In-place swap of `current` — returns previous.  Paired with
153    /// `restore_current` this lets hot loops avoid cloning `vars`/`root`
154    /// per iteration.
155    #[inline]
156    pub fn swap_current(&mut self, new: Val) -> Val {
157        std::mem::replace(&mut self.current, new)
158    }
159
160    #[inline]
161    pub fn restore_current(&mut self, old: Val) {
162        self.current = old;
163    }
164
165    #[inline]
166    pub fn get_var(&self, name: &str) -> Option<&Val> {
167        self.vars.iter().rev().find(|(k, _)| k.as_ref() == name).map(|(_, v)| v)
168    }
169
170    #[inline]
171    pub fn has_var(&self, name: &str) -> bool {
172        self.vars.iter().any(|(k, _)| k.as_ref() == name)
173    }
174
175    pub fn with_var(&self, name: &str, val: Val) -> Self {
176        let mut vars = self.vars.clone();
177        if let Some(pos) = vars.iter().position(|(k, _)| k.as_ref() == name) {
178            vars[pos].1 = val;
179        } else {
180            vars.push((Arc::from(name), val));
181        }
182        Self { vars, root: self.root.clone(), current: self.current.clone(), registry: self.registry.clone(), raw_bytes: self.raw_bytes.clone() }
183    }
184
185    /// Hot-loop helper: bind `name → val` and swap `current`, returning
186    /// the previous state.  If `name` was already bound we remember the
187    /// previous value; otherwise we remember that the slot was freshly
188    /// pushed so `pop_lam` can truncate it off again.
189    #[inline]
190    pub fn push_lam(&mut self, name: Option<&str>, val: Val) -> LamFrame {
191        let prev_current = std::mem::replace(&mut self.current, val.clone());
192        let prev_var = match name {
193            None => LamVarPrev::None,
194            Some(n) => {
195                if let Some(pos) = self.vars.iter().position(|(k, _)| k.as_ref() == n) {
196                    let prev = std::mem::replace(&mut self.vars[pos].1, val);
197                    LamVarPrev::Replaced(pos, prev)
198                } else {
199                    self.vars.push((Arc::from(n), val));
200                    LamVarPrev::Pushed
201                }
202            }
203        };
204        LamFrame { prev_current, prev_var }
205    }
206
207    #[inline]
208    pub fn pop_lam(&mut self, frame: LamFrame) {
209        self.current = frame.prev_current;
210        match frame.prev_var {
211            LamVarPrev::None => {}
212            LamVarPrev::Pushed => { self.vars.pop(); }
213            LamVarPrev::Replaced(pos, prev) => { self.vars[pos].1 = prev; }
214        }
215    }
216
217}
218
219// ── Public entry points ───────────────────────────────────────────────────────
220
221pub fn evaluate(expr: &Expr, root: &serde_json::Value) -> Result<serde_json::Value, EvalError> {
222    let val = Val::from(root);
223    Ok(eval(expr, &Env::new(val))?.into())
224}
225
226pub fn evaluate_with(
227    expr: &Expr,
228    root: &serde_json::Value,
229    registry: Arc<MethodRegistry>,
230) -> Result<serde_json::Value, EvalError> {
231    let val = Val::from(root);
232    Ok(eval(expr, &Env::new_with_registry(val, registry))?.into())
233}
234
235/// Evaluate `expr` with the original JSON source bytes retained.  Enables
236/// SIMD byte-scan fast paths for `$..key` descent queries.
237pub fn evaluate_with_raw(
238    expr: &Expr,
239    root: &serde_json::Value,
240    registry: Arc<MethodRegistry>,
241    raw_bytes: Arc<[u8]>,
242) -> Result<serde_json::Value, EvalError> {
243    let val = Val::from(root);
244    Ok(eval(expr, &Env::new_with_raw(val, registry, raw_bytes))?.into())
245}
246
247// ── Core evaluator ────────────────────────────────────────────────────────────
248
249pub(super) fn eval(expr: &Expr, env: &Env) -> Result<Val, EvalError> {
250    match expr {
251        Expr::Null      => Ok(Val::Null),
252        Expr::Bool(b)   => Ok(Val::Bool(*b)),
253        Expr::Int(n)    => Ok(Val::Int(*n)),
254        Expr::Float(f)  => Ok(Val::Float(*f)),
255        Expr::Str(s)    => Ok(Val::Str(Arc::from(s.as_str()))),
256
257        Expr::FString(parts) => eval_fstring(parts, env),
258
259        Expr::Root    => Ok(env.root.clone()),
260        Expr::Current => Ok(env.current.clone()),
261
262        Expr::Ident(name) => {
263            if let Some(v) = env.get_var(name) { return Ok(v.clone()); }
264            Ok(env.current.get_field(name))
265        }
266
267        Expr::Chain(base, steps) => {
268            // SIMD enclosing-object fast path: `$..find(@.k == lit)` with raw
269            // bytes — scan locates each object whose `k` field equals `lit`
270            // without walking the tree.
271            if let (Expr::Root, Some(Step::Method(name, args)), Some(bytes))
272                = (&**base, steps.first(), env.raw_bytes.as_ref())
273            {
274                if name == "deep_find" && !args.is_empty() {
275                    if let Some(conjuncts) = canonical_field_eq_literals(args) {
276                        let spans = if conjuncts.len() == 1 {
277                            super::scan::find_enclosing_objects_eq(
278                                bytes, &conjuncts[0].0, &conjuncts[0].1,
279                            )
280                        } else {
281                            super::scan::find_enclosing_objects_eq_multi(
282                                bytes, &conjuncts,
283                            )
284                        };
285                        let mut vals: Vec<Val> = Vec::with_capacity(spans.len());
286                        for s in &spans {
287                            if let Ok(v) = serde_json::from_slice::<serde_json::Value>(
288                                &bytes[s.start..s.end]
289                            ) {
290                                vals.push(Val::from(&v));
291                            }
292                        }
293                        let mut val = Val::arr(vals);
294                        for step in &steps[1..] { val = eval_step(val, step, env)?; }
295                        return Ok(val);
296                    }
297                    // Single-conjunct numeric range: `$..find(@.k op num)`
298                    // where `op` ∈ `<`, `<=`, `>`, `>=`.  Extends the byte-scan
299                    // fast path past pure equality literals.
300                    if args.len() == 1 {
301                        let e = match &args[0] { Arg::Pos(e) | Arg::Named(_, e) => e };
302                        if let Some((field, op, thresh)) = canonical_field_cmp_literal(e) {
303                            let spans = super::scan::find_enclosing_objects_cmp(
304                                bytes, &field, op, thresh,
305                            );
306                            let mut vals: Vec<Val> = Vec::with_capacity(spans.len());
307                            for s in &spans {
308                                if let Ok(v) = serde_json::from_slice::<serde_json::Value>(
309                                    &bytes[s.start..s.end]
310                                ) {
311                                    vals.push(Val::from(&v));
312                                }
313                            }
314                            let mut val = Val::arr(vals);
315                            for step in &steps[1..] { val = eval_step(val, step, env)?; }
316                            return Ok(val);
317                        }
318                    }
319                    // Mixed multi-conjunct: eq and numeric-range predicates
320                    // together, e.g. `$..find(@.status == "shipped", @.total > 500)`.
321                    // Pure-eq and single-cmp already handled above; this path
322                    // picks up any remaining combination.
323                    if let Some(conjuncts) = canonical_field_mixed_predicates(args) {
324                        let spans = super::scan::find_enclosing_objects_mixed(
325                            bytes, &conjuncts,
326                        );
327                        let mut vals: Vec<Val> = Vec::with_capacity(spans.len());
328                        for s in &spans {
329                            if let Ok(v) = serde_json::from_slice::<serde_json::Value>(
330                                &bytes[s.start..s.end]
331                            ) {
332                                vals.push(Val::from(&v));
333                            }
334                        }
335                        let mut val = Val::arr(vals);
336                        for step in &steps[1..] { val = eval_step(val, step, env)?; }
337                        return Ok(val);
338                    }
339                }
340            }
341            // SIMD byte-chain fast path: `$..key<rest>` with raw bytes —
342            // chains of descendant/quantifier/filter-eq stay as byte spans
343            // and never materialise intermediate Vals.
344            if let (Expr::Root, Some(Step::Descendant(name)), Some(bytes))
345                = (&**base, steps.first(), env.raw_bytes.as_ref())
346            {
347                let (mut val, consumed) = byte_chain_eval(bytes, name, steps);
348                for step in &steps[consumed..] { val = eval_step(val, step, env)?; }
349                return Ok(val);
350            }
351            let mut val = eval(base, env)?;
352            for step in steps { val = eval_step(val, step, env)?; }
353            Ok(val)
354        }
355
356        Expr::UnaryNeg(e) => match eval(e, env)? {
357            Val::Int(n)   => Ok(Val::Int(-n)),
358            Val::Float(f) => Ok(Val::Float(-f)),
359            _ => err!("unary minus requires a number"),
360        },
361
362        Expr::Not(e)  => Ok(Val::Bool(!is_truthy(&eval(e, env)?))),
363        Expr::BinOp(l, op, r) => eval_binop(l, *op, r, env),
364
365        Expr::Coalesce(lhs, rhs) => {
366            let v = eval(lhs, env)?;
367            if !v.is_null() { Ok(v) } else { eval(rhs, env) }
368        }
369
370        Expr::Kind { expr, ty, negate } => {
371            let v = eval(expr, env)?;
372            let m = kind_matches(&v, *ty);
373            Ok(Val::Bool(if *negate { !m } else { m }))
374        }
375
376        Expr::Object(fields) => eval_object(fields, env),
377
378        Expr::Array(elems) => {
379            let mut out = Vec::new();
380            for elem in elems {
381                match elem {
382                    ArrayElem::Expr(e)   => out.push(eval(e, env)?),
383                    ArrayElem::Spread(e) => match eval(e, env)? {
384                        Val::Arr(a) => {
385                            let items = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
386                            out.extend(items);
387                        }
388                        Val::IntVec(a) => out.extend(a.iter().map(|n| Val::Int(*n))),
389                        Val::FloatVec(a) => out.extend(a.iter().map(|f| Val::Float(*f))),
390                        v => out.push(v),
391                    },
392                }
393            }
394            Ok(Val::arr(out))
395        }
396
397        Expr::Pipeline { base, steps } => eval_pipeline(base, steps, env),
398
399        Expr::ListComp { expr, vars, iter, cond } => {
400            let items = eval_iter(iter, env)?;
401            let mut out = Vec::new();
402            let mut env_mut = env.clone();
403            for item in items {
404                let frames = bind_vars_mut(&mut env_mut, vars, item);
405                let keep = match cond {
406                    Some(c) => match eval(c, &env_mut) {
407                        Ok(v)  => is_truthy(&v),
408                        Err(e) => { unbind_vars_mut(&mut env_mut, frames); return Err(e); }
409                    },
410                    None => true,
411                };
412                if keep {
413                    match eval(expr, &env_mut) {
414                        Ok(v)  => out.push(v),
415                        Err(e) => { unbind_vars_mut(&mut env_mut, frames); return Err(e); }
416                    }
417                }
418                unbind_vars_mut(&mut env_mut, frames);
419            }
420            Ok(Val::arr(out))
421        }
422
423        Expr::DictComp { key, val, vars, iter, cond } => {
424            let items = eval_iter(iter, env)?;
425            let mut map: IndexMap<Arc<str>, Val> = IndexMap::with_capacity(items.len());
426            let mut env_mut = env.clone();
427            for item in items {
428                let frames = bind_vars_mut(&mut env_mut, vars, item);
429                let keep = match cond {
430                    Some(c) => match eval(c, &env_mut) {
431                        Ok(v)  => is_truthy(&v),
432                        Err(e) => { unbind_vars_mut(&mut env_mut, frames); return Err(e); }
433                    },
434                    None => true,
435                };
436                if keep {
437                    let k: Arc<str> = match eval(key, &env_mut) {
438                        Ok(Val::Str(s)) => s,
439                        Ok(other)       => Arc::<str>::from(val_to_key(&other)),
440                        Err(e) => { unbind_vars_mut(&mut env_mut, frames); return Err(e); }
441                    };
442                    match eval(val, &env_mut) {
443                        Ok(v)  => { map.insert(k, v); }
444                        Err(e) => { unbind_vars_mut(&mut env_mut, frames); return Err(e); }
445                    }
446                }
447                unbind_vars_mut(&mut env_mut, frames);
448            }
449            Ok(Val::obj(map))
450        }
451
452        Expr::SetComp { expr, vars, iter, cond } | Expr::GenComp { expr, vars, iter, cond } => {
453            let items = eval_iter(iter, env)?;
454            let mut seen: std::collections::HashSet<String> =
455                std::collections::HashSet::with_capacity(items.len());
456            let mut out = Vec::with_capacity(items.len());
457            let mut env_mut = env.clone();
458            for item in items {
459                let frames = bind_vars_mut(&mut env_mut, vars, item);
460                let keep = match cond {
461                    Some(c) => match eval(c, &env_mut) {
462                        Ok(v)  => is_truthy(&v),
463                        Err(e) => { unbind_vars_mut(&mut env_mut, frames); return Err(e); }
464                    },
465                    None => true,
466                };
467                if keep {
468                    match eval(expr, &env_mut) {
469                        Ok(v)  => if seen.insert(val_to_key(&v)) { out.push(v); },
470                        Err(e) => { unbind_vars_mut(&mut env_mut, frames); return Err(e); }
471                    }
472                }
473                unbind_vars_mut(&mut env_mut, frames);
474            }
475            Ok(Val::arr(out))
476        }
477
478        Expr::Lambda { .. } => err!("lambda cannot be used as standalone value"),
479
480        Expr::Let { name, init, body } => {
481            let v = eval(init, env)?;
482            eval(body, &env.with_var(name, v))
483        }
484
485        Expr::IfElse { cond, then_, else_ } => {
486            if is_truthy(&eval(cond, env)?) { eval(then_, env) } else { eval(else_, env) }
487        }
488
489        Expr::GlobalCall { name, args } => eval_global(name, args, env),
490
491        Expr::Cast { expr, ty } => {
492            let v = eval(expr, env)?;
493            cast_val(&v, *ty)
494        }
495
496        Expr::Patch { root, ops } => eval_patch(root, ops, env),
497        Expr::DeleteMark =>
498            err!("DELETE can only appear as a patch-field value"),
499    }
500}
501
502// ── Patch ─────────────────────────────────────────────────────────────────────
503
504use super::ast::{PatchOp, PathStep};
505
506enum PatchResult { Replace(Val), Delete }
507
508fn eval_patch(root: &Expr, ops: &[PatchOp], env: &Env) -> Result<Val, EvalError> {
509    let mut doc = eval(root, env)?;
510    for op in ops {
511        if let Some(c) = &op.cond {
512            let cenv = env.with_current(doc.clone());
513            if !is_truthy(&eval(c, &cenv)?) { continue; }
514        }
515        match apply_patch_step(doc, &op.path, 0, &op.val, env)? {
516            PatchResult::Replace(v) => doc = v,
517            PatchResult::Delete     => doc = Val::Null,
518        }
519    }
520    Ok(doc)
521}
522
523fn apply_patch_step(
524    v:        Val,
525    path:     &[PathStep],
526    i:        usize,
527    val_expr: &Expr,
528    env:      &Env,
529) -> Result<PatchResult, EvalError> {
530    if i == path.len() {
531        if matches!(val_expr, Expr::DeleteMark) {
532            return Ok(PatchResult::Delete);
533        }
534        let nv = eval(val_expr, &env.with_current(v))?;
535        return Ok(PatchResult::Replace(nv));
536    }
537    match &path[i] {
538        PathStep::Field(name) => {
539            // Take parent first (clone only if refcount > 1), then mem::replace
540            // the child out of its slot so the recursive call owns it with
541            // refcount=1 — chains of into_map/into_vec stay alloc-free.
542            let mut m = v.into_map().unwrap_or_default();
543            let existing = if let Some(slot) = m.get_mut(name.as_str()) {
544                std::mem::replace(slot, Val::Null)
545            } else { Val::Null };
546            let child = apply_patch_step(existing, path, i+1, val_expr, env)?;
547            match child {
548                PatchResult::Delete => { m.shift_remove(name.as_str()); }
549                PatchResult::Replace(nv) => { m.insert(Arc::from(name.as_str()), nv); }
550            }
551            Ok(PatchResult::Replace(Val::obj(m)))
552        }
553        PathStep::Index(idx) => {
554            let mut a = v.into_vec().unwrap_or_default();
555            let resolved = resolve_idx(*idx, a.len() as i64);
556            let existing = if resolved < a.len() {
557                std::mem::replace(&mut a[resolved], Val::Null)
558            } else { Val::Null };
559            let child = apply_patch_step(existing, path, i+1, val_expr, env)?;
560            match child {
561                PatchResult::Delete => {
562                    if resolved < a.len() { a.remove(resolved); }
563                }
564                PatchResult::Replace(nv) => {
565                    if resolved < a.len() { a[resolved] = nv; }
566                }
567            }
568            Ok(PatchResult::Replace(Val::arr(a)))
569        }
570        PathStep::DynIndex(expr) => {
571            let idx_val = eval(expr, env)?;
572            let idx = idx_val.as_i64().ok_or_else(|| {
573                EvalError(format!("patch dyn-index: expected integer, got {}", idx_val.type_name()))
574            })?;
575            let mut a = v.into_vec().unwrap_or_default();
576            let resolved = resolve_idx(idx, a.len() as i64);
577            let existing = if resolved < a.len() {
578                std::mem::replace(&mut a[resolved], Val::Null)
579            } else { Val::Null };
580            let child = apply_patch_step(existing, path, i+1, val_expr, env)?;
581            match child {
582                PatchResult::Delete => {
583                    if resolved < a.len() { a.remove(resolved); }
584                }
585                PatchResult::Replace(nv) => {
586                    if resolved < a.len() { a[resolved] = nv; }
587                }
588            }
589            Ok(PatchResult::Replace(Val::arr(a)))
590        }
591        PathStep::Wildcard => {
592            let mut arr = v.into_vec().ok_or_else(|| EvalError("patch [*]: expected array".into()))?;
593            // Two-pointer in-place compact: read each slot, mutate/skip,
594            // write kept results back at `write_idx`.  Reuses `arr`'s
595            // allocation instead of building a fresh Vec.
596            let mut write_idx = 0usize;
597            for read_idx in 0..arr.len() {
598                let item = std::mem::replace(&mut arr[read_idx], Val::Null);
599                match apply_patch_step(item, path, i+1, val_expr, env)? {
600                    PatchResult::Delete => {}
601                    PatchResult::Replace(nv) => {
602                        arr[write_idx] = nv;
603                        write_idx += 1;
604                    }
605                }
606            }
607            arr.truncate(write_idx);
608            Ok(PatchResult::Replace(Val::arr(arr)))
609        }
610        PathStep::WildcardFilter(pred) => {
611            let mut arr = v.into_vec().ok_or_else(|| EvalError("patch [* if]: expected array".into()))?;
612            let mut env_mut = env.clone();
613            let mut write_idx = 0usize;
614            for read_idx in 0..arr.len() {
615                let item = std::mem::replace(&mut arr[read_idx], Val::Null);
616                let frame = env_mut.push_lam(None, item.clone());
617                let include = match eval(pred, &env_mut) {
618                    Ok(v)  => is_truthy(&v),
619                    Err(e) => { env_mut.pop_lam(frame); return Err(e); }
620                };
621                env_mut.pop_lam(frame);
622                if include {
623                    match apply_patch_step(item, path, i+1, val_expr, env)? {
624                        PatchResult::Delete => {}
625                        PatchResult::Replace(nv) => {
626                            arr[write_idx] = nv;
627                            write_idx += 1;
628                        }
629                    }
630                } else {
631                    arr[write_idx] = item;
632                    write_idx += 1;
633                }
634            }
635            arr.truncate(write_idx);
636            Ok(PatchResult::Replace(Val::arr(arr)))
637        }
638        PathStep::Descendant(name) => {
639            let v = descend_apply_patch(v, name, path, i, val_expr, env)?;
640            Ok(PatchResult::Replace(v))
641        }
642    }
643}
644
645/// Descendant patch walker — DFS through the subtree.  At every object that
646/// has `name`, apply the remaining path (starting at `i+1`) to the value of
647/// `name`.  Children are visited *before* the current level so freshly-written
648/// values are not re-walked (avoids runaway rewrites when the new value
649/// itself contains `name`).
650fn descend_apply_patch(
651    v:        Val,
652    name:     &str,
653    path:     &[PathStep],
654    i:        usize,
655    val_expr: &Expr,
656    env:      &Env,
657) -> Result<Val, EvalError> {
658    match v {
659        Val::Obj(m) => {
660            let mut map = Arc::try_unwrap(m).unwrap_or_else(|m| (*m).clone());
661            // Recurse into children first, using original values.  In-place
662            // slot swap instead of shift_remove+insert (which is O(n) per
663            // key → O(n²) per map on wide objects).
664            let n = map.len();
665            for idx in 0..n {
666                let child = if let Some((_, v)) = map.get_index_mut(idx) {
667                    std::mem::replace(v, Val::Null)
668                } else { continue };
669                let replaced = descend_apply_patch(child, name, path, i, val_expr, env)?;
670                if let Some((_, slot)) = map.get_index_mut(idx) { *slot = replaced; }
671            }
672            // Apply at this level.
673            if map.contains_key(name) {
674                let existing = map.get(name).cloned().unwrap_or(Val::Null);
675                let r = apply_patch_step(existing, path, i + 1, val_expr, env)?;
676                match r {
677                    PatchResult::Delete      => { map.shift_remove(name); }
678                    PatchResult::Replace(nv) => { map.insert(Arc::from(name), nv); }
679                }
680            }
681            Ok(Val::obj(map))
682        }
683        Val::Arr(a) => {
684            let mut vec = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
685            for slot in vec.iter_mut() {
686                let old = std::mem::replace(slot, Val::Null);
687                *slot = descend_apply_patch(old, name, path, i, val_expr, env)?;
688            }
689            Ok(Val::arr(vec))
690        }
691        other => Ok(other),
692    }
693}
694
695/// Runtime cast dispatcher — powers the `as` operator.  Semantics mirror
696/// the existing `.to_string()` / `.to_bool()` / `.to_number()` methods
697/// for overlapping types; `int`/`float` are new targets that narrow
698/// numeric values.
699fn cast_val(v: &Val, ty: super::ast::CastType) -> Result<Val, EvalError> {
700    use super::ast::CastType;
701    match ty {
702        CastType::Str => Ok(Val::Str(Arc::from(match v {
703            Val::Null       => "null".to_string(),
704            Val::Bool(b)    => b.to_string(),
705            Val::Int(n)     => n.to_string(),
706            Val::Float(f)   => f.to_string(),
707            Val::Str(s)     => s.to_string(),
708            other           => super::eval::util::val_to_string(other),
709        }.as_str()))),
710        CastType::Bool => Ok(Val::Bool(match v {
711            Val::Null       => false,
712            Val::Bool(b)    => *b,
713            Val::Int(n)     => *n != 0,
714            Val::Float(f)   => *f != 0.0,
715            Val::Str(s)       => !s.is_empty(),
716            Val::StrSlice(r)  => !r.is_empty(),
717            Val::Arr(a)       => !a.is_empty(),
718            Val::IntVec(a)    => !a.is_empty(),
719            Val::FloatVec(a)  => !a.is_empty(),
720            Val::StrVec(a)       => !a.is_empty(),
721            Val::StrSliceVec(a)  => !a.is_empty(),
722            Val::ObjVec(d)       => !d.rows.is_empty(),
723            Val::Obj(o)       => !o.is_empty(),
724            Val::ObjSmall(p)  => !p.is_empty(),
725        })),
726        CastType::Number | CastType::Float => match v {
727            Val::Int(n)     => Ok(Val::Float(*n as f64)),
728            Val::Float(_)   => Ok(v.clone()),
729            Val::Str(s)     => s.parse::<f64>().map(Val::Float)
730                                .map_err(|e| EvalError(format!("as float: {}", e))),
731            Val::Bool(b)    => Ok(Val::Float(if *b { 1.0 } else { 0.0 })),
732            Val::Null       => Ok(Val::Float(0.0)),
733            _               => err!("as float: cannot convert"),
734        },
735        CastType::Int => match v {
736            Val::Int(_)     => Ok(v.clone()),
737            Val::Float(f)   => Ok(Val::Int(*f as i64)),
738            Val::Str(s)     => s.parse::<i64>().map(Val::Int)
739                                .or_else(|_| s.parse::<f64>().map(|f| Val::Int(f as i64)))
740                                .map_err(|e| EvalError(format!("as int: {}", e))),
741            Val::Bool(b)    => Ok(Val::Int(if *b { 1 } else { 0 })),
742            Val::Null       => Ok(Val::Int(0)),
743            _               => err!("as int: cannot convert"),
744        },
745        CastType::Array => match v {
746            Val::Arr(_)     => Ok(v.clone()),
747            Val::Null       => Ok(Val::arr(Vec::new())),
748            other           => Ok(Val::arr(vec![other.clone()])),
749        },
750        CastType::Object => match v {
751            Val::Obj(_)     => Ok(v.clone()),
752            _               => err!("as object: cannot convert non-object"),
753        },
754        CastType::Null => Ok(Val::Null),
755    }
756}
757
758// ── Pipeline ──────────────────────────────────────────────────────────────────
759
760fn eval_pipeline(base: &Expr, steps: &[PipeStep], env: &Env) -> Result<Val, EvalError> {
761    let mut current = eval(base, env)?;
762    let mut env = env.clone();
763    for step in steps {
764        match step {
765            PipeStep::Forward(rhs) => current = eval_pipe(current, rhs, &env)?,
766            PipeStep::Bind(target) => env = apply_bind(target, &current, env)?,
767        }
768    }
769    Ok(current)
770}
771
772fn apply_bind(target: &BindTarget, val: &Val, env: Env) -> Result<Env, EvalError> {
773    match target {
774        BindTarget::Name(name) => Ok(env.with_var(name, val.clone())),
775        BindTarget::Obj { fields, rest } => {
776            let obj = val.as_object()
777                .ok_or_else(|| EvalError("bind destructure: expected object".into()))?;
778            let mut e = env;
779            for f in fields {
780                e = e.with_var(f, obj.get(f.as_str()).cloned().unwrap_or(Val::Null));
781            }
782            if let Some(rest_name) = rest {
783                let mut remainder: IndexMap<Arc<str>, Val> = IndexMap::new();
784                for (k, v) in obj {
785                    if !fields.iter().any(|f| f.as_str() == k.as_ref()) {
786                        remainder.insert(k.clone(), v.clone());
787                    }
788                }
789                e = e.with_var(rest_name, Val::obj(remainder));
790            }
791            Ok(e)
792        }
793        BindTarget::Arr(names) => {
794            let len = val.arr_len()
795                .ok_or_else(|| EvalError("bind destructure: expected array".into()))?;
796            let mut e = env;
797            for (i, name) in names.iter().enumerate() {
798                let v = if i < len { val.get_index(i as i64) } else { Val::Null };
799                e = e.with_var(name, v);
800            }
801            Ok(e)
802        }
803    }
804}
805
806fn eval_pipe(left: Val, rhs: &Expr, env: &Env) -> Result<Val, EvalError> {
807    match rhs {
808        Expr::Ident(name) => {
809            if env.has_var(name) {
810                eval(rhs, &env.with_current(left))
811            } else {
812                dispatch_method(left, name, &[], env)
813            }
814        }
815        Expr::Chain(base, steps) => {
816            if let Expr::Ident(name) = base.as_ref() {
817                if !env.has_var(name) {
818                    let mut v = dispatch_method(left, name, &[], env)?;
819                    for step in steps { v = eval_step(v, step, env)?; }
820                    return Ok(v);
821                }
822            }
823            eval(rhs, &env.with_current(left))
824        }
825        _ => eval(rhs, &env.with_current(left)),
826    }
827}
828
829/// Walk as many chain steps as possible on raw byte spans, then materialise.
830///
831/// Supports: `Descendant`, `Quantifier(First)` with any length, `Quantifier(One)`
832/// only when exactly one span remains (otherwise falls through so the materialised
833/// path can raise the proper error), `InlineFilter` / `.filter(...)` whose
834/// predicate is a canonical equality literal.  All other steps terminate the
835/// byte chain; the caller materialises and resumes normal step evaluation.
836///
837/// Returns `(value, steps_consumed)` where `steps_consumed >= 1` because the
838/// entry `Descendant` step is always handled here.
839/// A "first-selector" step: `.first()` method call or `Quantifier::First`.
840/// When a `Descendant(k)` is immediately followed by one of these, the
841/// scan can early-exit on the first match per span.
842fn is_first_selector(step: &Step) -> bool {
843    match step {
844        Step::Quantifier(QuantifierKind::First) => true,
845        Step::Method(name, args) if args.is_empty() && name == "first" => true,
846        _ => false,
847    }
848}
849
850fn byte_chain_eval(
851    bytes: &[u8],
852    root_key: &str,
853    steps: &[Step],
854) -> (Val, usize) {
855    // Early-exit: `$..key.first()` / `$..key!` only needs the first match.
856    let first_after_initial = steps.get(1).map(is_first_selector).unwrap_or(false);
857    let mut spans: Vec<super::scan::ValueSpan> = if first_after_initial {
858        super::scan::find_first_key_value_span(bytes, root_key)
859            .into_iter().collect()
860    } else {
861        super::scan::find_key_value_spans(bytes, root_key)
862    };
863    let mut scalar = false;
864    let mut consumed = 1usize;
865
866    for (idx, step) in steps.iter().enumerate().skip(1) {
867        match step {
868            Step::Descendant(k) => {
869                // Early-exit when the very next step is a first-selector:
870                // only find one inner match per outer span, skip the rest.
871                let next_first = steps.get(idx + 1)
872                    .map(is_first_selector).unwrap_or(false);
873                let mut next = Vec::with_capacity(spans.len());
874                for s in &spans {
875                    let sub = &bytes[s.start..s.end];
876                    if next_first {
877                        if let Some(s2) = super::scan::find_first_key_value_span(sub, k) {
878                            next.push(super::scan::ValueSpan {
879                                start: s.start + s2.start,
880                                end:   s.start + s2.end,
881                            });
882                        }
883                    } else {
884                        for s2 in super::scan::find_key_value_spans(sub, k) {
885                            next.push(super::scan::ValueSpan {
886                                start: s.start + s2.start,
887                                end:   s.start + s2.end,
888                            });
889                        }
890                    }
891                }
892                spans = next;
893                scalar = false;
894            }
895            Step::Quantifier(QuantifierKind::First) => {
896                spans.truncate(1);
897                scalar = true;
898            }
899            Step::Quantifier(QuantifierKind::One) => {
900                if spans.len() != 1 { break; }
901                scalar = true;
902            }
903            // `.first()` / `.last()` arrive as method calls (no `!` / `?` syntax).
904            Step::Method(name, args) if args.is_empty() && name == "first" => {
905                spans.truncate(1);
906                scalar = true;
907            }
908            Step::Method(name, args) if args.is_empty() && name == "last" => {
909                if let Some(last) = spans.pop() { spans = vec![last]; }
910                scalar = true;
911            }
912            Step::InlineFilter(pred) => match canonical_eq_literal(pred) {
913                Some(lit) => {
914                    spans.retain(|s| {
915                        s.end - s.start == lit.len()
916                            && &bytes[s.start..s.end] == &lit[..]
917                    });
918                    scalar = false;
919                }
920                None => break,
921            },
922            Step::Method(name, args)
923                if name == "filter" && args.len() == 1 =>
924            {
925                let pred = match &args[0] { Arg::Pos(e) | Arg::Named(_, e) => e };
926                match canonical_eq_literal(pred) {
927                    Some(lit) => {
928                        spans.retain(|s| {
929                            s.end - s.start == lit.len()
930                                && &bytes[s.start..s.end] == &lit[..]
931                        });
932                        scalar = false;
933                    }
934                    None => break,
935                }
936            }
937            _ => break,
938        }
939        consumed += 1;
940    }
941
942    // Numeric-fold fast path: trailing `.sum()/.avg()/.min()/.max()/.count()/.len()`
943    // with no args — skip Val materialisation, parse numbers inline.
944    if !scalar {
945        if let Some(Step::Method(name, args)) = steps.get(consumed) {
946            if args.is_empty() {
947                let tail_ok = steps.len() == consumed + 1;
948                if tail_ok {
949                    match name.as_str() {
950                        "count" | "len" => {
951                            return (Val::Int(spans.len() as i64), consumed + 1);
952                        }
953                        "sum" => {
954                            let f = super::scan::fold_nums(bytes, &spans);
955                            let v = if f.count == 0 { Val::Int(0) }
956                                else if f.is_float { Val::Float(f.float_sum) }
957                                else { Val::Int(f.int_sum) };
958                            return (v, consumed + 1);
959                        }
960                        "avg" => {
961                            let f = super::scan::fold_nums(bytes, &spans);
962                            let v = if f.count == 0 { Val::Null }
963                                else { Val::Float(f.float_sum / f.count as f64) };
964                            return (v, consumed + 1);
965                        }
966                        "min" => {
967                            let f = super::scan::fold_nums(bytes, &spans);
968                            let v = if !f.any { Val::Null }
969                                else if f.is_float { Val::Float(f.min_f) }
970                                else { Val::Int(f.min_i) };
971                            return (v, consumed + 1);
972                        }
973                        "max" => {
974                            let f = super::scan::fold_nums(bytes, &spans);
975                            let v = if !f.any { Val::Null }
976                                else if f.is_float { Val::Float(f.max_f) }
977                                else { Val::Int(f.max_i) };
978                            return (v, consumed + 1);
979                        }
980                        _ => {}
981                    }
982                }
983            }
984        }
985    }
986
987    let mut materialised: Vec<Val> = Vec::with_capacity(spans.len());
988    for s in &spans {
989        match serde_json::from_slice::<serde_json::Value>(&bytes[s.start..s.end]) {
990            Ok(v) => materialised.push(Val::from(&v)),
991            Err(_) => {}
992        }
993    }
994
995    let out = if scalar {
996        materialised.into_iter().next().unwrap_or(Val::Null)
997    } else {
998        Val::arr(materialised)
999    };
1000    (out, consumed)
1001}
1002
1003/// Recognise `@ == lit` / `lit == @` predicates where `lit` is a canonical
1004/// JSON literal (int, string, bool, null).  Returns the serialised literal
1005/// bytes that can be fed to `scan::extract_values_eq`.  Float literals are
1006/// deliberately *not* accepted — `1.0` vs `1` and representation variance
1007/// make bytewise comparison unsafe.
1008fn canonical_eq_literal(pred: &Expr) -> Option<Vec<u8>> {
1009    let (l, r) = match pred {
1010        Expr::BinOp(l, BinOp::Eq, r) => (&**l, &**r),
1011        _ => return None,
1012    };
1013    let lit = match (l, r) {
1014        (Expr::Current, lit) => lit,
1015        (lit, Expr::Current) => lit,
1016        _ => return None,
1017    };
1018    match lit {
1019        Expr::Int(n)  => Some(n.to_string().into_bytes()),
1020        Expr::Bool(b) => Some(if *b { b"true".to_vec() } else { b"false".to_vec() }),
1021        Expr::Null    => Some(b"null".to_vec()),
1022        Expr::Str(s)  => serde_json::to_vec(&serde_json::Value::String(s.clone())).ok(),
1023        _ => None,
1024    }
1025}
1026
1027/// Recognise `@.field == lit` / `lit == @.field` predicates. Returns the
1028/// field name and serialised literal bytes suitable for
1029/// `scan::find_enclosing_objects_eq`.  Float literals rejected for the same
1030/// reason as `canonical_eq_literal`.  Only a single leading field step is
1031/// supported (no nested `@.a.b`).
1032/// N-conjunct version: every arg must be a canonical `@.k == lit` predicate,
1033/// else returns None.  Empty input returns None.
1034pub(crate) fn canonical_field_eq_literals(args: &[Arg]) -> Option<Vec<(String, Vec<u8>)>> {
1035    if args.is_empty() || args.len() > 64 { return None; }
1036    let mut out = Vec::with_capacity(args.len());
1037    for a in args {
1038        let e = match a { Arg::Pos(e) | Arg::Named(_, e) => e };
1039        out.push(canonical_field_eq_literal(e)?);
1040    }
1041    Some(out)
1042}
1043
1044/// Recognise `@.field op num` / `num op @.field` predicates where `op` is
1045/// one of `<`, `<=`, `>`, `>=` and the literal is a finite JSON number.
1046/// Returns `(field, ScanCmp, threshold)` normalised so `@.field` is always
1047/// on the LHS of the comparison (operator flipped when the literal was on
1048/// the LHS).
1049pub(crate) fn canonical_field_cmp_literal(
1050    pred: &Expr,
1051) -> Option<(String, super::scan::ScanCmp, f64)> {
1052    use super::scan::ScanCmp;
1053    let (l, op, r) = match pred {
1054        Expr::BinOp(l, op @ (BinOp::Lt | BinOp::Lte | BinOp::Gt | BinOp::Gte), r) =>
1055            (&**l, *op, &**r),
1056        _ => return None,
1057    };
1058    fn as_current_field(e: &Expr) -> Option<String> {
1059        if let Expr::Chain(base, steps) = e {
1060            if matches!(**base, Expr::Current) && steps.len() == 1 {
1061                if let Step::Field(name) = &steps[0] { return Some(name.clone()); }
1062            }
1063        }
1064        None
1065    }
1066    fn as_num(e: &Expr) -> Option<f64> {
1067        match e {
1068            Expr::Int(n)   => Some(*n as f64),
1069            Expr::Float(f) => if f.is_finite() { Some(*f) } else { None },
1070            _ => None,
1071        }
1072    }
1073    let (field, thresh, flip) = match (as_current_field(l), as_num(r)) {
1074        (Some(f), Some(n)) => (f, n, false),
1075        _ => match (as_current_field(r), as_num(l)) {
1076            (Some(f), Some(n)) => (f, n, true),
1077            _ => return None,
1078        },
1079    };
1080    let scan_op = match (op, flip) {
1081        (BinOp::Lt,  false) | (BinOp::Gt,  true)  => ScanCmp::Lt,
1082        (BinOp::Lte, false) | (BinOp::Gte, true)  => ScanCmp::Lte,
1083        (BinOp::Gt,  false) | (BinOp::Lt,  true)  => ScanCmp::Gt,
1084        (BinOp::Gte, false) | (BinOp::Lte, true)  => ScanCmp::Gte,
1085        _ => return None,
1086    };
1087    Some((field, scan_op, thresh))
1088}
1089
1090/// Canonicalise a predicate list that mixes `@.k == lit` and `@.k op num`
1091/// conjuncts into a list of `(field, ScanPred)` pairs the mixed byte scan
1092/// can consume.  Returns `None` if any conjunct fits neither shape.
1093/// Empty input or more than 64 conjuncts → `None`.
1094pub(crate) fn canonical_field_mixed_predicates(
1095    args: &[Arg],
1096) -> Option<Vec<(String, super::scan::ScanPred)>> {
1097    use super::scan::ScanPred;
1098    if args.is_empty() || args.len() > 64 { return None; }
1099    let mut out = Vec::with_capacity(args.len());
1100    for a in args {
1101        let e = match a { Arg::Pos(e) | Arg::Named(_, e) => e };
1102        if let Some((k, lit)) = canonical_field_eq_literal(e) {
1103            out.push((k, ScanPred::Eq(lit)));
1104        } else if let Some((k, op, n)) = canonical_field_cmp_literal(e) {
1105            out.push((k, ScanPred::Cmp(op, n)));
1106        } else {
1107            return None;
1108        }
1109    }
1110    Some(out)
1111}
1112
1113pub(crate) fn canonical_field_eq_literal(pred: &Expr) -> Option<(String, Vec<u8>)> {
1114    let (l, r) = match pred {
1115        Expr::BinOp(l, BinOp::Eq, r) => (&**l, &**r),
1116        _ => return None,
1117    };
1118    fn as_current_field(e: &Expr) -> Option<String> {
1119        if let Expr::Chain(base, steps) = e {
1120            if matches!(**base, Expr::Current) && steps.len() == 1 {
1121                if let Step::Field(name) = &steps[0] {
1122                    return Some(name.clone());
1123                }
1124            }
1125        }
1126        None
1127    }
1128    let (field, lit) = if let Some(f) = as_current_field(l) { (f, r) }
1129                       else if let Some(f) = as_current_field(r) { (f, l) }
1130                       else { return None };
1131    let bytes = match lit {
1132        Expr::Int(n)  => n.to_string().into_bytes(),
1133        Expr::Bool(b) => if *b { b"true".to_vec() } else { b"false".to_vec() },
1134        Expr::Null    => b"null".to_vec(),
1135        Expr::Str(s)  => serde_json::to_vec(&serde_json::Value::String(s.clone())).ok()?,
1136        _ => return None,
1137    };
1138    Some((field, bytes))
1139}
1140
1141// ── Step evaluation ───────────────────────────────────────────────────────────
1142
1143fn eval_step(val: Val, step: &Step, env: &Env) -> Result<Val, EvalError> {
1144    match step {
1145        Step::Field(name)    => Ok(val.get_field(name)),
1146        Step::OptField(name) => {
1147            if val.is_null() { Ok(Val::Null) } else { Ok(val.get_field(name)) }
1148        }
1149        Step::Descendant(name) => {
1150            let mut found = Vec::new();
1151            collect_desc(&val, name, &mut found);
1152            Ok(Val::arr(found))
1153        }
1154        Step::DescendAll => {
1155            let mut found = Vec::new();
1156            collect_all(&val, &mut found);
1157            Ok(Val::arr(found))
1158        }
1159        Step::InlineFilter(pred) => {
1160            let items = match val {
1161                Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
1162                other => vec![other],
1163            };
1164            let mut out = Vec::new();
1165            let mut env_mut = env.clone();
1166            for item in items {
1167                let frame = env_mut.push_lam(None, item.clone());
1168                let truthy = match eval(pred, &env_mut) {
1169                    Ok(v)  => is_truthy(&v),
1170                    Err(e) => { env_mut.pop_lam(frame); return Err(e); }
1171                };
1172                env_mut.pop_lam(frame);
1173                if truthy { out.push(item); }
1174            }
1175            Ok(Val::arr(out))
1176        }
1177        Step::Quantifier(kind) => {
1178            use super::ast::QuantifierKind;
1179            match kind {
1180                QuantifierKind::First => {
1181                    Ok(match val {
1182                        Val::Arr(a) => a.first().cloned().unwrap_or(Val::Null),
1183                        other => other,
1184                    })
1185                }
1186                QuantifierKind::One => {
1187                    match val {
1188                        Val::Arr(a) if a.len() == 1 => Ok(a[0].clone()),
1189                        Val::Arr(a) => err!("quantifier !: expected exactly one element, got {}", a.len()),
1190                        other => Ok(other),
1191                    }
1192                }
1193            }
1194        }
1195        Step::Index(i) => Ok(val.get_index(*i)),
1196        Step::DynIndex(expr) => {
1197            let key = eval(expr, env)?;
1198            match key {
1199                Val::Int(i)  => Ok(val.get_index(i)),
1200                Val::Str(s)  => Ok(val.get_field(s.as_ref())),
1201                _ => err!("dynamic index must be a number or string"),
1202            }
1203        }
1204        Step::Slice(from, to) => {
1205            match val {
1206                Val::Arr(a) => {
1207                    let len = a.len() as i64;
1208                    let s = resolve_idx(from.unwrap_or(0), len);
1209                    let e = resolve_idx(to.unwrap_or(len), len);
1210                    let items = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
1211                    let s = s.min(items.len());
1212                    let e = e.min(items.len());
1213                    Ok(Val::arr(items[s..e].to_vec()))
1214                }
1215                Val::IntVec(a) => {
1216                    let len = a.len() as i64;
1217                    let s = resolve_idx(from.unwrap_or(0), len);
1218                    let e = resolve_idx(to.unwrap_or(len), len);
1219                    let items = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
1220                    let s = s.min(items.len());
1221                    let e = e.min(items.len());
1222                    Ok(Val::int_vec(items[s..e].to_vec()))
1223                }
1224                Val::FloatVec(a) => {
1225                    let len = a.len() as i64;
1226                    let s = resolve_idx(from.unwrap_or(0), len);
1227                    let e = resolve_idx(to.unwrap_or(len), len);
1228                    let items = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
1229                    let s = s.min(items.len());
1230                    let e = e.min(items.len());
1231                    Ok(Val::float_vec(items[s..e].to_vec()))
1232                }
1233                _ => Ok(Val::Null),
1234            }
1235        }
1236        Step::Method(name, args)    => dispatch_method(val, name, args, env),
1237        Step::OptMethod(name, args) => {
1238            if val.is_null() { Ok(Val::Null) } else { dispatch_method(val, name, args, env) }
1239        }
1240    }
1241}
1242
1243fn resolve_idx(i: i64, len: i64) -> usize {
1244    (if i < 0 { (len + i).max(0) } else { i }) as usize
1245}
1246
1247fn collect_desc(v: &Val, name: &str, out: &mut Vec<Val>) {
1248    match v {
1249        Val::Obj(m) => {
1250            if let Some(v) = m.get(name) { out.push(v.clone()); }
1251            for v in m.values() { collect_desc(v, name, out); }
1252        }
1253        Val::Arr(a) => { for item in a.as_ref() { collect_desc(item, name, out); } }
1254        _ => {}
1255    }
1256}
1257
1258fn collect_all(v: &Val, out: &mut Vec<Val>) {
1259    match v {
1260        Val::Obj(m) => {
1261            out.push(v.clone());
1262            for child in m.values() { collect_all(child, out); }
1263        }
1264        Val::Arr(a) => {
1265            for item in a.as_ref() { collect_all(item, out); }
1266        }
1267        other => out.push(other.clone()),
1268    }
1269}
1270
1271// ── Method dispatch ───────────────────────────────────────────────────────────
1272
1273pub(super) fn dispatch_method(recv: Val, name: &str, args: &[Arg], env: &Env) -> Result<Val, EvalError> {
1274    if let Some(f) = builtins::global().get(name) {
1275        return f(recv, args, env);
1276    }
1277    if !env.registry.is_empty() {
1278        if let Some(method) = env.registry.get(name) {
1279            let evaluated: Result<Vec<Val>, EvalError> =
1280                args.iter().map(|a| eval_pos(a, env)).collect();
1281            return method.call(recv, &evaluated?);
1282        }
1283    }
1284    err!("unknown method '{}'", name)
1285}
1286
1287// ── Object construction ───────────────────────────────────────────────────────
1288
1289fn eval_object(fields: &[ObjField], env: &Env) -> Result<Val, EvalError> {
1290    let mut map: IndexMap<Arc<str>, Val> = IndexMap::new();
1291    for field in fields {
1292        match field {
1293            ObjField::Short(name) => {
1294                let v = if let Some(v) = env.get_var(name) { v.clone() }
1295                        else { env.current.get_field(name) };
1296                if !v.is_null() { map.insert(Arc::from(name.as_str()), v); }
1297            }
1298            ObjField::Kv { key, val, optional, cond } => {
1299                if let Some(c) = cond {
1300                    if !is_truthy(&eval(c, env)?) { continue; }
1301                }
1302                let v = eval(val, env)?;
1303                if *optional && v.is_null() { continue; }
1304                map.insert(Arc::from(key.as_str()), v);
1305            }
1306            ObjField::Dynamic { key, val } => {
1307                let k: Arc<str> = match eval(key, env)? {
1308                    Val::Str(s) => s,
1309                    other       => Arc::<str>::from(val_to_key(&other)),
1310                };
1311                map.insert(k, eval(val, env)?);
1312            }
1313            ObjField::Spread(expr) => {
1314                if let Val::Obj(other) = eval(expr, env)? {
1315                    let entries = Arc::try_unwrap(other).unwrap_or_else(|m| (*m).clone());
1316                    for (k, v) in entries { map.insert(k, v); }
1317                }
1318            }
1319            ObjField::SpreadDeep(expr) => {
1320                if let Val::Obj(other) = eval(expr, env)? {
1321                    let base = std::mem::take(&mut map);
1322                    let merged = deep_merge_concat(Val::obj(base), Val::Obj(other));
1323                    if let Val::Obj(m) = merged {
1324                        map = Arc::try_unwrap(m).unwrap_or_else(|m| (*m).clone());
1325                    }
1326                }
1327            }
1328        }
1329    }
1330    Ok(Val::obj(map))
1331}
1332
1333// ── F-string ──────────────────────────────────────────────────────────────────
1334
1335fn eval_fstring(parts: &[FStringPart], env: &Env) -> Result<Val, EvalError> {
1336    let mut out = String::new();
1337    for part in parts {
1338        match part {
1339            FStringPart::Lit(s) => out.push_str(s),
1340            FStringPart::Interp { expr, fmt } => {
1341                let val = eval(expr, env)?;
1342                let s = match fmt {
1343                    None                     => val_to_string(&val),
1344                    Some(FmtSpec::Spec(spec)) => apply_fmt_spec(&val, spec),
1345                    Some(FmtSpec::Pipe(method)) => {
1346                        val_to_string(&dispatch_method(val, method, &[], env)?)
1347                    }
1348                };
1349                out.push_str(&s);
1350            }
1351        }
1352    }
1353    Ok(Val::Str(Arc::from(out.as_str())))
1354}
1355
1356fn apply_fmt_spec(val: &Val, spec: &str) -> String {
1357    if let Some(rest) = spec.strip_suffix('f') {
1358        if let Some(prec_str) = rest.strip_prefix('.') {
1359            if let Ok(prec) = prec_str.parse::<usize>() {
1360                if let Some(f) = val.as_f64() { return format!("{:.prec$}", f); }
1361            }
1362        }
1363    }
1364    if spec == "d" {
1365        if let Some(i) = val.as_i64() { return format!("{}", i); }
1366    }
1367    let s = val_to_string(val);
1368    if let Some(w) = spec.strip_prefix('>').and_then(|s| s.parse::<usize>().ok()) { return format!("{:>w$}", s); }
1369    if let Some(w) = spec.strip_prefix('<').and_then(|s| s.parse::<usize>().ok()) { return format!("{:<w$}", s); }
1370    if let Some(w) = spec.strip_prefix('^').and_then(|s| s.parse::<usize>().ok()) { return format!("{:^w$}", s); }
1371    if let Some(w) = spec.strip_prefix('0').and_then(|s| s.parse::<usize>().ok()) {
1372        if let Some(i) = val.as_i64() { return format!("{:0>w$}", i); }
1373    }
1374    s
1375}
1376
1377// ── Binary operators ──────────────────────────────────────────────────────────
1378
1379fn eval_binop(l: &Expr, op: BinOp, r: &Expr, env: &Env) -> Result<Val, EvalError> {
1380    match op {
1381        BinOp::And => {
1382            let lv = eval(l, env)?;
1383            if !is_truthy(&lv) { return Ok(Val::Bool(false)); }
1384            Ok(Val::Bool(is_truthy(&eval(r, env)?)))
1385        }
1386        BinOp::Or => {
1387            let lv = eval(l, env)?;
1388            if is_truthy(&lv) { return Ok(lv); }
1389            eval(r, env)
1390        }
1391        _ => {
1392            let lv = eval(l, env)?;
1393            let rv = eval(r, env)?;
1394            match op {
1395                BinOp::Add  => add_vals(lv, rv),
1396                BinOp::Sub  => num_op(lv, rv, |a, b| a - b, |a, b| a - b),
1397                BinOp::Mul  => num_op(lv, rv, |a, b| a * b, |a, b| a * b),
1398                BinOp::Div  => {
1399                    let b = rv.as_f64().unwrap_or(0.0);
1400                    if b == 0.0 { return err!("division by zero"); }
1401                    Ok(Val::Float(lv.as_f64().unwrap_or(0.0) / b))
1402                }
1403                BinOp::Mod   => num_op(lv, rv, |a, b| a % b, |a, b| a % b),
1404                BinOp::Eq    => Ok(Val::Bool(vals_eq(&lv, &rv))),
1405                BinOp::Neq   => Ok(Val::Bool(!vals_eq(&lv, &rv))),
1406                BinOp::Lt    => Ok(Val::Bool(cmp_vals(&lv, &rv) == std::cmp::Ordering::Less)),
1407                BinOp::Lte   => Ok(Val::Bool(cmp_vals(&lv, &rv) != std::cmp::Ordering::Greater)),
1408                BinOp::Gt    => Ok(Val::Bool(cmp_vals(&lv, &rv) == std::cmp::Ordering::Greater)),
1409                BinOp::Gte   => Ok(Val::Bool(cmp_vals(&lv, &rv) != std::cmp::Ordering::Less)),
1410                BinOp::Fuzzy => {
1411                    let ls = match &lv { Val::Str(s) => s.to_lowercase(), _ => val_to_string(&lv).to_lowercase() };
1412                    let rs = match &rv { Val::Str(s) => s.to_lowercase(), _ => val_to_string(&rv).to_lowercase() };
1413                    Ok(Val::Bool(ls.contains(&rs) || rs.contains(&ls)))
1414                }
1415                BinOp::And | BinOp::Or => unreachable!(),
1416            }
1417        }
1418    }
1419}
1420
1421// ── Global functions ──────────────────────────────────────────────────────────
1422
1423fn eval_global(name: &str, args: &[Arg], env: &Env) -> Result<Val, EvalError> {
1424    match name {
1425        "coalesce" => {
1426            for arg in args {
1427                let v = eval_pos(arg, env)?;
1428                if !v.is_null() { return Ok(v); }
1429            }
1430            Ok(Val::Null)
1431        }
1432        "chain" | "join" => {
1433            let mut out = Vec::new();
1434            for arg in args {
1435                match eval_pos(arg, env)? {
1436                    Val::Arr(a) => {
1437                        let items = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
1438                        out.extend(items);
1439                    }
1440                    Val::IntVec(a) => out.extend(a.iter().map(|n| Val::Int(*n))),
1441                    Val::FloatVec(a) => out.extend(a.iter().map(|f| Val::Float(*f))),
1442                    v => out.push(v),
1443                }
1444            }
1445            Ok(Val::arr(out))
1446        }
1447        "zip"         => func_arrays::global_zip(args, env),
1448        "zip_longest" => func_arrays::global_zip_longest(args, env),
1449        "product"     => func_arrays::global_product(args, env),
1450        "range"       => eval_range(args, env),
1451        other => {
1452            if let Some(first) = args.first() {
1453                let recv = eval_pos(first, env)?;
1454                dispatch_method(recv, other, args.get(1..).unwrap_or(&[]), env)
1455            } else {
1456                dispatch_method(env.current.clone(), other, &[], env)
1457            }
1458        }
1459    }
1460}
1461
1462// ── range() generator ────────────────────────────────────────────────────────
1463//
1464// `range(n)`            -> [0, 1, …, n-1]
1465// `range(from, upto)`   -> [from, from+1, …, upto-1]
1466// `range(from, upto, step)` -> arithmetic progression; step may be negative.
1467//
1468// Returns an eager `Val::Arr` (jetro is value-oriented, not streaming).
1469// Int-only; non-numeric args produce a descriptive error.  Empty when
1470// the step points away from `upto` or when `step == 0`.
1471
1472fn eval_range(args: &[Arg], env: &Env) -> Result<Val, EvalError> {
1473    let n = args.len();
1474    if n == 0 || n > 3 {
1475        return err!("range: expected 1..3 args, got {}", n);
1476    }
1477    let mut nums = Vec::with_capacity(n);
1478    for a in args {
1479        let v = eval_pos(a, env)?;
1480        let i = v.as_i64().ok_or_else(|| EvalError("range: expected integer arg".into()))?;
1481        nums.push(i);
1482    }
1483    let (from, upto, step) = match nums.as_slice() {
1484        [n]          => (0, *n, 1i64),
1485        [f, u]       => (*f, *u, 1i64),
1486        [f, u, s]    => (*f, *u, *s),
1487        _            => unreachable!(),
1488    };
1489    if step == 0 { return Ok(Val::int_vec(Vec::new())); }
1490    let len_hint: usize = if step > 0 && upto > from {
1491        (((upto - from) + step - 1) / step).max(0) as usize
1492    } else if step < 0 && upto < from {
1493        (((from - upto) + (-step) - 1) / (-step)).max(0) as usize
1494    } else { 0 };
1495    let mut out: Vec<i64> = Vec::with_capacity(len_hint);
1496    let mut i = from;
1497    if step > 0 {
1498        while i < upto { out.push(i); i += step; }
1499    } else {
1500        while i > upto { out.push(i); i += step; }
1501    }
1502    Ok(Val::int_vec(out))
1503}
1504
1505// ── Apply helpers ─────────────────────────────────────────────────────────────
1506
1507pub(super) fn apply_item(item: Val, arg: &Arg, env: &Env) -> Result<Val, EvalError> {
1508    match arg {
1509        Arg::Pos(expr) | Arg::Named(_, expr) => apply_expr_item(item, expr, env),
1510    }
1511}
1512
1513/// Hot-loop variant of `apply_item` that binds into a caller-supplied
1514/// mutable Env via `push_lam`/`pop_lam` — zero `SmallVec`/`Arc` clones
1515/// per call.  Caller clones the Env once before the loop.
1516#[inline]
1517pub(super) fn apply_item_mut(item: Val, arg: &Arg, env: &mut Env) -> Result<Val, EvalError> {
1518    let expr = match arg { Arg::Pos(e) | Arg::Named(_, e) => e };
1519    match expr {
1520        Expr::Lambda { params, body } => {
1521            let name = params.first().map(|s| s.as_str());
1522            let frame = env.push_lam(name, item);
1523            let r = eval(body, env);
1524            env.pop_lam(frame);
1525            r
1526        }
1527        _ => {
1528            let frame = env.push_lam(None, item);
1529            let r = eval(expr, env);
1530            env.pop_lam(frame);
1531            r
1532        }
1533    }
1534}
1535
1536/// Hot-loop variant of `apply_item2`.  Binds one or two params (or just
1537/// `current` for zero-param lambdas) via in-place Env mutation.
1538#[inline]
1539pub(super) fn apply_item2_mut(a: Val, b: Val, arg: &Arg, env: &mut Env) -> Result<Val, EvalError> {
1540    match arg {
1541        Arg::Pos(Expr::Lambda { params, body }) | Arg::Named(_, Expr::Lambda { params, body }) => {
1542            match params.as_slice() {
1543                [] => {
1544                    let frame = env.push_lam(None, b);
1545                    let r = eval(body, env);
1546                    env.pop_lam(frame);
1547                    r
1548                }
1549                [p] => {
1550                    let frame = env.push_lam(Some(p), b);
1551                    let r = eval(body, env);
1552                    env.pop_lam(frame);
1553                    r
1554                }
1555                [p1, p2, ..] => {
1556                    // Two-param: push p1=a, then p2=b (pop order reversed).
1557                    let f1 = env.push_lam(Some(p1), a);
1558                    let f2 = env.push_lam(Some(p2), b);
1559                    let r = eval(body, env);
1560                    env.pop_lam(f2);
1561                    env.pop_lam(f1);
1562                    r
1563                }
1564            }
1565        }
1566        _ => apply_item_mut(b, arg, env),
1567    }
1568}
1569
1570fn apply_expr_item(item: Val, expr: &Expr, env: &Env) -> Result<Val, EvalError> {
1571    match expr {
1572        Expr::Lambda { params, body } => {
1573            let inner = if params.is_empty() {
1574                env.with_current(item)
1575            } else {
1576                let mut e = env.with_var(&params[0], item.clone());
1577                e.current = item;
1578                e
1579            };
1580            eval(body, &inner)
1581        }
1582        _ => eval(expr, &env.with_current(item)),
1583    }
1584}
1585
1586pub(super) fn eval_pos(arg: &Arg, env: &Env) -> Result<Val, EvalError> {
1587    match arg { Arg::Pos(e) | Arg::Named(_, e) => eval(e, env) }
1588}
1589
1590pub(super) fn first_i64_arg(args: &[Arg], env: &Env) -> Result<i64, EvalError> {
1591    args.first()
1592        .map(|a| eval_pos(a, env)?.as_i64().ok_or_else(|| EvalError("expected integer arg".into())))
1593        .transpose()
1594        .map(|v| v.unwrap_or(1))
1595}
1596
1597pub(super) fn str_arg(args: &[Arg], idx: usize, env: &Env) -> Result<String, EvalError> {
1598    args.get(idx)
1599        .map(|a| eval_pos(a, env))
1600        .transpose()?
1601        .map(|v| val_to_string(&v))
1602        .ok_or_else(|| EvalError(format!("missing string arg at position {}", idx)))
1603}
1604
1605// ── Comprehension helpers ─────────────────────────────────────────────────────
1606
1607fn eval_iter(iter: &Expr, env: &Env) -> Result<Vec<Val>, EvalError> {
1608    match eval(iter, env)? {
1609        Val::Arr(a) => Ok(Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone())),
1610        Val::Obj(m) => {
1611            let entries = Arc::try_unwrap(m).unwrap_or_else(|m| (*m).clone());
1612            Ok(entries.into_iter().map(|(k, v)| {
1613                let mut o: IndexMap<Arc<str>, Val> = IndexMap::new();
1614                o.insert(Arc::from("key"),   Val::Str(k));
1615                o.insert(Arc::from("value"), v);
1616                Val::obj(o)
1617            }).collect())
1618        }
1619        other => Ok(vec![other]),
1620    }
1621}
1622
1623/// In-place comprehension bind — one or two vars pushed + `current`
1624/// swapped.  Returns a pair of frames (second is `None` for 0/1-var
1625/// forms) so the caller can unwind with `unbind_vars_mut`.
1626#[inline]
1627fn bind_vars_mut(env: &mut Env, vars: &[String], item: Val) -> (LamFrame, Option<LamFrame>) {
1628    match vars {
1629        [] => (env.push_lam(None, item), None),
1630        [v] => (env.push_lam(Some(v), item), None),
1631        [v1, v2, ..] => {
1632            let idx = item.get("index").cloned().unwrap_or(Val::Null);
1633            let val = item.get("value").cloned().unwrap_or_else(|| item.clone());
1634            let f1 = env.push_lam(Some(v1), idx);
1635            // Push v2 with val; also sets current to val (matches legacy).
1636            let f2 = env.push_lam(Some(v2), val);
1637            (f1, Some(f2))
1638        }
1639    }
1640}
1641
1642#[inline]
1643fn unbind_vars_mut(env: &mut Env, frames: (LamFrame, Option<LamFrame>)) {
1644    if let Some(f2) = frames.1 { env.pop_lam(f2); }
1645    env.pop_lam(frames.0);
1646}