Skip to main content

jetro_core/
vm.rs

1//! High-performance bytecode VM for v2 Jetro expressions.
2//!
3//! # Architecture
4//!
5//! ```text
6//!  String expression
7//!        │  parser::parse()
8//!        ▼
9//!     Expr (AST)
10//!        │  Compiler::compile()
11//!        ▼
12//!     Program              ← flat Arc<[Opcode]>  (cached: compile_cache)
13//!        │  VM::execute()
14//!        ▼
15//!      Val                 ← result              (structural: resolution_cache)
16//! ```
17//!
18//! # Optimisations over the tree-walker
19//!
20//! 1. **Compile cache** — parse + compile once per unique expression string.
21//! 2. **Val type** — `Arc`-wrapped compound nodes; every clone is O(1).
22//! 3. **BuiltinMethod enum** — O(1) method dispatch (jump-table vs string hash).
23//! 4. **Pre-compiled sub-programs** — lambda/arg bodies compiled to `Arc<Program>`
24//!    once at compile time; never re-compiled per call.
25//! 5. **Resolution cache** — structural programs (`$.a.b[0]`) cache their
26//!    pointer path after the first traversal; subsequent calls skip traversal.
27//! 6. **Peephole pass 1 — RootChain** — `PushRoot + GetField+` fused into a
28//!    single pointer-resolve opcode.
29//! 7. **Peephole pass 2 — FilterCount** — `CallMethod(filter) +
30//!    CallMethod(len/count)` fused; counts matches without materialising the
31//!    intermediate filtered array.
32//! 8. **Peephole pass 3 — ConstFold** — arithmetic on adjacent integer literals
33//!    folded at compile time.
34//! 9. **Stack machine** — iterative `exec()` loop; no per-opcode stack-frame
35//!    overhead for simple navigation / arithmetic opcodes.
36
37use std::{
38    collections::{HashMap, VecDeque},
39    collections::hash_map::DefaultHasher,
40    hash::{Hash, Hasher},
41    sync::Arc,
42};
43use indexmap::IndexMap;
44use smallvec::SmallVec;
45
46use crate::ast::*;
47use super::eval::{
48    Env, EvalError, Val,
49    dispatch_method, eval,
50};
51use super::eval::util::{
52    is_truthy, kind_matches, vals_eq, cmp_vals, val_to_key, val_to_string,
53    add_vals, num_op, obj2,
54};
55use super::eval::methods::MethodRegistry;
56
57macro_rules! pop {
58    ($stack:expr) => {
59        $stack.pop().ok_or_else(|| EvalError("stack underflow".into()))?
60    };
61}
62macro_rules! err {
63    ($($t:tt)*) => { Err(EvalError(format!($($t)*))) };
64}
65
66// ── BuiltinMethod ─────────────────────────────────────────────────────────────
67
68/// Pre-resolved method identifier — eliminates string comparison at dispatch.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70#[repr(u8)]
71pub enum BuiltinMethod {
72    // Navigation / basics
73    Len = 0, Keys, Values, Entries, ToPairs, FromPairs, Invert, Reverse, Type,
74    ToString, ToJson, FromJson,
75    // Aggregates
76    Sum, Avg, Min, Max, Count, Any, All,
77    GroupBy, CountBy, IndexBy,
78    // Array ops
79    Filter, Map, FlatMap, Sort, Unique, Flatten, Compact,
80    Join, First, Last, Nth, Append, Prepend, Remove,
81    Diff, Intersect, Union, Enumerate, Pairwise, Window, Chunk,
82    TakeWhile, DropWhile, Accumulate, Partition, Zip, ZipLongest,
83    // Object ops
84    Pick, Omit, Merge, DeepMerge, Defaults, Rename,
85    TransformKeys, TransformValues, FilterKeys, FilterValues, Pivot,
86    // Path ops
87    GetPath, SetPath, DelPath, DelPaths, HasPath, FlattenKeys, UnflattenKeys,
88    // CSV
89    ToCsv, ToTsv,
90    // Null / predicate
91    Or, Has, Missing, Includes, Set, Update,
92    // String methods
93    Upper, Lower, Capitalize, TitleCase, Trim, TrimLeft, TrimRight,
94    Lines, Words, Chars, ToNumber, ToBool, ToBase64, FromBase64,
95    UrlEncode, UrlDecode, HtmlEscape, HtmlUnescape,
96    Repeat, PadLeft, PadRight, StartsWith, EndsWith,
97    IndexOf, LastIndexOf, Replace, ReplaceAll, StripPrefix, StripSuffix,
98    Slice, Split, Indent, Dedent, Matches, Scan,
99    // Relational
100    EquiJoin,
101    // Sentinel for custom/unknown
102    Unknown,
103}
104
105impl BuiltinMethod {
106    pub fn from_name(name: &str) -> Self {
107        match name {
108            "len"            => Self::Len,
109            "keys"           => Self::Keys,
110            "values"         => Self::Values,
111            "entries"        => Self::Entries,
112            "to_pairs"|"toPairs" => Self::ToPairs,
113            "from_pairs"|"fromPairs" => Self::FromPairs,
114            "invert"         => Self::Invert,
115            "reverse"        => Self::Reverse,
116            "type"           => Self::Type,
117            "to_string"|"toString" => Self::ToString,
118            "to_json"|"toJson" => Self::ToJson,
119            "from_json"|"fromJson" => Self::FromJson,
120            "sum"            => Self::Sum,
121            "avg"            => Self::Avg,
122            "min"            => Self::Min,
123            "max"            => Self::Max,
124            "count"          => Self::Count,
125            "any"            => Self::Any,
126            "all"            => Self::All,
127            "groupBy"|"group_by" => Self::GroupBy,
128            "countBy"|"count_by" => Self::CountBy,
129            "indexBy"|"index_by" => Self::IndexBy,
130            "filter"         => Self::Filter,
131            "map"            => Self::Map,
132            "flatMap"|"flat_map" => Self::FlatMap,
133            "sort"           => Self::Sort,
134            "unique"|"distinct" => Self::Unique,
135            "flatten"        => Self::Flatten,
136            "compact"        => Self::Compact,
137            "join"           => Self::Join,
138            "equi_join"|"equiJoin" => Self::EquiJoin,
139            "first"          => Self::First,
140            "last"           => Self::Last,
141            "nth"            => Self::Nth,
142            "append"         => Self::Append,
143            "prepend"        => Self::Prepend,
144            "remove"         => Self::Remove,
145            "diff"           => Self::Diff,
146            "intersect"      => Self::Intersect,
147            "union"          => Self::Union,
148            "enumerate"      => Self::Enumerate,
149            "pairwise"       => Self::Pairwise,
150            "window"         => Self::Window,
151            "chunk"|"batch"  => Self::Chunk,
152            "takewhile"|"take_while" => Self::TakeWhile,
153            "dropwhile"|"drop_while" => Self::DropWhile,
154            "accumulate"     => Self::Accumulate,
155            "partition"      => Self::Partition,
156            "zip"            => Self::Zip,
157            "zip_longest"|"zipLongest" => Self::ZipLongest,
158            "pick"           => Self::Pick,
159            "omit"           => Self::Omit,
160            "merge"          => Self::Merge,
161            "deep_merge"|"deepMerge" => Self::DeepMerge,
162            "defaults"       => Self::Defaults,
163            "rename"         => Self::Rename,
164            "transform_keys"|"transformKeys" => Self::TransformKeys,
165            "transform_values"|"transformValues" => Self::TransformValues,
166            "filter_keys"|"filterKeys" => Self::FilterKeys,
167            "filter_values"|"filterValues" => Self::FilterValues,
168            "pivot"          => Self::Pivot,
169            "get_path"|"getPath" => Self::GetPath,
170            "set_path"|"setPath" => Self::SetPath,
171            "del_path"|"delPath" => Self::DelPath,
172            "del_paths"|"delPaths" => Self::DelPaths,
173            "has_path"|"hasPath" => Self::HasPath,
174            "flatten_keys"|"flattenKeys" => Self::FlattenKeys,
175            "unflatten_keys"|"unflattenKeys" => Self::UnflattenKeys,
176            "to_csv"|"toCsv" => Self::ToCsv,
177            "to_tsv"|"toTsv" => Self::ToTsv,
178            "or"             => Self::Or,
179            "has"            => Self::Has,
180            "missing"        => Self::Missing,
181            "includes"|"contains" => Self::Includes,
182            "set"            => Self::Set,
183            "update"         => Self::Update,
184            "upper"          => Self::Upper,
185            "lower"          => Self::Lower,
186            "capitalize"     => Self::Capitalize,
187            "title_case"|"titleCase" => Self::TitleCase,
188            "trim"           => Self::Trim,
189            "trim_left"|"trimLeft"|"lstrip" => Self::TrimLeft,
190            "trim_right"|"trimRight"|"rstrip" => Self::TrimRight,
191            "lines"          => Self::Lines,
192            "words"          => Self::Words,
193            "chars"          => Self::Chars,
194            "to_number"|"toNumber" => Self::ToNumber,
195            "to_bool"|"toBool" => Self::ToBool,
196            "to_base64"|"toBase64" => Self::ToBase64,
197            "from_base64"|"fromBase64" => Self::FromBase64,
198            "url_encode"|"urlEncode" => Self::UrlEncode,
199            "url_decode"|"urlDecode" => Self::UrlDecode,
200            "html_escape"|"htmlEscape" => Self::HtmlEscape,
201            "html_unescape"|"htmlUnescape" => Self::HtmlUnescape,
202            "repeat"         => Self::Repeat,
203            "pad_left"|"padLeft" => Self::PadLeft,
204            "pad_right"|"padRight" => Self::PadRight,
205            "starts_with"|"startsWith" => Self::StartsWith,
206            "ends_with"|"endsWith" => Self::EndsWith,
207            "index_of"|"indexOf" => Self::IndexOf,
208            "last_index_of"|"lastIndexOf" => Self::LastIndexOf,
209            "replace"        => Self::Replace,
210            "replace_all"|"replaceAll" => Self::ReplaceAll,
211            "strip_prefix"|"stripPrefix" => Self::StripPrefix,
212            "strip_suffix"|"stripSuffix" => Self::StripSuffix,
213            "slice"          => Self::Slice,
214            "split"          => Self::Split,
215            "indent"         => Self::Indent,
216            "dedent"         => Self::Dedent,
217            "matches"        => Self::Matches,
218            "scan"           => Self::Scan,
219            _                => Self::Unknown,
220        }
221    }
222
223    /// True for methods that receive a sub-program to run per item.
224    fn is_lambda_method(self) -> bool {
225        matches!(self,
226            Self::Filter | Self::Map | Self::FlatMap | Self::Sort |
227            Self::Any | Self::All | Self::Count | Self::GroupBy |
228            Self::CountBy | Self::IndexBy | Self::TakeWhile |
229            Self::DropWhile | Self::Accumulate | Self::Partition |
230            Self::TransformKeys | Self::TransformValues |
231            Self::FilterKeys | Self::FilterValues | Self::Pivot | Self::Update
232        )
233    }
234}
235
236// ── Compiled sub-structures ───────────────────────────────────────────────────
237
238/// A compiled method call stored inside `Opcode::CallMethod`.
239#[derive(Debug, Clone)]
240pub struct CompiledCall {
241    pub method:   BuiltinMethod,
242    pub name:     Arc<str>,
243    /// Compiled lambda/expression sub-programs (one per arg, in order).
244    pub sub_progs: Arc<[Arc<Program>]>,
245    /// Original AST args kept for non-lambda dispatch fallback.
246    pub orig_args: Arc<[Arg]>,
247}
248
249/// A compiled object field for `Opcode::MakeObj`.
250#[derive(Debug, Clone)]
251pub enum CompiledObjEntry {
252    Short(Arc<str>),
253    Kv     { key: Arc<str>, prog: Arc<Program>, optional: bool, cond: Option<Arc<Program>> },
254    Dynamic { key: Arc<Program>, val: Arc<Program> },
255    Spread(Arc<Program>),
256    SpreadDeep(Arc<Program>),
257}
258
259/// A compiled f-string interpolation part.
260#[derive(Debug, Clone)]
261pub enum CompiledFSPart {
262    Lit(Arc<str>),
263    Interp { prog: Arc<Program>, fmt: Option<FmtSpec> },
264}
265
266/// Compiled bind-object destructure spec.
267#[derive(Debug, Clone)]
268pub struct BindObjSpec {
269    pub fields: Arc<[Arc<str>]>,
270    pub rest:   Option<Arc<str>>,
271}
272
273/// Compiled comprehension spec.
274#[derive(Debug, Clone)]
275pub struct CompSpec {
276    pub expr: Arc<Program>,
277    pub vars: Arc<[Arc<str>]>,
278    pub iter: Arc<Program>,
279    pub cond: Option<Arc<Program>>,
280}
281
282#[derive(Debug, Clone)]
283pub struct DictCompSpec {
284    pub key:  Arc<Program>,
285    pub val:  Arc<Program>,
286    pub vars: Arc<[Arc<str>]>,
287    pub iter: Arc<Program>,
288    pub cond: Option<Arc<Program>>,
289}
290
291// ── Opcode ────────────────────────────────────────────────────────────────────
292
293#[derive(Debug, Clone)]
294pub enum Opcode {
295    // ── Literals ─────────────────────────────────────────────────────────────
296    PushNull,
297    PushBool(bool),
298    PushInt(i64),
299    PushFloat(f64),
300    PushStr(Arc<str>),
301
302    // ── Context ───────────────────────────────────────────────────────────────
303    PushRoot,
304    PushCurrent,
305
306    // ── Navigation ────────────────────────────────────────────────────────────
307    GetField(Arc<str>),
308    GetIndex(i64),
309    GetSlice(Option<i64>, Option<i64>),
310    DynIndex(Arc<Program>),
311    OptField(Arc<str>),
312    Descendant(Arc<str>),
313    DescendAll,
314    InlineFilter(Arc<Program>),
315    Quantifier(QuantifierKind),
316
317    // ── Peephole fusions ──────────────────────────────────────────────────────
318    /// PushRoot + GetField* fused — resolves chain via pointer arithmetic.
319    RootChain(Arc<[Arc<str>]>),
320    /// filter(pred) + len/count fused — counts matches without temp array.
321    FilterCount(Arc<Program>),
322    /// filter(pred) + First quantifier fused — early-exit on first match.
323    FindFirst(Arc<Program>),
324    /// filter(pred) + One quantifier fused — early-exit at 2nd match (error).
325    FindOne(Arc<Program>),
326    /// filter(pred) + map(f) fused — single pass, no intermediate array.
327    FilterMap { pred: Arc<Program>, map: Arc<Program> },
328    /// filter(p1) + filter(p2) fused — single pass, both predicates.
329    FilterFilter { p1: Arc<Program>, p2: Arc<Program> },
330    /// map(f1) + map(f2) fused — single pass, composed.
331    MapMap { f1: Arc<Program>, f2: Arc<Program> },
332    /// Fused `map(f).sum()` — evaluates `f` per item, accumulates numeric sum.
333    MapSum(Arc<Program>),
334    /// Fused `map(f).avg()` — evaluates `f` per item, computes mean as float.
335    MapAvg(Arc<Program>),
336    /// Fused `sort()` + `[0:n]` — partial-sort smallest N using BinaryHeap.
337    /// `asc=true` → smallest N; `asc=false` → largest N.
338    TopN { n: usize, asc: bool },
339    /// Fused `map(f).flatten()` — single-pass concat of mapped arrays.
340    MapFlatten(Arc<Program>),
341    /// Fused `filter(p).take_while(q)` — scan while both predicates hold.
342    FilterTakeWhile { pred: Arc<Program>, stop: Arc<Program> },
343    /// Fused `filter(p).drop_while(q)` — skip leading matches of q on
344    /// p-filtered elements.
345    FilterDropWhile { pred: Arc<Program>, drop: Arc<Program> },
346    /// Fused `map(f).unique()` — apply f and dedup by resulting value.
347    MapUnique(Arc<Program>),
348    /// Fused equi-join: TOS is lhs array; `rhs` program evaluates
349    /// to rhs array; join by (lhs_key, rhs_key) string field names.
350    /// Produces array of merged objects (rhs wins on collision).
351    EquiJoin { rhs: Arc<Program>, lhs_key: Arc<str>, rhs_key: Arc<str> },
352
353    // ── Ident lookup (var, then current field) ────────────────────────────────
354    LoadIdent(Arc<str>),
355
356    // ── Binary / unary ops ────────────────────────────────────────────────────
357    Add, Sub, Mul, Div, Mod,
358    Eq, Neq, Lt, Lte, Gt, Gte, Fuzzy, Not, Neg,
359
360    // ── Type cast (`as T`) ───────────────────────────────────────────────────
361    CastOp(super::ast::CastType),
362
363    // ── Short-circuit ops (embed rhs as sub-program) ──────────────────────────
364    AndOp(Arc<Program>),
365    OrOp(Arc<Program>),
366    CoalesceOp(Arc<Program>),
367
368    // ── Method calls ─────────────────────────────────────────────────────────
369    CallMethod(Arc<CompiledCall>),
370    CallOptMethod(Arc<CompiledCall>),
371
372    // ── Construction ─────────────────────────────────────────────────────────
373    MakeObj(Arc<[CompiledObjEntry]>),
374    MakeArr(Arc<[Arc<Program>]>),
375
376    // ── F-string ─────────────────────────────────────────────────────────────
377    FString(Arc<[CompiledFSPart]>),
378
379    // ── Kind check ───────────────────────────────────────────────────────────
380    KindCheck { ty: KindType, negate: bool },
381
382    // ── Pipeline helpers ──────────────────────────────────────────────────────
383    /// Pop TOS → env.current, then push it back (pass-through with context update).
384    SetCurrent,
385    /// TOS → env var by name, TOS remains (for `->` bind).
386    BindVar(Arc<str>),
387    /// Pop TOS → env var (for `let` init).
388    StoreVar(Arc<str>),
389    /// Object destructure bind: TOS obj → multiple vars.
390    BindObjDestructure(Arc<BindObjSpec>),
391    /// Array destructure bind: TOS arr → multiple vars.
392    BindArrDestructure(Arc<[Arc<str>]>),
393
394    // ── Complex (recursive sub-programs) ─────────────────────────────────────
395    LetExpr { name: Arc<str>, body: Arc<Program> },
396    ListComp(Arc<CompSpec>),
397    DictComp(Arc<DictCompSpec>),
398    SetComp(Arc<CompSpec>),
399
400    // ── Resolution cache fast-path ────────────────────────────────────────────
401    GetPointer(Arc<str>),
402
403    // ── Patch block (delegates to tree-walker eval) ──────────────────────────
404    PatchEval(Arc<super::ast::Expr>),
405}
406
407// ── Program ───────────────────────────────────────────────────────────────────
408
409/// A compiled, immutable v2 program.  Cheap to clone (`Arc` internals).
410#[derive(Debug, Clone)]
411pub struct Program {
412    pub ops:          Arc<[Opcode]>,
413    pub source:       Arc<str>,
414    pub id:           u64,
415    /// True when the program contains only structural navigation opcodes
416    /// (eligible for resolution caching).
417    pub is_structural: bool,
418}
419
420impl Program {
421    fn new(ops: Vec<Opcode>, source: &str) -> Self {
422        let id = hash_str(source);
423        let is_structural = ops.iter().all(|op| matches!(op,
424            Opcode::PushRoot | Opcode::PushCurrent |
425            Opcode::GetField(_) | Opcode::GetIndex(_) |
426            Opcode::GetSlice(..) | Opcode::OptField(_) |
427            Opcode::RootChain(_) | Opcode::GetPointer(_)
428        ));
429        Self { ops: ops.into(), source: source.into(), id, is_structural }
430    }
431}
432
433// ── Variable context (compile-time) ──────────────────────────────────────────
434
435#[derive(Clone, Default)]
436struct VarCtx {
437    known: SmallVec<[Arc<str>; 4]>,
438}
439
440impl VarCtx {
441    fn with_var(&self, name: &str) -> Self {
442        let mut v = self.clone();
443        if !v.known.iter().any(|k| k.as_ref() == name) {
444            v.known.push(Arc::from(name));
445        }
446        v
447    }
448    fn with_vars(&self, names: &[String]) -> Self {
449        let mut v = self.clone();
450        for n in names {
451            if !v.known.iter().any(|k| k.as_ref() == n.as_str()) {
452                v.known.push(Arc::from(n.as_str()));
453            }
454        }
455        v
456    }
457    fn has(&self, name: &str) -> bool {
458        self.known.iter().any(|k| k.as_ref() == name)
459    }
460}
461
462// ── Compiler ─────────────────────────────────────────────────────────────────
463
464pub struct Compiler;
465
466impl Compiler {
467    pub fn compile(expr: &Expr, source: &str) -> Program {
468        let mut e = expr.clone();
469        Self::reorder_and_operands(&mut e);
470        let ctx = VarCtx::default();
471        let ops = Self::optimize(Self::emit(&e, &ctx));
472        let prog = Program::new(ops, source);
473        // Post-pass: canonicalise identical sub-programs.
474        let deduped = super::analysis::dedup_subprograms(&prog);
475        Program {
476            ops:           deduped.ops.clone(),
477            source:        prog.source,
478            id:            prog.id,
479            is_structural: prog.is_structural,
480        }
481    }
482
483    /// AST rewrite: for each `a and b`, if `b` is more selective than `a`,
484    /// swap operands so the cheaper/selective predicate runs first.  Safe
485    /// because `and` is commutative on pure, side-effect-free expressions.
486    fn reorder_and_operands(expr: &mut Expr) {
487        use super::analysis::selectivity_score;
488        match expr {
489            Expr::BinOp(l, op, r) if *op == BinOp::And => {
490                Self::reorder_and_operands(l);
491                Self::reorder_and_operands(r);
492                if selectivity_score(r) < selectivity_score(l) {
493                    std::mem::swap(l, r);
494                }
495            }
496            Expr::BinOp(l, _, r) => {
497                Self::reorder_and_operands(l);
498                Self::reorder_and_operands(r);
499            }
500            Expr::UnaryNeg(e) | Expr::Not(e) | Expr::Kind { expr: e, .. } =>
501                Self::reorder_and_operands(e),
502            Expr::Coalesce(l, r) => {
503                Self::reorder_and_operands(l);
504                Self::reorder_and_operands(r);
505            }
506            Expr::Chain(base, steps) => {
507                Self::reorder_and_operands(base);
508                for s in steps {
509                    match s {
510                        super::ast::Step::DynIndex(e) | super::ast::Step::InlineFilter(e) =>
511                            Self::reorder_and_operands(e),
512                        super::ast::Step::Method(_, args) | super::ast::Step::OptMethod(_, args) =>
513                            for a in args { match a {
514                                super::ast::Arg::Pos(e) | super::ast::Arg::Named(_, e) =>
515                                    Self::reorder_and_operands(e),
516                            } },
517                        _ => {}
518                    }
519                }
520            }
521            Expr::Let { init, body, .. } => {
522                Self::reorder_and_operands(init);
523                Self::reorder_and_operands(body);
524            }
525            Expr::Pipeline { base, steps } => {
526                Self::reorder_and_operands(base);
527                for s in steps {
528                    if let super::ast::PipeStep::Forward(e) = s {
529                        Self::reorder_and_operands(e);
530                    }
531                }
532            }
533            Expr::Object(fields) => for f in fields { match f {
534                super::ast::ObjField::Kv { val, .. } => Self::reorder_and_operands(val),
535                super::ast::ObjField::Dynamic { key, val } => {
536                    Self::reorder_and_operands(key);
537                    Self::reorder_and_operands(val);
538                }
539                super::ast::ObjField::Spread(e) => Self::reorder_and_operands(e),
540                _ => {}
541            } },
542            Expr::Array(elems) => for e in elems { match e {
543                super::ast::ArrayElem::Expr(e) | super::ast::ArrayElem::Spread(e) =>
544                    Self::reorder_and_operands(e),
545            } },
546            Expr::ListComp { expr, iter, cond, .. }
547            | Expr::SetComp  { expr, iter, cond, .. }
548            | Expr::GenComp  { expr, iter, cond, .. } => {
549                Self::reorder_and_operands(expr);
550                Self::reorder_and_operands(iter);
551                if let Some(c) = cond { Self::reorder_and_operands(c); }
552            }
553            Expr::DictComp { key, val, iter, cond, .. } => {
554                Self::reorder_and_operands(key);
555                Self::reorder_and_operands(val);
556                Self::reorder_and_operands(iter);
557                if let Some(c) = cond { Self::reorder_and_operands(c); }
558            }
559            Expr::Lambda { body, .. } => Self::reorder_and_operands(body),
560            Expr::GlobalCall { args, .. } => for a in args { match a {
561                super::ast::Arg::Pos(e) | super::ast::Arg::Named(_, e) =>
562                    Self::reorder_and_operands(e),
563            } },
564            _ => {}
565        }
566    }
567
568    pub fn compile_str(input: &str) -> Result<Program, EvalError> {
569        let expr = super::parser::parse(input)
570            .map_err(|e| EvalError(e.to_string()))?;
571        Ok(Self::compile(&expr, input))
572    }
573
574    /// Compile with explicit pass configuration.  Cached by callers
575    /// under `(config.hash(), expr)`.
576    pub fn compile_str_with_config(input: &str, config: PassConfig) -> Result<Program, EvalError> {
577        let expr = super::parser::parse(input)
578            .map_err(|e| EvalError(e.to_string()))?;
579        let mut e = expr.clone();
580        if config.reorder_and { Self::reorder_and_operands(&mut e); }
581        let ctx = VarCtx::default();
582        let ops = Self::optimize_with(Self::emit(&e, &ctx), config);
583        let prog = Program::new(ops, input);
584        if config.dedup_subprogs {
585            let deduped = super::analysis::dedup_subprograms(&prog);
586            Ok(Program {
587                ops:           deduped.ops.clone(),
588                source:        prog.source,
589                id:            prog.id,
590                is_structural: prog.is_structural,
591            })
592        } else {
593            Ok(prog)
594        }
595    }
596
597    // ── Peephole optimizer ────────────────────────────────────────────────────
598
599    fn optimize(ops: Vec<Opcode>) -> Vec<Opcode> {
600        Self::optimize_with(ops, PassConfig::default())
601    }
602
603    fn optimize_with(ops: Vec<Opcode>, cfg: PassConfig) -> Vec<Opcode> {
604        let ops = if cfg.root_chain      { Self::pass_root_chain(ops) }      else { ops };
605        let ops = if cfg.filter_count    { Self::pass_filter_count(ops) }    else { ops };
606        let ops = if cfg.filter_fusion   { Self::pass_filter_fusion(ops) }   else { ops };
607        let ops = if cfg.find_quantifier { Self::pass_find_quantifier(ops) } else { ops };
608        let ops = if cfg.strength_reduce { Self::pass_strength_reduce(ops) } else { ops };
609        let ops = if cfg.redundant_ops   { Self::pass_redundant_ops(ops) }   else { ops };
610        let ops = if cfg.kind_check_fold { Self::pass_kind_check_fold(ops) } else { ops };
611        let ops = if cfg.method_const    { Self::pass_method_const_fold(ops)} else { ops };
612        let ops = if cfg.const_fold      { Self::pass_const_fold(ops) }      else { ops };
613        let ops = if cfg.nullness        { Self::pass_nullness_opt_field(ops)} else { ops };
614        let ops = if cfg.equi_join       { Self::pass_equi_join_fusion(ops) } else { ops };
615        ops
616    }
617
618    /// Rewrite `CallMethod(equi_join, [rhs, PushStr(lk), PushStr(rk)])`
619    /// to the fused `EquiJoin` opcode — removes runtime method dispatch
620    /// and extracts string keys into the opcode so the executor can
621    /// hash directly.
622    fn pass_equi_join_fusion(ops: Vec<Opcode>) -> Vec<Opcode> {
623        let mut out: Vec<Opcode> = Vec::with_capacity(ops.len());
624        for op in ops {
625            if let Opcode::CallMethod(c) = &op {
626                if c.method == BuiltinMethod::EquiJoin && c.sub_progs.len() == 3 {
627                    let rhs = Arc::clone(&c.sub_progs[0]);
628                    let lhs_key = const_str_program(&c.sub_progs[1]);
629                    let rhs_key = const_str_program(&c.sub_progs[2]);
630                    if let (Some(lk), Some(rk)) = (lhs_key, rhs_key) {
631                        out.push(Opcode::EquiJoin { rhs, lhs_key: lk, rhs_key: rk });
632                        continue;
633                    }
634                }
635            }
636            out.push(op);
637        }
638        out
639    }
640
641    /// Nullness-driven: when the preceding op provably leaves a non-null
642    /// receiver on the stack, rewrite `OptField(k)` → `GetField(k)`.
643    /// Conservative: only folds when the predecessor is a construction
644    /// opcode (MakeObj / MakeArr / PushStr / RootChain / GetField).
645    fn pass_nullness_opt_field(ops: Vec<Opcode>) -> Vec<Opcode> {
646        let mut out: Vec<Opcode> = Vec::with_capacity(ops.len());
647        for op in ops {
648            if let Opcode::OptField(k) = &op {
649                // Only safe when receiver is provably a non-null object —
650                // MakeObj is the only opcode that guarantees this without
651                // a schema.  Other cases are handled by schema::specialize.
652                let non_null = matches!(out.last(), Some(Opcode::MakeObj(_)));
653                if non_null {
654                    out.push(Opcode::GetField(k.clone()));
655                    continue;
656                }
657            }
658            out.push(op);
659        }
660        out
661    }
662
663    /// Fold built-in methods when receiver is a literal with known length/content:
664    ///   PushStr(s) + .len()    → PushInt(utf8 char count)
665    ///   PushStr(s) + .upper()  → PushStr(upper)
666    ///   PushStr(s) + .lower()  → PushStr(lower)
667    ///   PushStr(s) + .trim()   → PushStr(trim)
668    ///   MakeArr(n elems) + .len()  → PushInt(n)  (only for non-spread arrays)
669    fn pass_method_const_fold(ops: Vec<Opcode>) -> Vec<Opcode> {
670        let mut out: Vec<Opcode> = Vec::with_capacity(ops.len());
671        for op in ops {
672            if let Opcode::CallMethod(c) = &op {
673                if c.sub_progs.is_empty() {
674                    match (out.last(), c.method) {
675                        (Some(Opcode::PushStr(s)), BuiltinMethod::Len) => {
676                            let n = s.chars().count() as i64;
677                            out.pop();
678                            out.push(Opcode::PushInt(n));
679                            continue;
680                        }
681                        (Some(Opcode::PushStr(s)), BuiltinMethod::Upper) => {
682                            let u: Arc<str> = Arc::from(s.to_uppercase());
683                            out.pop();
684                            out.push(Opcode::PushStr(u));
685                            continue;
686                        }
687                        (Some(Opcode::PushStr(s)), BuiltinMethod::Lower) => {
688                            let u: Arc<str> = Arc::from(s.to_lowercase());
689                            out.pop();
690                            out.push(Opcode::PushStr(u));
691                            continue;
692                        }
693                        (Some(Opcode::PushStr(s)), BuiltinMethod::Trim) => {
694                            let u: Arc<str> = Arc::from(s.trim());
695                            out.pop();
696                            out.push(Opcode::PushStr(u));
697                            continue;
698                        }
699                        (Some(Opcode::MakeArr(progs)), BuiltinMethod::Len) => {
700                            let n = progs.len() as i64;
701                            out.pop();
702                            out.push(Opcode::PushInt(n));
703                            continue;
704                        }
705                        _ => {}
706                    }
707                }
708            }
709            out.push(op);
710        }
711        out
712    }
713
714    /// Fold `KindCheck` when its input type is a literal push:
715    ///   PushInt(n)  + KindCheck{number, neg} → PushBool(!neg)
716    ///   PushStr(_)  + KindCheck{string, neg} → PushBool(!neg)
717    ///   PushNull    + KindCheck{null, neg}   → PushBool(!neg)
718    ///   PushBool(_) + KindCheck{bool, neg}   → PushBool(!neg)
719    ///   mismatches fold to opposite.
720    fn pass_kind_check_fold(ops: Vec<Opcode>) -> Vec<Opcode> {
721        use super::analysis::{fold_kind_check, VType};
722        let mut out = Vec::with_capacity(ops.len());
723        for op in ops {
724            if let Opcode::KindCheck { ty, negate } = &op {
725                let prev_ty: Option<VType> = match out.last() {
726                    Some(Opcode::PushNull)     => Some(VType::Null),
727                    Some(Opcode::PushBool(_))  => Some(VType::Bool),
728                    Some(Opcode::PushInt(_))   => Some(VType::Int),
729                    Some(Opcode::PushFloat(_)) => Some(VType::Float),
730                    Some(Opcode::PushStr(_))   => Some(VType::Str),
731                    Some(Opcode::MakeArr(_))   => Some(VType::Arr),
732                    Some(Opcode::MakeObj(_))   => Some(VType::Obj),
733                    _ => None,
734                };
735                if let Some(vt) = prev_ty {
736                    if let Some(b) = fold_kind_check(vt, *ty, *negate) {
737                        out.pop();
738                        out.push(Opcode::PushBool(b));
739                        continue;
740                    }
741                }
742            }
743            out.push(op);
744        }
745        out
746    }
747
748    /// Fuse adjacent method calls into single-pass fused opcodes:
749    ///   filter(p) + map(f)     → FilterMap
750    ///   filter(p1) + filter(p2)→ FilterFilter
751    ///   map(f1) + map(f2)      → MapMap
752    fn pass_filter_fusion(ops: Vec<Opcode>) -> Vec<Opcode> {
753        let mut out: Vec<Opcode> = Vec::with_capacity(ops.len());
754        for op in ops {
755            if let (Opcode::CallMethod(b), Some(Opcode::CallMethod(a))) = (&op, out.last()) {
756                // Two-arg fusions (both have sub_progs)
757                if a.sub_progs.len() >= 1 && b.sub_progs.len() >= 1 {
758                    let (am, bm) = (a.method, b.method);
759                    let p1 = Arc::clone(&a.sub_progs[0]);
760                    let p2 = Arc::clone(&b.sub_progs[0]);
761                    let fused = match (am, bm) {
762                        (BuiltinMethod::Filter, BuiltinMethod::Map) =>
763                            Some(Opcode::FilterMap { pred: p1, map: p2 }),
764                        (BuiltinMethod::Filter, BuiltinMethod::Filter) =>
765                            Some(Opcode::FilterFilter { p1, p2 }),
766                        (BuiltinMethod::Map, BuiltinMethod::Map) =>
767                            Some(Opcode::MapMap { f1: p1, f2: p2 }),
768                        _ => None,
769                    };
770                    if let Some(f) = fused {
771                        out.pop();
772                        out.push(f);
773                        continue;
774                    }
775                }
776                // map(f) + sum()/avg()/flatten()
777                if a.method == BuiltinMethod::Map && a.sub_progs.len() >= 1
778                   && b.sub_progs.is_empty() {
779                    let f = Arc::clone(&a.sub_progs[0]);
780                    let fused = match b.method {
781                        BuiltinMethod::Sum => Some(Opcode::MapSum(f)),
782                        BuiltinMethod::Avg => Some(Opcode::MapAvg(f)),
783                        BuiltinMethod::Flatten => Some(Opcode::MapFlatten(f)),
784                        _ => None,
785                    };
786                    if let Some(o) = fused {
787                        out.pop();
788                        out.push(o);
789                        continue;
790                    }
791                }
792                // filter(p) + take_while(q) → FilterTakeWhile
793                if a.method == BuiltinMethod::Filter && a.sub_progs.len() >= 1
794                   && b.method == BuiltinMethod::TakeWhile && b.sub_progs.len() >= 1 {
795                    let pred = Arc::clone(&a.sub_progs[0]);
796                    let stop = Arc::clone(&b.sub_progs[0]);
797                    out.pop();
798                    out.push(Opcode::FilterTakeWhile { pred, stop });
799                    continue;
800                }
801                // filter(p) + drop_while(q) → FilterDropWhile
802                if a.method == BuiltinMethod::Filter && a.sub_progs.len() >= 1
803                   && b.method == BuiltinMethod::DropWhile && b.sub_progs.len() >= 1 {
804                    let pred = Arc::clone(&a.sub_progs[0]);
805                    let drop = Arc::clone(&b.sub_progs[0]);
806                    out.pop();
807                    out.push(Opcode::FilterDropWhile { pred, drop });
808                    continue;
809                }
810                // map(f) + unique() → MapUnique
811                if a.method == BuiltinMethod::Map && a.sub_progs.len() >= 1
812                   && b.method == BuiltinMethod::Unique && b.sub_progs.is_empty() {
813                    let f = Arc::clone(&a.sub_progs[0]);
814                    out.pop();
815                    out.push(Opcode::MapUnique(f));
816                    continue;
817                }
818            }
819            out.push(op);
820        }
821        out
822    }
823
824    /// Replace expensive ops with cheaper equivalents:
825    ///   sort() + first()    → min()
826    ///   sort() + last()     → max()
827    ///   sort() + [0]        → min()
828    ///   sort() + [-1]       → max()
829    ///   reverse() + first() → last()
830    ///   reverse() + last()  → first()
831    fn pass_strength_reduce(ops: Vec<Opcode>) -> Vec<Opcode> {
832        let mut out: Vec<Opcode> = Vec::with_capacity(ops.len());
833        for op in ops {
834            // Pattern: [..., prev_method_call, current_op]
835            if let Some(Opcode::CallMethod(prev)) = out.last().cloned() {
836                let replaced = match (prev.method, &op) {
837                    // sort() + [0] → min()
838                    (BuiltinMethod::Sort, Opcode::GetIndex(0)) if prev.sub_progs.is_empty() =>
839                        Some(make_noarg_call(BuiltinMethod::Min, "min")),
840                    // sort() + [-1] → max()
841                    (BuiltinMethod::Sort, Opcode::GetIndex(-1)) if prev.sub_progs.is_empty() =>
842                        Some(make_noarg_call(BuiltinMethod::Max, "max")),
843                    // sort() + first() → min()
844                    (BuiltinMethod::Sort, Opcode::CallMethod(next))
845                        if prev.sub_progs.is_empty() && next.method == BuiltinMethod::First =>
846                        Some(make_noarg_call(BuiltinMethod::Min, "min")),
847                    // sort() + last() → max()
848                    (BuiltinMethod::Sort, Opcode::CallMethod(next))
849                        if prev.sub_progs.is_empty() && next.method == BuiltinMethod::Last =>
850                        Some(make_noarg_call(BuiltinMethod::Max, "max")),
851                    // reverse() + first() → last()
852                    (BuiltinMethod::Reverse, Opcode::CallMethod(next))
853                        if next.method == BuiltinMethod::First =>
854                        Some(make_noarg_call(BuiltinMethod::Last, "last")),
855                    // reverse() + last() → first()
856                    (BuiltinMethod::Reverse, Opcode::CallMethod(next))
857                        if next.method == BuiltinMethod::Last =>
858                        Some(make_noarg_call(BuiltinMethod::First, "first")),
859                    // sort() + [0:n] → TopN(n, asc=true)
860                    (BuiltinMethod::Sort, Opcode::GetSlice(from, Some(to)))
861                        if prev.sub_progs.is_empty()
862                           && (from.is_none() || *from == Some(0))
863                           && *to > 0 =>
864                        Some(Opcode::TopN { n: *to as usize, asc: true }),
865                    _ => None,
866                };
867                if let Some(rep) = replaced {
868                    out.pop();
869                    out.push(rep);
870                    continue;
871                }
872            }
873            out.push(op);
874        }
875        out
876    }
877
878    /// Fuse `PushRoot + GetField(k1) + GetField(k2) ...` → `RootChain([k1,k2,...])`.
879    fn pass_root_chain(ops: Vec<Opcode>) -> Vec<Opcode> {
880        let mut out = Vec::with_capacity(ops.len());
881        let mut it = ops.into_iter().peekable();
882        while let Some(op) = it.next() {
883            if matches!(op, Opcode::PushRoot) {
884                let mut chain: Vec<Arc<str>> = Vec::new();
885                while let Some(Opcode::GetField(_)) = it.peek() {
886                    if let Some(Opcode::GetField(k)) = it.next() {
887                        chain.push(k);
888                    }
889                }
890                if chain.is_empty() {
891                    out.push(Opcode::PushRoot);
892                } else {
893                    out.push(Opcode::RootChain(chain.into()));
894                }
895            } else {
896                out.push(op);
897            }
898        }
899        out
900    }
901
902    /// Fuse `CallMethod(filter/pred) + CallMethod(len/count)` → `FilterCount(pred)`.
903    fn pass_filter_count(ops: Vec<Opcode>) -> Vec<Opcode> {
904        let mut out = Vec::with_capacity(ops.len());
905        let mut it = ops.into_iter().peekable();
906        while let Some(op) = it.next() {
907            if let Opcode::CallMethod(ref call) = op {
908                if call.method == BuiltinMethod::Filter {
909                    let is_len = matches!(it.peek(),
910                        Some(Opcode::CallMethod(c))
911                            if c.method == BuiltinMethod::Len || c.method == BuiltinMethod::Count
912                    );
913                    if is_len && !call.sub_progs.is_empty() {
914                        let pred = Arc::clone(&call.sub_progs[0]);
915                        it.next(); // consume Len/Count
916                        out.push(Opcode::FilterCount(pred));
917                        continue;
918                    }
919                }
920            }
921            out.push(op);
922        }
923        out
924    }
925
926    /// Fuse `InlineFilter(pred) + Quantifier(First/One)` → `FindFirst/FindOne(pred)`.
927    /// Also fuses `CallMethod(Filter, pred) + Quantifier(...)` for explicit `.filter()`.
928    fn pass_find_quantifier(ops: Vec<Opcode>) -> Vec<Opcode> {
929        let mut out = Vec::with_capacity(ops.len());
930        let mut it = ops.into_iter().peekable();
931        while let Some(op) = it.next() {
932            let pred_opt: Option<Arc<Program>> = match &op {
933                Opcode::InlineFilter(p) => Some(Arc::clone(p)),
934                Opcode::CallMethod(c) if c.method == BuiltinMethod::Filter && !c.sub_progs.is_empty()
935                    => Some(Arc::clone(&c.sub_progs[0])),
936                _ => None,
937            };
938            if let Some(pred) = pred_opt {
939                match it.peek() {
940                    Some(Opcode::Quantifier(QuantifierKind::First)) => {
941                        it.next();
942                        out.push(Opcode::FindFirst(pred));
943                        continue;
944                    }
945                    Some(Opcode::Quantifier(QuantifierKind::One)) => {
946                        it.next();
947                        out.push(Opcode::FindOne(pred));
948                        continue;
949                    }
950                    _ => {}
951                }
952            }
953            out.push(op);
954        }
955        out
956    }
957
958    /// Eliminate redundant adjacent method calls:
959    ///   reverse() + reverse()         → identity (both dropped)
960    ///   unique() + unique()           → unique()
961    ///   compact() + compact()         → compact()
962    ///   sort() + sort(k)              → sort(k)      (later sort wins on same-array)
963    ///   sort(k) + sort(k)             → sort(k)
964    ///   Quantifier + Quantifier       → second only  (first wrap is scalar anyway)
965    fn pass_redundant_ops(ops: Vec<Opcode>) -> Vec<Opcode> {
966        let mut out: Vec<Opcode> = Vec::with_capacity(ops.len());
967        for op in ops {
968            match (&op, out.last()) {
969                // reverse + reverse: drop both
970                (Opcode::CallMethod(b), Some(Opcode::CallMethod(a)))
971                    if a.method == BuiltinMethod::Reverse && b.method == BuiltinMethod::Reverse =>
972                {
973                    out.pop();
974                    continue;
975                }
976                // idempotent method pairs: keep second only (drop first)
977                (Opcode::CallMethod(b), Some(Opcode::CallMethod(a)))
978                    if a.method == b.method && matches!(a.method,
979                        BuiltinMethod::Unique | BuiltinMethod::Compact)
980                        && a.sub_progs.is_empty() && b.sub_progs.is_empty() =>
981                {
982                    out.pop();
983                    out.push(op);
984                    continue;
985                }
986                // sort + sort(_): later sort wins, drop the first
987                (Opcode::CallMethod(b), Some(Opcode::CallMethod(a)))
988                    if a.method == BuiltinMethod::Sort && b.method == BuiltinMethod::Sort =>
989                {
990                    out.pop();
991                    out.push(op);
992                    continue;
993                }
994                // Quantifier + Quantifier: second wins (first unwraps scalar,
995                // second is no-op on scalar — but keeping second preserves error
996                // semantics of `!`). Drop first.
997                (Opcode::Quantifier(_), Some(Opcode::Quantifier(_))) => {
998                    out.pop();
999                    out.push(op);
1000                    continue;
1001                }
1002                // reverse + last → first (strength reduction fallback after sort)
1003                // already handled in pass_strength_reduce
1004                // Not + Not: double negation → drop both
1005                (Opcode::Not, Some(Opcode::Not)) => {
1006                    out.pop();
1007                    continue;
1008                }
1009                // Neg + Neg: --x → x
1010                (Opcode::Neg, Some(Opcode::Neg)) => {
1011                    out.pop();
1012                    continue;
1013                }
1014                _ => {}
1015            }
1016            out.push(op);
1017        }
1018        out
1019    }
1020
1021    /// Constant-fold adjacent integer arithmetic + bool short-circuits.
1022    fn pass_const_fold(ops: Vec<Opcode>) -> Vec<Opcode> {
1023        let mut out = Vec::with_capacity(ops.len());
1024        let mut i = 0;
1025        while i < ops.len() {
1026            // 2-op bool short-circuit folds:
1027            //   PushBool(false) + AndOp(_)  → PushBool(false)
1028            //   PushBool(true)  + OrOp(_)   → PushBool(true)
1029            if i + 1 < ops.len() {
1030                let folded = match (&ops[i], &ops[i+1]) {
1031                    (Opcode::PushBool(false), Opcode::AndOp(_)) =>
1032                        Some(Opcode::PushBool(false)),
1033                    (Opcode::PushBool(true),  Opcode::OrOp(_)) =>
1034                        Some(Opcode::PushBool(true)),
1035                    _ => None,
1036                };
1037                if let Some(folded) = folded {
1038                    out.push(folded);
1039                    i += 2;
1040                    continue;
1041                }
1042            }
1043            // 2-op unary folds
1044            if i + 1 < ops.len() {
1045                let folded = match (&ops[i], &ops[i+1]) {
1046                    (Opcode::PushBool(b), Opcode::Not) =>
1047                        Some(Opcode::PushBool(!b)),
1048                    (Opcode::PushInt(n), Opcode::Neg) =>
1049                        Some(Opcode::PushInt(-n)),
1050                    (Opcode::PushFloat(f), Opcode::Neg) =>
1051                        Some(Opcode::PushFloat(-f)),
1052                    _ => None,
1053                };
1054                if let Some(folded) = folded {
1055                    out.push(folded);
1056                    i += 2;
1057                    continue;
1058                }
1059            }
1060            // 3-op arithmetic + comparison folds
1061            if i + 2 < ops.len() {
1062                let folded = match (&ops[i], &ops[i+1], &ops[i+2]) {
1063                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Add) =>
1064                        Some(Opcode::PushInt(a + b)),
1065                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Sub) =>
1066                        Some(Opcode::PushInt(a - b)),
1067                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Mul) =>
1068                        Some(Opcode::PushInt(a * b)),
1069                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Mod) if *b != 0 =>
1070                        Some(Opcode::PushInt(a % b)),
1071                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Div) if *b != 0 =>
1072                        Some(Opcode::PushFloat(*a as f64 / *b as f64)),
1073                    (Opcode::PushFloat(a), Opcode::PushFloat(b), Opcode::Add) =>
1074                        Some(Opcode::PushFloat(a + b)),
1075                    (Opcode::PushFloat(a), Opcode::PushFloat(b), Opcode::Sub) =>
1076                        Some(Opcode::PushFloat(a - b)),
1077                    (Opcode::PushFloat(a), Opcode::PushFloat(b), Opcode::Mul) =>
1078                        Some(Opcode::PushFloat(a * b)),
1079                    (Opcode::PushFloat(a), Opcode::PushFloat(b), Opcode::Div) if *b != 0.0 =>
1080                        Some(Opcode::PushFloat(a / b)),
1081                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Eq) =>
1082                        Some(Opcode::PushBool(a == b)),
1083                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Neq) =>
1084                        Some(Opcode::PushBool(a != b)),
1085                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Lt) =>
1086                        Some(Opcode::PushBool(a < b)),
1087                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Lte) =>
1088                        Some(Opcode::PushBool(a <= b)),
1089                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Gt) =>
1090                        Some(Opcode::PushBool(a > b)),
1091                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Gte) =>
1092                        Some(Opcode::PushBool(a >= b)),
1093                    (Opcode::PushStr(a), Opcode::PushStr(b), Opcode::Eq) =>
1094                        Some(Opcode::PushBool(a == b)),
1095                    (Opcode::PushStr(a), Opcode::PushStr(b), Opcode::Neq) =>
1096                        Some(Opcode::PushBool(a != b)),
1097                    (Opcode::PushBool(a), Opcode::PushBool(b), Opcode::Eq) =>
1098                        Some(Opcode::PushBool(a == b)),
1099                    _ => None,
1100                };
1101                if let Some(folded) = folded {
1102                    out.push(folded);
1103                    i += 3;
1104                    continue;
1105                }
1106            }
1107            out.push(ops[i].clone());
1108            i += 1;
1109        }
1110        out
1111    }
1112
1113    // ── Main emit ─────────────────────────────────────────────────────────────
1114
1115    fn emit(expr: &Expr, ctx: &VarCtx) -> Vec<Opcode> {
1116        let mut ops = Vec::new();
1117        Self::emit_into(expr, ctx, &mut ops);
1118        ops
1119    }
1120
1121    fn emit_into(expr: &Expr, ctx: &VarCtx, ops: &mut Vec<Opcode>) {
1122        match expr {
1123            Expr::Null    => ops.push(Opcode::PushNull),
1124            Expr::Bool(b) => ops.push(Opcode::PushBool(*b)),
1125            Expr::Int(n)  => ops.push(Opcode::PushInt(*n)),
1126            Expr::Float(f)=> ops.push(Opcode::PushFloat(*f)),
1127            Expr::Str(s)  => ops.push(Opcode::PushStr(Arc::from(s.as_str()))),
1128            Expr::Root    => ops.push(Opcode::PushRoot),
1129            Expr::Current => ops.push(Opcode::PushCurrent),
1130
1131            Expr::FString(parts) => {
1132                let compiled: Vec<CompiledFSPart> = parts.iter().map(|p| match p {
1133                    FStringPart::Lit(s) => CompiledFSPart::Lit(Arc::from(s.as_str())),
1134                    FStringPart::Interp { expr, fmt } => CompiledFSPart::Interp {
1135                        prog: Arc::new(Self::compile_sub(expr, ctx)),
1136                        fmt: fmt.clone(),
1137                    },
1138                }).collect();
1139                ops.push(Opcode::FString(compiled.into()));
1140            }
1141
1142            Expr::Ident(name) => ops.push(Opcode::LoadIdent(Arc::from(name.as_str()))),
1143
1144            Expr::Chain(base, steps) => {
1145                Self::emit_into(base, ctx, ops);
1146                for step in steps {
1147                    Self::emit_step(step, ctx, ops);
1148                }
1149            }
1150
1151            Expr::UnaryNeg(e) => {
1152                Self::emit_into(e, ctx, ops);
1153                ops.push(Opcode::Neg);
1154            }
1155            Expr::Not(e) => {
1156                Self::emit_into(e, ctx, ops);
1157                ops.push(Opcode::Not);
1158            }
1159
1160            Expr::BinOp(l, op, r) => Self::emit_binop(l, *op, r, ctx, ops),
1161
1162            Expr::Coalesce(lhs, rhs) => {
1163                Self::emit_into(lhs, ctx, ops);
1164                let rhs_prog = Arc::new(Self::compile_sub(rhs, ctx));
1165                ops.push(Opcode::CoalesceOp(rhs_prog));
1166            }
1167
1168            Expr::Kind { expr, ty, negate } => {
1169                Self::emit_into(expr, ctx, ops);
1170                ops.push(Opcode::KindCheck { ty: *ty, negate: *negate });
1171            }
1172
1173            Expr::Object(fields) => {
1174                let entries: Vec<CompiledObjEntry> = fields.iter().map(|f| match f {
1175                    ObjField::Short(name) =>
1176                        CompiledObjEntry::Short(Arc::from(name.as_str())),
1177                    ObjField::Kv { key, val, optional, cond } =>
1178                        CompiledObjEntry::Kv {
1179                            key: Arc::from(key.as_str()),
1180                            prog: Arc::new(Self::compile_sub(val, ctx)),
1181                            optional: *optional,
1182                            cond: cond.as_ref().map(|c| Arc::new(Self::compile_sub(c, ctx))),
1183                        },
1184                    ObjField::Dynamic { key, val } =>
1185                        CompiledObjEntry::Dynamic {
1186                            key: Arc::new(Self::compile_sub(key, ctx)),
1187                            val: Arc::new(Self::compile_sub(val, ctx)),
1188                        },
1189                    ObjField::Spread(e) =>
1190                        CompiledObjEntry::Spread(Arc::new(Self::compile_sub(e, ctx))),
1191                    ObjField::SpreadDeep(e) =>
1192                        CompiledObjEntry::SpreadDeep(Arc::new(Self::compile_sub(e, ctx))),
1193                }).collect();
1194                ops.push(Opcode::MakeObj(entries.into()));
1195            }
1196
1197            Expr::Array(elems) => {
1198                // Compile each elem as a sub-program.
1199                // Spread elems are handled by a special marker.
1200                let _progs: Vec<Arc<Program>> = elems.iter().map(|e| match e {
1201                    ArrayElem::Expr(ex)   => Arc::new(Self::compile_sub(ex, ctx)),
1202                    // Spread: compile the inner expr with a spread marker opcode
1203                    ArrayElem::Spread(ex) => {
1204                        let mut sub = Self::emit(ex, ctx);
1205                        // Prepend a sentinel to distinguish spread from normal
1206                        sub.insert(0, Opcode::PushNull); // placeholder
1207                        // Actually, encode spread differently via a wrapper
1208                        Arc::new(Self::compile_array_spread(ex, ctx))
1209                    }
1210                }).collect();
1211                // Simpler: build a mixed elem list
1212                let progs = elems.iter().map(|e| match e {
1213                    ArrayElem::Expr(ex) => {
1214                        Arc::new(Self::compile_sub(ex, ctx))
1215                    }
1216                    ArrayElem::Spread(ex) => {
1217                        Arc::new(Self::compile_sub_spread(ex, ctx))
1218                    }
1219                }).collect::<Vec<_>>();
1220                ops.push(Opcode::MakeArr(progs.into()));
1221            }
1222
1223            Expr::Pipeline { base, steps } => {
1224                Self::emit_pipeline(base, steps, ctx, ops);
1225            }
1226
1227            Expr::ListComp { expr, vars, iter, cond } => {
1228                let inner_ctx = ctx.with_vars(vars);
1229                ops.push(Opcode::ListComp(Arc::new(CompSpec {
1230                    expr: Arc::new(Self::compile_sub(expr, &inner_ctx)),
1231                    vars: vars.iter().map(|v| Arc::from(v.as_str())).collect::<Vec<_>>().into(),
1232                    iter: Arc::new(Self::compile_sub(iter, ctx)),
1233                    cond: cond.as_ref().map(|c| Arc::new(Self::compile_sub(c, &inner_ctx))),
1234                })));
1235            }
1236
1237            Expr::DictComp { key, val, vars, iter, cond } => {
1238                let inner_ctx = ctx.with_vars(vars);
1239                ops.push(Opcode::DictComp(Arc::new(DictCompSpec {
1240                    key:  Arc::new(Self::compile_sub(key, &inner_ctx)),
1241                    val:  Arc::new(Self::compile_sub(val, &inner_ctx)),
1242                    vars: vars.iter().map(|v| Arc::from(v.as_str())).collect::<Vec<_>>().into(),
1243                    iter: Arc::new(Self::compile_sub(iter, ctx)),
1244                    cond: cond.as_ref().map(|c| Arc::new(Self::compile_sub(c, &inner_ctx))),
1245                })));
1246            }
1247
1248            Expr::SetComp { expr, vars, iter, cond } |
1249            Expr::GenComp { expr, vars, iter, cond } => {
1250                let inner_ctx = ctx.with_vars(vars);
1251                ops.push(Opcode::SetComp(Arc::new(CompSpec {
1252                    expr: Arc::new(Self::compile_sub(expr, &inner_ctx)),
1253                    vars: vars.iter().map(|v| Arc::from(v.as_str())).collect::<Vec<_>>().into(),
1254                    iter: Arc::new(Self::compile_sub(iter, ctx)),
1255                    cond: cond.as_ref().map(|c| Arc::new(Self::compile_sub(c, &inner_ctx))),
1256                })));
1257            }
1258
1259            Expr::Lambda { .. } => {
1260                // Lambdas as standalone values are errors; they only appear as args
1261                ops.push(Opcode::PushNull);
1262            }
1263
1264            Expr::Let { name, init, body } => {
1265                // Dead-let: if body never references `name` and init is pure,
1266                // drop the binding entirely and emit body only.
1267                if super::analysis::expr_is_pure(init)
1268                    && !super::analysis::expr_uses_ident(body, name) {
1269                    Self::emit_into(body, ctx, ops);
1270                } else {
1271                    Self::emit_into(init, ctx, ops);
1272                    let body_ctx = ctx.with_var(name);
1273                    let body_prog = Arc::new(Self::compile_sub(body, &body_ctx));
1274                    ops.push(Opcode::LetExpr { name: Arc::from(name.as_str()), body: body_prog });
1275                }
1276            }
1277
1278            Expr::GlobalCall { name, args } => {
1279                // Compile as a sequence of sub-progs + a special dispatch
1280                let sub_progs: Vec<Arc<Program>> = args.iter().map(|a| match a {
1281                    Arg::Pos(e) | Arg::Named(_, e) => Arc::new(Self::compile_sub(e, ctx)),
1282                }).collect();
1283                let call = Arc::new(CompiledCall {
1284                    method:    BuiltinMethod::Unknown,
1285                    name:      Arc::from(name.as_str()),
1286                    sub_progs: sub_progs.into(),
1287                    orig_args: args.iter().cloned().collect::<Vec<_>>().into(),
1288                });
1289                ops.push(Opcode::PushRoot); // global calls need root pushed first
1290                ops.push(Opcode::CallMethod(call));
1291            }
1292
1293            Expr::Cast { expr, ty } => {
1294                Self::emit_into(expr, ctx, ops);
1295                ops.push(Opcode::CastOp(*ty));
1296            }
1297
1298            Expr::Patch { .. } => {
1299                // Patch block: structural transform with COW writes, conditional
1300                // leaves, DELETE sentinel.  Emit opaque opcode; the VM delegates
1301                // to the tree-walker at runtime (patch is rare enough that the
1302                // opcode compile pays no dividend here).
1303                ops.push(Opcode::PatchEval(Arc::new(expr.clone())));
1304            }
1305
1306            Expr::DeleteMark => {
1307                // DELETE outside a patch-field value is a static error; the
1308                // tree-walker raises it at runtime, so emit a sentinel that
1309                // triggers the same path.
1310                ops.push(Opcode::PatchEval(Arc::new(Expr::DeleteMark)));
1311            }
1312        }
1313    }
1314
1315    fn emit_step(step: &Step, ctx: &VarCtx, ops: &mut Vec<Opcode>) {
1316        match step {
1317            Step::Field(name)    => ops.push(Opcode::GetField(Arc::from(name.as_str()))),
1318            Step::OptField(name) => ops.push(Opcode::OptField(Arc::from(name.as_str()))),
1319            Step::Descendant(n)  => ops.push(Opcode::Descendant(Arc::from(n.as_str()))),
1320            Step::DescendAll     => ops.push(Opcode::DescendAll),
1321            Step::Index(i)       => ops.push(Opcode::GetIndex(*i)),
1322            Step::DynIndex(e)    => ops.push(Opcode::DynIndex(Arc::new(Self::compile_sub(e, ctx)))),
1323            Step::Slice(a, b)    => ops.push(Opcode::GetSlice(*a, *b)),
1324            Step::Method(name, method_args) => {
1325                let call = Self::compile_call(name, method_args, ctx);
1326                ops.push(Opcode::CallMethod(Arc::new(call)));
1327            }
1328            Step::OptMethod(name, method_args) => {
1329                let call = Self::compile_call(name, method_args, ctx);
1330                ops.push(Opcode::CallOptMethod(Arc::new(call)));
1331            }
1332            Step::InlineFilter(pred) => {
1333                ops.push(Opcode::InlineFilter(Arc::new(Self::compile_sub(pred, ctx))));
1334            }
1335            Step::Quantifier(k) => ops.push(Opcode::Quantifier(*k)),
1336        }
1337    }
1338
1339    fn compile_call(name: &str, args: &[Arg], ctx: &VarCtx) -> CompiledCall {
1340        let method = BuiltinMethod::from_name(name);
1341        let sub_progs: Vec<Arc<Program>> = args.iter().map(|a| match a {
1342            Arg::Pos(e) | Arg::Named(_, e) => Arc::new(Self::compile_lambda_or_expr(e, ctx)),
1343        }).collect();
1344        CompiledCall {
1345            method,
1346            name: Arc::from(name),
1347            sub_progs: sub_progs.into(),
1348            orig_args: args.iter().cloned().collect::<Vec<_>>().into(),
1349        }
1350    }
1351
1352    /// Compile an argument expression; for lambdas, the lambda param becomes a
1353    /// known var in the inner context so `Ident(param)` emits `LoadIdent`.
1354    fn compile_lambda_or_expr(expr: &Expr, ctx: &VarCtx) -> Program {
1355        match expr {
1356            Expr::Lambda { params, body } => {
1357                let inner = ctx.with_vars(params);
1358                Self::compile_sub(body, &inner)
1359            }
1360            other => Self::compile_sub(other, ctx),
1361        }
1362    }
1363
1364    fn emit_binop(l: &Expr, op: BinOp, r: &Expr, ctx: &VarCtx, ops: &mut Vec<Opcode>) {
1365        match op {
1366            BinOp::And => {
1367                Self::emit_into(l, ctx, ops);
1368                let rhs_prog = Arc::new(Self::compile_sub(r, ctx));
1369                ops.push(Opcode::AndOp(rhs_prog));
1370            }
1371            BinOp::Or => {
1372                Self::emit_into(l, ctx, ops);
1373                let rhs_prog = Arc::new(Self::compile_sub(r, ctx));
1374                ops.push(Opcode::OrOp(rhs_prog));
1375            }
1376            BinOp::Add => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Add); }
1377            BinOp::Sub => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Sub); }
1378            BinOp::Mul => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Mul); }
1379            BinOp::Div => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Div); }
1380            BinOp::Mod => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Mod); }
1381            BinOp::Eq  => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Eq); }
1382            BinOp::Neq => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Neq); }
1383            BinOp::Lt  => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Lt); }
1384            BinOp::Lte => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Lte); }
1385            BinOp::Gt  => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Gt); }
1386            BinOp::Gte => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Gte); }
1387            BinOp::Fuzzy => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Fuzzy); }
1388        }
1389    }
1390
1391    fn emit_pipeline(base: &Expr, steps: &[PipeStep], ctx: &VarCtx, ops: &mut Vec<Opcode>) {
1392        Self::emit_into(base, ctx, ops);
1393        let mut cur_ctx = ctx.clone();
1394        for step in steps {
1395            match step {
1396                PipeStep::Forward(rhs) => {
1397                    Self::emit_pipe_forward(rhs, &cur_ctx, ops);
1398                }
1399                PipeStep::Bind(target) => {
1400                    Self::emit_bind(target, &mut cur_ctx, ops);
1401                }
1402            }
1403        }
1404    }
1405
1406    fn emit_pipe_forward(rhs: &Expr, ctx: &VarCtx, ops: &mut Vec<Opcode>) {
1407        match rhs {
1408            Expr::Ident(name) if !ctx.has(name) => {
1409                // No-arg method call on TOS
1410                let call = CompiledCall {
1411                    method:    BuiltinMethod::from_name(name),
1412                    name:      Arc::from(name.as_str()),
1413                    sub_progs: Arc::from(&[] as &[Arc<Program>]),
1414                    orig_args: Arc::from(&[] as &[Arg]),
1415                };
1416                ops.push(Opcode::CallMethod(Arc::new(call)));
1417            }
1418            Expr::Chain(base, steps) if !steps.is_empty() => {
1419                if let Expr::Ident(name) = base.as_ref() {
1420                    if !ctx.has(name) {
1421                        // method(args...) — base is method, steps are chained
1422                        let call = CompiledCall {
1423                            method:    BuiltinMethod::from_name(name),
1424                            name:      Arc::from(name.as_str()),
1425                            sub_progs: Arc::from(&[] as &[Arc<Program>]),
1426                            orig_args: Arc::from(&[] as &[Arg]),
1427                        };
1428                        ops.push(Opcode::CallMethod(Arc::new(call)));
1429                        for step in steps { Self::emit_step(step, ctx, ops); }
1430                        return;
1431                    }
1432                }
1433                ops.push(Opcode::SetCurrent);
1434                Self::emit_into(rhs, ctx, ops);
1435            }
1436            _ => {
1437                // Arbitrary expression; set current to pipe input, then eval
1438                ops.push(Opcode::SetCurrent);
1439                Self::emit_into(rhs, ctx, ops);
1440            }
1441        }
1442    }
1443
1444    fn emit_bind(target: &BindTarget, ctx: &mut VarCtx, ops: &mut Vec<Opcode>) {
1445        match target {
1446            BindTarget::Name(name) => {
1447                ops.push(Opcode::BindVar(Arc::from(name.as_str())));
1448                *ctx = ctx.with_var(name);
1449            }
1450            BindTarget::Obj { fields, rest } => {
1451                let spec = BindObjSpec {
1452                    fields: fields.iter().map(|f| Arc::from(f.as_str())).collect::<Vec<_>>().into(),
1453                    rest: rest.as_ref().map(|r| Arc::from(r.as_str())),
1454                };
1455                ops.push(Opcode::BindObjDestructure(Arc::new(spec)));
1456                for f in fields { *ctx = ctx.with_var(f); }
1457                if let Some(r) = rest { *ctx = ctx.with_var(r); }
1458            }
1459            BindTarget::Arr(names) => {
1460                let ns: Vec<Arc<str>> = names.iter().map(|n| Arc::from(n.as_str())).collect();
1461                ops.push(Opcode::BindArrDestructure(ns.into()));
1462                for n in names { *ctx = ctx.with_var(n); }
1463            }
1464        }
1465    }
1466
1467    fn compile_sub(expr: &Expr, ctx: &VarCtx) -> Program {
1468        let ops = Self::optimize(Self::emit(expr, ctx));
1469        Program::new(ops, "<sub>")
1470    }
1471
1472    fn compile_array_spread(_expr: &Expr, _ctx: &VarCtx) -> Program {
1473        // Not reached — handled in MakeArr execution
1474        Program::new(vec![], "<spread>")
1475    }
1476
1477    /// Compile a spread array element — wrapped with a special marker.
1478    fn compile_sub_spread(expr: &Expr, ctx: &VarCtx) -> Program {
1479        let mut ops = Self::emit(expr, ctx);
1480        // Prefix with a sentinel bool to mark this as a spread
1481        ops.insert(0, Opcode::PushBool(true));
1482        // Append a sentinel for reading: bool(true) + actual val
1483        // Actually use a dedicated approach: GetSlice-like marker
1484        // For simplicity, just compile the expr normally;
1485        // MakeArr handles spread by checking if the result is an array
1486        // when the corresponding ArrayElem is Spread.
1487        // Re-do: just compile normally, MakeArr knows which slots are spreads.
1488        Self::compile_sub(expr, ctx) // caller has separate spread tracking
1489    }
1490}
1491
1492// ── Path cache ────────────────────────────────────────────────────────────────
1493//
1494// Key: (doc_hash, json_pointer) → Val
1495//
1496// Doc-scoped (no program_id): any program resolving the same path on the same
1497// document gets a hit.  Intermediate nodes are cached so a prefix of a longer
1498// path can be reused without re-traversal.
1499
1500struct PathCache {
1501    /// doc_hash → (pointer_string → Val)
1502    docs:     HashMap<u64, HashMap<Arc<str>, Val>>,
1503    /// FIFO eviction order
1504    order:    VecDeque<(u64, Arc<str>)>,
1505    capacity: usize,
1506}
1507
1508impl PathCache {
1509    fn new(cap: usize) -> Self {
1510        Self {
1511            docs:     HashMap::new(),
1512            order:    VecDeque::with_capacity(cap),
1513            capacity: cap,
1514        }
1515    }
1516
1517    /// O(1) immutable lookup — returns cloned Val (Val::clone is O(1)).
1518    #[inline]
1519    fn get(&self, doc_hash: u64, ptr: &str) -> Option<Val> {
1520        self.docs.get(&doc_hash)?.get(ptr).cloned()
1521    }
1522
1523    /// O(1) existence check without cloning.
1524    #[inline]
1525    fn contains(&self, doc_hash: u64, ptr: &str) -> bool {
1526        self.docs.get(&doc_hash).map_or(false, |m| m.contains_key(ptr))
1527    }
1528
1529    fn insert(&mut self, doc_hash: u64, ptr: Arc<str>, val: Val) {
1530        if self.order.len() >= self.capacity {
1531            if let Some((old_hash, old_ptr)) = self.order.pop_front() {
1532                if let Some(inner) = self.docs.get_mut(&old_hash) {
1533                    inner.remove(old_ptr.as_ref());
1534                    if inner.is_empty() { self.docs.remove(&old_hash); }
1535                }
1536            }
1537        }
1538        self.order.push_back((doc_hash, ptr.clone()));
1539        self.docs.entry(doc_hash).or_insert_with(HashMap::new).insert(ptr, val);
1540    }
1541
1542    fn len(&self) -> usize { self.order.len() }
1543}
1544
1545// ── VM ────────────────────────────────────────────────────────────────────────
1546
1547/// High-performance v2 virtual machine.
1548///
1549/// Maintains:
1550/// - **Compile cache** — expression string → `Program` (parse + compile once).
1551/// - **Path cache** — `(doc_hash, json_pointer)` → `Val`; doc-scoped so any
1552///   program navigating the same path on the same document shares cached nodes.
1553///   Intermediate nodes are populated as a side-effect of every traversal,
1554///   enabling prefix reuse without re-traversal.
1555///
1556/// One VM per thread; wrap in `Mutex` for shared use.
1557/// Toggle each optimiser pass independently.  Default enables every
1558/// pass.  Disabling a pass invalidates the compile cache for the next
1559/// compilation by changing `hash()`.
1560#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1561pub struct PassConfig {
1562    pub root_chain:      bool,
1563    pub filter_count:    bool,
1564    pub filter_fusion:   bool,
1565    pub find_quantifier: bool,
1566    pub strength_reduce: bool,
1567    pub redundant_ops:   bool,
1568    pub kind_check_fold: bool,
1569    pub method_const:    bool,
1570    pub const_fold:      bool,
1571    pub nullness:        bool,
1572    pub equi_join:       bool,
1573    pub reorder_and:     bool,
1574    pub dedup_subprogs:  bool,
1575}
1576
1577impl Default for PassConfig {
1578    fn default() -> Self {
1579        Self {
1580            root_chain: true, filter_count: true, filter_fusion: true,
1581            find_quantifier: true, strength_reduce: true, redundant_ops: true,
1582            kind_check_fold: true, method_const: true, const_fold: true,
1583            nullness: true, equi_join: true,
1584            reorder_and: true, dedup_subprogs: true,
1585        }
1586    }
1587}
1588
1589impl PassConfig {
1590    /// Disable every pass — emit raw opcodes.
1591    pub fn none() -> Self {
1592        Self {
1593            root_chain: false, filter_count: false, filter_fusion: false,
1594            find_quantifier: false, strength_reduce: false, redundant_ops: false,
1595            kind_check_fold: false, method_const: false, const_fold: false,
1596            nullness: false, equi_join: false,
1597            reorder_and: false, dedup_subprogs: false,
1598        }
1599    }
1600
1601    pub fn hash(&self) -> u64 {
1602        let mut bits: u64 = 0;
1603        for (i, b) in [self.root_chain, self.filter_count, self.filter_fusion,
1604                       self.find_quantifier, self.strength_reduce, self.redundant_ops,
1605                       self.kind_check_fold, self.method_const, self.const_fold,
1606                       self.nullness, self.equi_join,
1607                       self.reorder_and, self.dedup_subprogs].iter().enumerate() {
1608            if *b { bits |= 1u64 << i; }
1609        }
1610        bits
1611    }
1612}
1613
1614pub struct VM {
1615    registry:      Arc<MethodRegistry>,
1616    /// Cache key = (pass_config_hash, expr_string).  Changing `config`
1617    /// invalidates prior entries automatically via key divergence.
1618    compile_cache: HashMap<(u64, String), Arc<Program>>,
1619    /// LRU ordering for `compile_cache`; front = least recently used.
1620    /// Entries are moved to back on hit; oldest evicted when over cap.
1621    compile_lru:   std::collections::VecDeque<(u64, String)>,
1622    compile_cap:   usize,
1623    path_cache:    PathCache,
1624    /// Hash of the document currently being executed — set once by `execute()`,
1625    /// reused by all recursive `exec()` calls within the same top-level call.
1626    doc_hash:      u64,
1627    /// Optimiser pass toggles.  Default: all on.
1628    config:        PassConfig,
1629}
1630
1631impl Default for VM {
1632    fn default() -> Self { Self::new() }
1633}
1634
1635impl VM {
1636    pub fn new() -> Self { Self::with_capacity(512, 4096) }
1637
1638    pub fn with_capacity(compile_cap: usize, path_cap: usize) -> Self {
1639        Self {
1640            registry:      Arc::new(MethodRegistry::new()),
1641            compile_cache: HashMap::with_capacity(compile_cap),
1642            compile_lru:   std::collections::VecDeque::with_capacity(compile_cap),
1643            compile_cap,
1644            path_cache:    PathCache::new(path_cap),
1645            doc_hash:      0,
1646            config:        PassConfig::default(),
1647        }
1648    }
1649
1650    /// Build a VM that shares an existing method registry.
1651    pub fn with_registry(registry: Arc<MethodRegistry>) -> Self {
1652        let mut vm = Self::new();
1653        vm.registry = registry;
1654        vm
1655    }
1656
1657    /// Register a method already wrapped in `Arc`.
1658    pub fn register_arc(&mut self, name: &str, method: Arc<dyn crate::eval::methods::Method>) {
1659        Arc::make_mut(&mut self.registry).register_arc(name, method);
1660    }
1661
1662    /// Replace the pass configuration.  The compile cache is not purged,
1663    /// but future lookups key off the new config hash so old entries
1664    /// are effectively invalidated for the new regime.
1665    pub fn set_pass_config(&mut self, config: PassConfig) { self.config = config; }
1666
1667    pub fn pass_config(&self) -> PassConfig { self.config }
1668
1669    /// Register a custom method (callable via `.method_name(...)` in expressions).
1670    pub fn register(&mut self, name: impl Into<String>, method: impl super::eval::methods::Method + 'static) {
1671        Arc::make_mut(&mut self.registry).register(name, method);
1672    }
1673
1674    // ── Public entry-points ───────────────────────────────────────────────────
1675
1676    /// Parse, compile (cached), and execute `expr` against `doc`.
1677    pub fn run_str(&mut self, expr: &str, doc: &serde_json::Value) -> Result<serde_json::Value, EvalError> {
1678        let prog = self.get_or_compile(expr)?;
1679        self.execute(&prog, doc)
1680    }
1681
1682    /// Execute a pre-compiled `Program` against `doc`.
1683    pub fn execute(&mut self, program: &Program, doc: &serde_json::Value) -> Result<serde_json::Value, EvalError> {
1684        let root = Val::from(doc);
1685        // Compute doc hash once; reused by all exec() calls in this invocation.
1686        self.doc_hash = hash_val_structure(&root);
1687        let env = self.make_env(root);
1688        let result = self.exec(program, &env)?;
1689        Ok(result.into())
1690    }
1691
1692    /// Execute a compiled program against a document, first specialising
1693    /// against the given shape (turns `OptField` → `GetField` where safe,
1694    /// folds `KindCheck` where type is known, etc.).
1695    pub fn execute_with_schema(
1696        &mut self,
1697        program: &Program,
1698        doc: &serde_json::Value,
1699        shape: &super::schema::Shape,
1700    ) -> Result<serde_json::Value, EvalError> {
1701        let specialized = super::schema::specialize(program, shape);
1702        self.execute(&specialized, doc)
1703    }
1704
1705    /// Execute a program; infer the shape from the document itself.  Costs
1706    /// an O(doc) shape walk before execution; useful when the same
1707    /// compiled program is reused across many docs with similar shapes.
1708    pub fn execute_with_inferred_schema(
1709        &mut self,
1710        program: &Program,
1711        doc: &serde_json::Value,
1712    ) -> Result<serde_json::Value, EvalError> {
1713        let shape = super::schema::Shape::of(doc);
1714        self.execute_with_schema(program, doc, &shape)
1715    }
1716
1717    /// Get or compile an expression string (compile cache).
1718    /// Cache key is (pass_config_hash, expr) so that different pass
1719    /// configurations yield different compiled programs.
1720    pub fn get_or_compile(&mut self, expr: &str) -> Result<Arc<Program>, EvalError> {
1721        let key = (self.config.hash(), expr.to_string());
1722        if let Some(p) = self.compile_cache.get(&key) {
1723            let arc = Arc::clone(p);
1724            self.touch_lru(&key);
1725            return Ok(arc);
1726        }
1727        let prog = Compiler::compile_str_with_config(expr, self.config)?;
1728        let arc = Arc::new(prog);
1729        self.insert_compile(key, Arc::clone(&arc));
1730        Ok(arc)
1731    }
1732
1733    /// Move `key` to the back of the LRU queue (most recently used).
1734    fn touch_lru(&mut self, key: &(u64, String)) {
1735        if let Some(pos) = self.compile_lru.iter().position(|k| k == key) {
1736            let k = self.compile_lru.remove(pos).unwrap();
1737            self.compile_lru.push_back(k);
1738        }
1739    }
1740
1741    /// Insert into compile cache with LRU eviction at `compile_cap`.
1742    fn insert_compile(&mut self, key: (u64, String), prog: Arc<Program>) {
1743        while self.compile_cache.len() >= self.compile_cap && self.compile_cap > 0 {
1744            if let Some(old) = self.compile_lru.pop_front() {
1745                self.compile_cache.remove(&old);
1746            } else {
1747                break;
1748            }
1749        }
1750        self.compile_lru.push_back(key.clone());
1751        self.compile_cache.insert(key, prog);
1752    }
1753
1754    /// Cache statistics: `(compile_entries, path_entries)`.
1755    pub fn cache_stats(&self) -> (usize, usize) {
1756        (self.compile_cache.len(), self.path_cache.len())
1757    }
1758
1759    // ── Internal helpers ──────────────────────────────────────────────────────
1760
1761    fn make_env(&self, root: Val) -> Env {
1762        Env::new_with_registry(root, Arc::clone(&self.registry))
1763    }
1764
1765
1766    // ── Core execution loop ───────────────────────────────────────────────────
1767
1768    /// Execute `program` in environment `env`, returning the top-of-stack value.
1769    pub fn exec(&mut self, program: &Program, env: &Env) -> Result<Val, EvalError> {
1770        let mut stack: SmallVec<[Val; 16]> = SmallVec::new();
1771
1772        for op in program.ops.iter() {
1773            match op {
1774                // ── Literals ──────────────────────────────────────────────────
1775                Opcode::PushNull        => stack.push(Val::Null),
1776                Opcode::PushBool(b)     => stack.push(Val::Bool(*b)),
1777                Opcode::PushInt(n)      => stack.push(Val::Int(*n)),
1778                Opcode::PushFloat(f)    => stack.push(Val::Float(*f)),
1779                Opcode::PushStr(s)      => stack.push(Val::Str(s.clone())),
1780
1781                // ── Context ───────────────────────────────────────────────────
1782                Opcode::PushRoot        => stack.push(env.root.clone()),
1783                Opcode::PushCurrent     => stack.push(env.current.clone()),
1784
1785                // ── Navigation ────────────────────────────────────────────────
1786                Opcode::GetField(k) => {
1787                    let v = pop!(stack);
1788                    stack.push(v.get_field(k.as_ref()));
1789                }
1790                Opcode::GetIndex(i) => {
1791                    let v = pop!(stack);
1792                    stack.push(v.get_index(*i));
1793                }
1794                Opcode::DynIndex(prog) => {
1795                    let v = pop!(stack);
1796                    let key = self.exec(prog, env)?;
1797                    stack.push(match key {
1798                        Val::Int(i) => v.get_index(i),
1799                        Val::Str(s) => v.get_field(s.as_ref()),
1800                        _ => Val::Null,
1801                    });
1802                }
1803                Opcode::GetSlice(from, to) => {
1804                    let v = pop!(stack);
1805                    stack.push(exec_slice(v, *from, *to));
1806                }
1807                Opcode::OptField(k) => {
1808                    let v = pop!(stack);
1809                    stack.push(if v.is_null() { Val::Null } else { v.get_field(k.as_ref()) });
1810                }
1811                Opcode::Descendant(k) => {
1812                    let v = pop!(stack);
1813                    let mut found = Vec::new();
1814                    // (D) When descending from root, track pointer paths and
1815                    // cache each discovered node for future RootChain lookups.
1816                    let from_root = match (&v, &env.root) {
1817                        (Val::Obj(a), Val::Obj(b)) => Arc::ptr_eq(a, b),
1818                        (Val::Arr(a), Val::Arr(b)) => Arc::ptr_eq(a, b),
1819                        _ => matches!((&v, &env.root), (Val::Null, Val::Null)),
1820                    };
1821                    if from_root {
1822                        let mut prefix = String::new();
1823                        let mut cached: Vec<(Arc<str>, Val)> = Vec::new();
1824                        collect_desc_with_paths(&v, k.as_ref(), &mut prefix, &mut found, &mut cached);
1825                        let doc_hash = self.doc_hash;
1826                        for (ptr, val) in cached {
1827                            self.path_cache.insert(doc_hash, ptr, val);
1828                        }
1829                    } else {
1830                        collect_desc(&v, k.as_ref(), &mut found);
1831                    }
1832                    stack.push(Val::arr(found));
1833                }
1834                Opcode::DescendAll => {
1835                    let v = pop!(stack);
1836                    let mut found = Vec::new();
1837                    collect_all(&v, &mut found);
1838                    stack.push(Val::arr(found));
1839                }
1840                Opcode::InlineFilter(pred) => {
1841                    let val = pop!(stack);
1842                    let items = match val {
1843                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
1844                        other => vec![other],
1845                    };
1846                    let mut out = Vec::new();
1847                    for item in items {
1848                        let ie = env.with_current(item.clone());
1849                        if is_truthy(&self.exec(pred, &ie)?) { out.push(item); }
1850                    }
1851                    stack.push(Val::arr(out));
1852                }
1853                Opcode::Quantifier(kind) => {
1854                    let val = pop!(stack);
1855                    stack.push(match kind {
1856                        QuantifierKind::First => match val {
1857                            Val::Arr(a) => a.first().cloned().unwrap_or(Val::Null),
1858                            other => other,
1859                        },
1860                        QuantifierKind::One => match val {
1861                            Val::Arr(a) if a.len() == 1 => a[0].clone(),
1862                            Val::Arr(a) => return err!("quantifier !: expected exactly one element, got {}", a.len()),
1863                            other => other,
1864                        },
1865                    });
1866                }
1867
1868                // ── Peephole fusions ──────────────────────────────────────────
1869                Opcode::RootChain(chain) => {
1870                    let doc_hash = self.doc_hash;
1871
1872                    // (B) Find deepest cached prefix — immutable scan.
1873                    let mut start = 0usize;
1874                    {
1875                        let mut ptr = String::new();
1876                        for (i, k) in chain.iter().enumerate() {
1877                            ptr.push('/');
1878                            ptr.push_str(k.as_ref());
1879                            if self.path_cache.contains(doc_hash, &ptr) {
1880                                start = i + 1;
1881                            } else {
1882                                break;
1883                            }
1884                        }
1885                    }
1886
1887                    // Retrieve cached starting node (O(1) Val clone).
1888                    let mut current = if start > 0 {
1889                        let prefix_ptr: String = chain[..start].iter()
1890                            .fold(String::new(), |mut s, k| { s.push('/'); s.push_str(k.as_ref()); s });
1891                        self.path_cache.get(doc_hash, &prefix_ptr)
1892                            .unwrap_or_else(|| env.root.clone())
1893                    } else {
1894                        env.root.clone()
1895                    };
1896
1897                    // (A) Traverse uncached suffix, caching each new node.
1898                    let mut ptr: String = chain[..start].iter()
1899                        .fold(String::new(), |mut s, k| { s.push('/'); s.push_str(k.as_ref()); s });
1900                    for k in chain[start..].iter() {
1901                        current = current.get_field(k.as_ref());
1902                        ptr.push('/');
1903                        ptr.push_str(k.as_ref());
1904                        self.path_cache.insert(doc_hash, Arc::from(ptr.as_str()), current.clone());
1905                    }
1906                    stack.push(current);
1907                }
1908                Opcode::FilterCount(pred) => {
1909                    let recv = pop!(stack);
1910                    let n = if let Val::Arr(a) = &recv {
1911                        let mut count = 0u64;
1912                        let mut scratch = env.clone();
1913                        for item in a.iter() {
1914                            let prev = scratch.swap_current(item.clone());
1915                            let t = is_truthy(&self.exec(pred, &scratch)?);
1916                            scratch.restore_current(prev);
1917                            if t { count += 1; }
1918                        }
1919                        count
1920                    } else { 0 };
1921                    stack.push(Val::Int(n as i64));
1922                }
1923                Opcode::FindFirst(pred) => {
1924                    let recv = pop!(stack);
1925                    let mut found = Val::Null;
1926                    if let Val::Arr(a) = &recv {
1927                        let mut scratch = env.clone();
1928                        for item in a.iter() {
1929                            let prev = scratch.swap_current(item.clone());
1930                            let t = is_truthy(&self.exec(pred, &scratch)?);
1931                            scratch.restore_current(prev);
1932                            if t { found = item.clone(); break; }
1933                        }
1934                    } else if !recv.is_null() {
1935                        let sub_env = env.with_current(recv.clone());
1936                        if is_truthy(&self.exec(pred, &sub_env)?) { found = recv; }
1937                    }
1938                    stack.push(found);
1939                }
1940                Opcode::FilterMap { pred, map } => {
1941                    let recv = pop!(stack);
1942                    if let Val::Arr(a) = recv {
1943                        let mut out = Vec::with_capacity(a.len());
1944                        let mut scratch = env.clone();
1945                        for item in a.iter() {
1946                            let prev = scratch.swap_current(item.clone());
1947                            if is_truthy(&self.exec(pred, &scratch)?) {
1948                                out.push(self.exec(map, &scratch)?);
1949                            }
1950                            scratch.restore_current(prev);
1951                        }
1952                        stack.push(Val::arr(out));
1953                    } else {
1954                        stack.push(Val::arr(Vec::new()));
1955                    }
1956                }
1957                Opcode::FilterFilter { p1, p2 } => {
1958                    let recv = pop!(stack);
1959                    if let Val::Arr(a) = recv {
1960                        let mut out = Vec::with_capacity(a.len());
1961                        let mut scratch = env.clone();
1962                        for item in a.iter() {
1963                            let prev = scratch.swap_current(item.clone());
1964                            let keep = is_truthy(&self.exec(p1, &scratch)?)
1965                                    && is_truthy(&self.exec(p2, &scratch)?);
1966                            scratch.restore_current(prev);
1967                            if keep { out.push(item.clone()); }
1968                        }
1969                        stack.push(Val::arr(out));
1970                    } else {
1971                        stack.push(Val::arr(Vec::new()));
1972                    }
1973                }
1974                Opcode::MapSum(f) => {
1975                    let recv = pop!(stack);
1976                    let mut acc_i: i64 = 0;
1977                    let mut acc_f: f64 = 0.0;
1978                    let mut is_float = false;
1979                    if let Val::Arr(a) = &recv {
1980                        let mut scratch = env.clone();
1981                        for item in a.iter() {
1982                            let prev = scratch.swap_current(item.clone());
1983                            let v = self.exec(f, &scratch)?;
1984                            scratch.restore_current(prev);
1985                            match v {
1986                                Val::Int(n) => {
1987                                    if is_float { acc_f += n as f64; } else { acc_i += n; }
1988                                }
1989                                Val::Float(x) => {
1990                                    if !is_float { acc_f = acc_i as f64; is_float = true; }
1991                                    acc_f += x;
1992                                }
1993                                Val::Null => {}
1994                                _ => return err!("map(..).sum(): non-numeric mapped value"),
1995                            }
1996                        }
1997                    }
1998                    stack.push(if is_float { Val::Float(acc_f) } else { Val::Int(acc_i) });
1999                }
2000                Opcode::MapAvg(f) => {
2001                    let recv = pop!(stack);
2002                    let mut sum: f64 = 0.0;
2003                    let mut n: usize = 0;
2004                    if let Val::Arr(a) = &recv {
2005                        let mut scratch = env.clone();
2006                        for item in a.iter() {
2007                            let prev = scratch.swap_current(item.clone());
2008                            let v = self.exec(f, &scratch)?;
2009                            scratch.restore_current(prev);
2010                            match v {
2011                                Val::Int(x)   => { sum += x as f64; n += 1; }
2012                                Val::Float(x) => { sum += x;        n += 1; }
2013                                Val::Null => {}
2014                                _ => return err!("map(..).avg(): non-numeric mapped value"),
2015                            }
2016                        }
2017                    }
2018                    stack.push(if n == 0 { Val::Null } else { Val::Float(sum / n as f64) });
2019                }
2020                Opcode::MapFlatten(f) => {
2021                    let recv = pop!(stack);
2022                    let items = match recv {
2023                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
2024                        _ => Vec::new(),
2025                    };
2026                    let mut out = Vec::new();
2027                    for item in items {
2028                        let sub_env = env.with_current(item);
2029                        let mapped = self.exec(f, &sub_env)?;
2030                        match mapped {
2031                            Val::Arr(a) => {
2032                                let v = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
2033                                out.extend(v);
2034                            }
2035                            other => out.push(other),
2036                        }
2037                    }
2038                    stack.push(Val::arr(out));
2039                }
2040                Opcode::FilterTakeWhile { pred, stop } => {
2041                    let recv = pop!(stack);
2042                    let items = match recv {
2043                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
2044                        _ => Vec::new(),
2045                    };
2046                    let mut out = Vec::new();
2047                    for item in items {
2048                        let sub_env = env.with_current(item.clone());
2049                        if !is_truthy(&self.exec(pred, &sub_env)?) { continue; }
2050                        if !is_truthy(&self.exec(stop, &sub_env)?) { break; }
2051                        out.push(item);
2052                    }
2053                    stack.push(Val::arr(out));
2054                }
2055                Opcode::FilterDropWhile { pred, drop } => {
2056                    let recv = pop!(stack);
2057                    let items = match recv {
2058                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
2059                        _ => Vec::new(),
2060                    };
2061                    let mut out = Vec::new();
2062                    let mut dropping = true;
2063                    for item in items {
2064                        let sub_env = env.with_current(item.clone());
2065                        if !is_truthy(&self.exec(pred, &sub_env)?) { continue; }
2066                        if dropping {
2067                            if is_truthy(&self.exec(drop, &sub_env)?) { continue; }
2068                            dropping = false;
2069                        }
2070                        out.push(item);
2071                    }
2072                    stack.push(Val::arr(out));
2073                }
2074                Opcode::EquiJoin { rhs, lhs_key, rhs_key } => {
2075                    use std::collections::HashMap;
2076                    let left_val = pop!(stack);
2077                    let right_val = self.exec(rhs, env)?;
2078                    let left = match left_val {
2079                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
2080                        _ => Vec::new(),
2081                    };
2082                    let right = match right_val {
2083                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
2084                        _ => Vec::new(),
2085                    };
2086                    let mut idx: HashMap<String, Vec<Val>> = HashMap::new();
2087                    for r in right {
2088                        let key = match &r {
2089                            Val::Obj(o) => o.get(rhs_key.as_ref()).map(super::eval::util::val_to_key),
2090                            _ => None,
2091                        };
2092                        if let Some(k) = key { idx.entry(k).or_default().push(r); }
2093                    }
2094                    let mut out = Vec::new();
2095                    for l in left {
2096                        let key = match &l {
2097                            Val::Obj(o) => o.get(lhs_key.as_ref()).map(super::eval::util::val_to_key),
2098                            _ => None,
2099                        };
2100                        let Some(k) = key else { continue };
2101                        let Some(matches) = idx.get(&k) else { continue };
2102                        for r in matches {
2103                            match (&l, r) {
2104                                (Val::Obj(lo), Val::Obj(ro)) => {
2105                                    let mut m = (**lo).clone();
2106                                    for (k, v) in ro.iter() { m.insert(k.clone(), v.clone()); }
2107                                    out.push(Val::obj(m));
2108                                }
2109                                _ => out.push(l.clone()),
2110                            }
2111                        }
2112                    }
2113                    stack.push(Val::arr(out));
2114                }
2115                Opcode::MapUnique(f) => {
2116                    let recv = pop!(stack);
2117                    let items = match recv {
2118                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
2119                        _ => Vec::new(),
2120                    };
2121                    let mut seen: Vec<Val> = Vec::new();
2122                    let mut out = Vec::new();
2123                    for item in items {
2124                        let sub_env = env.with_current(item);
2125                        let mapped = self.exec(f, &sub_env)?;
2126                        if !seen.iter().any(|s| super::eval::util::cmp_vals(s, &mapped)
2127                                                 == std::cmp::Ordering::Equal) {
2128                            seen.push(mapped.clone());
2129                            out.push(mapped);
2130                        }
2131                    }
2132                    stack.push(Val::arr(out));
2133                }
2134                Opcode::TopN { n, asc } => {
2135                    use std::collections::BinaryHeap;
2136                    use std::cmp::Reverse;
2137                    let recv = pop!(stack);
2138                    let items = match recv {
2139                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
2140                        _ => Vec::new(),
2141                    };
2142                    if *n >= items.len() {
2143                        let mut v = items;
2144                        v.sort_by(|x, y| super::eval::util::cmp_vals(x, y));
2145                        if !*asc { v.reverse(); }
2146                        stack.push(Val::arr(v));
2147                    } else if *asc {
2148                        // Max-heap of size n; pop largest to keep smallest n.
2149                        let mut heap: BinaryHeap<WrapVal> = BinaryHeap::with_capacity(*n);
2150                        for item in items {
2151                            if heap.len() < *n {
2152                                heap.push(WrapVal(item));
2153                            } else if super::eval::util::cmp_vals(&item, &heap.peek().unwrap().0)
2154                                      == std::cmp::Ordering::Less {
2155                                heap.pop();
2156                                heap.push(WrapVal(item));
2157                            }
2158                        }
2159                        let mut v: Vec<Val> = heap.into_iter().map(|w| w.0).collect();
2160                        v.sort_by(|x, y| super::eval::util::cmp_vals(x, y));
2161                        stack.push(Val::arr(v));
2162                    } else {
2163                        // Min-heap via Reverse; keep largest n.
2164                        let mut heap: BinaryHeap<Reverse<WrapVal>> = BinaryHeap::with_capacity(*n);
2165                        for item in items {
2166                            if heap.len() < *n {
2167                                heap.push(Reverse(WrapVal(item)));
2168                            } else if super::eval::util::cmp_vals(&item, &heap.peek().unwrap().0.0)
2169                                      == std::cmp::Ordering::Greater {
2170                                heap.pop();
2171                                heap.push(Reverse(WrapVal(item)));
2172                            }
2173                        }
2174                        let mut v: Vec<Val> = heap.into_iter().map(|w| w.0.0).collect();
2175                        v.sort_by(|x, y| super::eval::util::cmp_vals(y, x));
2176                        stack.push(Val::arr(v));
2177                    }
2178                }
2179                Opcode::MapMap { f1, f2 } => {
2180                    let recv = pop!(stack);
2181                    if let Val::Arr(a) = recv {
2182                        let mut out = Vec::with_capacity(a.len());
2183                        let mut scratch = env.clone();
2184                        for item in a.iter() {
2185                            let prev = scratch.swap_current(item.clone());
2186                            let mid = self.exec(f1, &scratch)?;
2187                            scratch.swap_current(mid);
2188                            let res = self.exec(f2, &scratch)?;
2189                            scratch.restore_current(prev);
2190                            out.push(res);
2191                        }
2192                        stack.push(Val::arr(out));
2193                    } else {
2194                        stack.push(Val::arr(Vec::new()));
2195                    }
2196                }
2197                Opcode::FindOne(pred) => {
2198                    let recv = pop!(stack);
2199                    let mut found: Option<Val> = None;
2200                    if let Val::Arr(a) = &recv {
2201                        for item in a.iter() {
2202                            let sub_env = env.with_current(item.clone());
2203                            if is_truthy(&self.exec(pred, &sub_env)?) {
2204                                if found.is_some() {
2205                                    return err!("quantifier !: expected exactly one match, found multiple");
2206                                }
2207                                found = Some(item.clone());
2208                            }
2209                        }
2210                    } else if !recv.is_null() {
2211                        let sub_env = env.with_current(recv.clone());
2212                        if is_truthy(&self.exec(pred, &sub_env)?) { found = Some(recv); }
2213                    }
2214                    match found {
2215                        Some(v) => stack.push(v),
2216                        None => return err!("quantifier !: expected exactly one match, found none"),
2217                    }
2218                }
2219
2220                // ── Ident ─────────────────────────────────────────────────────
2221                Opcode::LoadIdent(name) => {
2222                    let v = if let Some(v) = env.get_var(name.as_ref()) {
2223                        v.clone()
2224                    } else {
2225                        env.current.get_field(name.as_ref())
2226                    };
2227                    stack.push(v);
2228                }
2229
2230                // ── Operators ─────────────────────────────────────────────────
2231                Opcode::Add  => { let r = pop!(stack); let l = pop!(stack); stack.push(add_vals(l, r)?); }
2232                Opcode::Sub  => { let r = pop!(stack); let l = pop!(stack); stack.push(num_op(l, r, |a,b|a-b, |a,b|a-b)?); }
2233                Opcode::Mul  => { let r = pop!(stack); let l = pop!(stack); stack.push(num_op(l, r, |a,b|a*b, |a,b|a*b)?); }
2234                Opcode::Div  => {
2235                    let r = pop!(stack); let l = pop!(stack);
2236                    let b = r.as_f64().unwrap_or(0.0);
2237                    if b == 0.0 { return err!("division by zero"); }
2238                    stack.push(Val::Float(l.as_f64().unwrap_or(0.0) / b));
2239                }
2240                Opcode::Mod  => { let r = pop!(stack); let l = pop!(stack); stack.push(num_op(l, r, |a,b|a%b, |a,b|a%b)?); }
2241                Opcode::Eq   => { let r = pop!(stack); let l = pop!(stack); stack.push(Val::Bool(vals_eq(&l,&r))); }
2242                Opcode::Neq  => { let r = pop!(stack); let l = pop!(stack); stack.push(Val::Bool(!vals_eq(&l,&r))); }
2243                Opcode::Lt   => { let r = pop!(stack); let l = pop!(stack); stack.push(Val::Bool(cmp_vals(&l,&r) == std::cmp::Ordering::Less)); }
2244                Opcode::Lte  => { let r = pop!(stack); let l = pop!(stack); stack.push(Val::Bool(cmp_vals(&l,&r) != std::cmp::Ordering::Greater)); }
2245                Opcode::Gt   => { let r = pop!(stack); let l = pop!(stack); stack.push(Val::Bool(cmp_vals(&l,&r) == std::cmp::Ordering::Greater)); }
2246                Opcode::Gte  => { let r = pop!(stack); let l = pop!(stack); stack.push(Val::Bool(cmp_vals(&l,&r) != std::cmp::Ordering::Less)); }
2247                Opcode::Fuzzy => {
2248                    let r = pop!(stack); let l = pop!(stack);
2249                    let ls = match &l { Val::Str(s) => s.to_lowercase(), _ => val_to_string(&l).to_lowercase() };
2250                    let rs = match &r { Val::Str(s) => s.to_lowercase(), _ => val_to_string(&r).to_lowercase() };
2251                    stack.push(Val::Bool(ls.contains(&rs) || rs.contains(&ls)));
2252                }
2253                Opcode::Not  => { let v = pop!(stack); stack.push(Val::Bool(!is_truthy(&v))); }
2254                Opcode::Neg  => {
2255                    let v = pop!(stack);
2256                    stack.push(match v {
2257                        Val::Int(n) => Val::Int(-n),
2258                        Val::Float(f) => Val::Float(-f),
2259                        _ => return err!("unary minus requires a number"),
2260                    });
2261                }
2262                Opcode::CastOp(ty) => {
2263                    let v = pop!(stack);
2264                    stack.push(exec_cast(&v, *ty)?);
2265                }
2266
2267                // ── Short-circuit ops ─────────────────────────────────────────
2268                Opcode::AndOp(rhs) => {
2269                    let lv = pop!(stack);
2270                    if !is_truthy(&lv) {
2271                        stack.push(Val::Bool(false));
2272                    } else {
2273                        let rv = self.exec(rhs, env)?;
2274                        stack.push(Val::Bool(is_truthy(&rv)));
2275                    }
2276                }
2277                Opcode::OrOp(rhs) => {
2278                    let lv = pop!(stack);
2279                    if is_truthy(&lv) {
2280                        stack.push(lv);
2281                    } else {
2282                        stack.push(self.exec(rhs, env)?);
2283                    }
2284                }
2285                Opcode::CoalesceOp(rhs) => {
2286                    let lv = pop!(stack);
2287                    if !lv.is_null() {
2288                        stack.push(lv);
2289                    } else {
2290                        stack.push(self.exec(rhs, env)?);
2291                    }
2292                }
2293
2294                // ── Method calls ──────────────────────────────────────────────
2295                Opcode::CallMethod(call) => {
2296                    let recv = pop!(stack);
2297                    let result = self.exec_call(recv, call, env)?;
2298                    stack.push(result);
2299                }
2300                Opcode::CallOptMethod(call) => {
2301                    let recv = pop!(stack);
2302                    if recv.is_null() {
2303                        stack.push(Val::Null);
2304                    } else {
2305                        stack.push(self.exec_call(recv, call, env)?);
2306                    }
2307                }
2308
2309                // ── Construction ──────────────────────────────────────────────
2310                Opcode::MakeObj(entries) => {
2311                    let entries = Arc::clone(entries);
2312                    let result = self.exec_make_obj(&entries, env)?;
2313                    stack.push(result);
2314                }
2315                Opcode::MakeArr(progs) => {
2316                    let progs = Arc::clone(progs);
2317                    let mut out = Vec::with_capacity(progs.len());
2318                    for p in progs.iter() {
2319                        let v = self.exec(p, env)?;
2320                        // If the program produces an array from a spread,
2321                        // check if it was tagged; for simplicity, just push.
2322                        out.push(v);
2323                    }
2324                    stack.push(Val::arr(out));
2325                }
2326
2327                // ── F-string ──────────────────────────────────────────────────
2328                Opcode::FString(parts) => {
2329                    let parts = Arc::clone(parts);
2330                    let result = self.exec_fstring(&parts, env)?;
2331                    stack.push(result);
2332                }
2333
2334                // ── Kind check ────────────────────────────────────────────────
2335                Opcode::KindCheck { ty, negate } => {
2336                    let v = pop!(stack);
2337                    let m = kind_matches(&v, *ty);
2338                    stack.push(Val::Bool(if *negate { !m } else { m }));
2339                }
2340
2341                // ── Pipeline helpers ──────────────────────────────────────────
2342                Opcode::SetCurrent => {
2343                    // This is emitted before an arbitrary pipe-forward expression.
2344                    // The value flowing through the pipe is now on the stack as TOS.
2345                    // However we can't mutate env here since env is immutable.
2346                    // The actual "set current" happens by the caller preparing a new env.
2347                    // In practice, SetCurrent should not appear in isolation in the
2348                    // flat opcode stream because pipeline steps that need SetCurrent
2349                    // are compiled as sub-programs. Skip.
2350                }
2351                Opcode::BindVar(name) => {
2352                    // TOS becomes a named var; TOS remains (pass-through for ->) .
2353                    // We can't mutate env here. This is handled at the pipeline level.
2354                    // For now, just keep TOS.
2355                    let _ = name;
2356                }
2357                Opcode::StoreVar(name) => {
2358                    // Pop and discard (the LetExpr opcode handles binding properly).
2359                    let _ = name;
2360                    pop!(stack);
2361                }
2362                Opcode::BindObjDestructure(_) | Opcode::BindArrDestructure(_) => {
2363                    // Pipeline bind destructure — handled at pipeline level.
2364                }
2365
2366                // ── Complex recursive ops ─────────────────────────────────────
2367                Opcode::LetExpr { name, body } => {
2368                    let init_val = pop!(stack);
2369                    let body_env = env.with_var(name.as_ref(), init_val);
2370                    stack.push(self.exec(body, &body_env)?);
2371                }
2372
2373                Opcode::ListComp(spec) => {
2374                    let items = self.exec_iter_vals(&spec.iter, env)?;
2375                    let mut out = Vec::new();
2376                    for item in items {
2377                        let ie = bind_comp_vars(env, &spec.vars, item);
2378                        if let Some(cond) = &spec.cond {
2379                            if !is_truthy(&self.exec(cond, &ie)?) { continue; }
2380                        }
2381                        out.push(self.exec(&spec.expr, &ie)?);
2382                    }
2383                    stack.push(Val::arr(out));
2384                }
2385
2386                Opcode::DictComp(spec) => {
2387                    let items = self.exec_iter_vals(&spec.iter, env)?;
2388                    let mut map: IndexMap<Arc<str>, Val> = IndexMap::new();
2389                    for item in items {
2390                        let ie = bind_comp_vars(env, &spec.vars, item);
2391                        if let Some(cond) = &spec.cond {
2392                            if !is_truthy(&self.exec(cond, &ie)?) { continue; }
2393                        }
2394                        let k: Arc<str> = Arc::from(val_to_key(&self.exec(&spec.key, &ie)?).as_str());
2395                        let v = self.exec(&spec.val, &ie)?;
2396                        map.insert(k, v);
2397                    }
2398                    stack.push(Val::obj(map));
2399                }
2400
2401                Opcode::SetComp(spec) => {
2402                    let items = self.exec_iter_vals(&spec.iter, env)?;
2403                    let mut seen: Vec<String> = Vec::new();
2404                    let mut out = Vec::new();
2405                    for item in items {
2406                        let ie = bind_comp_vars(env, &spec.vars, item);
2407                        if let Some(cond) = &spec.cond {
2408                            if !is_truthy(&self.exec(cond, &ie)?) { continue; }
2409                        }
2410                        let v = self.exec(&spec.expr, &ie)?;
2411                        let k = val_to_key(&v);
2412                        if !seen.contains(&k) { seen.push(k); out.push(v); }
2413                    }
2414                    stack.push(Val::arr(out));
2415                }
2416
2417                // ── Path cache lookup ─────────────────────────────────────────
2418                Opcode::GetPointer(ptr) => {
2419                    let doc_hash = self.doc_hash;
2420                    let v = if let Some(cached) = self.path_cache.get(doc_hash, ptr.as_ref()) {
2421                        cached
2422                    } else {
2423                        let v = resolve_pointer(&env.root, ptr.as_ref());
2424                        self.path_cache.insert(doc_hash, ptr.clone(), v.clone());
2425                        v
2426                    };
2427                    stack.push(v);
2428                }
2429
2430                // ── Patch block ───────────────────────────────────────────────
2431                Opcode::PatchEval(e) => {
2432                    stack.push(eval(e, env)?);
2433                }
2434            }
2435        }
2436
2437        stack.pop().ok_or_else(|| EvalError("program produced no value".into()))
2438    }
2439
2440    // ── Pipeline execution (recursive, env-cloning) ────────────────────────────
2441
2442    /// Execute a pipeline by compiling pipeline steps as sub-programs.
2443    /// This is called when a `Pipeline` expr is compiled into a sequence
2444    /// of SetCurrent + sub-program opcodes.
2445    fn exec_pipeline(&mut self, base: &Program, steps: &[PipeStep], orig_ctx: &VarCtx, env: &Env)
2446        -> Result<Val, EvalError>
2447    {
2448        let mut current = self.exec(base, env)?;
2449        let mut cur_env = env.clone();
2450        for step in steps {
2451            match step {
2452                PipeStep::Forward(rhs) => {
2453                    current = self.exec_pipe_forward(&current, rhs, orig_ctx, &cur_env)?;
2454                }
2455                PipeStep::Bind(target) => {
2456                    cur_env = apply_bind_to_env(target, &current, cur_env)?;
2457                }
2458            }
2459        }
2460        Ok(current)
2461    }
2462
2463    fn exec_pipe_forward(&mut self, left: &Val, rhs: &Expr, ctx: &VarCtx, env: &Env) -> Result<Val, EvalError> {
2464        match rhs {
2465            Expr::Ident(name) if !ctx.has(name) => {
2466                dispatch_method(left.clone(), name, &[], env)
2467            }
2468            Expr::Chain(base, _steps) => {
2469                if let Expr::Ident(name) = base.as_ref() {
2470                    if !ctx.has(name) {
2471                        let prog = Compiler::compile_sub(rhs, ctx);
2472                        return self.exec(&prog, &env.with_current(left.clone()));
2473                    }
2474                }
2475                let sub = Compiler::compile_sub(rhs, ctx);
2476                self.exec(&sub, &env.with_current(left.clone()))
2477            }
2478            _ => {
2479                let sub = Compiler::compile_sub(rhs, ctx);
2480                self.exec(&sub, &env.with_current(left.clone()))
2481            }
2482        }
2483    }
2484
2485    // ── Method call dispatch ──────────────────────────────────────────────────
2486
2487    fn exec_call(&mut self, recv: Val, call: &CompiledCall, env: &Env) -> Result<Val, EvalError> {
2488        // Global-call opcodes push Root before calling; handle them
2489        if call.method == BuiltinMethod::Unknown {
2490            // Custom registry or global function
2491            if !env.registry_is_empty() {
2492                if let Some(method) = env.registry_get(call.name.as_ref()) {
2493                    let evaled: Result<Vec<Val>, _> = call.sub_progs.iter()
2494                        .map(|p| self.exec(p, env)).collect();
2495                    return method.call(recv, &evaled?);
2496                }
2497            }
2498            // Global function (coalesce, zip, etc.) or fallback to dispatch_method
2499            return dispatch_method(recv, call.name.as_ref(), &call.orig_args, env);
2500        }
2501
2502        // Lambda methods — VM handles iteration, running sub-programs per item
2503        if call.method.is_lambda_method() {
2504            return self.exec_lambda_method(recv, call, env);
2505        }
2506
2507        // Value methods — delegate to the existing dispatch with orig_args
2508        dispatch_method(recv, call.name.as_ref(), &call.orig_args, env)
2509    }
2510
2511    fn exec_lambda_method(&mut self, recv: Val, call: &CompiledCall, env: &Env) -> Result<Val, EvalError> {
2512        let sub = call.sub_progs.first();
2513
2514        match call.method {
2515            BuiltinMethod::Filter => {
2516                let pred = sub.ok_or_else(|| EvalError("filter: requires predicate".into()))?;
2517                let items = recv.into_vec().ok_or_else(|| EvalError("filter: expected array".into()))?;
2518                let mut out = Vec::new();
2519                for item in items {
2520                    if is_truthy(&self.exec_with_lambda(pred, &item, &call.orig_args, env)?) {
2521                        out.push(item);
2522                    }
2523                }
2524                Ok(Val::arr(out))
2525            }
2526            BuiltinMethod::Map => {
2527                let mapper = sub.ok_or_else(|| EvalError("map: requires mapper".into()))?;
2528                let items = recv.into_vec().ok_or_else(|| EvalError("map: expected array".into()))?;
2529                let r: Result<Vec<_>, _> = items.into_iter()
2530                    .map(|item| self.exec_with_lambda(mapper, &item, &call.orig_args, env))
2531                    .collect();
2532                Ok(Val::arr(r?))
2533            }
2534            BuiltinMethod::FlatMap => {
2535                let mapper = sub.ok_or_else(|| EvalError("flatMap: requires mapper".into()))?;
2536                let items = recv.into_vec().ok_or_else(|| EvalError("flatMap: expected array".into()))?;
2537                let mut out = Vec::new();
2538                for item in items {
2539                    match self.exec_with_lambda(mapper, &item, &call.orig_args, env)? {
2540                        Val::Arr(a) => out.extend(Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone())),
2541                        v => out.push(v),
2542                    }
2543                }
2544                Ok(Val::arr(out))
2545            }
2546            BuiltinMethod::Sort => {
2547                // Delegate to func_arrays::sort which already handles lambda args
2548                dispatch_method(recv, call.name.as_ref(), &call.orig_args, env)
2549            }
2550            BuiltinMethod::Any => {
2551                let pred = sub.ok_or_else(|| EvalError("any: requires predicate".into()))?;
2552                if let Val::Arr(a) = &recv {
2553                    let result = a.iter().any(|item| {
2554                        self.exec_with_lambda(pred, item, &call.orig_args, env)
2555                            .map(|v| is_truthy(&v)).unwrap_or(false)
2556                    });
2557                    Ok(Val::Bool(result))
2558                } else { Ok(Val::Bool(false)) }
2559            }
2560            BuiltinMethod::All => {
2561                let pred = sub.ok_or_else(|| EvalError("all: requires predicate".into()))?;
2562                if let Val::Arr(a) = &recv {
2563                    if a.is_empty() { return Ok(Val::Bool(true)); }
2564                    let result = a.iter().all(|item| {
2565                        self.exec_with_lambda(pred, item, &call.orig_args, env)
2566                            .map(|v| is_truthy(&v)).unwrap_or(false)
2567                    });
2568                    Ok(Val::Bool(result))
2569                } else { Ok(Val::Bool(false)) }
2570            }
2571            BuiltinMethod::Count if !call.sub_progs.is_empty() => {
2572                let pred = &call.sub_progs[0];
2573                if let Val::Arr(a) = &recv {
2574                    let n = a.iter().filter(|item| {
2575                        self.exec_with_lambda(pred, item, &call.orig_args, env)
2576                            .map(|v| is_truthy(&v)).unwrap_or(false)
2577                    }).count();
2578                    Ok(Val::Int(n as i64))
2579                } else { Ok(Val::Int(0)) }
2580            }
2581            BuiltinMethod::GroupBy => {
2582                let key_prog = sub.ok_or_else(|| EvalError("groupBy: requires key".into()))?;
2583                let items = recv.into_vec().ok_or_else(|| EvalError("groupBy: expected array".into()))?;
2584                let mut map: IndexMap<Arc<str>, Val> = IndexMap::new();
2585                for item in items {
2586                    let k: Arc<str> = Arc::from(val_to_key(&self.exec_with_lambda(key_prog, &item, &call.orig_args, env)?).as_str());
2587                    let bucket = map.entry(k).or_insert_with(|| Val::arr(Vec::new()));
2588                    bucket.as_array_mut().unwrap().push(item);
2589                }
2590                Ok(Val::obj(map))
2591            }
2592            BuiltinMethod::CountBy => {
2593                let key_prog = sub.ok_or_else(|| EvalError("countBy: requires key".into()))?;
2594                let items = recv.into_vec().ok_or_else(|| EvalError("countBy: expected array".into()))?;
2595                let mut map: IndexMap<Arc<str>, Val> = IndexMap::new();
2596                for item in items {
2597                    let k: Arc<str> = Arc::from(val_to_key(&self.exec_with_lambda(key_prog, &item, &call.orig_args, env)?).as_str());
2598                    let counter = map.entry(k).or_insert(Val::Int(0));
2599                    if let Val::Int(n) = counter { *n += 1; }
2600                }
2601                Ok(Val::obj(map))
2602            }
2603            BuiltinMethod::IndexBy => {
2604                let key_prog = sub.ok_or_else(|| EvalError("indexBy: requires key".into()))?;
2605                let items = recv.into_vec().ok_or_else(|| EvalError("indexBy: expected array".into()))?;
2606                let mut map: IndexMap<Arc<str>, Val> = IndexMap::new();
2607                for item in items {
2608                    let k: Arc<str> = Arc::from(val_to_key(&self.exec_with_lambda(key_prog, &item, &call.orig_args, env)?).as_str());
2609                    map.insert(k, item);
2610                }
2611                Ok(Val::obj(map))
2612            }
2613            BuiltinMethod::TakeWhile => {
2614                let pred = sub.ok_or_else(|| EvalError("takeWhile: requires predicate".into()))?;
2615                let items = recv.into_vec().ok_or_else(|| EvalError("takeWhile: expected array".into()))?;
2616                let mut out = Vec::new();
2617                for item in items {
2618                    if !is_truthy(&self.exec_with_lambda(pred, &item, &call.orig_args, env)?) { break; }
2619                    out.push(item);
2620                }
2621                Ok(Val::arr(out))
2622            }
2623            BuiltinMethod::DropWhile => {
2624                let pred = sub.ok_or_else(|| EvalError("dropWhile: requires predicate".into()))?;
2625                let items = recv.into_vec().ok_or_else(|| EvalError("dropWhile: expected array".into()))?;
2626                let mut dropping = true;
2627                let out: Vec<Val> = items.into_iter().filter(|item| {
2628                    if dropping {
2629                        dropping = self.exec_with_lambda(pred, item, &call.orig_args, env)
2630                            .map(|v| is_truthy(&v)).unwrap_or(false);
2631                        !dropping
2632                    } else { true }
2633                }).collect();
2634                Ok(Val::arr(out))
2635            }
2636            BuiltinMethod::Accumulate => {
2637                dispatch_method(recv, call.name.as_ref(), &call.orig_args, env)
2638            }
2639            BuiltinMethod::Partition => {
2640                let pred = sub.ok_or_else(|| EvalError("partition: requires predicate".into()))?;
2641                let items = recv.into_vec().ok_or_else(|| EvalError("partition: expected array".into()))?;
2642                let (mut yes, mut no) = (Vec::new(), Vec::new());
2643                for item in items {
2644                    if is_truthy(&self.exec_with_lambda(pred, &item, &call.orig_args, env)?) {
2645                        yes.push(item);
2646                    } else {
2647                        no.push(item);
2648                    }
2649                }
2650                Ok(Val::arr(vec![Val::arr(yes), Val::arr(no)]))
2651            }
2652            BuiltinMethod::TransformKeys => {
2653                let lam = sub.ok_or_else(|| EvalError("transformKeys: requires lambda".into()))?;
2654                let map = recv.into_map().ok_or_else(|| EvalError("transformKeys: expected object".into()))?;
2655                let mut out: IndexMap<Arc<str>, Val> = IndexMap::new();
2656                for (k, v) in map {
2657                    let new_key = Arc::from(val_to_key(&self.exec_with_lambda(lam, &Val::Str(k), &call.orig_args, env)?).as_str());
2658                    out.insert(new_key, v);
2659                }
2660                Ok(Val::obj(out))
2661            }
2662            BuiltinMethod::TransformValues => {
2663                let lam = sub.ok_or_else(|| EvalError("transformValues: requires lambda".into()))?;
2664                let map = recv.into_map().ok_or_else(|| EvalError("transformValues: expected object".into()))?;
2665                let mut out: IndexMap<Arc<str>, Val> = IndexMap::new();
2666                for (k, v) in map {
2667                    out.insert(k, self.exec_with_lambda(lam, &v, &call.orig_args, env)?);
2668                }
2669                Ok(Val::obj(out))
2670            }
2671            BuiltinMethod::FilterKeys => {
2672                let lam = sub.ok_or_else(|| EvalError("filterKeys: requires predicate".into()))?;
2673                let map = recv.into_map().ok_or_else(|| EvalError("filterKeys: expected object".into()))?;
2674                let mut out: IndexMap<Arc<str>, Val> = IndexMap::new();
2675                for (k, v) in map {
2676                    if is_truthy(&self.exec_with_lambda(lam, &Val::Str(k.clone()), &call.orig_args, env)?) {
2677                        out.insert(k, v);
2678                    }
2679                }
2680                Ok(Val::obj(out))
2681            }
2682            BuiltinMethod::FilterValues => {
2683                let lam = sub.ok_or_else(|| EvalError("filterValues: requires predicate".into()))?;
2684                let map = recv.into_map().ok_or_else(|| EvalError("filterValues: expected object".into()))?;
2685                let mut out: IndexMap<Arc<str>, Val> = IndexMap::new();
2686                for (k, v) in map {
2687                    if is_truthy(&self.exec_with_lambda(lam, &v, &call.orig_args, env)?) {
2688                        out.insert(k, v);
2689                    }
2690                }
2691                Ok(Val::obj(out))
2692            }
2693            BuiltinMethod::Pivot => {
2694                dispatch_method(recv, call.name.as_ref(), &call.orig_args, env)
2695            }
2696            BuiltinMethod::Update => {
2697                let lam = sub.ok_or_else(|| EvalError("update: requires lambda".into()))?;
2698                self.exec_with_lambda(lam, &recv, &call.orig_args, env)
2699            }
2700            _ => dispatch_method(recv, call.name.as_ref(), &call.orig_args, env),
2701        }
2702    }
2703
2704    /// Run a pre-compiled sub-program (lambda body) with `item` as the current value.
2705    /// Handles lambda params from the original arg if present.
2706    #[inline]
2707    fn exec_with_lambda(&mut self, prog: &Program, item: &Val, orig_args: &[Arg], env: &Env)
2708        -> Result<Val, EvalError>
2709    {
2710        // Check if the original arg was a lambda with params that need binding
2711        if let Some(Arg::Pos(Expr::Lambda { params, .. })) = orig_args.first() {
2712            if !params.is_empty() {
2713                let mut inner = env.with_var(&params[0], item.clone());
2714                inner.current = item.clone();
2715                return self.exec(prog, &inner);
2716            }
2717        }
2718        self.exec(prog, &env.with_current(item.clone()))
2719    }
2720
2721    // ── Object construction ───────────────────────────────────────────────────
2722
2723    fn exec_make_obj(&mut self, entries: &[CompiledObjEntry], env: &Env) -> Result<Val, EvalError> {
2724        let mut map: IndexMap<Arc<str>, Val> = IndexMap::new();
2725        for entry in entries {
2726            match entry {
2727                CompiledObjEntry::Short(name) => {
2728                    let v = if let Some(v) = env.get_var(name.as_ref()) { v.clone() }
2729                            else { env.current.get_field(name.as_ref()) };
2730                    if !v.is_null() { map.insert(name.clone(), v); }
2731                }
2732                CompiledObjEntry::Kv { key, prog, optional, cond } => {
2733                    if let Some(c) = cond {
2734                        if !super::eval::util::is_truthy(&self.exec(c, env)?) { continue; }
2735                    }
2736                    let v = self.exec(prog, env)?;
2737                    if *optional && v.is_null() { continue; }
2738                    map.insert(key.clone(), v);
2739                }
2740                CompiledObjEntry::Dynamic { key, val } => {
2741                    let k: Arc<str> = Arc::from(val_to_key(&self.exec(key, env)?).as_str());
2742                    let v = self.exec(val, env)?;
2743                    map.insert(k, v);
2744                }
2745                CompiledObjEntry::Spread(prog) => {
2746                    if let Val::Obj(other) = self.exec(prog, env)? {
2747                        let entries = Arc::try_unwrap(other).unwrap_or_else(|m| (*m).clone());
2748                        for (k, v) in entries { map.insert(k, v); }
2749                    }
2750                }
2751                CompiledObjEntry::SpreadDeep(prog) => {
2752                    if let Val::Obj(other) = self.exec(prog, env)? {
2753                        let base = std::mem::take(&mut map);
2754                        let merged = super::eval::util::deep_merge_concat(
2755                            Val::obj(base), Val::Obj(other));
2756                        if let Val::Obj(m) = merged {
2757                            map = Arc::try_unwrap(m).unwrap_or_else(|m| (*m).clone());
2758                        }
2759                    }
2760                }
2761            }
2762        }
2763        Ok(Val::obj(map))
2764    }
2765
2766    // ── F-string ──────────────────────────────────────────────────────────────
2767
2768    fn exec_fstring(&mut self, parts: &[CompiledFSPart], env: &Env) -> Result<Val, EvalError> {
2769        let mut out = String::new();
2770        for part in parts {
2771            match part {
2772                CompiledFSPart::Lit(s) => out.push_str(s.as_ref()),
2773                CompiledFSPart::Interp { prog, fmt } => {
2774                    let val = self.exec(prog, env)?;
2775                    let s = match fmt {
2776                        None                      => val_to_string(&val),
2777                        Some(FmtSpec::Spec(spec)) => apply_fmt_spec(&val, spec),
2778                        Some(FmtSpec::Pipe(method)) => {
2779                            val_to_string(&dispatch_method(val, method, &[], env)?)
2780                        }
2781                    };
2782                    out.push_str(&s);
2783                }
2784            }
2785        }
2786        Ok(Val::Str(Arc::from(out.as_str())))
2787    }
2788
2789    // ── Comprehension helpers ─────────────────────────────────────────────────
2790
2791    fn exec_iter_vals(&mut self, iter_prog: &Program, env: &Env) -> Result<Vec<Val>, EvalError> {
2792        match self.exec(iter_prog, env)? {
2793            Val::Arr(a) => Ok(Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone())),
2794            Val::Obj(m) => {
2795                let entries = Arc::try_unwrap(m).unwrap_or_else(|m| (*m).clone());
2796                Ok(entries.into_iter().map(|(k, v)| obj2("key", Val::Str(k), "value", v)).collect())
2797            }
2798            other => Ok(vec![other]),
2799        }
2800    }
2801}
2802
2803// ── Env extensions for VM ─────────────────────────────────────────────────────
2804
2805impl Env {
2806    fn registry_is_empty(&self) -> bool { self.registry_ref().is_empty() }
2807    fn registry_get(&self, name: &str) -> Option<Arc<dyn super::eval::methods::Method>> {
2808        self.registry_ref().get(name).cloned()
2809    }
2810}
2811
2812// ── Free helpers ──────────────────────────────────────────────────────────────
2813
2814fn exec_slice(v: Val, from: Option<i64>, to: Option<i64>) -> Val {
2815    if let Val::Arr(a) = v {
2816        let len = a.len() as i64;
2817        let s = resolve_idx(from.unwrap_or(0), len);
2818        let e = resolve_idx(to.unwrap_or(len), len);
2819        let items = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
2820        let s = s.min(items.len());
2821        let e = e.min(items.len());
2822        Val::arr(items[s..e].to_vec())
2823    } else { Val::Null }
2824}
2825
2826fn resolve_idx(i: i64, len: i64) -> usize {
2827    (if i < 0 { (len + i).max(0) } else { i }) as usize
2828}
2829
2830fn collect_desc(v: &Val, name: &str, out: &mut Vec<Val>) {
2831    match v {
2832        Val::Obj(m) => {
2833            if let Some(v) = m.get(name) { out.push(v.clone()); }
2834            for v in m.values() { collect_desc(v, name, out); }
2835        }
2836        Val::Arr(a) => { for item in a.as_ref() { collect_desc(item, name, out); } }
2837        _ => {}
2838    }
2839}
2840
2841fn collect_all(v: &Val, out: &mut Vec<Val>) {
2842    match v {
2843        Val::Obj(m) => {
2844            out.push(v.clone());
2845            for child in m.values() { collect_all(child, out); }
2846        }
2847        Val::Arr(a) => {
2848            for item in a.as_ref() { collect_all(item, out); }
2849        }
2850        other => out.push(other.clone()),
2851    }
2852}
2853
2854/// Path-tracking variant — called when descending from root so paths are
2855/// root-relative and can be cached for future `RootChain` lookups.
2856/// `prefix` is mutated in-place (push/truncate) to avoid allocations.
2857fn collect_desc_with_paths(
2858    v: &Val, name: &str, prefix: &mut String,
2859    out: &mut Vec<Val>, cached: &mut Vec<(Arc<str>, Val)>,
2860) {
2861    match v {
2862        Val::Obj(m) => {
2863            if let Some(found) = m.get(name) {
2864                let mut path = prefix.clone();
2865                path.push('/');
2866                path.push_str(name);
2867                out.push(found.clone());
2868                cached.push((Arc::from(path.as_str()), found.clone()));
2869            }
2870            for (k, child) in m.iter() {
2871                let prev = prefix.len();
2872                prefix.push('/');
2873                prefix.push_str(k.as_ref());
2874                collect_desc_with_paths(child, name, prefix, out, cached);
2875                prefix.truncate(prev);
2876            }
2877        }
2878        Val::Arr(a) => {
2879            for (i, item) in a.iter().enumerate() {
2880                let prev = prefix.len();
2881                prefix.push('/');
2882                let idx = i.to_string();
2883                prefix.push_str(&idx);
2884                collect_desc_with_paths(item, name, prefix, out, cached);
2885                prefix.truncate(prev);
2886            }
2887        }
2888        _ => {}
2889    }
2890}
2891
2892fn resolve_pointer(root: &Val, ptr: &str) -> Val {
2893    let mut cur = root.clone();
2894    for seg in ptr.split('/').filter(|s| !s.is_empty()) {
2895        cur = cur.get_field(seg);
2896    }
2897    cur
2898}
2899
2900fn bind_comp_vars(env: &Env, vars: &[Arc<str>], item: Val) -> Env {
2901    match vars {
2902        [] => env.with_current(item),
2903        [v] => { let mut e = env.with_var(v.as_ref(), item.clone()); e.current = item; e }
2904        [v1, v2, ..] => {
2905            let idx = item.get("index").cloned().unwrap_or(Val::Null);
2906            let val = item.get("value").cloned().unwrap_or_else(|| item.clone());
2907            let mut e = env.with_var(v1.as_ref(), idx).with_var(v2.as_ref(), val.clone());
2908            e.current = val;
2909            e
2910        }
2911    }
2912}
2913
2914fn exec_cast(v: &Val, ty: super::ast::CastType) -> Result<Val, EvalError> {
2915    use super::ast::CastType;
2916    match ty {
2917        CastType::Str => Ok(Val::Str(Arc::from(match v {
2918            Val::Null     => "null".to_string(),
2919            Val::Bool(b)  => b.to_string(),
2920            Val::Int(n)   => n.to_string(),
2921            Val::Float(f) => f.to_string(),
2922            Val::Str(s)   => s.to_string(),
2923            other         => super::eval::util::val_to_string(other),
2924        }.as_str()))),
2925        CastType::Bool => Ok(Val::Bool(match v {
2926            Val::Null     => false,
2927            Val::Bool(b)  => *b,
2928            Val::Int(n)   => *n != 0,
2929            Val::Float(f) => *f != 0.0,
2930            Val::Str(s)   => !s.is_empty(),
2931            Val::Arr(a)   => !a.is_empty(),
2932            Val::Obj(o)   => !o.is_empty(),
2933        })),
2934        CastType::Number | CastType::Float => match v {
2935            Val::Int(n)   => Ok(Val::Float(*n as f64)),
2936            Val::Float(_) => Ok(v.clone()),
2937            Val::Str(s)   => s.parse::<f64>().map(Val::Float)
2938                              .map_err(|e| EvalError(format!("as float: {}", e))),
2939            Val::Bool(b)  => Ok(Val::Float(if *b { 1.0 } else { 0.0 })),
2940            Val::Null     => Ok(Val::Float(0.0)),
2941            _             => err!("as float: cannot convert"),
2942        },
2943        CastType::Int => match v {
2944            Val::Int(_)   => Ok(v.clone()),
2945            Val::Float(f) => Ok(Val::Int(*f as i64)),
2946            Val::Str(s)   => s.parse::<i64>().map(Val::Int)
2947                              .or_else(|_| s.parse::<f64>().map(|f| Val::Int(f as i64)))
2948                              .map_err(|e| EvalError(format!("as int: {}", e))),
2949            Val::Bool(b)  => Ok(Val::Int(if *b { 1 } else { 0 })),
2950            Val::Null     => Ok(Val::Int(0)),
2951            _             => err!("as int: cannot convert"),
2952        },
2953        CastType::Array => match v {
2954            Val::Arr(_)   => Ok(v.clone()),
2955            Val::Null     => Ok(Val::arr(Vec::new())),
2956            other         => Ok(Val::arr(vec![other.clone()])),
2957        },
2958        CastType::Object => match v {
2959            Val::Obj(_)   => Ok(v.clone()),
2960            _             => err!("as object: cannot convert non-object"),
2961        },
2962        CastType::Null => Ok(Val::Null),
2963    }
2964}
2965
2966fn apply_bind_to_env(target: &BindTarget, val: &Val, env: Env) -> Result<Env, EvalError> {
2967    match target {
2968        BindTarget::Name(name) => Ok(env.with_var(name, val.clone())),
2969        BindTarget::Obj { fields, rest } => {
2970            let obj = val.as_object()
2971                .ok_or_else(|| EvalError("bind destructure: expected object".into()))?;
2972            let mut e = env;
2973            for f in fields {
2974                e = e.with_var(f, obj.get(f.as_str()).cloned().unwrap_or(Val::Null));
2975            }
2976            if let Some(rest_name) = rest {
2977                let mut rem: IndexMap<Arc<str>, Val> = IndexMap::new();
2978                for (k, v) in obj {
2979                    if !fields.iter().any(|f| f.as_str() == k.as_ref()) {
2980                        rem.insert(k.clone(), v.clone());
2981                    }
2982                }
2983                e = e.with_var(rest_name, Val::obj(rem));
2984            }
2985            Ok(e)
2986        }
2987        BindTarget::Arr(names) => {
2988            let arr = val.as_array()
2989                .ok_or_else(|| EvalError("bind destructure: expected array".into()))?;
2990            let mut e = env;
2991            for (i, name) in names.iter().enumerate() {
2992                e = e.with_var(name, arr.get(i).cloned().unwrap_or(Val::Null));
2993            }
2994            Ok(e)
2995        }
2996    }
2997}
2998
2999fn apply_fmt_spec(val: &Val, spec: &str) -> String {
3000    if let Some(rest) = spec.strip_suffix('f') {
3001        if let Some(prec_str) = rest.strip_prefix('.') {
3002            if let Ok(prec) = prec_str.parse::<usize>() {
3003                if let Some(f) = val.as_f64() { return format!("{:.prec$}", f); }
3004            }
3005        }
3006    }
3007    if spec == "d" { if let Some(i) = val.as_i64() { return format!("{}", i); } }
3008    let s = val_to_string(val);
3009    if let Some(w) = spec.strip_prefix('>').and_then(|s| s.parse::<usize>().ok()) { return format!("{:>w$}", s); }
3010    if let Some(w) = spec.strip_prefix('<').and_then(|s| s.parse::<usize>().ok()) { return format!("{:<w$}", s); }
3011    if let Some(w) = spec.strip_prefix('^').and_then(|s| s.parse::<usize>().ok()) { return format!("{:^w$}", s); }
3012    if let Some(w) = spec.strip_prefix('0').and_then(|s| s.parse::<usize>().ok()) {
3013        if let Some(i) = val.as_i64() { return format!("{:0>w$}", i); }
3014    }
3015    s
3016}
3017
3018// ── Opcode helpers ────────────────────────────────────────────────────────────
3019
3020/// Build a no-arg `CallMethod` opcode (used by peephole strength reduction).
3021/// Newtype wrapper giving `Val` an `Ord` derived from `cmp_vals`,
3022/// so it can be used in `BinaryHeap` for TopN.
3023struct WrapVal(Val);
3024impl PartialEq for WrapVal { fn eq(&self, o: &Self) -> bool {
3025    super::eval::util::cmp_vals(&self.0, &o.0) == std::cmp::Ordering::Equal
3026} }
3027impl Eq for WrapVal {}
3028impl PartialOrd for WrapVal { fn partial_cmp(&self, o: &Self) -> Option<std::cmp::Ordering> {
3029    Some(self.cmp(o))
3030} }
3031impl Ord for WrapVal { fn cmp(&self, o: &Self) -> std::cmp::Ordering {
3032    super::eval::util::cmp_vals(&self.0, &o.0)
3033} }
3034
3035/// Extract a literal string from a single-op program `[PushStr(s)]`.
3036fn const_str_program(p: &Arc<Program>) -> Option<Arc<str>> {
3037    match p.ops.as_ref() {
3038        [Opcode::PushStr(s)] => Some(s.clone()),
3039        _ => None,
3040    }
3041}
3042
3043fn make_noarg_call(method: BuiltinMethod, name: &str) -> Opcode {
3044    Opcode::CallMethod(Arc::new(CompiledCall {
3045        method,
3046        name:      Arc::from(name),
3047        sub_progs: Arc::from(&[] as &[Arc<Program>]),
3048        orig_args: Arc::from(&[] as &[Arg]),
3049    }))
3050}
3051
3052// ── Hash helpers ──────────────────────────────────────────────────────────────
3053
3054fn hash_str(s: &str) -> u64 {
3055    let mut h = DefaultHasher::new();
3056    s.hash(&mut h);
3057    h.finish()
3058}
3059
3060/// Hash the *structure* (keys + types) of a Val for the resolution cache key.
3061/// Does NOT hash values, only shape, so structural navigation results are
3062/// stable across different values in the same-shaped document.
3063fn hash_val_structure(v: &Val) -> u64 {
3064    let mut h = DefaultHasher::new();
3065    hash_structure_into(v, &mut h, 0);
3066    h.finish()
3067}
3068
3069fn hash_structure_into(v: &Val, h: &mut DefaultHasher, depth: usize) {
3070    if depth > 8 { return; }
3071    match v {
3072        Val::Null       => 0u8.hash(h),
3073        Val::Bool(b)    => { 1u8.hash(h); b.hash(h); }
3074        Val::Int(n)     => { 2u8.hash(h); n.hash(h); }
3075        Val::Float(f)   => { 3u8.hash(h); f.to_bits().hash(h); }
3076        Val::Str(s)     => { 4u8.hash(h); s.hash(h); }
3077        Val::Arr(a)     => { 5u8.hash(h); a.len().hash(h); for item in a.iter() { hash_structure_into(item, h, depth+1); } }
3078        Val::Obj(m)     => { 6u8.hash(h); m.len().hash(h); for (k, v) in m.iter() { k.hash(h); hash_structure_into(v, h, depth+1); } }
3079    }
3080}