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;
55
56pub use value::Val;
57pub use methods::{Method, MethodRegistry};
58use util::*;
59
60// ── Error ─────────────────────────────────────────────────────────────────────
61
62#[derive(Debug, Clone)]
63pub struct EvalError(pub String);
64
65impl std::fmt::Display for EvalError {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        write!(f, "eval error: {}", self.0)
68    }
69}
70impl std::error::Error for EvalError {}
71
72macro_rules! err {
73    ($($t:tt)*) => { Err(EvalError(format!($($t)*))) };
74}
75
76// ── Environment ───────────────────────────────────────────────────────────────
77// SmallVec<4> keeps ≤4 bindings on the stack — covers the vast majority of
78// queries without a heap allocation.  Linear scan is fine for n ≤ 4.
79
80#[derive(Clone)]
81pub struct Env {
82    vars:     SmallVec<[(Arc<str>, Val); 4]>,
83    pub root:    Val,
84    pub current: Val,
85    registry: Arc<MethodRegistry>,
86}
87
88impl Env {
89    fn new(root: Val) -> Self {
90        Self {
91            vars: SmallVec::new(),
92            root: root.clone(),
93            current: root,
94            registry: Arc::new(MethodRegistry::new()),
95        }
96    }
97
98    pub fn new_with_registry(root: Val, registry: Arc<MethodRegistry>) -> Self {
99        Self { vars: SmallVec::new(), root: root.clone(), current: root, registry }
100    }
101
102    #[inline]
103    pub(super) fn registry_ref(&self) -> &MethodRegistry { &self.registry }
104
105    #[inline]
106    pub fn with_current(&self, current: Val) -> Self {
107        Self {
108            vars: self.vars.clone(),
109            root: self.root.clone(),
110            current,
111            registry: self.registry.clone(),
112        }
113    }
114
115    /// In-place swap of `current` — returns previous.  Paired with
116    /// `restore_current` this lets hot loops avoid cloning `vars`/`root`
117    /// per iteration.
118    #[inline]
119    pub fn swap_current(&mut self, new: Val) -> Val {
120        std::mem::replace(&mut self.current, new)
121    }
122
123    #[inline]
124    pub fn restore_current(&mut self, old: Val) {
125        self.current = old;
126    }
127
128    #[inline]
129    pub fn get_var(&self, name: &str) -> Option<&Val> {
130        self.vars.iter().rev().find(|(k, _)| k.as_ref() == name).map(|(_, v)| v)
131    }
132
133    #[inline]
134    pub fn has_var(&self, name: &str) -> bool {
135        self.vars.iter().any(|(k, _)| k.as_ref() == name)
136    }
137
138    pub fn with_var(&self, name: &str, val: Val) -> Self {
139        let mut vars = self.vars.clone();
140        if let Some(pos) = vars.iter().position(|(k, _)| k.as_ref() == name) {
141            vars[pos].1 = val;
142        } else {
143            vars.push((Arc::from(name), val));
144        }
145        Self { vars, root: self.root.clone(), current: self.current.clone(), registry: self.registry.clone() }
146    }
147
148    fn with_vars2(&self, n1: &str, v1: Val, n2: &str, v2: Val) -> Self {
149        let mut vars = self.vars.clone();
150        if let Some(p) = vars.iter().position(|(k, _)| k.as_ref() == n1) { vars[p].1 = v1; }
151        else { vars.push((Arc::from(n1), v1)); }
152        if let Some(p) = vars.iter().position(|(k, _)| k.as_ref() == n2) { vars[p].1 = v2; }
153        else { vars.push((Arc::from(n2), v2)); }
154        Self { vars, root: self.root.clone(), current: self.current.clone(), registry: self.registry.clone() }
155    }
156
157    fn extend_vars(&self, extra: impl IntoIterator<Item = (Arc<str>, Val)>) -> Self {
158        let mut vars = self.vars.clone();
159        for (name, val) in extra {
160            if let Some(pos) = vars.iter().position(|(k, _)| *k == name) {
161                vars[pos].1 = val;
162            } else {
163                vars.push((name, val));
164            }
165        }
166        Self { vars, root: self.root.clone(), current: self.current.clone(), registry: self.registry.clone() }
167    }
168}
169
170// ── Public entry points ───────────────────────────────────────────────────────
171
172pub fn evaluate(expr: &Expr, root: &serde_json::Value) -> Result<serde_json::Value, EvalError> {
173    let val = Val::from(root);
174    Ok(eval(expr, &Env::new(val))?.into())
175}
176
177pub fn evaluate_with(
178    expr: &Expr,
179    root: &serde_json::Value,
180    registry: Arc<MethodRegistry>,
181) -> Result<serde_json::Value, EvalError> {
182    let val = Val::from(root);
183    Ok(eval(expr, &Env::new_with_registry(val, registry))?.into())
184}
185
186// ── Core evaluator ────────────────────────────────────────────────────────────
187
188pub(super) fn eval(expr: &Expr, env: &Env) -> Result<Val, EvalError> {
189    match expr {
190        Expr::Null      => Ok(Val::Null),
191        Expr::Bool(b)   => Ok(Val::Bool(*b)),
192        Expr::Int(n)    => Ok(Val::Int(*n)),
193        Expr::Float(f)  => Ok(Val::Float(*f)),
194        Expr::Str(s)    => Ok(Val::Str(Arc::from(s.as_str()))),
195
196        Expr::FString(parts) => eval_fstring(parts, env),
197
198        Expr::Root    => Ok(env.root.clone()),
199        Expr::Current => Ok(env.current.clone()),
200
201        Expr::Ident(name) => {
202            if let Some(v) = env.get_var(name) { return Ok(v.clone()); }
203            Ok(env.current.get_field(name))
204        }
205
206        Expr::Chain(base, steps) => {
207            let mut val = eval(base, env)?;
208            for step in steps { val = eval_step(val, step, env)?; }
209            Ok(val)
210        }
211
212        Expr::UnaryNeg(e) => match eval(e, env)? {
213            Val::Int(n)   => Ok(Val::Int(-n)),
214            Val::Float(f) => Ok(Val::Float(-f)),
215            _ => err!("unary minus requires a number"),
216        },
217
218        Expr::Not(e)  => Ok(Val::Bool(!is_truthy(&eval(e, env)?))),
219        Expr::BinOp(l, op, r) => eval_binop(l, *op, r, env),
220
221        Expr::Coalesce(lhs, rhs) => {
222            let v = eval(lhs, env)?;
223            if !v.is_null() { Ok(v) } else { eval(rhs, env) }
224        }
225
226        Expr::Kind { expr, ty, negate } => {
227            let v = eval(expr, env)?;
228            let m = kind_matches(&v, *ty);
229            Ok(Val::Bool(if *negate { !m } else { m }))
230        }
231
232        Expr::Object(fields) => eval_object(fields, env),
233
234        Expr::Array(elems) => {
235            let mut out = Vec::new();
236            for elem in elems {
237                match elem {
238                    ArrayElem::Expr(e)   => out.push(eval(e, env)?),
239                    ArrayElem::Spread(e) => match eval(e, env)? {
240                        Val::Arr(a) => {
241                            let items = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
242                            out.extend(items);
243                        }
244                        v => out.push(v),
245                    },
246                }
247            }
248            Ok(Val::arr(out))
249        }
250
251        Expr::Pipeline { base, steps } => eval_pipeline(base, steps, env),
252
253        Expr::ListComp { expr, vars, iter, cond } => {
254            let items = eval_iter(iter, env)?;
255            let mut out = Vec::new();
256            for item in items {
257                let ie = bind_vars(env, vars, item);
258                if let Some(c) = cond { if !is_truthy(&eval(c, &ie)?) { continue; } }
259                out.push(eval(expr, &ie)?);
260            }
261            Ok(Val::arr(out))
262        }
263
264        Expr::DictComp { key, val, vars, iter, cond } => {
265            let items = eval_iter(iter, env)?;
266            let mut map: IndexMap<Arc<str>, Val> = IndexMap::new();
267            for item in items {
268                let ie = bind_vars(env, vars, item);
269                if let Some(c) = cond { if !is_truthy(&eval(c, &ie)?) { continue; } }
270                let k: Arc<str> = Arc::from(val_to_key(&eval(key, &ie)?).as_str());
271                map.insert(k, eval(val, &ie)?);
272            }
273            Ok(Val::obj(map))
274        }
275
276        Expr::SetComp { expr, vars, iter, cond } | Expr::GenComp { expr, vars, iter, cond } => {
277            let items = eval_iter(iter, env)?;
278            let mut seen: Vec<String> = Vec::new();
279            let mut out = Vec::new();
280            for item in items {
281                let ie = bind_vars(env, vars, item);
282                if let Some(c) = cond { if !is_truthy(&eval(c, &ie)?) { continue; } }
283                let v = eval(expr, &ie)?;
284                let k = val_to_key(&v);
285                if !seen.contains(&k) { seen.push(k); out.push(v); }
286            }
287            Ok(Val::arr(out))
288        }
289
290        Expr::Lambda { .. } => err!("lambda cannot be used as standalone value"),
291
292        Expr::Let { name, init, body } => {
293            let v = eval(init, env)?;
294            eval(body, &env.with_var(name, v))
295        }
296
297        Expr::GlobalCall { name, args } => eval_global(name, args, env),
298
299        Expr::Cast { expr, ty } => {
300            let v = eval(expr, env)?;
301            cast_val(&v, *ty)
302        }
303
304        Expr::Patch { root, ops } => eval_patch(root, ops, env),
305        Expr::DeleteMark =>
306            err!("DELETE can only appear as a patch-field value"),
307    }
308}
309
310// ── Patch ─────────────────────────────────────────────────────────────────────
311
312use super::ast::{PatchOp, PathStep};
313
314enum PatchResult { Replace(Val), Delete }
315
316fn eval_patch(root: &Expr, ops: &[PatchOp], env: &Env) -> Result<Val, EvalError> {
317    let mut doc = eval(root, env)?;
318    for op in ops {
319        if let Some(c) = &op.cond {
320            let cenv = env.with_current(doc.clone());
321            if !is_truthy(&eval(c, &cenv)?) { continue; }
322        }
323        match apply_patch_step(doc, &op.path, 0, &op.val, env)? {
324            PatchResult::Replace(v) => doc = v,
325            PatchResult::Delete     => doc = Val::Null,
326        }
327    }
328    Ok(doc)
329}
330
331fn apply_patch_step(
332    v:        Val,
333    path:     &[PathStep],
334    i:        usize,
335    val_expr: &Expr,
336    env:      &Env,
337) -> Result<PatchResult, EvalError> {
338    if i == path.len() {
339        if matches!(val_expr, Expr::DeleteMark) {
340            return Ok(PatchResult::Delete);
341        }
342        let nv = eval(val_expr, &env.with_current(v))?;
343        return Ok(PatchResult::Replace(nv));
344    }
345    match &path[i] {
346        PathStep::Field(name) => {
347            let existing = v.get_field(name);
348            let child = apply_patch_step(existing, path, i+1, val_expr, env)?;
349            let mut m = v.into_map().unwrap_or_default();
350            match child {
351                PatchResult::Delete => { m.shift_remove(name.as_str()); }
352                PatchResult::Replace(nv) => { m.insert(Arc::from(name.as_str()), nv); }
353            }
354            Ok(PatchResult::Replace(Val::obj(m)))
355        }
356        PathStep::Index(idx) => {
357            let existing = v.get_index(*idx);
358            let child = apply_patch_step(existing, path, i+1, val_expr, env)?;
359            let mut a = v.into_vec().unwrap_or_default();
360            let resolved = resolve_idx(*idx, a.len() as i64);
361            match child {
362                PatchResult::Delete => {
363                    if resolved < a.len() { a.remove(resolved); }
364                }
365                PatchResult::Replace(nv) => {
366                    if resolved < a.len() { a[resolved] = nv; }
367                }
368            }
369            Ok(PatchResult::Replace(Val::arr(a)))
370        }
371        PathStep::Wildcard => {
372            let arr = v.into_vec().ok_or_else(|| EvalError("patch [*]: expected array".into()))?;
373            let mut out = Vec::with_capacity(arr.len());
374            for item in arr {
375                match apply_patch_step(item, path, i+1, val_expr, env)? {
376                    PatchResult::Delete => {}
377                    PatchResult::Replace(nv) => out.push(nv),
378                }
379            }
380            Ok(PatchResult::Replace(Val::arr(out)))
381        }
382        PathStep::WildcardFilter(pred) => {
383            let arr = v.into_vec().ok_or_else(|| EvalError("patch [* if]: expected array".into()))?;
384            let mut out = Vec::with_capacity(arr.len());
385            for item in arr {
386                let include = is_truthy(&eval(pred, &env.with_current(item.clone()))?);
387                if include {
388                    match apply_patch_step(item, path, i+1, val_expr, env)? {
389                        PatchResult::Delete => {}
390                        PatchResult::Replace(nv) => out.push(nv),
391                    }
392                } else {
393                    out.push(item);
394                }
395            }
396            Ok(PatchResult::Replace(Val::arr(out)))
397        }
398        PathStep::Descendant(_) =>
399            err!("descendant paths (..) in patch are not yet supported"),
400    }
401}
402
403/// Runtime cast dispatcher — powers the `as` operator.  Semantics mirror
404/// the existing `.to_string()` / `.to_bool()` / `.to_number()` methods
405/// for overlapping types; `int`/`float` are new targets that narrow
406/// numeric values.
407fn cast_val(v: &Val, ty: super::ast::CastType) -> Result<Val, EvalError> {
408    use super::ast::CastType;
409    match ty {
410        CastType::Str => Ok(Val::Str(Arc::from(match v {
411            Val::Null       => "null".to_string(),
412            Val::Bool(b)    => b.to_string(),
413            Val::Int(n)     => n.to_string(),
414            Val::Float(f)   => f.to_string(),
415            Val::Str(s)     => s.to_string(),
416            other           => super::eval::util::val_to_string(other),
417        }.as_str()))),
418        CastType::Bool => Ok(Val::Bool(match v {
419            Val::Null       => false,
420            Val::Bool(b)    => *b,
421            Val::Int(n)     => *n != 0,
422            Val::Float(f)   => *f != 0.0,
423            Val::Str(s)     => !s.is_empty(),
424            Val::Arr(a)     => !a.is_empty(),
425            Val::Obj(o)     => !o.is_empty(),
426        })),
427        CastType::Number | CastType::Float => match v {
428            Val::Int(n)     => Ok(Val::Float(*n as f64)),
429            Val::Float(_)   => Ok(v.clone()),
430            Val::Str(s)     => s.parse::<f64>().map(Val::Float)
431                                .map_err(|e| EvalError(format!("as float: {}", e))),
432            Val::Bool(b)    => Ok(Val::Float(if *b { 1.0 } else { 0.0 })),
433            Val::Null       => Ok(Val::Float(0.0)),
434            _               => err!("as float: cannot convert"),
435        },
436        CastType::Int => match v {
437            Val::Int(_)     => Ok(v.clone()),
438            Val::Float(f)   => Ok(Val::Int(*f as i64)),
439            Val::Str(s)     => s.parse::<i64>().map(Val::Int)
440                                .or_else(|_| s.parse::<f64>().map(|f| Val::Int(f as i64)))
441                                .map_err(|e| EvalError(format!("as int: {}", e))),
442            Val::Bool(b)    => Ok(Val::Int(if *b { 1 } else { 0 })),
443            Val::Null       => Ok(Val::Int(0)),
444            _               => err!("as int: cannot convert"),
445        },
446        CastType::Array => match v {
447            Val::Arr(_)     => Ok(v.clone()),
448            Val::Null       => Ok(Val::arr(Vec::new())),
449            other           => Ok(Val::arr(vec![other.clone()])),
450        },
451        CastType::Object => match v {
452            Val::Obj(_)     => Ok(v.clone()),
453            _               => err!("as object: cannot convert non-object"),
454        },
455        CastType::Null => Ok(Val::Null),
456    }
457}
458
459// ── Pipeline ──────────────────────────────────────────────────────────────────
460
461fn eval_pipeline(base: &Expr, steps: &[PipeStep], env: &Env) -> Result<Val, EvalError> {
462    let mut current = eval(base, env)?;
463    let mut env = env.clone();
464    for step in steps {
465        match step {
466            PipeStep::Forward(rhs) => current = eval_pipe(current, rhs, &env)?,
467            PipeStep::Bind(target) => env = apply_bind(target, &current, env)?,
468        }
469    }
470    Ok(current)
471}
472
473fn apply_bind(target: &BindTarget, val: &Val, env: Env) -> Result<Env, EvalError> {
474    match target {
475        BindTarget::Name(name) => Ok(env.with_var(name, val.clone())),
476        BindTarget::Obj { fields, rest } => {
477            let obj = val.as_object()
478                .ok_or_else(|| EvalError("bind destructure: expected object".into()))?;
479            let mut e = env;
480            for f in fields {
481                e = e.with_var(f, obj.get(f.as_str()).cloned().unwrap_or(Val::Null));
482            }
483            if let Some(rest_name) = rest {
484                let mut remainder: IndexMap<Arc<str>, Val> = IndexMap::new();
485                for (k, v) in obj {
486                    if !fields.iter().any(|f| f.as_str() == k.as_ref()) {
487                        remainder.insert(k.clone(), v.clone());
488                    }
489                }
490                e = e.with_var(rest_name, Val::obj(remainder));
491            }
492            Ok(e)
493        }
494        BindTarget::Arr(names) => {
495            let arr = val.as_array()
496                .ok_or_else(|| EvalError("bind destructure: expected array".into()))?;
497            let mut e = env;
498            for (i, name) in names.iter().enumerate() {
499                e = e.with_var(name, arr.get(i).cloned().unwrap_or(Val::Null));
500            }
501            Ok(e)
502        }
503    }
504}
505
506fn eval_pipe(left: Val, rhs: &Expr, env: &Env) -> Result<Val, EvalError> {
507    match rhs {
508        Expr::Ident(name) => {
509            if env.has_var(name) {
510                eval(rhs, &env.with_current(left))
511            } else {
512                dispatch_method(left, name, &[], env)
513            }
514        }
515        Expr::Chain(base, steps) => {
516            if let Expr::Ident(name) = base.as_ref() {
517                if !env.has_var(name) {
518                    let mut v = dispatch_method(left, name, &[], env)?;
519                    for step in steps { v = eval_step(v, step, env)?; }
520                    return Ok(v);
521                }
522            }
523            eval(rhs, &env.with_current(left))
524        }
525        _ => eval(rhs, &env.with_current(left)),
526    }
527}
528
529// ── Step evaluation ───────────────────────────────────────────────────────────
530
531fn eval_step(val: Val, step: &Step, env: &Env) -> Result<Val, EvalError> {
532    match step {
533        Step::Field(name)    => Ok(val.get_field(name)),
534        Step::OptField(name) => {
535            if val.is_null() { Ok(Val::Null) } else { Ok(val.get_field(name)) }
536        }
537        Step::Descendant(name) => {
538            let mut found = Vec::new();
539            collect_desc(&val, name, &mut found);
540            Ok(Val::arr(found))
541        }
542        Step::DescendAll => {
543            let mut found = Vec::new();
544            collect_all(&val, &mut found);
545            Ok(Val::arr(found))
546        }
547        Step::InlineFilter(pred) => {
548            let items = match val {
549                Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
550                other => vec![other],
551            };
552            let mut out = Vec::new();
553            for item in items {
554                let ie = env.with_current(item.clone());
555                if is_truthy(&eval(pred, &ie)?) { out.push(item); }
556            }
557            Ok(Val::arr(out))
558        }
559        Step::Quantifier(kind) => {
560            use super::ast::QuantifierKind;
561            match kind {
562                QuantifierKind::First => {
563                    Ok(match val {
564                        Val::Arr(a) => a.first().cloned().unwrap_or(Val::Null),
565                        other => other,
566                    })
567                }
568                QuantifierKind::One => {
569                    match val {
570                        Val::Arr(a) if a.len() == 1 => Ok(a[0].clone()),
571                        Val::Arr(a) => err!("quantifier !: expected exactly one element, got {}", a.len()),
572                        other => Ok(other),
573                    }
574                }
575            }
576        }
577        Step::Index(i) => Ok(val.get_index(*i)),
578        Step::DynIndex(expr) => {
579            let key = eval(expr, env)?;
580            match key {
581                Val::Int(i)  => Ok(val.get_index(i)),
582                Val::Str(s)  => Ok(val.get_field(s.as_ref())),
583                _ => err!("dynamic index must be a number or string"),
584            }
585        }
586        Step::Slice(from, to) => {
587            if let Val::Arr(a) = val {
588                let len = a.len() as i64;
589                let s = resolve_idx(from.unwrap_or(0), len);
590                let e = resolve_idx(to.unwrap_or(len), len);
591                let items = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
592                let s = s.min(items.len());
593                let e = e.min(items.len());
594                Ok(Val::arr(items[s..e].to_vec()))
595            } else { Ok(Val::Null) }
596        }
597        Step::Method(name, args)    => dispatch_method(val, name, args, env),
598        Step::OptMethod(name, args) => {
599            if val.is_null() { Ok(Val::Null) } else { dispatch_method(val, name, args, env) }
600        }
601    }
602}
603
604fn resolve_idx(i: i64, len: i64) -> usize {
605    (if i < 0 { (len + i).max(0) } else { i }) as usize
606}
607
608fn collect_desc(v: &Val, name: &str, out: &mut Vec<Val>) {
609    match v {
610        Val::Obj(m) => {
611            if let Some(v) = m.get(name) { out.push(v.clone()); }
612            for v in m.values() { collect_desc(v, name, out); }
613        }
614        Val::Arr(a) => { for item in a.as_ref() { collect_desc(item, name, out); } }
615        _ => {}
616    }
617}
618
619fn collect_all(v: &Val, out: &mut Vec<Val>) {
620    match v {
621        Val::Obj(m) => {
622            out.push(v.clone());
623            for child in m.values() { collect_all(child, out); }
624        }
625        Val::Arr(a) => {
626            for item in a.as_ref() { collect_all(item, out); }
627        }
628        other => out.push(other.clone()),
629    }
630}
631
632// ── Method dispatch ───────────────────────────────────────────────────────────
633
634pub(super) fn dispatch_method(recv: Val, name: &str, args: &[Arg], env: &Env) -> Result<Val, EvalError> {
635    if let Some(f) = builtins::global().get(name) {
636        return f(recv, args, env);
637    }
638    if !env.registry.is_empty() {
639        if let Some(method) = env.registry.get(name) {
640            let evaluated: Result<Vec<Val>, EvalError> =
641                args.iter().map(|a| eval_pos(a, env)).collect();
642            return method.call(recv, &evaluated?);
643        }
644    }
645    err!("unknown method '{}'", name)
646}
647
648// ── Object construction ───────────────────────────────────────────────────────
649
650fn eval_object(fields: &[ObjField], env: &Env) -> Result<Val, EvalError> {
651    let mut map: IndexMap<Arc<str>, Val> = IndexMap::new();
652    for field in fields {
653        match field {
654            ObjField::Short(name) => {
655                let v = if let Some(v) = env.get_var(name) { v.clone() }
656                        else { env.current.get_field(name) };
657                if !v.is_null() { map.insert(Arc::from(name.as_str()), v); }
658            }
659            ObjField::Kv { key, val, optional, cond } => {
660                if let Some(c) = cond {
661                    if !is_truthy(&eval(c, env)?) { continue; }
662                }
663                let v = eval(val, env)?;
664                if *optional && v.is_null() { continue; }
665                map.insert(Arc::from(key.as_str()), v);
666            }
667            ObjField::Dynamic { key, val } => {
668                let k: Arc<str> = Arc::from(val_to_key(&eval(key, env)?).as_str());
669                map.insert(k, eval(val, env)?);
670            }
671            ObjField::Spread(expr) => {
672                if let Val::Obj(other) = eval(expr, env)? {
673                    let entries = Arc::try_unwrap(other).unwrap_or_else(|m| (*m).clone());
674                    for (k, v) in entries { map.insert(k, v); }
675                }
676            }
677            ObjField::SpreadDeep(expr) => {
678                if let Val::Obj(other) = eval(expr, env)? {
679                    let base = std::mem::take(&mut map);
680                    let merged = deep_merge_concat(Val::obj(base), Val::Obj(other));
681                    if let Val::Obj(m) = merged {
682                        map = Arc::try_unwrap(m).unwrap_or_else(|m| (*m).clone());
683                    }
684                }
685            }
686        }
687    }
688    Ok(Val::obj(map))
689}
690
691// ── F-string ──────────────────────────────────────────────────────────────────
692
693fn eval_fstring(parts: &[FStringPart], env: &Env) -> Result<Val, EvalError> {
694    let mut out = String::new();
695    for part in parts {
696        match part {
697            FStringPart::Lit(s) => out.push_str(s),
698            FStringPart::Interp { expr, fmt } => {
699                let val = eval(expr, env)?;
700                let s = match fmt {
701                    None                     => val_to_string(&val),
702                    Some(FmtSpec::Spec(spec)) => apply_fmt_spec(&val, spec),
703                    Some(FmtSpec::Pipe(method)) => {
704                        val_to_string(&dispatch_method(val, method, &[], env)?)
705                    }
706                };
707                out.push_str(&s);
708            }
709        }
710    }
711    Ok(Val::Str(Arc::from(out.as_str())))
712}
713
714fn apply_fmt_spec(val: &Val, spec: &str) -> String {
715    if let Some(rest) = spec.strip_suffix('f') {
716        if let Some(prec_str) = rest.strip_prefix('.') {
717            if let Ok(prec) = prec_str.parse::<usize>() {
718                if let Some(f) = val.as_f64() { return format!("{:.prec$}", f); }
719            }
720        }
721    }
722    if spec == "d" {
723        if let Some(i) = val.as_i64() { return format!("{}", i); }
724    }
725    let s = val_to_string(val);
726    if let Some(w) = spec.strip_prefix('>').and_then(|s| s.parse::<usize>().ok()) { return format!("{:>w$}", s); }
727    if let Some(w) = spec.strip_prefix('<').and_then(|s| s.parse::<usize>().ok()) { return format!("{:<w$}", s); }
728    if let Some(w) = spec.strip_prefix('^').and_then(|s| s.parse::<usize>().ok()) { return format!("{:^w$}", s); }
729    if let Some(w) = spec.strip_prefix('0').and_then(|s| s.parse::<usize>().ok()) {
730        if let Some(i) = val.as_i64() { return format!("{:0>w$}", i); }
731    }
732    s
733}
734
735// ── Binary operators ──────────────────────────────────────────────────────────
736
737fn eval_binop(l: &Expr, op: BinOp, r: &Expr, env: &Env) -> Result<Val, EvalError> {
738    match op {
739        BinOp::And => {
740            let lv = eval(l, env)?;
741            if !is_truthy(&lv) { return Ok(Val::Bool(false)); }
742            Ok(Val::Bool(is_truthy(&eval(r, env)?)))
743        }
744        BinOp::Or => {
745            let lv = eval(l, env)?;
746            if is_truthy(&lv) { return Ok(lv); }
747            eval(r, env)
748        }
749        _ => {
750            let lv = eval(l, env)?;
751            let rv = eval(r, env)?;
752            match op {
753                BinOp::Add  => add_vals(lv, rv),
754                BinOp::Sub  => num_op(lv, rv, |a, b| a - b, |a, b| a - b),
755                BinOp::Mul  => num_op(lv, rv, |a, b| a * b, |a, b| a * b),
756                BinOp::Div  => {
757                    let b = rv.as_f64().unwrap_or(0.0);
758                    if b == 0.0 { return err!("division by zero"); }
759                    Ok(Val::Float(lv.as_f64().unwrap_or(0.0) / b))
760                }
761                BinOp::Mod   => num_op(lv, rv, |a, b| a % b, |a, b| a % b),
762                BinOp::Eq    => Ok(Val::Bool(vals_eq(&lv, &rv))),
763                BinOp::Neq   => Ok(Val::Bool(!vals_eq(&lv, &rv))),
764                BinOp::Lt    => Ok(Val::Bool(cmp_vals(&lv, &rv) == std::cmp::Ordering::Less)),
765                BinOp::Lte   => Ok(Val::Bool(cmp_vals(&lv, &rv) != std::cmp::Ordering::Greater)),
766                BinOp::Gt    => Ok(Val::Bool(cmp_vals(&lv, &rv) == std::cmp::Ordering::Greater)),
767                BinOp::Gte   => Ok(Val::Bool(cmp_vals(&lv, &rv) != std::cmp::Ordering::Less)),
768                BinOp::Fuzzy => {
769                    let ls = match &lv { Val::Str(s) => s.to_lowercase(), _ => val_to_string(&lv).to_lowercase() };
770                    let rs = match &rv { Val::Str(s) => s.to_lowercase(), _ => val_to_string(&rv).to_lowercase() };
771                    Ok(Val::Bool(ls.contains(&rs) || rs.contains(&ls)))
772                }
773                BinOp::And | BinOp::Or => unreachable!(),
774            }
775        }
776    }
777}
778
779// ── Global functions ──────────────────────────────────────────────────────────
780
781fn eval_global(name: &str, args: &[Arg], env: &Env) -> Result<Val, EvalError> {
782    match name {
783        "coalesce" => {
784            for arg in args {
785                let v = eval_pos(arg, env)?;
786                if !v.is_null() { return Ok(v); }
787            }
788            Ok(Val::Null)
789        }
790        "chain" | "join" => {
791            let mut out = Vec::new();
792            for arg in args {
793                match eval_pos(arg, env)? {
794                    Val::Arr(a) => {
795                        let items = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
796                        out.extend(items);
797                    }
798                    v => out.push(v),
799                }
800            }
801            Ok(Val::arr(out))
802        }
803        "zip"         => func_arrays::global_zip(args, env),
804        "zip_longest" => func_arrays::global_zip_longest(args, env),
805        "product"     => func_arrays::global_product(args, env),
806        other => {
807            if let Some(first) = args.first() {
808                let recv = eval_pos(first, env)?;
809                dispatch_method(recv, other, args.get(1..).unwrap_or(&[]), env)
810            } else {
811                dispatch_method(env.current.clone(), other, &[], env)
812            }
813        }
814    }
815}
816
817// ── Apply helpers ─────────────────────────────────────────────────────────────
818
819pub(super) fn apply_item(item: Val, arg: &Arg, env: &Env) -> Result<Val, EvalError> {
820    match arg {
821        Arg::Pos(expr) | Arg::Named(_, expr) => apply_expr_item(item, expr, env),
822    }
823}
824
825pub(super) fn apply_item2(a: Val, b: Val, arg: &Arg, env: &Env) -> Result<Val, EvalError> {
826    match arg {
827        Arg::Pos(Expr::Lambda { params, body }) | Arg::Named(_, Expr::Lambda { params, body }) => {
828            let inner = match params.as_slice() {
829                []           => env.with_current(b),
830                [p]          => env.with_var(p, b),
831                [p1, p2, ..] => env.with_vars2(p1, a, p2, b),
832            };
833            eval(body, &inner)
834        }
835        _ => apply_item(b, arg, env),
836    }
837}
838
839fn apply_expr_item(item: Val, expr: &Expr, env: &Env) -> Result<Val, EvalError> {
840    match expr {
841        Expr::Lambda { params, body } => {
842            let inner = if params.is_empty() {
843                env.with_current(item)
844            } else {
845                let mut e = env.with_var(&params[0], item.clone());
846                e.current = item;
847                e
848            };
849            eval(body, &inner)
850        }
851        _ => eval(expr, &env.with_current(item)),
852    }
853}
854
855pub(super) fn eval_pos(arg: &Arg, env: &Env) -> Result<Val, EvalError> {
856    match arg { Arg::Pos(e) | Arg::Named(_, e) => eval(e, env) }
857}
858
859pub(super) fn first_i64_arg(args: &[Arg], env: &Env) -> Result<i64, EvalError> {
860    args.first()
861        .map(|a| eval_pos(a, env)?.as_i64().ok_or_else(|| EvalError("expected integer arg".into())))
862        .transpose()
863        .map(|v| v.unwrap_or(1))
864}
865
866pub(super) fn str_arg(args: &[Arg], idx: usize, env: &Env) -> Result<String, EvalError> {
867    args.get(idx)
868        .map(|a| eval_pos(a, env))
869        .transpose()?
870        .map(|v| val_to_string(&v))
871        .ok_or_else(|| EvalError(format!("missing string arg at position {}", idx)))
872}
873
874// ── Comprehension helpers ─────────────────────────────────────────────────────
875
876fn eval_iter(iter: &Expr, env: &Env) -> Result<Vec<Val>, EvalError> {
877    match eval(iter, env)? {
878        Val::Arr(a) => Ok(Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone())),
879        Val::Obj(m) => {
880            let entries = Arc::try_unwrap(m).unwrap_or_else(|m| (*m).clone());
881            Ok(entries.into_iter().map(|(k, v)| {
882                let mut o: IndexMap<Arc<str>, Val> = IndexMap::new();
883                o.insert(Arc::from("key"),   Val::Str(k));
884                o.insert(Arc::from("value"), v);
885                Val::obj(o)
886            }).collect())
887        }
888        other => Ok(vec![other]),
889    }
890}
891
892fn bind_vars(env: &Env, vars: &[String], item: Val) -> Env {
893    match vars {
894        [] => env.with_current(item),
895        [v] => { let mut e = env.with_var(v, item.clone()); e.current = item; e }
896        [v1, v2, ..] => {
897            let idx = item.get("index").cloned().unwrap_or(Val::Null);
898            let val = item.get("value").cloned().unwrap_or_else(|| item.clone());
899            let mut e = env.with_vars2(v1, idx, v2, val.clone());
900            e.current = val;
901            e
902        }
903    }
904}