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    sync::atomic::{AtomicU64, Ordering},
43};
44use indexmap::IndexMap;
45use memchr::memchr;
46use memchr::memmem;
47use smallvec::SmallVec;
48
49use crate::ast::*;
50use super::eval::{
51    Env, EvalError, Val,
52    dispatch_method, eval,
53};
54use super::eval::util::{
55    is_truthy, kind_matches, vals_eq, cmp_vals, val_to_key, val_to_string,
56    add_vals, num_op, obj2,
57};
58use super::eval::methods::MethodRegistry;
59
60macro_rules! pop {
61    ($stack:expr) => {
62        $stack.pop().ok_or_else(|| EvalError("stack underflow".into()))?
63    };
64}
65macro_rules! err {
66    ($($t:tt)*) => { Err(EvalError(format!($($t)*))) };
67}
68
69// ── BuiltinMethod ─────────────────────────────────────────────────────────────
70
71/// Pre-resolved method identifier — eliminates string comparison at dispatch.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73#[repr(u8)]
74pub enum BuiltinMethod {
75    // Navigation / basics
76    Len = 0, Keys, Values, Entries, ToPairs, FromPairs, Invert, Reverse, Type,
77    ToString, ToJson, FromJson,
78    // Aggregates
79    Sum, Avg, Min, Max, Count, Any, All,
80    GroupBy, CountBy, IndexBy,
81    // Array ops
82    Filter, Map, FlatMap, Sort, Unique, Flatten, Compact,
83    Join, First, Last, Nth, Append, Prepend, Remove,
84    Diff, Intersect, Union, Enumerate, Pairwise, Window, Chunk,
85    TakeWhile, DropWhile, Accumulate, Partition, Zip, ZipLongest,
86    // Object ops
87    Pick, Omit, Merge, DeepMerge, Defaults, Rename,
88    TransformKeys, TransformValues, FilterKeys, FilterValues, Pivot,
89    // Path ops
90    GetPath, SetPath, DelPath, DelPaths, HasPath, FlattenKeys, UnflattenKeys,
91    // CSV
92    ToCsv, ToTsv,
93    // Null / predicate
94    Or, Has, Missing, Includes, Set, Update,
95    // String methods
96    Upper, Lower, Capitalize, TitleCase, Trim, TrimLeft, TrimRight,
97    Lines, Words, Chars, ToNumber, ToBool, ToBase64, FromBase64,
98    UrlEncode, UrlDecode, HtmlEscape, HtmlUnescape,
99    Repeat, PadLeft, PadRight, StartsWith, EndsWith,
100    IndexOf, LastIndexOf, Replace, ReplaceAll, StripPrefix, StripSuffix,
101    Slice, Split, Indent, Dedent, Matches, Scan,
102    // Relational
103    EquiJoin,
104    // Sentinel for custom/unknown
105    Unknown,
106}
107
108impl BuiltinMethod {
109    pub fn from_name(name: &str) -> Self {
110        match name {
111            "len"            => Self::Len,
112            "keys"           => Self::Keys,
113            "values"         => Self::Values,
114            "entries"        => Self::Entries,
115            "to_pairs"|"toPairs" => Self::ToPairs,
116            "from_pairs"|"fromPairs" => Self::FromPairs,
117            "invert"         => Self::Invert,
118            "reverse"        => Self::Reverse,
119            "type"           => Self::Type,
120            "to_string"|"toString" => Self::ToString,
121            "to_json"|"toJson" => Self::ToJson,
122            "from_json"|"fromJson" => Self::FromJson,
123            "sum"            => Self::Sum,
124            "avg"            => Self::Avg,
125            "min"            => Self::Min,
126            "max"            => Self::Max,
127            "count"          => Self::Count,
128            "any"            => Self::Any,
129            "all"            => Self::All,
130            "groupBy"|"group_by" => Self::GroupBy,
131            "countBy"|"count_by" => Self::CountBy,
132            "indexBy"|"index_by" => Self::IndexBy,
133            "filter"         => Self::Filter,
134            "map"            => Self::Map,
135            "flatMap"|"flat_map" => Self::FlatMap,
136            "sort"           => Self::Sort,
137            "unique"|"distinct" => Self::Unique,
138            "flatten"        => Self::Flatten,
139            "compact"        => Self::Compact,
140            "join"           => Self::Join,
141            "equi_join"|"equiJoin" => Self::EquiJoin,
142            "first"          => Self::First,
143            "last"           => Self::Last,
144            "nth"            => Self::Nth,
145            "append"         => Self::Append,
146            "prepend"        => Self::Prepend,
147            "remove"         => Self::Remove,
148            "diff"           => Self::Diff,
149            "intersect"      => Self::Intersect,
150            "union"          => Self::Union,
151            "enumerate"      => Self::Enumerate,
152            "pairwise"       => Self::Pairwise,
153            "window"         => Self::Window,
154            "chunk"|"batch"  => Self::Chunk,
155            "takewhile"|"take_while" => Self::TakeWhile,
156            "dropwhile"|"drop_while" => Self::DropWhile,
157            "accumulate"     => Self::Accumulate,
158            "partition"      => Self::Partition,
159            "zip"            => Self::Zip,
160            "zip_longest"|"zipLongest" => Self::ZipLongest,
161            "pick"           => Self::Pick,
162            "omit"           => Self::Omit,
163            "merge"          => Self::Merge,
164            "deep_merge"|"deepMerge" => Self::DeepMerge,
165            "defaults"       => Self::Defaults,
166            "rename"         => Self::Rename,
167            "transform_keys"|"transformKeys" => Self::TransformKeys,
168            "transform_values"|"transformValues" => Self::TransformValues,
169            "filter_keys"|"filterKeys" => Self::FilterKeys,
170            "filter_values"|"filterValues" => Self::FilterValues,
171            "pivot"          => Self::Pivot,
172            "get_path"|"getPath" => Self::GetPath,
173            "set_path"|"setPath" => Self::SetPath,
174            "del_path"|"delPath" => Self::DelPath,
175            "del_paths"|"delPaths" => Self::DelPaths,
176            "has_path"|"hasPath" => Self::HasPath,
177            "flatten_keys"|"flattenKeys" => Self::FlattenKeys,
178            "unflatten_keys"|"unflattenKeys" => Self::UnflattenKeys,
179            "to_csv"|"toCsv" => Self::ToCsv,
180            "to_tsv"|"toTsv" => Self::ToTsv,
181            "or"             => Self::Or,
182            "has"            => Self::Has,
183            "missing"        => Self::Missing,
184            "includes"|"contains" => Self::Includes,
185            "set"            => Self::Set,
186            "update"         => Self::Update,
187            "upper"          => Self::Upper,
188            "lower"          => Self::Lower,
189            "capitalize"     => Self::Capitalize,
190            "title_case"|"titleCase" => Self::TitleCase,
191            "trim"           => Self::Trim,
192            "trim_left"|"trimLeft"|"lstrip" => Self::TrimLeft,
193            "trim_right"|"trimRight"|"rstrip" => Self::TrimRight,
194            "lines"          => Self::Lines,
195            "words"          => Self::Words,
196            "chars"          => Self::Chars,
197            "to_number"|"toNumber" => Self::ToNumber,
198            "to_bool"|"toBool" => Self::ToBool,
199            "to_base64"|"toBase64" => Self::ToBase64,
200            "from_base64"|"fromBase64" => Self::FromBase64,
201            "url_encode"|"urlEncode" => Self::UrlEncode,
202            "url_decode"|"urlDecode" => Self::UrlDecode,
203            "html_escape"|"htmlEscape" => Self::HtmlEscape,
204            "html_unescape"|"htmlUnescape" => Self::HtmlUnescape,
205            "repeat"         => Self::Repeat,
206            "pad_left"|"padLeft" => Self::PadLeft,
207            "pad_right"|"padRight" => Self::PadRight,
208            "starts_with"|"startsWith" => Self::StartsWith,
209            "ends_with"|"endsWith" => Self::EndsWith,
210            "index_of"|"indexOf" => Self::IndexOf,
211            "last_index_of"|"lastIndexOf" => Self::LastIndexOf,
212            "replace"        => Self::Replace,
213            "replace_all"|"replaceAll" => Self::ReplaceAll,
214            "strip_prefix"|"stripPrefix" => Self::StripPrefix,
215            "strip_suffix"|"stripSuffix" => Self::StripSuffix,
216            "slice"          => Self::Slice,
217            "split"          => Self::Split,
218            "indent"         => Self::Indent,
219            "dedent"         => Self::Dedent,
220            "matches"        => Self::Matches,
221            "scan"           => Self::Scan,
222            _                => Self::Unknown,
223        }
224    }
225
226    /// True for methods that receive a sub-program to run per item.
227    fn is_lambda_method(self) -> bool {
228        matches!(self,
229            Self::Filter | Self::Map | Self::FlatMap | Self::Sort |
230            Self::Any | Self::All | Self::Count | Self::GroupBy |
231            Self::CountBy | Self::IndexBy | Self::TakeWhile |
232            Self::DropWhile | Self::Accumulate | Self::Partition |
233            Self::TransformKeys | Self::TransformValues |
234            Self::FilterKeys | Self::FilterValues | Self::Pivot | Self::Update
235        )
236    }
237}
238
239// ── Compiled sub-structures ───────────────────────────────────────────────────
240
241/// A compiled method call stored inside `Opcode::CallMethod`.
242#[derive(Debug, Clone)]
243pub struct CompiledCall {
244    pub method:   BuiltinMethod,
245    pub name:     Arc<str>,
246    /// Compiled lambda/expression sub-programs (one per arg, in order).
247    pub sub_progs: Arc<[Arc<Program>]>,
248    /// Original AST args kept for non-lambda dispatch fallback.
249    pub orig_args: Arc<[Arg]>,
250}
251
252/// A compiled object field for `Opcode::MakeObj`.
253#[derive(Debug, Clone)]
254pub enum CompiledObjEntry {
255    /// `{ name }` / `{ name, … }` shorthand — reads `env.current.name`
256    /// (or a bound variable of that name).  `ic` is a per-entry inline
257    /// cache hint so that repeated MakeObj calls over objects that share
258    /// shape skip the IndexMap key-hash on hit.
259    Short { name: Arc<str>, ic: Arc<AtomicU64> },
260    Kv     { key: Arc<str>, prog: Arc<Program>, optional: bool, cond: Option<Arc<Program>> },
261    /// Specialised `Kv` where the value is a pure path from current:
262    /// `{ key: @.a.b[0] }` compiles to `KvPath` so `exec_make_obj` can
263    /// walk `env.current` through the pre-resolved steps without a
264    /// sub-program exec.  `optional=true` mirrors `?` in the source —
265    /// the field is omitted when the walk lands on `Null`.
266    /// `ics[i]` is an inline-cache slot for `steps[i]` — only used when
267    /// the step is `Field`.
268    KvPath { key: Arc<str>, steps: Arc<[KvStep]>, optional: bool, ics: Arc<[AtomicU64]> },
269    Dynamic { key: Arc<Program>, val: Arc<Program> },
270    Spread(Arc<Program>),
271    SpreadDeep(Arc<Program>),
272}
273
274/// Single step in a pre-resolved `KvPath` projection.
275#[derive(Debug, Clone)]
276pub enum KvStep {
277    Field(Arc<str>),
278    Index(i64),
279}
280
281/// A compiled f-string interpolation part.
282#[derive(Debug, Clone)]
283pub enum CompiledFSPart {
284    Lit(Arc<str>),
285    Interp { prog: Arc<Program>, fmt: Option<FmtSpec> },
286}
287
288/// Compiled bind-object destructure spec.
289#[derive(Debug, Clone)]
290pub struct BindObjSpec {
291    pub fields: Arc<[Arc<str>]>,
292    pub rest:   Option<Arc<str>>,
293}
294
295/// Compiled comprehension spec.
296#[derive(Debug, Clone)]
297pub struct CompSpec {
298    pub expr: Arc<Program>,
299    pub vars: Arc<[Arc<str>]>,
300    pub iter: Arc<Program>,
301    pub cond: Option<Arc<Program>>,
302}
303
304#[derive(Debug, Clone)]
305pub struct DictCompSpec {
306    pub key:  Arc<Program>,
307    pub val:  Arc<Program>,
308    pub vars: Arc<[Arc<str>]>,
309    pub iter: Arc<Program>,
310    pub cond: Option<Arc<Program>>,
311}
312
313// ── Opcode ────────────────────────────────────────────────────────────────────
314
315#[derive(Debug, Clone)]
316pub enum Opcode {
317    // ── Literals ─────────────────────────────────────────────────────────────
318    PushNull,
319    PushBool(bool),
320    PushInt(i64),
321    PushFloat(f64),
322    PushStr(Arc<str>),
323
324    // ── Context ───────────────────────────────────────────────────────────────
325    PushRoot,
326    PushCurrent,
327
328    // ── Navigation ────────────────────────────────────────────────────────────
329    GetField(Arc<str>),
330    GetIndex(i64),
331    GetSlice(Option<i64>, Option<i64>),
332    DynIndex(Arc<Program>),
333    OptField(Arc<str>),
334    Descendant(Arc<str>),
335    DescendAll,
336    InlineFilter(Arc<Program>),
337    Quantifier(QuantifierKind),
338
339    // ── Peephole fusions ──────────────────────────────────────────────────────
340    /// PushRoot + GetField* fused — resolves chain via pointer arithmetic.
341    RootChain(Arc<[Arc<str>]>),
342    /// GetField(k1) + GetField(k2) + … fused — walks TOS through N fields.
343    /// Applies mid-program where `RootChain` does not match (e.g. after a
344    /// method call or filter produces an object on stack).
345    /// Carries a per-step inline-cache array so that `map(a.b.c)` over a
346    /// shape-uniform array of objects hits `get_index(cached_slot)`
347    /// instead of re-hashing the key at every iteration.
348    FieldChain(Arc<FieldChainData>),
349    /// filter(pred) + len/count fused — counts matches without temp array.
350    FilterCount(Arc<Program>),
351    /// filter(pred) + First quantifier fused — early-exit on first match.
352    FindFirst(Arc<Program>),
353    /// filter(pred) + One quantifier fused — early-exit at 2nd match (error).
354    FindOne(Arc<Program>),
355    /// filter(pred) + map(f) fused — single pass, no intermediate array.
356    FilterMap { pred: Arc<Program>, map: Arc<Program> },
357    /// filter(p1) + filter(p2) fused — single pass, both predicates.
358    FilterFilter { p1: Arc<Program>, p2: Arc<Program> },
359    /// map(f1) + map(f2) fused — single pass, composed.
360    MapMap { f1: Arc<Program>, f2: Arc<Program> },
361    /// map(f) + filter(p) fused — single pass; emit `f(x)` only when `p(f(x))` holds.
362    MapFilter { map: Arc<Program>, pred: Arc<Program> },
363    /// Fused `map(f).sum()` — evaluates `f` per item, accumulates numeric sum.
364    MapSum(Arc<Program>),
365    /// Fused `map(@.to_json()).join(sep)` — single-pass stringify + concat.
366    /// Skips the intermediate Vec<Val::Str> (N Arc allocations) and writes
367    /// each item's JSON form straight into one output buffer.  Columnar
368    /// receivers (`Val::IntVec` / `Val::FloatVec`) shortcut further via
369    /// native number-formatting in a tight loop.
370    MapToJsonJoin { sep_prog: Arc<Program> },
371    /// Fused `.trim().upper()` — one allocation instead of two, ASCII fast-path.
372    StrTrimUpper,
373    /// Fused `.trim().lower()` — one allocation instead of two, ASCII fast-path.
374    StrTrimLower,
375    /// Fused `.upper().trim()` — one allocation instead of two.
376    StrUpperTrim,
377    /// Fused `.lower().trim()` — one allocation instead of two.
378    StrLowerTrim,
379    /// Fused `.split(sep).reverse().join(sep)` — byte-scan segments and
380    /// emit reversed join into one buffer.  No intermediate `Vec<Arc<str>>`.
381    StrSplitReverseJoin { sep: Arc<str> },
382    /// Fused `map(@.replace(lit, lit))` (and `replace_all`) — literal needle
383    /// and replacement inlined; skips per-item sub_prog evaluation for arg
384    /// strings.  `all=true` means replace every occurrence, else only first.
385    MapReplaceLit { needle: Arc<str>, with: Arc<str>, all: bool },
386    /// Fused `map(@.upper().replace(lit, lit))` (and `replace_all`) — scan
387    /// bytes once: ASCII-upper + memchr needle scan into a single pre-sized
388    /// output String per item. Falls back to non-ASCII path for Unicode.
389    MapUpperReplaceLit { needle: Arc<str>, with: Arc<str>, all: bool },
390    /// Fused `map(@.lower().replace(lit, lit))` (and `replace_all`) — same as
391    /// above but ASCII-lower.
392    MapLowerReplaceLit { needle: Arc<str>, with: Arc<str>, all: bool },
393    /// Fused `map(prefix + @ + suffix)` — per item, allocate exact-size
394    /// `Arc<str>` with one uninit slice + copy_nonoverlapping. Either
395    /// prefix or suffix may be empty for the 2-operand forms.
396    MapStrConcat { prefix: Arc<str>, suffix: Arc<str> },
397    /// Fused `map(@.split(sep).map(len).sum())` — emits IntVec of per-row
398    /// sum-of-segment-char-lengths. Uses byte-scan (memchr/memmem) for ASCII
399    /// source; falls back to char counting for Unicode.
400    MapSplitLenSum { sep: Arc<str> },
401    /// Fused `map({k1, k2, ..})` — map over an array projecting each object
402    /// to a fixed set of `Short`-form fields (bare identifiers). Avoids the
403    /// nested `MakeObj` dispatch per row and hoists key `Arc<str>` clones
404    /// outside the inner loop. Uses one IC slot per key for shape lookup.
405    MapProject { keys: Arc<[Arc<str>]>, ics: Arc<[std::sync::atomic::AtomicU64]> },
406    /// Fused `map(@.slice(lit, lit))` — per-row ASCII byte-range slice via
407    /// borrowed `Val::StrSlice` into the parent Arc.  Non-ASCII rows fall
408    /// through to character-index resolution.  Zero allocation per row
409    /// when the input is already `Val::Str` (just an Arc bump for the
410    /// borrowed view).
411    MapStrSlice { start: i64, end: Option<i64> },
412    /// Fused `map(f"…")` — map over an array applying an f-string to each
413    /// element. Skips the inner CallMethod dispatch / FString Arc-clone
414    /// per row; runs the f-string parts in a tight loop.
415    MapFString(Arc<[CompiledFSPart]>),
416    /// Fused `map(@.split(sep).count())` — byte-scan per row, returns Int;
417    /// zero per-row allocations.
418    MapSplitCount { sep: Arc<str> },
419    /// Fused `map(@.split(sep).count()).sum()` — scalar Int, no intermediate
420    /// `[Int,Int,...]` array. One memchr-backed scan per row, accumulated.
421    MapSplitCountSum { sep: Arc<str> },
422    /// Fused `map(@.split(sep).first())` — first segment only; one Arc per
423    /// row instead of N.
424    MapSplitFirst { sep: Arc<str> },
425    /// Fused `map(@.split(sep).nth(n))` — nth segment; one Arc per row.
426    MapSplitNth { sep: Arc<str>, n: usize },
427    /// Fused `map(f).avg()` — evaluates `f` per item, computes mean as float.
428    MapAvg(Arc<Program>),
429    /// Fused `filter(p).map(f).sum()` — single pass, numeric sum of mapped
430    /// values that pass the predicate.  No intermediate array.
431    FilterMapSum { pred: Arc<Program>, map: Arc<Program> },
432    /// Fused `filter(p).map(f).avg()` — mean as float over mapped values
433    /// that pass the predicate.
434    FilterMapAvg { pred: Arc<Program>, map: Arc<Program> },
435    /// Fused `filter(p).map(f).first()` — early-exit: apply `map` once,
436    /// to the first item that satisfies `pred`.
437    FilterMapFirst { pred: Arc<Program>, map: Arc<Program> },
438    /// Fused `filter(p).map(f).min()` — single pass, numeric min over
439    /// mapped values that pass the predicate.  No intermediate array.
440    FilterMapMin { pred: Arc<Program>, map: Arc<Program> },
441    /// Fused `filter(p).map(f).max()` — single pass, numeric max over
442    /// mapped values that pass the predicate.
443    FilterMapMax { pred: Arc<Program>, map: Arc<Program> },
444    /// Fused `filter(p).last()` — reverse scan, return last item
445    /// satisfying `pred` (or Null when none match / input is Null).
446    FilterLast { pred: Arc<Program> },
447    /// Fused `sort()` + `[0:n]` — partial-sort smallest N using BinaryHeap.
448    /// `asc=true` → smallest N; `asc=false` → largest N.
449    TopN { n: usize, asc: bool },
450    /// Fused `unique()` + `count()`/`len()` — count distinct elements without
451    /// materialising the deduped array.
452    UniqueCount,
453    /// Fused `sort_by(k).first()` / `.last()` — O(N) single pass instead of
454    /// an O(N log N) sort followed by discard.  Preserves stable-sort ordering:
455    /// `max=false` returns the *earliest* item whose key is minimal
456    /// (matches `sort_by(k).first()`); `max=true` returns the *latest* item
457    /// whose key is maximal (matches `sort_by(k).last()`).
458    ArgExtreme { key: Arc<Program>, lam_param: Option<Arc<str>>, max: bool },
459    /// Fused `map(f).flatten()` — single-pass concat of mapped arrays.
460    MapFlatten(Arc<Program>),
461    /// Fused `map(f).first()` — apply `f` only to the first element.
462    /// Empty input → Null (matches plain `first()` on `[]`).
463    MapFirst(Arc<Program>),
464    /// Fused `map(f).last()` — apply `f` only to the last element.
465    MapLast(Arc<Program>),
466    /// Fused `map(f).min()` — single-pass numeric min over mapped values.
467    MapMin(Arc<Program>),
468    /// Fused `map(f).max()` — single-pass numeric max over mapped values.
469    MapMax(Arc<Program>),
470
471    // ── Field-specialised fusions (Tier 3) ────────────────────────────────────
472    /// `map(k).sum()` where `k` is a single field ident. Skips sub-program exec.
473    MapFieldSum(Arc<str>),
474    /// `map(k).avg()` where `k` is a single field ident.
475    MapFieldAvg(Arc<str>),
476    /// `map(k).min()` where `k` is a single field ident.
477    MapFieldMin(Arc<str>),
478    /// `map(k).max()` where `k` is a single field ident.
479    MapFieldMax(Arc<str>),
480    /// `map(k)` where `k` is a single field ident — emit array of field values.
481    MapField(Arc<str>),
482    /// `map(a.b.c)` on arr-of-obj → walk chain per item, push resulting
483    /// Val (Null if any step hits a non-Obj or missing key).
484    MapFieldChain(Arc<[Arc<str>]>),
485    /// `map(k).unique()` where `k` is a single field ident. FxHashSet dedup.
486    MapFieldUnique(Arc<str>),
487    /// `map(a.b.c).unique()` — walk chain + inline dedup, no intermediate array.
488    MapFieldChainUnique(Arc<[Arc<str>]>),
489
490    // ── Flatten-chain fusion (Tier 1) ─────────────────────────────────────────
491    /// `.map(k1).flatten().map(k2).flatten()…` collapsed into a single walk.
492    /// Input is an array of objects; each step descends the named array-valued
493    /// field and concatenates. `N` levels → `N+1` buffers (current+next) instead
494    /// of `2N` allocations.
495    FlatMapChain(Arc<[Arc<str>]>),
496
497    // ── Predicate specialisation (Tier 4) ─────────────────────────────────────
498    /// `filter(k == lit)` — predicate is equality of a single field to a literal.
499    FilterFieldEqLit(Arc<str>, Val),
500    /// `filter(k <op> lit)` — predicate is a comparison of a single field to a literal.
501    FilterFieldCmpLit(Arc<str>, super::ast::BinOp, Val),
502    /// `filter(k1 <op> k2)` — predicate compares two fields of the same item.
503    FilterFieldCmpField(Arc<str>, super::ast::BinOp, Arc<str>),
504    /// `filter(kp == lit).map(kproj)` fused — single pass, no intermediate array.
505    FilterFieldEqLitMapField(Arc<str>, Val, Arc<str>),
506    /// `filter(kp <cop> lit).map(kproj)` fused — single pass, no intermediate array.
507    FilterFieldCmpLitMapField(Arc<str>, super::ast::BinOp, Val, Arc<str>),
508    /// `filter(f1 == l1 AND f2 == l2 AND …).count()` fused — zero alloc,
509    /// one IC slot per conjunct field.
510    FilterFieldsAllEqLitCount(Arc<[(Arc<str>, Val)]>),
511    /// `filter(f1 <o1> l1 AND f2 <o2> l2 AND …).count()` fused — general cmp.
512    FilterFieldsAllCmpLitCount(Arc<[(Arc<str>, super::ast::BinOp, Val)]>),
513    /// `filter(k == lit).count()` — count without materialising.
514    FilterFieldEqLitCount(Arc<str>, Val),
515    /// `filter(k <op> lit).count()` — count cmp without materialising.
516    FilterFieldCmpLitCount(Arc<str>, super::ast::BinOp, Val),
517    /// `filter(k1 <op> k2).count()` — cross-field count.
518    FilterFieldCmpFieldCount(Arc<str>, super::ast::BinOp, Arc<str>),
519    /// `filter(@ <op> lit)` — predicate on the current element itself.
520    /// Columnar fast path: IntVec/FloatVec receivers loop on the raw
521    /// slice and emit a typed vec; Arr falls back to element iteration.
522    FilterCurrentCmpLit(super::ast::BinOp, Val),
523    /// `filter(@.starts_with(lit))` — columnar prefix compare on StrVec.
524    FilterStrVecStartsWith(Arc<str>),
525    /// `filter(@.ends_with(lit))` — columnar suffix compare on StrVec.
526    FilterStrVecEndsWith(Arc<str>),
527    /// `filter(@.contains(lit))` — SIMD substring (memchr::memmem) on StrVec.
528    FilterStrVecContains(Arc<str>),
529    /// `map(@.upper())` — ASCII-fast in-lane StrVec→StrVec.
530    MapStrVecUpper,
531    /// `map(@.lower())` — ASCII-fast in-lane StrVec→StrVec.
532    MapStrVecLower,
533    /// `map(@.trim())` — in-lane StrVec→StrVec.
534    MapStrVecTrim,
535    /// `map(@ <op> lit)` / `map(lit <op> @)` — columnar arith over
536    /// IntVec / FloatVec receivers.  `flipped=true` means the literal is
537    /// on the LHS (matters for Sub/Div).  Output lane:
538    ///   IntVec   × Int   × {Add,Sub,Mul,Mod} → IntVec
539    ///   IntVec   × Int   × Div               → FloatVec
540    ///   IntVec   × Float × *                 → FloatVec
541    ///   FloatVec × Int/Float × *             → FloatVec
542    MapNumVecArith { op: super::ast::BinOp, lit: Val, flipped: bool },
543    /// `map(-@)` — unary negation per element, preserves lane.
544    MapNumVecNeg,
545
546    // ── group_by specialisation (Tier 2) ──────────────────────────────────────
547    /// `group_by(k)` where `k` is a single field ident. Uses FxHashMap with
548    /// primitive-key fast path.
549    GroupByField(Arc<str>),
550    /// `.count_by(k)` with trivial field key — per-row `obj.get(k)` instead
551    /// of lambda dispatch; builds Val::Obj<Arc<str>, Int>.
552    CountByField(Arc<str>),
553    /// `.unique_by(k)` with trivial field key — per-row `obj.get(k)` direct.
554    UniqueByField(Arc<str>),
555    /// Fused `filter(p).take_while(q)` — scan while both predicates hold.
556    FilterTakeWhile { pred: Arc<Program>, stop: Arc<Program> },
557    /// Fused `filter(p).drop_while(q)` — skip leading matches of q on
558    /// p-filtered elements.
559    FilterDropWhile { pred: Arc<Program>, drop: Arc<Program> },
560    /// Fused `map(f).unique()` — apply f and dedup by resulting value.
561    MapUnique(Arc<Program>),
562    /// Fused equi-join: TOS is lhs array; `rhs` program evaluates
563    /// to rhs array; join by (lhs_key, rhs_key) string field names.
564    /// Produces array of merged objects (rhs wins on collision).
565    EquiJoin { rhs: Arc<Program>, lhs_key: Arc<str>, rhs_key: Arc<str> },
566
567    // ── Ident lookup (var, then current field) ────────────────────────────────
568    LoadIdent(Arc<str>),
569
570    // ── Binary / unary ops ────────────────────────────────────────────────────
571    Add, Sub, Mul, Div, Mod,
572    Eq, Neq, Lt, Lte, Gt, Gte, Fuzzy, Not, Neg,
573
574    // ── Type cast (`as T`) ───────────────────────────────────────────────────
575    CastOp(super::ast::CastType),
576
577    // ── Short-circuit ops (embed rhs as sub-program) ──────────────────────────
578    AndOp(Arc<Program>),
579    OrOp(Arc<Program>),
580    CoalesceOp(Arc<Program>),
581
582    // ── Method calls ─────────────────────────────────────────────────────────
583    CallMethod(Arc<CompiledCall>),
584    CallOptMethod(Arc<CompiledCall>),
585
586    // ── Construction ─────────────────────────────────────────────────────────
587    MakeObj(Arc<[CompiledObjEntry]>),
588    MakeArr(Arc<[Arc<Program>]>),
589
590    // ── F-string ─────────────────────────────────────────────────────────────
591    FString(Arc<[CompiledFSPart]>),
592
593    // ── Kind check ───────────────────────────────────────────────────────────
594    KindCheck { ty: KindType, negate: bool },
595
596    // ── Pipeline helpers ──────────────────────────────────────────────────────
597    /// Pop TOS → env.current, then push it back (pass-through with context update).
598    SetCurrent,
599    /// TOS → env var by name, TOS remains (for `->` bind).
600    BindVar(Arc<str>),
601    /// Pop TOS → env var (for `let` init).
602    StoreVar(Arc<str>),
603    /// Object destructure bind: TOS obj → multiple vars.
604    BindObjDestructure(Arc<BindObjSpec>),
605    /// Array destructure bind: TOS arr → multiple vars.
606    BindArrDestructure(Arc<[Arc<str>]>),
607
608    // ── Complex (recursive sub-programs) ─────────────────────────────────────
609    LetExpr { name: Arc<str>, body: Arc<Program> },
610    /// Python-style ternary: TOS is cond; branch into `then_` or `else_`.
611    /// Short-circuits — only the taken branch is executed.
612    IfElse { then_: Arc<Program>, else_: Arc<Program> },
613    ListComp(Arc<CompSpec>),
614    DictComp(Arc<DictCompSpec>),
615    SetComp(Arc<CompSpec>),
616
617    // ── Resolution cache fast-path ────────────────────────────────────────────
618    GetPointer(Arc<str>),
619
620    // ── Patch block (delegates to tree-walker eval) ──────────────────────────
621    PatchEval(Arc<super::ast::Expr>),
622}
623
624// ── Program ───────────────────────────────────────────────────────────────────
625
626/// A compiled, immutable v2 program.  Cheap to clone (`Arc` internals).
627#[derive(Debug, Clone)]
628pub struct Program {
629    pub ops:          Arc<[Opcode]>,
630    pub source:       Arc<str>,
631    pub id:           u64,
632    /// True when the program contains only structural navigation opcodes
633    /// (eligible for resolution caching).
634    pub is_structural: bool,
635    /// Inline caches — one `AtomicU64` slot per opcode.  Populated by
636    /// `Opcode::GetField` / `Opcode::OptField` / `Opcode::FieldChain`.
637    ///
638    /// Encoding: `stored_slot = slot_idx + 1` (0 reserved for "unset").
639    /// No Arc-ptr gating — the hit path is `get_index(slot)` + byte-eq
640    /// key verify.  That lets a single slot survive across different
641    /// `Arc<IndexMap>` instances of the same shape, which is the common
642    /// case for repeated queries over distinct docs and for shape-uniform
643    /// array iteration reaching the opcode inside a sub-program.
644    pub ics:          Arc<[AtomicU64]>,
645}
646
647impl Program {
648    fn new(ops: Vec<Opcode>, source: &str) -> Self {
649        let id = hash_str(source);
650        let is_structural = ops.iter().all(|op| matches!(op,
651            Opcode::PushRoot | Opcode::PushCurrent |
652            Opcode::GetField(_) | Opcode::GetIndex(_) |
653            Opcode::GetSlice(..) | Opcode::OptField(_) |
654            Opcode::RootChain(_) | Opcode::FieldChain(_) |
655            Opcode::GetPointer(_)
656        ));
657        let ics = fresh_ics(ops.len());
658        Self {
659            ops: ops.into(),
660            source: source.into(),
661            id,
662            is_structural,
663            ics,
664        }
665    }
666}
667
668/// Per-step inline caches for `Opcode::FieldChain`.  One `AtomicU64` slot per
669/// key in the chain — same encoding as `Program.ics` (`slot_idx + 1`, 0 unset).
670/// Lives inside the opcode rather than the top-level side-table because the
671/// chain length is known only at compile time of that specific opcode.
672#[derive(Debug)]
673pub struct FieldChainData {
674    pub keys: Arc<[Arc<str>]>,
675    pub ics:  Box<[AtomicU64]>,
676}
677
678impl FieldChainData {
679    pub fn new(keys: Arc<[Arc<str>]>) -> Self {
680        let n = keys.len();
681        let mut ics = Vec::with_capacity(n);
682        for _ in 0..n { ics.push(AtomicU64::new(0)); }
683        Self { keys, ics: ics.into_boxed_slice() }
684    }
685    #[inline] pub fn len(&self) -> usize { self.keys.len() }
686    #[inline] pub fn is_empty(&self) -> bool { self.keys.is_empty() }
687}
688
689impl std::ops::Deref for FieldChainData {
690    type Target = [Arc<str>];
691    #[inline] fn deref(&self) -> &[Arc<str>] { &self.keys }
692}
693
694/// Build a fresh IC side-table with one zeroed `AtomicU64` per opcode.
695/// Kept public so other modules that fabricate `Program` values (schema
696/// specialisation, analysis passes) can populate the field.
697pub fn fresh_ics(len: usize) -> Arc<[AtomicU64]> {
698    let mut v = Vec::with_capacity(len);
699    for _ in 0..len { v.push(AtomicU64::new(0)); }
700    v.into()
701}
702
703/// Look up `key` in `m`, using the IC slot as a speculative hint.
704///
705/// IC is ptr-independent: slot survives across different `Arc<IndexMap>`
706/// instances as long as shape (key ordering) is the same.  Hit path is
707/// `get_index(slot) + byte-eq key verify`; miss path is one `get_full`
708/// that also refreshes the slot.  Slot is encoded as `idx + 1` so zero
709/// stays reserved for "unset".
710#[inline]
711fn ic_get_field(m: &Arc<IndexMap<Arc<str>, Val>>, key: &str, ic: &AtomicU64) -> Val {
712    let cached = ic.load(Ordering::Relaxed);
713    if cached != 0 {
714        let slot = (cached - 1) as usize;
715        if let Some((k, v)) = m.get_index(slot) {
716            if k.as_ref() == key { return v.clone(); }
717        }
718    }
719    if let Some((idx, _, v)) = m.get_full(key) {
720        ic.store((idx as u64) + 1, Ordering::Relaxed);
721        v.clone()
722    } else {
723        Val::Null
724    }
725}
726
727/// Recognise `.map(k)` sub-programs that reduce to a single field access
728/// from the current item: `[PushCurrent, GetField(k)]` or bare `[GetField(k)]`.
729/// Lets MapSum/Min/Max/Avg skip the per-item `exec` dispatch.
730#[inline]
731fn trivial_push_str(ops: &[Opcode]) -> Option<Arc<str>> {
732    match ops {
733        [Opcode::PushStr(s)] => Some(s.clone()),
734        _ => None,
735    }
736}
737
738/// Allocate an `Arc<str>` of exactly `bytes.len()` and write ASCII-folded
739/// contents directly into the Arc payload — one allocation, no intermediate
740/// `String`.
741///
742/// # Safety invariants
743/// - Caller must ensure `bytes` is pure ASCII (all bytes < 128).
744///   ASCII case-fold preserves ASCII, which is valid UTF-8.
745/// - `Arc::get_mut(&mut arc).unwrap()` succeeds because `arc` was just
746///   returned by `new_uninit_slice`, so no other strong/weak refs exist.
747/// - All `bytes.len()` bytes are initialised before `assume_init`.
748/// - `Arc::from_raw(... as *const str)` layout-reinterprets the `Arc<[u8]>`
749///   as `Arc<str>`: both share `ArcInner<[u8]>` layout (fat pointer =
750///   data ptr + length), and the payload is valid UTF-8 by invariant 1.
751#[inline]
752fn ascii_fold_to_arc_str(bytes: &[u8], upper: bool) -> Arc<str> {
753    debug_assert!(bytes.is_ascii(), "ascii_fold_to_arc_str: non-ASCII input");
754    let mut arc = Arc::<[u8]>::new_uninit_slice(bytes.len());
755    let slot = Arc::get_mut(&mut arc).unwrap();
756    // SAFETY: see invariants above. `dst` points to `bytes.len()` uninit
757    // bytes owned exclusively by this Arc; writes stay in bounds.
758    unsafe {
759        let dst = slot.as_mut_ptr() as *mut u8;
760        std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len());
761        if upper {
762            for i in 0..bytes.len() { *dst.add(i) = (*dst.add(i)).to_ascii_uppercase(); }
763        } else {
764            for i in 0..bytes.len() { *dst.add(i) = (*dst.add(i)).to_ascii_lowercase(); }
765        }
766    }
767    // SAFETY: all bytes initialised by the loop above.
768    let arc_bytes: Arc<[u8]> = unsafe { arc.assume_init() };
769    // SAFETY: `Arc<[u8]>` and `Arc<str>` share layout (fat pointer over
770    // `ArcInner<T>`). Payload is valid UTF-8: ASCII in + ASCII-preserving
771    // transform = ASCII out.
772    unsafe { Arc::from_raw(Arc::into_raw(arc_bytes) as *const str) }
773}
774
775fn trivial_field(ops: &[Opcode]) -> Option<Arc<str>> {
776    match ops {
777        [Opcode::PushCurrent, Opcode::GetField(k)] => Some(k.clone()),
778        [Opcode::GetField(k)] => Some(k.clone()),
779        // Bare idents in lambda bodies compile to `LoadIdent(k)` which does
780        // var-lookup-then-field fallback; in sub-progs of map/filter/group_by
781        // the var slot is almost never shadowed by a field-name, so treat as
782        // a field read.
783        [Opcode::LoadIdent(k)] => Some(k.clone()),
784        _ => None,
785    }
786}
787
788/// Recognise `.map(a.b.c)` sub-programs that reduce to a walk of a
789/// nested field chain from the current item.  Patterns accepted:
790///   `[PushCurrent, GetField(k1), GetField(k2), …]`
791///   `[PushCurrent, FieldChain([k1, k2, …])]`
792///   `[GetField(k1), GetField(k2), …]`
793///   `[FieldChain([k1, k2, …])]`
794///   `[LoadIdent(k1), GetField(k2), …]`
795///   `[LoadIdent(k1), FieldChain([k2, k3, …])]`
796/// Returns `None` for single-field patterns (those go via `trivial_field`).
797#[inline]
798fn trivial_field_chain(ops: &[Opcode]) -> Option<Arc<[Arc<str>]>> {
799    let mut out: Vec<Arc<str>> = Vec::new();
800    let mut slice = ops;
801    // Optional leading PushCurrent is absorbed silently.
802    if let [Opcode::PushCurrent, rest @ ..] = slice { slice = rest; }
803    // First step: LoadIdent, GetField, or FieldChain.
804    match slice {
805        [Opcode::LoadIdent(k), rest @ ..] => { out.push(k.clone()); slice = rest; }
806        [Opcode::GetField(k), rest @ ..]  => { out.push(k.clone()); slice = rest; }
807        [Opcode::FieldChain(ks), rest @ ..] => {
808            for k in ks.iter() { out.push(k.clone()); }
809            slice = rest;
810        }
811        _ => return None,
812    }
813    // Remaining steps: any mix of GetField / FieldChain.
814    while !slice.is_empty() {
815        match slice {
816            [Opcode::GetField(k), rest @ ..]  => { out.push(k.clone()); slice = rest; }
817            [Opcode::FieldChain(ks), rest @ ..] => {
818                for k in ks.iter() { out.push(k.clone()); }
819                slice = rest;
820            }
821            _ => return None,
822        }
823    }
824    if out.len() < 2 { None } else { Some(Arc::from(out)) }
825}
826
827/// A literal primitive suitable for filter-predicate fusion.
828#[inline]
829fn trivial_literal(op: &Opcode) -> Option<Val> {
830    match op {
831        Opcode::PushNull => Some(Val::Null),
832        Opcode::PushBool(b) => Some(Val::Bool(*b)),
833        Opcode::PushInt(n) => Some(Val::Int(*n)),
834        Opcode::PushFloat(f) => Some(Val::Float(*f)),
835        Opcode::PushStr(s) => Some(Val::Str(s.clone())),
836        _ => None,
837    }
838}
839
840/// Detect `@ <op> lit` or `lit <op> @` — a filter predicate comparing
841/// the current element directly to a literal.  Used to lower filter
842/// on columnar IntVec/FloatVec receivers to a tight slice loop.
843fn detect_current_cmp_lit(ops: &[Opcode]) -> Option<(super::ast::BinOp, Val)> {
844    // Form: [PushCurrent, <lit>, <cmp>]
845    if let [Opcode::PushCurrent, a, b] = ops {
846        if let (Some(lit), Some(op)) = (trivial_literal(a), cmp_opcode(b)) {
847            return Some((op, lit));
848        }
849    }
850    // Form: [<lit>, PushCurrent, <cmp>]  →  flip cmp
851    if let [a, Opcode::PushCurrent, b] = ops {
852        if let (Some(lit), Some(op)) = (trivial_literal(a), cmp_opcode(b)) {
853            return Some((flip_cmp(op), lit));
854        }
855    }
856    None
857}
858
859/// Which StrVec string predicate is recognised at a filter site.
860#[derive(Debug, Clone, Copy)]
861enum StrVecPred { StartsWith, EndsWith, Contains }
862
863/// Detect `@.starts_with(lit)` / `@.ends_with(lit)` / `@.contains(lit)` /
864/// `@.includes(lit)` filter bodies.  Returns which predicate kind and the
865/// literal needle as `Arc<str>`.
866fn detect_current_str_method(ops: &[Opcode]) -> Option<(StrVecPred, Arc<str>)> {
867    // Form: [PushCurrent, CallMethod{<str method>, sub_progs:[[PushStr(lit)]]}]
868    if let [Opcode::PushCurrent, Opcode::CallMethod(b)] = ops {
869        if b.sub_progs.len() != 1 { return None; }
870        let sub = &b.sub_progs[0];
871        if sub.ops.len() != 1 { return None; }
872        let lit = match &sub.ops[0] {
873            Opcode::PushStr(s) => s.clone(),
874            _ => return None,
875        };
876        let kind = match b.method {
877            BuiltinMethod::StartsWith => StrVecPred::StartsWith,
878            BuiltinMethod::EndsWith   => StrVecPred::EndsWith,
879            // `contains` aliases to `includes` at parse time.
880            BuiltinMethod::Includes   => StrVecPred::Contains,
881            _ => return None,
882        };
883        return Some((kind, lit));
884    }
885    None
886}
887
888/// Detect `@.upper()` / `@.lower()` / `@.trim()` map bodies → in-lane StrVec op.
889#[derive(Debug, Clone, Copy)]
890enum StrVecMap { Upper, Lower, Trim }
891
892/// Detect `@ <op> lit` / `lit <op> @` arith map bodies.
893/// `op` is one of Add/Sub/Mul/Div/Mod.  `flipped=true` → literal on LHS.
894fn detect_current_arith_lit(ops: &[Opcode]) -> Option<(super::ast::BinOp, Val, bool)> {
895    use super::ast::BinOp::*;
896    let arith_op = |o: &Opcode| -> Option<super::ast::BinOp> {
897        Some(match o {
898            Opcode::Add => Add, Opcode::Sub => Sub,
899            Opcode::Mul => Mul, Opcode::Div => Div,
900            Opcode::Mod => Mod,
901            _ => return None,
902        })
903    };
904    // Form: [PushCurrent, <lit>, <arith>]
905    if let [Opcode::PushCurrent, a, b] = ops {
906        if let (Some(lit), Some(op)) = (trivial_literal(a), arith_op(b)) {
907            if matches!(lit, Val::Int(_) | Val::Float(_)) {
908                return Some((op, lit, false));
909            }
910        }
911    }
912    // Form: [<lit>, PushCurrent, <arith>]
913    if let [a, Opcode::PushCurrent, b] = ops {
914        if let (Some(lit), Some(op)) = (trivial_literal(a), arith_op(b)) {
915            if matches!(lit, Val::Int(_) | Val::Float(_)) {
916                return Some((op, lit, true));
917            }
918        }
919    }
920    None
921}
922
923/// Detect `[-@]` — unary negation of the current element.
924fn detect_current_neg(ops: &[Opcode]) -> bool {
925    matches!(ops, [Opcode::PushCurrent, Opcode::Neg])
926}
927
928fn detect_current_str_nullary(ops: &[Opcode]) -> Option<StrVecMap> {
929    if let [Opcode::PushCurrent, Opcode::CallMethod(b)] = ops {
930        if !b.sub_progs.is_empty() { return None; }
931        return Some(match b.method {
932            BuiltinMethod::Upper => StrVecMap::Upper,
933            BuiltinMethod::Lower => StrVecMap::Lower,
934            BuiltinMethod::Trim  => StrVecMap::Trim,
935            _ => return None,
936        });
937    }
938    None
939}
940
941/// Detect a filter-predicate sub-program of shape `field <op> literal`,
942/// `literal <op> field`, or `field1 <op> field2`. Returns one of three variants
943/// so the caller can pick the right fused opcode.
944#[derive(Debug)]
945enum FieldPred {
946    FieldCmpLit(Arc<str>, super::ast::BinOp, Val),
947    FieldCmpField(Arc<str>, super::ast::BinOp, Arc<str>),
948}
949
950fn flip_cmp(op: super::ast::BinOp) -> super::ast::BinOp {
951    use super::ast::BinOp::*;
952    match op {
953        Lt => Gt, Gt => Lt, Lte => Gte, Gte => Lte,
954        other => other,
955    }
956}
957
958fn cmp_opcode(op: &Opcode) -> Option<super::ast::BinOp> {
959    use super::ast::BinOp::*;
960    Some(match op {
961        Opcode::Eq => Eq, Opcode::Neq => Neq,
962        Opcode::Lt => Lt, Opcode::Lte => Lte,
963        Opcode::Gt => Gt, Opcode::Gte => Gte,
964        _ => return None,
965    })
966}
967
968/// Patterns recognised for a filter-lambda body:
969///   `[PushCurrent, GetField(k), PushLit, <cmp>]`
970///   `[PushLit, PushCurrent, GetField(k), <cmp>]`
971///   `[PushCurrent, GetField(k1), PushCurrent, GetField(k2), <cmp>]`
972fn detect_field_pred(ops: &[Opcode]) -> Option<FieldPred> {
973    // Helper: match a single-op "field read" — PushCurrent+GetField, GetField
974    // alone, or LoadIdent (var fallback to field).
975    #[inline]
976    fn field_read_prefix(ops: &[Opcode]) -> Option<(Arc<str>, usize)> {
977        match ops.first()? {
978            Opcode::LoadIdent(k) => Some((k.clone(), 1)),
979            Opcode::GetField(k) => Some((k.clone(), 1)),
980            Opcode::PushCurrent => {
981                if let Some(Opcode::GetField(k)) = ops.get(1) {
982                    Some((k.clone(), 2))
983                } else { None }
984            }
985            _ => None,
986        }
987    }
988    // Form 1: field <op> literal
989    if let Some((k, n)) = field_read_prefix(ops) {
990        if let (Some(lit_op), Some(cmp_op)) = (ops.get(n), ops.get(n + 1)) {
991            if ops.len() == n + 2 {
992                if let (Some(lit), Some(op)) = (trivial_literal(lit_op), cmp_opcode(cmp_op)) {
993                    return Some(FieldPred::FieldCmpLit(k, op, lit));
994                }
995            }
996        }
997        // Form 3: field1 <op> field2
998        if let Some((k2, n2_extra)) = ops.get(n).and_then(|_| {
999            let tail = &ops[n..];
1000            field_read_prefix(tail).map(|(kk, nn)| (kk, nn))
1001        }) {
1002            if let Some(cmp_op) = ops.get(n + n2_extra) {
1003                if ops.len() == n + n2_extra + 1 {
1004                    if let Some(op) = cmp_opcode(cmp_op) {
1005                        return Some(FieldPred::FieldCmpField(k, op, k2));
1006                    }
1007                }
1008            }
1009        }
1010    }
1011    // Form 2: literal <op> field (flip)
1012    if let Some(lit) = ops.first().and_then(trivial_literal) {
1013        if let Some((k, n2)) = field_read_prefix(&ops[1..]) {
1014            if let Some(cmp_op) = ops.get(1 + n2) {
1015                if ops.len() == 1 + n2 + 1 {
1016                    if let Some(op) = cmp_opcode(cmp_op) {
1017                        return Some(FieldPred::FieldCmpLit(k, flip_cmp(op), lit));
1018                    }
1019                }
1020            }
1021        }
1022    }
1023    None
1024}
1025
1026/// Detect a predicate body that is a conjunction (AND-chain) of
1027/// `field <cmp> lit` comparisons.  Returns the flat list of
1028/// `(field, cmp_op, lit)` triples when the entire pred reduces to
1029/// `f1 <o1> l1 AND f2 <o2> l2 AND ...`.
1030///
1031/// Pattern accepted (N ≥ 2):
1032///   `[⟨field cmp lit⟩, AndOp(⟨field cmp lit⟩), AndOp(⟨field cmp lit⟩), …]`
1033fn detect_field_cmp_conjuncts(ops: &[Opcode]) -> Option<Vec<(Arc<str>, super::ast::BinOp, Val)>> {
1034    let mut triples: Vec<(Arc<str>, super::ast::BinOp, Val)> = Vec::new();
1035    let first_and = ops.iter().position(|o| matches!(o, Opcode::AndOp(_)));
1036    let first_len = first_and.unwrap_or(ops.len());
1037    match detect_field_pred(&ops[..first_len])? {
1038        FieldPred::FieldCmpLit(k, op, lit) => triples.push((k, op, lit)),
1039        _ => return None,
1040    }
1041    for op in &ops[first_len..] {
1042        if let Opcode::AndOp(sub) = op {
1043            match detect_field_pred(&sub.ops)? {
1044                FieldPred::FieldCmpLit(k, op, lit) => triples.push((k, op, lit)),
1045                _ => return None,
1046            }
1047        } else {
1048            return None;
1049        }
1050    }
1051    if triples.len() >= 2 { Some(triples) } else { None }
1052}
1053
1054/// Convenience: all conjuncts are Eq → produce the flat (field, lit) form
1055/// used by `FilterFieldsAllEqLitCount`.
1056fn detect_field_eq_conjuncts(ops: &[Opcode]) -> Option<Vec<(Arc<str>, Val)>> {
1057    let triples = detect_field_cmp_conjuncts(ops)?;
1058    triples.into_iter()
1059        .map(|(k, op, v)| if matches!(op, super::ast::BinOp::Eq) { Some((k, v)) } else { None })
1060        .collect()
1061}
1062
1063/// Compare two `Val`s using a binary comparison operator. Only implements the
1064/// semantics needed for filter predicate fusion; falls back to cmp_vals for
1065/// ordering comparisons.
1066#[inline]
1067fn cmp_val_binop(a: &Val, op: super::ast::BinOp, b: &Val) -> bool {
1068    use super::ast::BinOp::*;
1069    use std::cmp::Ordering;
1070    match op {
1071        Eq => crate::eval::util::vals_eq(a, b),
1072        Neq => !crate::eval::util::vals_eq(a, b),
1073        Lt | Lte | Gt | Gte => {
1074            let ord = crate::eval::util::cmp_vals(a, b);
1075            match op {
1076                Lt  => ord == Ordering::Less,
1077                Lte => ord != Ordering::Greater,
1078                Gt  => ord == Ordering::Greater,
1079                Gte => ord != Ordering::Less,
1080                _ => unreachable!(),
1081            }
1082        }
1083        _ => false,
1084    }
1085}
1086
1087/// `group_by(k)` where `k` is a bare field ident. Builds an object whose keys
1088/// are the distinct field values stringified, mapping to arrays of items.
1089/// Preserves first-seen key order.
1090/// Resolve char-index based slice bounds into byte offsets.
1091/// Returns `(start_byte, end_byte)` within `src`.
1092fn slice_unicode_bounds(src: &str, start: i64, end: Option<i64>) -> (usize, usize) {
1093    let total_chars = src.chars().count() as i64;
1094    let start_u = if start < 0 {
1095        total_chars.saturating_sub(-start).max(0) as usize
1096    } else { start as usize };
1097    let end_u = match end {
1098        Some(e) if e < 0 => total_chars.saturating_sub(-e).max(0) as usize,
1099        Some(e) => e as usize,
1100        None    => total_chars as usize,
1101    };
1102    let mut start_b = src.len();
1103    let mut end_b = src.len();
1104    let mut found_start = false;
1105    for (ci, (bi, _)) in src.char_indices().enumerate() {
1106        if !found_start && ci == start_u {
1107            start_b = bi;
1108            found_start = true;
1109        }
1110        if ci == end_u {
1111            end_b = bi;
1112            return (start_b.min(end_b), end_b);
1113        }
1114    }
1115    if !found_start { start_b = src.len(); }
1116    (start_b, end_b)
1117}
1118
1119fn count_by_field(recv: &Val, k: &str) -> Val {
1120    let a = match recv {
1121        Val::Arr(a) => a,
1122        _ => return Val::obj(indexmap::IndexMap::new()),
1123    };
1124    let mut out: indexmap::IndexMap<Arc<str>, i64> = indexmap::IndexMap::with_capacity(16);
1125    let mut cached: Option<usize> = None;
1126    for item in a.iter() {
1127        let key: Arc<str> = if let Val::Obj(m) = item {
1128            let v = lookup_field_by_str_cached(m, k, &mut cached);
1129            match v {
1130                Some(Val::Str(s)) => s.clone(),
1131                Some(Val::StrSlice(r)) => r.to_arc(),
1132                Some(Val::Int(n)) => Arc::from(n.to_string()),
1133                Some(Val::Float(x)) => Arc::from(x.to_string()),
1134                Some(Val::Bool(b)) => Arc::from(if *b { "true" } else { "false" }),
1135                Some(Val::Null) | None => Arc::from("null"),
1136                Some(other) => Arc::from(format!("{:?}", other)),
1137            }
1138        } else if let Val::ObjSmall(ps) = item {
1139            let mut found: Option<&Val> = None;
1140            for (kk, vv) in ps.iter() {
1141                if kk.as_ref() == k { found = Some(vv); break; }
1142            }
1143            match found {
1144                Some(Val::Str(s)) => s.clone(),
1145                Some(Val::StrSlice(r)) => r.to_arc(),
1146                Some(Val::Int(n)) => Arc::from(n.to_string()),
1147                Some(Val::Float(x)) => Arc::from(x.to_string()),
1148                Some(Val::Bool(b)) => Arc::from(if *b { "true" } else { "false" }),
1149                Some(Val::Null) | None => Arc::from("null"),
1150                Some(other) => Arc::from(format!("{:?}", other)),
1151            }
1152        } else {
1153            Arc::from("null")
1154        };
1155        *out.entry(key).or_insert(0) += 1;
1156    }
1157    let finalised: indexmap::IndexMap<Arc<str>, Val> = out.into_iter()
1158        .map(|(k, n)| (k, Val::Int(n)))
1159        .collect();
1160    Val::obj(finalised)
1161}
1162
1163fn unique_by_field(recv: &Val, k: &str) -> Val {
1164    let a = match recv {
1165        Val::Arr(a) => a,
1166        _ => return Val::arr(Vec::new()),
1167    };
1168    let mut seen: indexmap::IndexSet<Arc<str>> = indexmap::IndexSet::with_capacity(a.len());
1169    let mut out: Vec<Val> = Vec::with_capacity(a.len());
1170    let mut cached: Option<usize> = None;
1171    for item in a.iter() {
1172        let key: Arc<str> = if let Val::Obj(m) = item {
1173            let v = lookup_field_by_str_cached(m, k, &mut cached);
1174            match v {
1175                Some(Val::Str(s)) => s.clone(),
1176                Some(Val::StrSlice(r)) => r.to_arc(),
1177                Some(Val::Int(n)) => Arc::from(n.to_string()),
1178                Some(Val::Float(x)) => Arc::from(x.to_string()),
1179                Some(Val::Bool(b)) => Arc::from(if *b { "true" } else { "false" }),
1180                Some(Val::Null) | None => Arc::from("null"),
1181                Some(other) => Arc::from(format!("{:?}", other)),
1182            }
1183        } else {
1184            Arc::from("null")
1185        };
1186        if seen.insert(key) { out.push(item.clone()); }
1187    }
1188    Val::arr(out)
1189}
1190
1191fn group_by_field(recv: &Val, k: &str) -> Val {
1192    let a = match recv {
1193        Val::Arr(a) => a,
1194        _ => return Val::obj(indexmap::IndexMap::new()),
1195    };
1196    let mut out: indexmap::IndexMap<Arc<str>, Vec<Val>> = indexmap::IndexMap::with_capacity(16);
1197    let mut cached: Option<usize> = None;
1198    for item in a.iter() {
1199        let key = if let Val::Obj(m) = item {
1200            let v = lookup_field_by_str_cached(m, k, &mut cached);
1201            match v {
1202                Some(Val::Str(s)) => s.clone(),
1203                Some(Val::Int(n)) => Arc::from(n.to_string()),
1204                Some(Val::Float(x)) => Arc::from(x.to_string()),
1205                Some(Val::Bool(b)) => Arc::from(if *b { "true" } else { "false" }),
1206                Some(Val::Null) | None => Arc::from("null"),
1207                Some(other) => Arc::from(format!("{:?}", other)),
1208            }
1209        } else {
1210            Arc::from("null")
1211        };
1212        out.entry(key).or_insert_with(|| Vec::with_capacity(4)).push(item.clone());
1213    }
1214    let finalised: indexmap::IndexMap<Arc<str>, Val> = out.into_iter()
1215        .map(|(k, v)| (k, Val::arr(v)))
1216        .collect();
1217    Val::obj(finalised)
1218}
1219
1220/// Shape-index cache: cheap inline-cache for repeated `m.get(k)` on arrays of
1221/// same-shape objects. First call stores `get_index_of(k)`; subsequent calls
1222/// try `get_index(i)` and verify key identity (Arc<str> pointer or bytes).
1223/// Fallback to `m.get(k)` on miss.
1224#[inline]
1225fn lookup_field_cached<'a>(
1226    m: &'a indexmap::IndexMap<Arc<str>, Val>,
1227    k: &Arc<str>,
1228    cached: &mut Option<usize>,
1229) -> Option<&'a Val> {
1230    if let Some(i) = *cached {
1231        if let Some((ki, vi)) = m.get_index(i) {
1232            if Arc::ptr_eq(ki, k) || ki.as_ref() == k.as_ref() {
1233                return Some(vi);
1234            }
1235        }
1236    }
1237    match m.get_full(k.as_ref()) {
1238        Some((i, _, v)) => { *cached = Some(i); Some(v) }
1239        None => { *cached = None; None }
1240    }
1241}
1242
1243/// Initial capacity hint for filter `out` Vecs.
1244/// Assumes ~25% selectivity; avoids zero-cap realloc storm while not
1245/// over-reserving on highly selective predicates.  Capped at receiver len.
1246#[inline]
1247fn filter_cap_hint(recv_len: usize) -> usize {
1248    (recv_len / 4 + 4).min(recv_len)
1249}
1250
1251/// Accumulate lambda pattern tag — selects which fused binop to run.
1252#[derive(Copy, Clone)]
1253enum AccumOp { Add, Sub, Mul }
1254
1255// ── Typed-numeric aggregate fast-paths ────────────────────────────────────────
1256// Direct loops over `&[Val]` for mono-typed or mixed Int/Float arrays.  Used
1257// by the bare `.sum()/.min()/.max()/.avg()` no-arg method-call fast path in
1258// `exec_call` — skips registry dispatch, `into_vec()` clone, and the extra
1259// `.filter().collect()` that `func_aggregates::collect_nums` performs.
1260//
1261// Semantics match `func_aggregates`: non-numeric items are skipped; Int-only
1262// arrays stay on `i64` (no lossy widening); Float appearance widens once.
1263
1264#[inline]
1265fn agg_sum_typed(a: &[Val]) -> Val {
1266    // Tight i64 loop until first Float; then switch to f64 loop.
1267    let mut i_acc: i64 = 0;
1268    let mut it = a.iter();
1269    while let Some(v) = it.next() {
1270        match v {
1271            Val::Int(n) => i_acc = i_acc.wrapping_add(*n),
1272            Val::Float(x) => {
1273                let mut f_acc = i_acc as f64 + *x;
1274                for v in it {
1275                    match v {
1276                        Val::Int(n)   => f_acc += *n as f64,
1277                        Val::Float(x) => f_acc += *x,
1278                        _ => {}
1279                    }
1280                }
1281                return Val::Float(f_acc);
1282            }
1283            _ => {} // skip non-numeric
1284        }
1285    }
1286    Val::Int(i_acc)
1287}
1288
1289#[inline]
1290fn agg_avg_typed(a: &[Val]) -> Val {
1291    let mut sum: f64 = 0.0;
1292    let mut n: usize = 0;
1293    for v in a {
1294        match v {
1295            Val::Int(x)   => { sum += *x as f64; n += 1; }
1296            Val::Float(x) => { sum += *x;        n += 1; }
1297            _ => {}
1298        }
1299    }
1300    if n == 0 { Val::Null } else { Val::Float(sum / n as f64) }
1301}
1302
1303#[inline]
1304fn agg_minmax_typed(a: &[Val], want_max: bool) -> Val {
1305    let mut it = a.iter();
1306    // Find first number.
1307    let first = loop {
1308        match it.next() {
1309            Some(v) if v.is_number() => break v,
1310            Some(_) => continue,
1311            None    => return Val::Null,
1312        }
1313    };
1314    match first {
1315        Val::Int(n0) => {
1316            let mut best: i64 = *n0;
1317            // Mono-Int tight loop; on first Float, promote.
1318            while let Some(v) = it.next() {
1319                match v {
1320                    Val::Int(n) => {
1321                        let n = *n;
1322                        if want_max { if n > best { best = n; } }
1323                        else        { if n < best { best = n; } }
1324                    }
1325                    Val::Float(x) => {
1326                        let x = *x;
1327                        let mut best_f = best as f64;
1328                        if want_max { if x > best_f { best_f = x; } }
1329                        else        { if x < best_f { best_f = x; } }
1330                        for v in it {
1331                            match v {
1332                                Val::Int(n) => {
1333                                    let n = *n as f64;
1334                                    if want_max { if n > best_f { best_f = n; } }
1335                                    else        { if n < best_f { best_f = n; } }
1336                                }
1337                                Val::Float(x) => {
1338                                    let x = *x;
1339                                    if want_max { if x > best_f { best_f = x; } }
1340                                    else        { if x < best_f { best_f = x; } }
1341                                }
1342                                _ => {}
1343                            }
1344                        }
1345                        return Val::Float(best_f);
1346                    }
1347                    _ => {}
1348                }
1349            }
1350            Val::Int(best)
1351        }
1352        Val::Float(x0) => {
1353            let mut best_f: f64 = *x0;
1354            for v in it {
1355                match v {
1356                    Val::Int(n) => {
1357                        let n = *n as f64;
1358                        if want_max { if n > best_f { best_f = n; } }
1359                        else        { if n < best_f { best_f = n; } }
1360                    }
1361                    Val::Float(x) => {
1362                        let x = *x;
1363                        if want_max { if x > best_f { best_f = x; } }
1364                        else        { if x < best_f { best_f = x; } }
1365                    }
1366                    _ => {}
1367                }
1368            }
1369            Val::Float(best_f)
1370        }
1371        _ => Val::Null,
1372    }
1373}
1374
1375/// `&str`-keyed variant of `lookup_field_cached`; ptr-eq shortcut is skipped
1376/// (caller doesn't hold an `Arc<str>`), so the hit path is byte-eq only.
1377#[inline]
1378fn lookup_field_by_str_cached<'a>(
1379    m: &'a indexmap::IndexMap<Arc<str>, Val>,
1380    k: &str,
1381    cached: &mut Option<usize>,
1382) -> Option<&'a Val> {
1383    if let Some(i) = *cached {
1384        if let Some((ki, vi)) = m.get_index(i) {
1385            if ki.as_ref() == k {
1386                return Some(vi);
1387            }
1388        }
1389    }
1390    match m.get_full(k) {
1391        Some((i, _, v)) => { *cached = Some(i); Some(v) }
1392        None => { *cached = None; None }
1393    }
1394}
1395
1396// ── Variable context (compile-time) ──────────────────────────────────────────
1397
1398#[derive(Clone, Default)]
1399struct VarCtx {
1400    known: SmallVec<[Arc<str>; 4]>,
1401}
1402
1403impl VarCtx {
1404    fn with_var(&self, name: &str) -> Self {
1405        let mut v = self.clone();
1406        if !v.known.iter().any(|k| k.as_ref() == name) {
1407            v.known.push(Arc::from(name));
1408        }
1409        v
1410    }
1411    fn with_vars(&self, names: &[String]) -> Self {
1412        let mut v = self.clone();
1413        for n in names {
1414            if !v.known.iter().any(|k| k.as_ref() == n.as_str()) {
1415                v.known.push(Arc::from(n.as_str()));
1416            }
1417        }
1418        v
1419    }
1420    fn has(&self, name: &str) -> bool {
1421        self.known.iter().any(|k| k.as_ref() == name)
1422    }
1423}
1424
1425// ── Compiler ─────────────────────────────────────────────────────────────────
1426
1427pub struct Compiler;
1428
1429impl Compiler {
1430    pub fn compile(expr: &Expr, source: &str) -> Program {
1431        let mut e = expr.clone();
1432        Self::reorder_and_operands(&mut e);
1433        let ctx = VarCtx::default();
1434        let ops = Self::optimize(Self::emit(&e, &ctx));
1435        let prog = Program::new(ops, source);
1436        // Post-pass: canonicalise identical sub-programs.
1437        let deduped = super::analysis::dedup_subprograms(&prog);
1438        let ics = fresh_ics(deduped.ops.len());
1439        Program {
1440            ops:           deduped.ops.clone(),
1441            source:        prog.source,
1442            id:            prog.id,
1443            is_structural: prog.is_structural,
1444            ics,
1445        }
1446    }
1447
1448    /// AST rewrite: for each `a and b`, if `b` is more selective than `a`,
1449    /// swap operands so the cheaper/selective predicate runs first.  Safe
1450    /// because `and` is commutative on pure, side-effect-free expressions.
1451    fn reorder_and_operands(expr: &mut Expr) {
1452        use super::analysis::selectivity_score;
1453        match expr {
1454            Expr::BinOp(l, op, r) if *op == BinOp::And => {
1455                Self::reorder_and_operands(l);
1456                Self::reorder_and_operands(r);
1457                if selectivity_score(r) < selectivity_score(l) {
1458                    std::mem::swap(l, r);
1459                }
1460            }
1461            Expr::BinOp(l, _, r) => {
1462                Self::reorder_and_operands(l);
1463                Self::reorder_and_operands(r);
1464            }
1465            Expr::UnaryNeg(e) | Expr::Not(e) | Expr::Kind { expr: e, .. } =>
1466                Self::reorder_and_operands(e),
1467            Expr::Coalesce(l, r) => {
1468                Self::reorder_and_operands(l);
1469                Self::reorder_and_operands(r);
1470            }
1471            Expr::Chain(base, steps) => {
1472                Self::reorder_and_operands(base);
1473                for s in steps {
1474                    match s {
1475                        super::ast::Step::DynIndex(e) | super::ast::Step::InlineFilter(e) =>
1476                            Self::reorder_and_operands(e),
1477                        super::ast::Step::Method(_, args) | super::ast::Step::OptMethod(_, args) =>
1478                            for a in args { match a {
1479                                super::ast::Arg::Pos(e) | super::ast::Arg::Named(_, e) =>
1480                                    Self::reorder_and_operands(e),
1481                            } },
1482                        _ => {}
1483                    }
1484                }
1485            }
1486            Expr::Let { init, body, .. } => {
1487                Self::reorder_and_operands(init);
1488                Self::reorder_and_operands(body);
1489            }
1490            Expr::Pipeline { base, steps } => {
1491                Self::reorder_and_operands(base);
1492                for s in steps {
1493                    if let super::ast::PipeStep::Forward(e) = s {
1494                        Self::reorder_and_operands(e);
1495                    }
1496                }
1497            }
1498            Expr::Object(fields) => for f in fields { match f {
1499                super::ast::ObjField::Kv { val, .. } => Self::reorder_and_operands(val),
1500                super::ast::ObjField::Dynamic { key, val } => {
1501                    Self::reorder_and_operands(key);
1502                    Self::reorder_and_operands(val);
1503                }
1504                super::ast::ObjField::Spread(e) => Self::reorder_and_operands(e),
1505                _ => {}
1506            } },
1507            Expr::Array(elems) => for e in elems { match e {
1508                super::ast::ArrayElem::Expr(e) | super::ast::ArrayElem::Spread(e) =>
1509                    Self::reorder_and_operands(e),
1510            } },
1511            Expr::ListComp { expr, iter, cond, .. }
1512            | Expr::SetComp  { expr, iter, cond, .. }
1513            | Expr::GenComp  { expr, iter, cond, .. } => {
1514                Self::reorder_and_operands(expr);
1515                Self::reorder_and_operands(iter);
1516                if let Some(c) = cond { Self::reorder_and_operands(c); }
1517            }
1518            Expr::DictComp { key, val, iter, cond, .. } => {
1519                Self::reorder_and_operands(key);
1520                Self::reorder_and_operands(val);
1521                Self::reorder_and_operands(iter);
1522                if let Some(c) = cond { Self::reorder_and_operands(c); }
1523            }
1524            Expr::Lambda { body, .. } => Self::reorder_and_operands(body),
1525            Expr::GlobalCall { args, .. } => for a in args { match a {
1526                super::ast::Arg::Pos(e) | super::ast::Arg::Named(_, e) =>
1527                    Self::reorder_and_operands(e),
1528            } },
1529            _ => {}
1530        }
1531    }
1532
1533    pub fn compile_str(input: &str) -> Result<Program, EvalError> {
1534        let expr = super::parser::parse(input)
1535            .map_err(|e| EvalError(e.to_string()))?;
1536        Ok(Self::compile(&expr, input))
1537    }
1538
1539    /// Compile with explicit pass configuration.  Cached by callers
1540    /// under `(config.hash(), expr)`.
1541    pub fn compile_str_with_config(input: &str, config: PassConfig) -> Result<Program, EvalError> {
1542        let expr = super::parser::parse(input)
1543            .map_err(|e| EvalError(e.to_string()))?;
1544        let mut e = expr.clone();
1545        if config.reorder_and { Self::reorder_and_operands(&mut e); }
1546        let ctx = VarCtx::default();
1547        let ops = Self::optimize_with(Self::emit(&e, &ctx), config);
1548        let prog = Program::new(ops, input);
1549        if config.dedup_subprogs {
1550            let deduped = super::analysis::dedup_subprograms(&prog);
1551            let ics = fresh_ics(deduped.ops.len());
1552            Ok(Program {
1553                ops:           deduped.ops.clone(),
1554                source:        prog.source,
1555                id:            prog.id,
1556                is_structural: prog.is_structural,
1557                ics,
1558            })
1559        } else {
1560            Ok(prog)
1561        }
1562    }
1563
1564    // ── Peephole optimizer ────────────────────────────────────────────────────
1565
1566    fn optimize(ops: Vec<Opcode>) -> Vec<Opcode> {
1567        Self::optimize_with(ops, PassConfig::default())
1568    }
1569
1570    fn optimize_with(ops: Vec<Opcode>, cfg: PassConfig) -> Vec<Opcode> {
1571        let ops = if cfg.root_chain      { Self::pass_root_chain(ops) }      else { ops };
1572        let ops = if cfg.field_chain     { Self::pass_field_chain(ops) }     else { ops };
1573        let ops = if cfg.filter_count    { Self::pass_filter_count(ops) }    else { ops };
1574        let ops = if cfg.filter_fusion   { Self::pass_filter_fusion(ops) }   else { ops };
1575        let ops = if cfg.filter_fusion   { Self::pass_string_chain_fusion(ops) } else { ops };
1576        let ops = if cfg.find_quantifier { Self::pass_find_quantifier(ops) } else { ops };
1577        let ops = if cfg.filter_fusion   { Self::pass_field_specialise(ops) } else { ops };
1578        let ops = Self::pass_list_comp_specialise(ops);
1579        let ops = if cfg.strength_reduce { Self::pass_strength_reduce(ops) } else { ops };
1580        let ops = if cfg.redundant_ops   { Self::pass_redundant_ops(ops) }   else { ops };
1581        let ops = if cfg.kind_check_fold { Self::pass_kind_check_fold(ops) } else { ops };
1582        let ops = if cfg.method_const    { Self::pass_method_const_fold(ops)} else { ops };
1583        let ops = if cfg.const_fold      { Self::pass_const_fold(ops) }      else { ops };
1584        let ops = if cfg.nullness        { Self::pass_nullness_opt_field(ops)} else { ops };
1585        let ops = if cfg.equi_join       { Self::pass_equi_join_fusion(ops) } else { ops };
1586        ops
1587    }
1588
1589    /// Rewrite `CallMethod(equi_join, [rhs, PushStr(lk), PushStr(rk)])`
1590    /// to the fused `EquiJoin` opcode — removes runtime method dispatch
1591    /// and extracts string keys into the opcode so the executor can
1592    /// hash directly.
1593    fn pass_equi_join_fusion(ops: Vec<Opcode>) -> Vec<Opcode> {
1594        let mut out: Vec<Opcode> = Vec::with_capacity(ops.len());
1595        for op in ops {
1596            if let Opcode::CallMethod(c) = &op {
1597                if c.method == BuiltinMethod::EquiJoin && c.sub_progs.len() == 3 {
1598                    let rhs = Arc::clone(&c.sub_progs[0]);
1599                    let lhs_key = const_str_program(&c.sub_progs[1]);
1600                    let rhs_key = const_str_program(&c.sub_progs[2]);
1601                    if let (Some(lk), Some(rk)) = (lhs_key, rhs_key) {
1602                        out.push(Opcode::EquiJoin { rhs, lhs_key: lk, rhs_key: rk });
1603                        continue;
1604                    }
1605                }
1606            }
1607            out.push(op);
1608        }
1609        out
1610    }
1611
1612    /// Nullness-driven: when the preceding op provably leaves a non-null
1613    /// receiver on the stack, rewrite `OptField(k)` → `GetField(k)`.
1614    /// Conservative: only folds when the predecessor is a construction
1615    /// opcode (MakeObj / MakeArr / PushStr / RootChain / GetField).
1616    fn pass_nullness_opt_field(ops: Vec<Opcode>) -> Vec<Opcode> {
1617        let mut out: Vec<Opcode> = Vec::with_capacity(ops.len());
1618        for op in ops {
1619            if let Opcode::OptField(k) = &op {
1620                // Only safe when receiver is provably a non-null object —
1621                // MakeObj is the only opcode that guarantees this without
1622                // a schema.  Other cases are handled by schema::specialize.
1623                let non_null = matches!(out.last(), Some(Opcode::MakeObj(_)));
1624                if non_null {
1625                    out.push(Opcode::GetField(k.clone()));
1626                    continue;
1627                }
1628            }
1629            out.push(op);
1630        }
1631        out
1632    }
1633
1634    /// Fold built-in methods when receiver is a literal with known length/content:
1635    ///   PushStr(s) + .len()    → PushInt(utf8 char count)
1636    ///   PushStr(s) + .upper()  → PushStr(upper)
1637    ///   PushStr(s) + .lower()  → PushStr(lower)
1638    ///   PushStr(s) + .trim()   → PushStr(trim)
1639    ///   MakeArr(n elems) + .len()  → PushInt(n)  (only for non-spread arrays)
1640    fn pass_method_const_fold(ops: Vec<Opcode>) -> Vec<Opcode> {
1641        let mut out: Vec<Opcode> = Vec::with_capacity(ops.len());
1642        for op in ops {
1643            if let Opcode::CallMethod(c) = &op {
1644                if c.sub_progs.is_empty() {
1645                    match (out.last(), c.method) {
1646                        (Some(Opcode::PushStr(s)), BuiltinMethod::Len) => {
1647                            let n = s.chars().count() as i64;
1648                            out.pop();
1649                            out.push(Opcode::PushInt(n));
1650                            continue;
1651                        }
1652                        (Some(Opcode::PushStr(s)), BuiltinMethod::Upper) => {
1653                            let u: Arc<str> = Arc::from(s.to_uppercase());
1654                            out.pop();
1655                            out.push(Opcode::PushStr(u));
1656                            continue;
1657                        }
1658                        (Some(Opcode::PushStr(s)), BuiltinMethod::Lower) => {
1659                            let u: Arc<str> = Arc::from(s.to_lowercase());
1660                            out.pop();
1661                            out.push(Opcode::PushStr(u));
1662                            continue;
1663                        }
1664                        (Some(Opcode::PushStr(s)), BuiltinMethod::Trim) => {
1665                            let u: Arc<str> = Arc::from(s.trim());
1666                            out.pop();
1667                            out.push(Opcode::PushStr(u));
1668                            continue;
1669                        }
1670                        (Some(Opcode::MakeArr(progs)), BuiltinMethod::Len) => {
1671                            let n = progs.len() as i64;
1672                            out.pop();
1673                            out.push(Opcode::PushInt(n));
1674                            continue;
1675                        }
1676                        _ => {}
1677                    }
1678                }
1679            }
1680            out.push(op);
1681        }
1682        out
1683    }
1684
1685    /// Fold `KindCheck` when its input type is a literal push:
1686    ///   PushInt(n)  + KindCheck{number, neg} → PushBool(!neg)
1687    ///   PushStr(_)  + KindCheck{string, neg} → PushBool(!neg)
1688    ///   PushNull    + KindCheck{null, neg}   → PushBool(!neg)
1689    ///   PushBool(_) + KindCheck{bool, neg}   → PushBool(!neg)
1690    ///   mismatches fold to opposite.
1691    fn pass_kind_check_fold(ops: Vec<Opcode>) -> Vec<Opcode> {
1692        use super::analysis::{fold_kind_check, VType};
1693        let mut out = Vec::with_capacity(ops.len());
1694        for op in ops {
1695            if let Opcode::KindCheck { ty, negate } = &op {
1696                let prev_ty: Option<VType> = match out.last() {
1697                    Some(Opcode::PushNull)     => Some(VType::Null),
1698                    Some(Opcode::PushBool(_))  => Some(VType::Bool),
1699                    Some(Opcode::PushInt(_))   => Some(VType::Int),
1700                    Some(Opcode::PushFloat(_)) => Some(VType::Float),
1701                    Some(Opcode::PushStr(_))   => Some(VType::Str),
1702                    Some(Opcode::MakeArr(_))   => Some(VType::Arr),
1703                    Some(Opcode::MakeObj(_))   => Some(VType::Obj),
1704                    _ => None,
1705                };
1706                if let Some(vt) = prev_ty {
1707                    if let Some(b) = fold_kind_check(vt, *ty, *negate) {
1708                        out.pop();
1709                        out.push(Opcode::PushBool(b));
1710                        continue;
1711                    }
1712                }
1713            }
1714            out.push(op);
1715        }
1716        out
1717    }
1718
1719    /// Fuse adjacent method calls into single-pass fused opcodes:
1720    ///   filter(p) + map(f)     → FilterMap
1721    ///   filter(p1) + filter(p2)→ FilterFilter
1722    ///   map(f1) + map(f2)      → MapMap
1723    fn pass_filter_fusion(ops: Vec<Opcode>) -> Vec<Opcode> {
1724        let mut out: Vec<Opcode> = Vec::with_capacity(ops.len());
1725        for op in ops {
1726            // FilterMap + sum()/avg()/first() → three-way fusion
1727            if let Opcode::CallMethod(b) = &op {
1728                if b.sub_progs.is_empty() {
1729                    if let Some(Opcode::FilterMap { pred, map }) = out.last() {
1730                        let pred = Arc::clone(pred);
1731                        let map = Arc::clone(map);
1732                        let fused = match b.method {
1733                            BuiltinMethod::Sum => Some(Opcode::FilterMapSum { pred, map }),
1734                            BuiltinMethod::Avg => Some(Opcode::FilterMapAvg { pred, map }),
1735                            BuiltinMethod::First => Some(Opcode::FilterMapFirst { pred, map }),
1736                            BuiltinMethod::Min => Some(Opcode::FilterMapMin { pred, map }),
1737                            BuiltinMethod::Max => Some(Opcode::FilterMapMax { pred, map }),
1738                            _ => None,
1739                        };
1740                        if let Some(o) = fused {
1741                            out.pop();
1742                            out.push(o);
1743                            continue;
1744                        }
1745                    }
1746                }
1747            }
1748            if let (Opcode::CallMethod(b), Some(Opcode::CallMethod(a))) = (&op, out.last()) {
1749                // Two-arg fusions (both have sub_progs)
1750                if a.sub_progs.len() >= 1 && b.sub_progs.len() >= 1 {
1751                    let (am, bm) = (a.method, b.method);
1752                    let p1 = Arc::clone(&a.sub_progs[0]);
1753                    let p2 = Arc::clone(&b.sub_progs[0]);
1754                    let fused = match (am, bm) {
1755                        (BuiltinMethod::Filter, BuiltinMethod::Map) =>
1756                            Some(Opcode::FilterMap { pred: p1, map: p2 }),
1757                        (BuiltinMethod::Filter, BuiltinMethod::Filter) =>
1758                            Some(Opcode::FilterFilter { p1, p2 }),
1759                        (BuiltinMethod::Map, BuiltinMethod::Map) =>
1760                            Some(Opcode::MapMap { f1: p1, f2: p2 }),
1761                        (BuiltinMethod::Map, BuiltinMethod::Filter) =>
1762                            Some(Opcode::MapFilter { map: p1, pred: p2 }),
1763                        _ => None,
1764                    };
1765                    if let Some(f) = fused {
1766                        out.pop();
1767                        out.push(f);
1768                        continue;
1769                    }
1770                }
1771                // map(f) + sum()/avg()/min()/max()/flatten()/first()/last()
1772                if a.method == BuiltinMethod::Map && a.sub_progs.len() >= 1
1773                   && b.sub_progs.is_empty() {
1774                    let f = Arc::clone(&a.sub_progs[0]);
1775                    let fused = match b.method {
1776                        BuiltinMethod::Sum => Some(Opcode::MapSum(f)),
1777                        BuiltinMethod::Avg => Some(Opcode::MapAvg(f)),
1778                        BuiltinMethod::Min => Some(Opcode::MapMin(f)),
1779                        BuiltinMethod::Max => Some(Opcode::MapMax(f)),
1780                        BuiltinMethod::Flatten => Some(Opcode::MapFlatten(f)),
1781                        BuiltinMethod::First => Some(Opcode::MapFirst(f)),
1782                        BuiltinMethod::Last => Some(Opcode::MapLast(f)),
1783                        _ => None,
1784                    };
1785                    if let Some(o) = fused {
1786                        out.pop();
1787                        out.push(o);
1788                        continue;
1789                    }
1790                }
1791                // filter(p) + last() → FilterLast (reverse scan, early exit).
1792                // filter(p) + first() is handled by pass_find_quantifier
1793                // (emits FindFirst).
1794                if a.method == BuiltinMethod::Filter && a.sub_progs.len() >= 1
1795                   && b.method == BuiltinMethod::Last && b.sub_progs.is_empty() {
1796                    let pred = Arc::clone(&a.sub_progs[0]);
1797                    out.pop();
1798                    out.push(Opcode::FilterLast { pred });
1799                    continue;
1800                }
1801                // filter(p) + take_while(q) → FilterTakeWhile
1802                if a.method == BuiltinMethod::Filter && a.sub_progs.len() >= 1
1803                   && b.method == BuiltinMethod::TakeWhile && b.sub_progs.len() >= 1 {
1804                    let pred = Arc::clone(&a.sub_progs[0]);
1805                    let stop = Arc::clone(&b.sub_progs[0]);
1806                    out.pop();
1807                    out.push(Opcode::FilterTakeWhile { pred, stop });
1808                    continue;
1809                }
1810                // filter(p) + drop_while(q) → FilterDropWhile
1811                if a.method == BuiltinMethod::Filter && a.sub_progs.len() >= 1
1812                   && b.method == BuiltinMethod::DropWhile && b.sub_progs.len() >= 1 {
1813                    let pred = Arc::clone(&a.sub_progs[0]);
1814                    let drop = Arc::clone(&b.sub_progs[0]);
1815                    out.pop();
1816                    out.push(Opcode::FilterDropWhile { pred, drop });
1817                    continue;
1818                }
1819                // map(f) + unique() → MapUnique
1820                if a.method == BuiltinMethod::Map && a.sub_progs.len() >= 1
1821                   && b.method == BuiltinMethod::Unique && b.sub_progs.is_empty() {
1822                    let f = Arc::clone(&a.sub_progs[0]);
1823                    out.pop();
1824                    out.push(Opcode::MapUnique(f));
1825                    continue;
1826                }
1827                // trim + upper/lower  and  upper/lower + trim  → fused StrXY.
1828                // Both calls take no arguments.
1829                if a.sub_progs.is_empty() && b.sub_progs.is_empty() {
1830                    let fused_str = match (a.method, b.method) {
1831                        (BuiltinMethod::Trim,  BuiltinMethod::Upper) => Some(Opcode::StrTrimUpper),
1832                        (BuiltinMethod::Trim,  BuiltinMethod::Lower) => Some(Opcode::StrTrimLower),
1833                        (BuiltinMethod::Upper, BuiltinMethod::Trim)  => Some(Opcode::StrUpperTrim),
1834                        (BuiltinMethod::Lower, BuiltinMethod::Trim)  => Some(Opcode::StrLowerTrim),
1835                        _ => None,
1836                    };
1837                    if let Some(o) = fused_str {
1838                        out.pop();
1839                        out.push(o);
1840                        continue;
1841                    }
1842                }
1843                // split(sep) + reverse() — detect; only actually fuse when next
1844                // op is join(sep) with the same literal sep.  Done in a
1845                // dedicated 3-way pass below via lookahead buffer.
1846                // map(@.to_json()) + join(sep) → MapToJsonJoin { sep_prog }
1847                // Body is one of:
1848                //   [PushCurrent, CallMethod(ToJson, empty)]     — `@.to_json()`
1849                //   [LoadIdent(_), CallMethod(ToJson, empty)]    — `lambda x: x.to_json()`
1850                if a.method == BuiltinMethod::Map && a.sub_progs.len() == 1
1851                   && b.method == BuiltinMethod::Join && b.sub_progs.len() == 1 {
1852                    let body = &a.sub_progs[0].ops;
1853                    let is_to_json_body = matches!(&body[..],
1854                        [Opcode::PushCurrent, Opcode::CallMethod(c)]
1855                            if c.method == BuiltinMethod::ToJson
1856                               && c.sub_progs.is_empty())
1857                        || matches!(&body[..],
1858                        [Opcode::LoadIdent(_), Opcode::CallMethod(c)]
1859                            if c.method == BuiltinMethod::ToJson
1860                               && c.sub_progs.is_empty());
1861                    if is_to_json_body {
1862                        let sep_prog = Arc::clone(&b.sub_progs[0]);
1863                        out.pop();
1864                        out.push(Opcode::MapToJsonJoin { sep_prog });
1865                        continue;
1866                    }
1867                }
1868            }
1869            // map(@.replace(lit, lit))   or   map(@.replace_all(lit, lit))
1870            // → MapReplaceLit { needle, with, all } — single-op CallMethod.
1871            //
1872            // Also detects two-step chains:
1873            //   map(@.upper().replace(lit, lit))  → MapUpperReplaceLit
1874            //   map(@.lower().replace(lit, lit))  → MapLowerReplaceLit
1875            if let Opcode::CallMethod(a) = &op {
1876                if a.method == BuiltinMethod::Map && a.sub_progs.len() == 1 {
1877                    let body = &a.sub_progs[0].ops;
1878                    // Body shape 1: [PushCurrent, CallMethod(Replace|ReplaceAll, [PushStr, PushStr])]
1879                    let fused = if let [Opcode::PushCurrent, Opcode::CallMethod(inner)] = &body[..] {
1880                        let is_replace = inner.method == BuiltinMethod::Replace
1881                                      || inner.method == BuiltinMethod::ReplaceAll;
1882                        if is_replace && inner.sub_progs.len() == 2 {
1883                            let n = trivial_push_str(&inner.sub_progs[0].ops);
1884                            let w = trivial_push_str(&inner.sub_progs[1].ops);
1885                            match (n, w) {
1886                                (Some(needle), Some(with)) => {
1887                                    let all = inner.method == BuiltinMethod::ReplaceAll;
1888                                    Some(Opcode::MapReplaceLit { needle, with, all })
1889                                }
1890                                _ => None,
1891                            }
1892                        } else { None }
1893                    } else if let [Opcode::PushCurrent,
1894                                    Opcode::CallMethod(case_op),
1895                                    Opcode::CallMethod(inner)] = &body[..] {
1896                        // Body shape 2: [PushCurrent, CallMethod(Upper|Lower,[]),
1897                        //                CallMethod(Replace|ReplaceAll, [PushStr, PushStr])]
1898                        let is_replace = inner.method == BuiltinMethod::Replace
1899                                      || inner.method == BuiltinMethod::ReplaceAll;
1900                        let is_case_nullary = case_op.sub_progs.is_empty()
1901                            && (case_op.method == BuiltinMethod::Upper
1902                             || case_op.method == BuiltinMethod::Lower);
1903                        if is_case_nullary && is_replace && inner.sub_progs.len() == 2 {
1904                            let n = trivial_push_str(&inner.sub_progs[0].ops);
1905                            let w = trivial_push_str(&inner.sub_progs[1].ops);
1906                            match (n, w) {
1907                                (Some(needle), Some(with)) => {
1908                                    let all = inner.method == BuiltinMethod::ReplaceAll;
1909                                    if case_op.method == BuiltinMethod::Upper {
1910                                        Some(Opcode::MapUpperReplaceLit { needle, with, all })
1911                                    } else {
1912                                        Some(Opcode::MapLowerReplaceLit { needle, with, all })
1913                                    }
1914                                }
1915                                _ => None,
1916                            }
1917                        } else { None }
1918                    } else { None };
1919                    if let Some(o) = fused {
1920                        out.push(o);
1921                        continue;
1922                    }
1923                }
1924            }
1925            // map(f"...") with no ident captures → MapFString(parts)
1926            // Body shape: [FString(parts)]
1927            if let Opcode::CallMethod(a) = &op {
1928                if a.method == BuiltinMethod::Map && a.sub_progs.len() == 1 {
1929                    let body = &a.sub_progs[0].ops;
1930                    if let [Opcode::FString(parts)] = &body[..] {
1931                        out.push(Opcode::MapFString(Arc::clone(parts)));
1932                        continue;
1933                    }
1934                }
1935            }
1936            // map(@.slice(lit, lit)) → MapStrSlice { start, end }
1937            // Body shape: [PushCurrent, CallMethod(Slice, [PushInt(a), PushInt(b)])]
1938            if let Opcode::CallMethod(a) = &op {
1939                if a.method == BuiltinMethod::Map && a.sub_progs.len() == 1 {
1940                    let body = &a.sub_progs[0].ops;
1941                    if let [Opcode::PushCurrent, Opcode::CallMethod(inner)] = &body[..] {
1942                        if inner.method == BuiltinMethod::Slice {
1943                            let start = match inner.sub_progs.first()
1944                                .map(|p| p.ops.as_ref()) {
1945                                Some([Opcode::PushInt(n)]) => Some(*n),
1946                                _ => None,
1947                            };
1948                            let end = match inner.sub_progs.get(1)
1949                                .map(|p| p.ops.as_ref()) {
1950                                Some([Opcode::PushInt(n)]) => Some(Some(*n)),
1951                                None => Some(None),
1952                                _ => None,
1953                            };
1954                            if let (Some(s), Some(e)) = (start, end) {
1955                                out.push(Opcode::MapStrSlice { start: s, end: e });
1956                                continue;
1957                            }
1958                        }
1959                    }
1960                }
1961            }
1962            // map({k1, k2, ..}) with all `Short` entries → MapProject
1963            if let Opcode::CallMethod(a) = &op {
1964                if a.method == BuiltinMethod::Map && a.sub_progs.len() == 1 {
1965                    let body = &a.sub_progs[0].ops;
1966                    if let [Opcode::MakeObj(entries)] = &body[..] {
1967                        let all_short: Option<Vec<Arc<str>>> = entries.iter()
1968                            .map(|e| match e {
1969                                CompiledObjEntry::Short { name, .. } => Some(name.clone()),
1970                                _ => None,
1971                            })
1972                            .collect();
1973                        if let Some(keys) = all_short {
1974                            if !keys.is_empty() {
1975                                let ics: Vec<std::sync::atomic::AtomicU64> =
1976                                    keys.iter().map(|_| std::sync::atomic::AtomicU64::new(0)).collect();
1977                                out.push(Opcode::MapProject {
1978                                    keys: keys.into(),
1979                                    ics: ics.into(),
1980                                });
1981                                continue;
1982                            }
1983                        }
1984                    }
1985                }
1986            }
1987            // map(@.split(sep).map(len).sum()) → MapSplitLenSum { sep }
1988            // Body shape (post pass_field_specialise rewrote map(len) as
1989            // MapFieldSum("len")):
1990            //   [PushCurrent, CallMethod(Split, [PushStr(sep)]), MapFieldSum("len")]
1991            if let Opcode::CallMethod(a) = &op {
1992                if a.method == BuiltinMethod::Map && a.sub_progs.len() == 1 {
1993                    let body = &a.sub_progs[0].ops;
1994                    let fused = if let [Opcode::PushCurrent,
1995                                         Opcode::CallMethod(split),
1996                                         Opcode::MapFieldSum(field)] = &body[..] {
1997                        if split.method == BuiltinMethod::Split
1998                           && split.sub_progs.len() == 1
1999                           && field.as_ref() == "len" {
2000                            let sep_opt = trivial_push_str(&split.sub_progs[0].ops);
2001                            sep_opt.map(|sep| Opcode::MapSplitLenSum { sep })
2002                        } else { None }
2003                    } else { None };
2004                    if let Some(o) = fused {
2005                        out.push(o);
2006                        continue;
2007                    }
2008                }
2009            }
2010            // map(prefix + @ + suffix), map(prefix + @), map(@ + suffix)
2011            //   → MapStrConcat { prefix, suffix }
2012            // Body shapes:
2013            //   [PushStr(p), PushCurrent, Add, PushStr(s), Add]     prefix+suffix
2014            //   [PushStr(p), PushCurrent, Add]                      prefix only
2015            //   [PushCurrent, PushStr(s), Add]                      suffix only
2016            if let Opcode::CallMethod(a) = &op {
2017                if a.method == BuiltinMethod::Map && a.sub_progs.len() == 1 {
2018                    let body = &a.sub_progs[0].ops;
2019                    let empty: Arc<str> = Arc::from("");
2020                    let fused = match &body[..] {
2021                        [Opcode::PushStr(p), Opcode::PushCurrent, Opcode::Add,
2022                         Opcode::PushStr(s), Opcode::Add] => Some(Opcode::MapStrConcat {
2023                            prefix: p.clone(), suffix: s.clone(),
2024                        }),
2025                        [Opcode::PushStr(p), Opcode::PushCurrent, Opcode::Add] =>
2026                            Some(Opcode::MapStrConcat {
2027                                prefix: p.clone(), suffix: empty.clone(),
2028                            }),
2029                        [Opcode::PushCurrent, Opcode::PushStr(s), Opcode::Add] =>
2030                            Some(Opcode::MapStrConcat {
2031                                prefix: empty.clone(), suffix: s.clone(),
2032                            }),
2033                        _ => None,
2034                    };
2035                    if let Some(o) = fused {
2036                        out.push(o);
2037                        continue;
2038                    }
2039                }
2040            }
2041            // MapSplitCount followed by Sum → MapSplitCountSum (scalar, no
2042            // intermediate Int array).
2043            if let Opcode::CallMethod(b) = &op {
2044                if b.method == BuiltinMethod::Sum && b.sub_progs.is_empty() {
2045                    if let Some(Opcode::MapSplitCount { sep }) = out.last() {
2046                        let sep = Arc::clone(sep);
2047                        out.pop();
2048                        out.push(Opcode::MapSplitCountSum { sep });
2049                        continue;
2050                    }
2051                }
2052            }
2053            // map(@.split(lit).count()|.first()|.nth(lit)) → MapSplitCount /
2054            // MapSplitFirst / MapSplitNth.  Eliminates N per-row Arcs from
2055            // split materialisation when the consumer only needs the count
2056            // or a single segment.
2057            if let Opcode::CallMethod(a) = &op {
2058                if a.method == BuiltinMethod::Map && a.sub_progs.len() == 1 {
2059                    let body = &a.sub_progs[0].ops;
2060                    let fused = if let [Opcode::PushCurrent,
2061                                         Opcode::CallMethod(split),
2062                                         Opcode::CallMethod(cons)] = &body[..] {
2063                        if split.method == BuiltinMethod::Split && split.sub_progs.len() == 1 {
2064                            let sep_opt = trivial_push_str(&split.sub_progs[0].ops);
2065                            match (sep_opt, cons.method, cons.sub_progs.len()) {
2066                                (Some(sep), BuiltinMethod::Count, 0)
2067                              | (Some(sep), BuiltinMethod::Len,   0) =>
2068                                    Some(Opcode::MapSplitCount { sep }),
2069                                (Some(sep), BuiltinMethod::First, 0) =>
2070                                    Some(Opcode::MapSplitFirst { sep }),
2071                                (Some(sep), BuiltinMethod::Nth,   1) => {
2072                                    if let [Opcode::PushInt(n)] = &cons.sub_progs[0].ops[..] {
2073                                        if *n >= 0 {
2074                                            Some(Opcode::MapSplitNth { sep, n: *n as usize })
2075                                        } else { None }
2076                                    } else { None }
2077                                }
2078                                _ => None,
2079                            }
2080                        } else { None }
2081                    } else { None };
2082                    if let Some(o) = fused {
2083                        out.push(o);
2084                        continue;
2085                    }
2086                }
2087            }
2088            out.push(op);
2089        }
2090        out
2091    }
2092
2093    /// Three-way string-method fusion: `split(s).reverse().join(s)` with
2094    /// matching string literal `s` collapses to `StrSplitReverseJoin`.
2095    fn pass_string_chain_fusion(ops: Vec<Opcode>) -> Vec<Opcode> {
2096        let mut out: Vec<Opcode> = Vec::with_capacity(ops.len());
2097        let mut i = 0;
2098        while i < ops.len() {
2099            if i + 2 < ops.len() {
2100                if let (Opcode::CallMethod(a),
2101                        Opcode::CallMethod(b),
2102                        Opcode::CallMethod(c)) = (&ops[i], &ops[i + 1], &ops[i + 2]) {
2103                    if a.method == BuiltinMethod::Split && a.sub_progs.len() == 1
2104                       && b.method == BuiltinMethod::Reverse && b.sub_progs.is_empty()
2105                       && c.method == BuiltinMethod::Join && c.sub_progs.len() == 1 {
2106                        let sep_a = trivial_push_str(&a.sub_progs[0].ops);
2107                        let sep_c = trivial_push_str(&c.sub_progs[0].ops);
2108                        if let (Some(s1), Some(s2)) = (sep_a, sep_c) {
2109                            if s1 == s2 {
2110                                out.push(Opcode::StrSplitReverseJoin { sep: s1 });
2111                                i += 3;
2112                                continue;
2113                            }
2114                        }
2115                    }
2116                }
2117            }
2118            out.push(ops[i].clone());
2119            i += 1;
2120        }
2121        out
2122    }
2123
2124    /// Lower generic fused opcodes to field-specialised variants when the
2125    /// sub-program is a trivial `GetField(k)` read. Runs AFTER
2126    /// pass_find_quantifier / pass_filter_count so those passes see the
2127    /// generic `CallMethod(Filter)` / `MapSum` forms first.
2128    fn pass_field_specialise(ops: Vec<Opcode>) -> Vec<Opcode> {
2129        let mut out2: Vec<Opcode> = Vec::with_capacity(ops.len());
2130        for op in ops {
2131            match op {
2132                Opcode::MapSum(ref f) => {
2133                    if let Some(k) = trivial_field(&f.ops) {
2134                        out2.push(Opcode::MapFieldSum(k)); continue;
2135                    }
2136                }
2137                Opcode::MapAvg(ref f) => {
2138                    if let Some(k) = trivial_field(&f.ops) {
2139                        out2.push(Opcode::MapFieldAvg(k)); continue;
2140                    }
2141                }
2142                Opcode::MapMin(ref f) => {
2143                    if let Some(k) = trivial_field(&f.ops) {
2144                        out2.push(Opcode::MapFieldMin(k)); continue;
2145                    }
2146                }
2147                Opcode::MapMax(ref f) => {
2148                    if let Some(k) = trivial_field(&f.ops) {
2149                        out2.push(Opcode::MapFieldMax(k)); continue;
2150                    }
2151                }
2152                Opcode::MapUnique(ref f) => {
2153                    if let Some(k) = trivial_field(&f.ops) {
2154                        out2.push(Opcode::MapFieldUnique(k)); continue;
2155                    }
2156                    if let Some(chain) = trivial_field_chain(&f.ops) {
2157                        out2.push(Opcode::MapFieldChainUnique(chain)); continue;
2158                    }
2159                }
2160                Opcode::FilterCount(ref pred) => {
2161                    if let Some(pairs) = detect_field_eq_conjuncts(&pred.ops) {
2162                        out2.push(Opcode::FilterFieldsAllEqLitCount(Arc::from(pairs)));
2163                        continue;
2164                    }
2165                    if let Some(triples) = detect_field_cmp_conjuncts(&pred.ops) {
2166                        out2.push(Opcode::FilterFieldsAllCmpLitCount(Arc::from(triples)));
2167                        continue;
2168                    }
2169                }
2170                Opcode::CallMethod(ref b) => {
2171                    // map(k)    → MapField(k)
2172                    if b.method == BuiltinMethod::Map && b.sub_progs.len() == 1 {
2173                        if let Some(k) = trivial_field(&b.sub_progs[0].ops) {
2174                            out2.push(Opcode::MapField(k)); continue;
2175                        }
2176                        if let Some(chain) = trivial_field_chain(&b.sub_progs[0].ops) {
2177                            out2.push(Opcode::MapFieldChain(chain)); continue;
2178                        }
2179                    }
2180                    // group_by(k) → GroupByField(k)
2181                    if b.method == BuiltinMethod::GroupBy && b.sub_progs.len() == 1 {
2182                        if let Some(k) = trivial_field(&b.sub_progs[0].ops) {
2183                            out2.push(Opcode::GroupByField(k)); continue;
2184                        }
2185                    }
2186                    // count_by(k) → CountByField(k)
2187                    if b.method == BuiltinMethod::CountBy && b.sub_progs.len() == 1 {
2188                        if let Some(k) = trivial_field(&b.sub_progs[0].ops) {
2189                            out2.push(Opcode::CountByField(k)); continue;
2190                        }
2191                    }
2192                    // unique_by(k) / uniqueBy(k) → UniqueByField(k)
2193                    if b.method == BuiltinMethod::Unknown
2194                       && matches!(b.name.as_ref(), "unique_by" | "uniqueBy")
2195                       && b.sub_progs.len() == 1 {
2196                        if let Some(k) = trivial_field(&b.sub_progs[0].ops) {
2197                            out2.push(Opcode::UniqueByField(k)); continue;
2198                        }
2199                    }
2200                    // filter(field <cmp> lit|field) → FilterField*
2201                    if b.method == BuiltinMethod::Filter && b.sub_progs.len() == 1 {
2202                        if let Some(p) = detect_field_pred(&b.sub_progs[0].ops) {
2203                            let lowered = match p {
2204                                FieldPred::FieldCmpLit(k, super::ast::BinOp::Eq, lit) =>
2205                                    Opcode::FilterFieldEqLit(k, lit),
2206                                FieldPred::FieldCmpLit(k, op, lit) =>
2207                                    Opcode::FilterFieldCmpLit(k, op, lit),
2208                                FieldPred::FieldCmpField(k1, op, k2) =>
2209                                    Opcode::FilterFieldCmpField(k1, op, k2),
2210                            };
2211                            out2.push(lowered); continue;
2212                        }
2213                        // filter(@ <cmp> lit) → FilterCurrentCmpLit
2214                        if let Some((op, lit)) = detect_current_cmp_lit(&b.sub_progs[0].ops) {
2215                            out2.push(Opcode::FilterCurrentCmpLit(op, lit));
2216                            continue;
2217                        }
2218                        // filter(@.starts_with/ends_with/contains(lit)) → FilterStrVec*
2219                        if let Some((kind, lit)) = detect_current_str_method(&b.sub_progs[0].ops) {
2220                            out2.push(match kind {
2221                                StrVecPred::StartsWith => Opcode::FilterStrVecStartsWith(lit),
2222                                StrVecPred::EndsWith   => Opcode::FilterStrVecEndsWith(lit),
2223                                StrVecPred::Contains   => Opcode::FilterStrVecContains(lit),
2224                            });
2225                            continue;
2226                        }
2227                    }
2228                    // map(@.upper/lower/trim()) → MapStrVec*
2229                    if b.method == BuiltinMethod::Map && b.sub_progs.len() == 1 {
2230                        if let Some(kind) = detect_current_str_nullary(&b.sub_progs[0].ops) {
2231                            out2.push(match kind {
2232                                StrVecMap::Upper => Opcode::MapStrVecUpper,
2233                                StrVecMap::Lower => Opcode::MapStrVecLower,
2234                                StrVecMap::Trim  => Opcode::MapStrVecTrim,
2235                            });
2236                            continue;
2237                        }
2238                        // map(@ <arith> lit) / map(lit <arith> @) → MapNumVecArith
2239                        if let Some((op, lit, flipped)) =
2240                            detect_current_arith_lit(&b.sub_progs[0].ops) {
2241                            out2.push(Opcode::MapNumVecArith { op, lit, flipped });
2242                            continue;
2243                        }
2244                        // map(-@) → MapNumVecNeg
2245                        if detect_current_neg(&b.sub_progs[0].ops) {
2246                            out2.push(Opcode::MapNumVecNeg);
2247                            continue;
2248                        }
2249                    }
2250                }
2251                _ => {}
2252            }
2253            out2.push(op);
2254        }
2255        // Third pass: fold `FilterField* + count()` into FilterField*Count,
2256        // and collapse chains of `MapField(k1) + MapFlatten(trivial k2) + ...`
2257        // into a single `FlatMapChain([k1,k2,...])`.
2258        let mut out3: Vec<Opcode> = Vec::with_capacity(out2.len());
2259        for op in out2 {
2260            // FilterField* + count()
2261            if let Opcode::CallMethod(ref b) = op {
2262                if b.method == BuiltinMethod::Count && b.sub_progs.is_empty() {
2263                    match out3.last().cloned() {
2264                        Some(Opcode::FilterFieldEqLit(k, lit)) => {
2265                            out3.pop();
2266                            out3.push(Opcode::FilterFieldEqLitCount(k, lit));
2267                            continue;
2268                        }
2269                        Some(Opcode::FilterFieldCmpLit(k, cop, lit)) => {
2270                            out3.pop();
2271                            out3.push(Opcode::FilterFieldCmpLitCount(k, cop, lit));
2272                            continue;
2273                        }
2274                        Some(Opcode::FilterFieldCmpField(k1, cop, k2)) => {
2275                            out3.pop();
2276                            out3.push(Opcode::FilterFieldCmpFieldCount(k1, cop, k2));
2277                            continue;
2278                        }
2279                        _ => {}
2280                    }
2281                }
2282            }
2283            // FilterField* + MapField(k) → FilterField*MapField (single pass)
2284            if let Opcode::MapField(ref kp) = op {
2285                match out3.last().cloned() {
2286                    Some(Opcode::FilterFieldEqLit(k, lit)) => {
2287                        out3.pop();
2288                        out3.push(Opcode::FilterFieldEqLitMapField(k, lit, kp.clone()));
2289                        continue;
2290                    }
2291                    Some(Opcode::FilterFieldCmpLit(k, cop, lit)) => {
2292                        out3.pop();
2293                        out3.push(Opcode::FilterFieldCmpLitMapField(k, cop, lit, kp.clone()));
2294                        continue;
2295                    }
2296                    _ => {}
2297                }
2298            }
2299            // MapField(k) + MapFlatten(trivial k2) → FlatMapChain([k, k2])
2300            if let Opcode::MapFlatten(ref f) = op {
2301                if let Some(k2) = trivial_field(&f.ops) {
2302                    match out3.last().cloned() {
2303                        Some(Opcode::MapField(k1)) => {
2304                            out3.pop();
2305                            out3.push(Opcode::FlatMapChain(Arc::from(vec![k1, k2])));
2306                            continue;
2307                        }
2308                        Some(Opcode::FlatMapChain(ks)) => {
2309                            let mut v: Vec<Arc<str>> = ks.iter().cloned().collect();
2310                            v.push(k2);
2311                            out3.pop();
2312                            out3.push(Opcode::FlatMapChain(Arc::from(v)));
2313                            continue;
2314                        }
2315                        _ => {}
2316                    }
2317                }
2318            }
2319            out3.push(op);
2320        }
2321        out3
2322    }
2323
2324    /// Lower simple single-var list comprehensions to the same fused
2325    /// opcodes we already emit for `.filter(k op lit).map(k2)` chains.
2326    /// Matches `[x.k2 for x in <iter> (if x.k op lit)]` — a common shape
2327    /// that otherwise pays ~4 opcode dispatches per iteration.
2328    fn pass_list_comp_specialise(ops: Vec<Opcode>) -> Vec<Opcode> {
2329        #[inline]
2330        fn proj_key(ops: &[Opcode], var: &str) -> Option<Arc<str>> {
2331            match ops {
2332                [Opcode::LoadIdent(v), Opcode::GetField(k)] if v.as_ref() == var =>
2333                    Some(k.clone()),
2334                _ => None,
2335            }
2336        }
2337        #[inline]
2338        fn cond_pred(ops: &[Opcode], var: &str)
2339            -> Option<(Arc<str>, super::ast::BinOp, Val)>
2340        {
2341            if ops.len() != 4 { return None; }
2342            let k = match (&ops[0], &ops[1]) {
2343                (Opcode::LoadIdent(v), Opcode::GetField(k)) if v.as_ref() == var =>
2344                    k.clone(),
2345                _ => return None,
2346            };
2347            let lit = trivial_literal(&ops[2])?;
2348            let op = cmp_opcode(&ops[3])?;
2349            Some((k, op, lit))
2350        }
2351
2352        let mut out: Vec<Opcode> = Vec::with_capacity(ops.len());
2353        for op in ops {
2354            if let Opcode::ListComp(ref spec) = op {
2355                if spec.vars.len() == 1 {
2356                    let var = spec.vars[0].as_ref();
2357                    if let Some(proj) = proj_key(&spec.expr.ops, var) {
2358                        match &spec.cond {
2359                            Some(cond) => {
2360                                if let Some((pk, cop, lit)) = cond_pred(&cond.ops, var) {
2361                                    for iop in spec.iter.ops.iter() {
2362                                        out.push(iop.clone());
2363                                    }
2364                                    if matches!(cop, super::ast::BinOp::Eq) {
2365                                        out.push(Opcode::FilterFieldEqLitMapField(pk, lit, proj));
2366                                    } else {
2367                                        out.push(Opcode::FilterFieldCmpLitMapField(pk, cop, lit, proj));
2368                                    }
2369                                    continue;
2370                                }
2371                            }
2372                            None => {
2373                                for iop in spec.iter.ops.iter() {
2374                                    out.push(iop.clone());
2375                                }
2376                                out.push(Opcode::MapField(proj));
2377                                continue;
2378                            }
2379                        }
2380                    }
2381                }
2382            }
2383            out.push(op);
2384        }
2385        out
2386    }
2387
2388    fn sort_lam_param(prev: &CompiledCall) -> Option<Arc<str>> {
2389        match prev.orig_args.first() {
2390            Some(Arg::Pos(Expr::Lambda { params, .. })) if !params.is_empty() =>
2391                Some(Arc::from(params[0].as_str())),
2392            _ => None,
2393        }
2394    }
2395
2396    /// Replace expensive ops with cheaper equivalents:
2397    ///   sort() + first()    → min()
2398    ///   sort() + last()     → max()
2399    ///   sort() + [0]        → min()
2400    ///   sort() + [-1]       → max()
2401    ///   reverse() + first() → last()
2402    ///   reverse() + last()  → first()
2403    ///   sort_by(k) + first()/last() → ArgExtreme (O(N) scan)
2404    fn pass_strength_reduce(ops: Vec<Opcode>) -> Vec<Opcode> {
2405        let mut out: Vec<Opcode> = Vec::with_capacity(ops.len());
2406        for op in ops {
2407            // Pattern: [..., prev_method_call, current_op]
2408            if let Some(Opcode::CallMethod(prev)) = out.last().cloned() {
2409                let replaced = match (prev.method, &op) {
2410                    // sort() + [0] → min()
2411                    (BuiltinMethod::Sort, Opcode::GetIndex(0)) if prev.sub_progs.is_empty() =>
2412                        Some(make_noarg_call(BuiltinMethod::Min, "min")),
2413                    // sort() + [-1] → max()
2414                    (BuiltinMethod::Sort, Opcode::GetIndex(-1)) if prev.sub_progs.is_empty() =>
2415                        Some(make_noarg_call(BuiltinMethod::Max, "max")),
2416                    // sort() + first() → min()
2417                    (BuiltinMethod::Sort, Opcode::CallMethod(next))
2418                        if prev.sub_progs.is_empty() && next.method == BuiltinMethod::First =>
2419                        Some(make_noarg_call(BuiltinMethod::Min, "min")),
2420                    // sort() + last() → max()
2421                    (BuiltinMethod::Sort, Opcode::CallMethod(next))
2422                        if prev.sub_progs.is_empty() && next.method == BuiltinMethod::Last =>
2423                        Some(make_noarg_call(BuiltinMethod::Max, "max")),
2424                    // sort_by(k) + first() → ArgExtreme{key, max=false}
2425                    (BuiltinMethod::Sort, Opcode::CallMethod(next))
2426                        if prev.sub_progs.len() == 1
2427                           && next.method == BuiltinMethod::First
2428                           && next.sub_progs.is_empty() =>
2429                        Some(Opcode::ArgExtreme {
2430                            key: Arc::clone(&prev.sub_progs[0]),
2431                            lam_param: Self::sort_lam_param(&prev),
2432                            max: false,
2433                        }),
2434                    // sort_by(k) + last() → ArgExtreme{key, max=true}
2435                    (BuiltinMethod::Sort, Opcode::CallMethod(next))
2436                        if prev.sub_progs.len() == 1
2437                           && next.method == BuiltinMethod::Last
2438                           && next.sub_progs.is_empty() =>
2439                        Some(Opcode::ArgExtreme {
2440                            key: Arc::clone(&prev.sub_progs[0]),
2441                            lam_param: Self::sort_lam_param(&prev),
2442                            max: true,
2443                        }),
2444                    // reverse() + first() → last()
2445                    (BuiltinMethod::Reverse, Opcode::CallMethod(next))
2446                        if next.method == BuiltinMethod::First =>
2447                        Some(make_noarg_call(BuiltinMethod::Last, "last")),
2448                    // reverse() + last() → first()
2449                    (BuiltinMethod::Reverse, Opcode::CallMethod(next))
2450                        if next.method == BuiltinMethod::Last =>
2451                        Some(make_noarg_call(BuiltinMethod::First, "first")),
2452                    // sort() + [0:n] → TopN(n, asc=true)
2453                    (BuiltinMethod::Sort, Opcode::GetSlice(from, Some(to)))
2454                        if prev.sub_progs.is_empty()
2455                           && (from.is_none() || *from == Some(0))
2456                           && *to > 0 =>
2457                        Some(Opcode::TopN { n: *to as usize, asc: true }),
2458                    // Cardinality-preserving op + len/count → drop the first op.
2459                    // sort / reverse preserve length by definition; map is
2460                    // 1:1 so it also preserves length, and `count` only needs
2461                    // the input array length.
2462                    (BuiltinMethod::Sort | BuiltinMethod::Reverse | BuiltinMethod::Map,
2463                     Opcode::CallMethod(next))
2464                        if next.sub_progs.is_empty()
2465                           && (next.method == BuiltinMethod::Len
2466                               || next.method == BuiltinMethod::Count) =>
2467                        Some(Opcode::CallMethod(Arc::clone(next))),
2468                    // Order-independent aggregate after sort/reverse → drop
2469                    // the reorder.  sum / avg / min / max only inspect the
2470                    // multiset of elements, not their order.
2471                    (BuiltinMethod::Sort | BuiltinMethod::Reverse,
2472                     Opcode::CallMethod(next))
2473                        if prev.sub_progs.is_empty()
2474                           && next.sub_progs.is_empty()
2475                           && matches!(next.method,
2476                                BuiltinMethod::Sum | BuiltinMethod::Avg
2477                              | BuiltinMethod::Min | BuiltinMethod::Max) =>
2478                        Some(Opcode::CallMethod(Arc::clone(next))),
2479                    // Idempotent: f(f(x)) == f(x).  `sort(k)` is idempotent
2480                    // only when both calls use the same key, so we restrict
2481                    // the no-arg case; `unique()` dedup is always idempotent.
2482                    (BuiltinMethod::Sort, Opcode::CallMethod(next))
2483                        if prev.sub_progs.is_empty()
2484                           && next.method == BuiltinMethod::Sort
2485                           && next.sub_progs.is_empty() =>
2486                        Some(Opcode::CallMethod(Arc::clone(next))),
2487                    (BuiltinMethod::Unique, Opcode::CallMethod(next))
2488                        if next.method == BuiltinMethod::Unique =>
2489                        Some(Opcode::CallMethod(Arc::clone(next))),
2490                    // unique() + count()/len() → UniqueCount (skip materialising dedup array).
2491                    (BuiltinMethod::Unique, Opcode::CallMethod(next))
2492                        if prev.sub_progs.is_empty()
2493                           && next.sub_progs.is_empty()
2494                           && (next.method == BuiltinMethod::Count
2495                               || next.method == BuiltinMethod::Len) =>
2496                        Some(Opcode::UniqueCount),
2497                    _ => None,
2498                };
2499                if let Some(rep) = replaced {
2500                    out.pop();
2501                    out.push(rep);
2502                    continue;
2503                }
2504                // Involution: reverse().reverse() → drop both.
2505                if prev.method == BuiltinMethod::Reverse && prev.sub_progs.is_empty() {
2506                    if let Opcode::CallMethod(next) = &op {
2507                        if next.method == BuiltinMethod::Reverse && next.sub_progs.is_empty() {
2508                            out.pop();
2509                            continue;
2510                        }
2511                    }
2512                }
2513            }
2514            out.push(op);
2515        }
2516        out
2517    }
2518
2519    /// Fuse runs of `GetField` not consumed by `pass_root_chain` into a
2520    /// single `FieldChain`.  Applies mid-program where the object on TOS
2521    /// came from elsewhere (method return, filter, comprehension).  Singletons
2522    /// are left as-is — fusion only triggers at length ≥ 2.
2523    fn pass_field_chain(ops: Vec<Opcode>) -> Vec<Opcode> {
2524        // Both GetField(k) and OptField(k) devolve to `get_field(k)` which
2525        // returns Null for non-objects, so OptField can be absorbed into a
2526        // FieldChain: null propagates through the remaining get_field calls.
2527        fn field_key(op: &Opcode) -> Option<Arc<str>> {
2528            match op {
2529                Opcode::GetField(k) | Opcode::OptField(k) => Some(Arc::clone(k)),
2530                _ => None,
2531            }
2532        }
2533        let mut out = Vec::with_capacity(ops.len());
2534        let mut it = ops.into_iter().peekable();
2535        while let Some(op) = it.next() {
2536            if let Some(k0) = field_key(&op) {
2537                if it.peek().and_then(field_key).is_some() {
2538                    let mut chain: Vec<Arc<str>> = vec![k0];
2539                    while let Some(k) = it.peek().and_then(field_key) {
2540                        it.next();
2541                        chain.push(k);
2542                    }
2543                    out.push(Opcode::FieldChain(Arc::new(FieldChainData::new(chain.into()))));
2544                    continue;
2545                }
2546                out.push(op);
2547            } else {
2548                out.push(op);
2549            }
2550        }
2551        out
2552    }
2553
2554    /// Fuse `PushRoot + GetField(k1) + GetField(k2) ...` → `RootChain([k1,k2,...])`.
2555    fn pass_root_chain(ops: Vec<Opcode>) -> Vec<Opcode> {
2556        let mut out = Vec::with_capacity(ops.len());
2557        let mut it = ops.into_iter().peekable();
2558        while let Some(op) = it.next() {
2559            if matches!(op, Opcode::PushRoot) {
2560                let mut chain: Vec<Arc<str>> = Vec::new();
2561                while let Some(Opcode::GetField(_)) = it.peek() {
2562                    if let Some(Opcode::GetField(k)) = it.next() {
2563                        chain.push(k);
2564                    }
2565                }
2566                if chain.is_empty() {
2567                    out.push(Opcode::PushRoot);
2568                } else {
2569                    out.push(Opcode::RootChain(chain.into()));
2570                }
2571            } else {
2572                out.push(op);
2573            }
2574        }
2575        out
2576    }
2577
2578    /// Fuse `CallMethod(filter/pred) + CallMethod(len/count)` → `FilterCount(pred)`.
2579    fn pass_filter_count(ops: Vec<Opcode>) -> Vec<Opcode> {
2580        let mut out = Vec::with_capacity(ops.len());
2581        let mut it = ops.into_iter().peekable();
2582        while let Some(op) = it.next() {
2583            if let Opcode::CallMethod(ref call) = op {
2584                let is_filter_like = call.method == BuiltinMethod::Filter
2585                    || (call.method == BuiltinMethod::Unknown
2586                        && matches!(call.name.as_ref(), "find" | "find_all" | "findAll"));
2587                if is_filter_like && call.sub_progs.len() == 1 {
2588                    let is_len = matches!(it.peek(),
2589                        Some(Opcode::CallMethod(c))
2590                            if c.method == BuiltinMethod::Len || c.method == BuiltinMethod::Count
2591                    );
2592                    if is_len {
2593                        let pred = Arc::clone(&call.sub_progs[0]);
2594                        it.next(); // consume Len/Count
2595                        out.push(Opcode::FilterCount(pred));
2596                        continue;
2597                    }
2598                }
2599            }
2600            out.push(op);
2601        }
2602        out
2603    }
2604
2605    /// Fuse `InlineFilter(pred) + Quantifier(First/One)` → `FindFirst/FindOne(pred)`.
2606    /// Also fuses `CallMethod(Filter, pred) + Quantifier(...)` for explicit `.filter()`.
2607    fn pass_find_quantifier(ops: Vec<Opcode>) -> Vec<Opcode> {
2608        let mut out = Vec::with_capacity(ops.len());
2609        let mut it = ops.into_iter().peekable();
2610        while let Some(op) = it.next() {
2611            let pred_opt: Option<Arc<Program>> = match &op {
2612                Opcode::InlineFilter(p) => Some(Arc::clone(p)),
2613                Opcode::CallMethod(c) if c.method == BuiltinMethod::Filter && !c.sub_progs.is_empty()
2614                    => Some(Arc::clone(&c.sub_progs[0])),
2615                _ => None,
2616            };
2617            if let Some(pred) = pred_opt {
2618                match it.peek() {
2619                    Some(Opcode::Quantifier(QuantifierKind::First)) => {
2620                        it.next();
2621                        out.push(Opcode::FindFirst(pred));
2622                        continue;
2623                    }
2624                    Some(Opcode::Quantifier(QuantifierKind::One)) => {
2625                        it.next();
2626                        out.push(Opcode::FindOne(pred));
2627                        continue;
2628                    }
2629                    // `.filter(p).first()` — scans until predicate holds, returns
2630                    // that item or null.  Skips materialising a filtered array.
2631                    Some(Opcode::CallMethod(c))
2632                        if c.method == BuiltinMethod::First && c.sub_progs.is_empty() =>
2633                    {
2634                        it.next();
2635                        out.push(Opcode::FindFirst(pred));
2636                        continue;
2637                    }
2638                    _ => {}
2639                }
2640            }
2641            out.push(op);
2642        }
2643        out
2644    }
2645
2646    /// Eliminate redundant adjacent method calls:
2647    ///   reverse() + reverse()         → identity (both dropped)
2648    ///   unique() + unique()           → unique()
2649    ///   compact() + compact()         → compact()
2650    ///   sort() + sort(k)              → sort(k)      (later sort wins on same-array)
2651    ///   sort(k) + sort(k)             → sort(k)
2652    ///   Quantifier + Quantifier       → second only  (first wrap is scalar anyway)
2653    fn pass_redundant_ops(ops: Vec<Opcode>) -> Vec<Opcode> {
2654        let mut out: Vec<Opcode> = Vec::with_capacity(ops.len());
2655        for op in ops {
2656            match (&op, out.last()) {
2657                // reverse + reverse: drop both
2658                (Opcode::CallMethod(b), Some(Opcode::CallMethod(a)))
2659                    if a.method == BuiltinMethod::Reverse && b.method == BuiltinMethod::Reverse =>
2660                {
2661                    out.pop();
2662                    continue;
2663                }
2664                // idempotent method pairs: keep second only (drop first)
2665                (Opcode::CallMethod(b), Some(Opcode::CallMethod(a)))
2666                    if a.method == b.method && matches!(a.method,
2667                        BuiltinMethod::Unique | BuiltinMethod::Compact)
2668                        && a.sub_progs.is_empty() && b.sub_progs.is_empty() =>
2669                {
2670                    out.pop();
2671                    out.push(op);
2672                    continue;
2673                }
2674                // sort + sort(_): later sort wins, drop the first
2675                (Opcode::CallMethod(b), Some(Opcode::CallMethod(a)))
2676                    if a.method == BuiltinMethod::Sort && b.method == BuiltinMethod::Sort =>
2677                {
2678                    out.pop();
2679                    out.push(op);
2680                    continue;
2681                }
2682                // Quantifier + Quantifier: second wins (first unwraps scalar,
2683                // second is no-op on scalar — but keeping second preserves error
2684                // semantics of `!`). Drop first.
2685                (Opcode::Quantifier(_), Some(Opcode::Quantifier(_))) => {
2686                    out.pop();
2687                    out.push(op);
2688                    continue;
2689                }
2690                // reverse + last → first (strength reduction fallback after sort)
2691                // already handled in pass_strength_reduce
2692                // Not + Not: double negation → drop both
2693                (Opcode::Not, Some(Opcode::Not)) => {
2694                    out.pop();
2695                    continue;
2696                }
2697                // Neg + Neg: --x → x
2698                (Opcode::Neg, Some(Opcode::Neg)) => {
2699                    out.pop();
2700                    continue;
2701                }
2702                _ => {}
2703            }
2704            out.push(op);
2705        }
2706        out
2707    }
2708
2709    /// Constant-fold adjacent integer arithmetic + bool short-circuits.
2710    fn pass_const_fold(ops: Vec<Opcode>) -> Vec<Opcode> {
2711        let mut out = Vec::with_capacity(ops.len());
2712        let mut i = 0;
2713        while i < ops.len() {
2714            // 2-op bool short-circuit folds:
2715            //   PushBool(false) + AndOp(_)  → PushBool(false)
2716            //   PushBool(true)  + OrOp(_)   → PushBool(true)
2717            if i + 1 < ops.len() {
2718                let folded = match (&ops[i], &ops[i+1]) {
2719                    (Opcode::PushBool(false), Opcode::AndOp(_)) =>
2720                        Some(Opcode::PushBool(false)),
2721                    (Opcode::PushBool(true),  Opcode::OrOp(_)) =>
2722                        Some(Opcode::PushBool(true)),
2723                    _ => None,
2724                };
2725                if let Some(folded) = folded {
2726                    out.push(folded);
2727                    i += 2;
2728                    continue;
2729                }
2730            }
2731            // 2-op unary folds
2732            if i + 1 < ops.len() {
2733                let folded = match (&ops[i], &ops[i+1]) {
2734                    (Opcode::PushBool(b), Opcode::Not) =>
2735                        Some(Opcode::PushBool(!b)),
2736                    (Opcode::PushInt(n), Opcode::Neg) =>
2737                        Some(Opcode::PushInt(-n)),
2738                    (Opcode::PushFloat(f), Opcode::Neg) =>
2739                        Some(Opcode::PushFloat(-f)),
2740                    _ => None,
2741                };
2742                if let Some(folded) = folded {
2743                    out.push(folded);
2744                    i += 2;
2745                    continue;
2746                }
2747            }
2748            // 3-op arithmetic + comparison folds
2749            if i + 2 < ops.len() {
2750                let folded = match (&ops[i], &ops[i+1], &ops[i+2]) {
2751                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Add) =>
2752                        Some(Opcode::PushInt(a + b)),
2753                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Sub) =>
2754                        Some(Opcode::PushInt(a - b)),
2755                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Mul) =>
2756                        Some(Opcode::PushInt(a * b)),
2757                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Mod) if *b != 0 =>
2758                        Some(Opcode::PushInt(a % b)),
2759                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Div) if *b != 0 =>
2760                        Some(Opcode::PushFloat(*a as f64 / *b as f64)),
2761                    (Opcode::PushFloat(a), Opcode::PushFloat(b), Opcode::Add) =>
2762                        Some(Opcode::PushFloat(a + b)),
2763                    (Opcode::PushFloat(a), Opcode::PushFloat(b), Opcode::Sub) =>
2764                        Some(Opcode::PushFloat(a - b)),
2765                    (Opcode::PushFloat(a), Opcode::PushFloat(b), Opcode::Mul) =>
2766                        Some(Opcode::PushFloat(a * b)),
2767                    (Opcode::PushFloat(a), Opcode::PushFloat(b), Opcode::Div) if *b != 0.0 =>
2768                        Some(Opcode::PushFloat(a / b)),
2769                    // Mixed int/float arithmetic
2770                    (Opcode::PushInt(a), Opcode::PushFloat(b), Opcode::Add) =>
2771                        Some(Opcode::PushFloat(*a as f64 + b)),
2772                    (Opcode::PushFloat(a), Opcode::PushInt(b), Opcode::Add) =>
2773                        Some(Opcode::PushFloat(a + *b as f64)),
2774                    (Opcode::PushInt(a), Opcode::PushFloat(b), Opcode::Sub) =>
2775                        Some(Opcode::PushFloat(*a as f64 - b)),
2776                    (Opcode::PushFloat(a), Opcode::PushInt(b), Opcode::Sub) =>
2777                        Some(Opcode::PushFloat(a - *b as f64)),
2778                    (Opcode::PushInt(a), Opcode::PushFloat(b), Opcode::Mul) =>
2779                        Some(Opcode::PushFloat(*a as f64 * b)),
2780                    (Opcode::PushFloat(a), Opcode::PushInt(b), Opcode::Mul) =>
2781                        Some(Opcode::PushFloat(a * *b as f64)),
2782                    (Opcode::PushInt(a), Opcode::PushFloat(b), Opcode::Div) if *b != 0.0 =>
2783                        Some(Opcode::PushFloat(*a as f64 / b)),
2784                    (Opcode::PushFloat(a), Opcode::PushInt(b), Opcode::Div) if *b != 0 =>
2785                        Some(Opcode::PushFloat(a / *b as f64)),
2786                    // Mixed int/float comparisons
2787                    (Opcode::PushInt(a), Opcode::PushFloat(b), Opcode::Lt) =>
2788                        Some(Opcode::PushBool((*a as f64) < *b)),
2789                    (Opcode::PushFloat(a), Opcode::PushInt(b), Opcode::Lt) =>
2790                        Some(Opcode::PushBool(*a < (*b as f64))),
2791                    (Opcode::PushInt(a), Opcode::PushFloat(b), Opcode::Gt) =>
2792                        Some(Opcode::PushBool((*a as f64) > *b)),
2793                    (Opcode::PushFloat(a), Opcode::PushInt(b), Opcode::Gt) =>
2794                        Some(Opcode::PushBool(*a > (*b as f64))),
2795                    (Opcode::PushInt(a), Opcode::PushFloat(b), Opcode::Lte) =>
2796                        Some(Opcode::PushBool((*a as f64) <= *b)),
2797                    (Opcode::PushFloat(a), Opcode::PushInt(b), Opcode::Lte) =>
2798                        Some(Opcode::PushBool(*a <= (*b as f64))),
2799                    (Opcode::PushInt(a), Opcode::PushFloat(b), Opcode::Gte) =>
2800                        Some(Opcode::PushBool((*a as f64) >= *b)),
2801                    (Opcode::PushFloat(a), Opcode::PushInt(b), Opcode::Gte) =>
2802                        Some(Opcode::PushBool(*a >= (*b as f64))),
2803                    // Float comparisons (parity with int)
2804                    (Opcode::PushFloat(a), Opcode::PushFloat(b), Opcode::Lt) =>
2805                        Some(Opcode::PushBool(a < b)),
2806                    (Opcode::PushFloat(a), Opcode::PushFloat(b), Opcode::Lte) =>
2807                        Some(Opcode::PushBool(a <= b)),
2808                    (Opcode::PushFloat(a), Opcode::PushFloat(b), Opcode::Gt) =>
2809                        Some(Opcode::PushBool(a > b)),
2810                    (Opcode::PushFloat(a), Opcode::PushFloat(b), Opcode::Gte) =>
2811                        Some(Opcode::PushBool(a >= b)),
2812                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Eq) =>
2813                        Some(Opcode::PushBool(a == b)),
2814                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Neq) =>
2815                        Some(Opcode::PushBool(a != b)),
2816                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Lt) =>
2817                        Some(Opcode::PushBool(a < b)),
2818                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Lte) =>
2819                        Some(Opcode::PushBool(a <= b)),
2820                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Gt) =>
2821                        Some(Opcode::PushBool(a > b)),
2822                    (Opcode::PushInt(a), Opcode::PushInt(b), Opcode::Gte) =>
2823                        Some(Opcode::PushBool(a >= b)),
2824                    (Opcode::PushStr(a), Opcode::PushStr(b), Opcode::Eq) =>
2825                        Some(Opcode::PushBool(a == b)),
2826                    (Opcode::PushStr(a), Opcode::PushStr(b), Opcode::Neq) =>
2827                        Some(Opcode::PushBool(a != b)),
2828                    (Opcode::PushStr(a), Opcode::PushStr(b), Opcode::Lt) =>
2829                        Some(Opcode::PushBool(a < b)),
2830                    (Opcode::PushStr(a), Opcode::PushStr(b), Opcode::Lte) =>
2831                        Some(Opcode::PushBool(a <= b)),
2832                    (Opcode::PushStr(a), Opcode::PushStr(b), Opcode::Gt) =>
2833                        Some(Opcode::PushBool(a > b)),
2834                    (Opcode::PushStr(a), Opcode::PushStr(b), Opcode::Gte) =>
2835                        Some(Opcode::PushBool(a >= b)),
2836                    (Opcode::PushStr(a), Opcode::PushStr(b), Opcode::Add) =>
2837                        Some(Opcode::PushStr(Arc::<str>::from(format!("{}{}", a, b)))),
2838                    (Opcode::PushBool(a), Opcode::PushBool(b), Opcode::Eq) =>
2839                        Some(Opcode::PushBool(a == b)),
2840                    _ => None,
2841                };
2842                if let Some(folded) = folded {
2843                    out.push(folded);
2844                    i += 3;
2845                    continue;
2846                }
2847            }
2848            out.push(ops[i].clone());
2849            i += 1;
2850        }
2851        out
2852    }
2853
2854    // ── Main emit ─────────────────────────────────────────────────────────────
2855
2856    fn emit(expr: &Expr, ctx: &VarCtx) -> Vec<Opcode> {
2857        let mut ops = Vec::new();
2858        Self::emit_into(expr, ctx, &mut ops);
2859        ops
2860    }
2861
2862    fn emit_into(expr: &Expr, ctx: &VarCtx, ops: &mut Vec<Opcode>) {
2863        match expr {
2864            Expr::Null    => ops.push(Opcode::PushNull),
2865            Expr::Bool(b) => ops.push(Opcode::PushBool(*b)),
2866            Expr::Int(n)  => ops.push(Opcode::PushInt(*n)),
2867            Expr::Float(f)=> ops.push(Opcode::PushFloat(*f)),
2868            Expr::Str(s)  => ops.push(Opcode::PushStr(Arc::from(s.as_str()))),
2869            Expr::Root    => ops.push(Opcode::PushRoot),
2870            Expr::Current => ops.push(Opcode::PushCurrent),
2871
2872            Expr::FString(parts) => {
2873                let compiled: Vec<CompiledFSPart> = parts.iter().map(|p| match p {
2874                    FStringPart::Lit(s) => CompiledFSPart::Lit(Arc::from(s.as_str())),
2875                    FStringPart::Interp { expr, fmt } => CompiledFSPart::Interp {
2876                        prog: Arc::new(Self::compile_sub(expr, ctx)),
2877                        fmt: fmt.clone(),
2878                    },
2879                }).collect();
2880                ops.push(Opcode::FString(compiled.into()));
2881            }
2882
2883            Expr::Ident(name) => ops.push(Opcode::LoadIdent(Arc::from(name.as_str()))),
2884
2885            Expr::Chain(base, steps) => {
2886                Self::emit_into(base, ctx, ops);
2887                for step in steps {
2888                    Self::emit_step(step, ctx, ops);
2889                }
2890            }
2891
2892            Expr::UnaryNeg(e) => {
2893                Self::emit_into(e, ctx, ops);
2894                ops.push(Opcode::Neg);
2895            }
2896            Expr::Not(e) => {
2897                Self::emit_into(e, ctx, ops);
2898                ops.push(Opcode::Not);
2899            }
2900
2901            Expr::BinOp(l, op, r) => Self::emit_binop(l, *op, r, ctx, ops),
2902
2903            Expr::Coalesce(lhs, rhs) => {
2904                Self::emit_into(lhs, ctx, ops);
2905                let rhs_prog = Arc::new(Self::compile_sub(rhs, ctx));
2906                ops.push(Opcode::CoalesceOp(rhs_prog));
2907            }
2908
2909            Expr::Kind { expr, ty, negate } => {
2910                Self::emit_into(expr, ctx, ops);
2911                ops.push(Opcode::KindCheck { ty: *ty, negate: *negate });
2912            }
2913
2914            Expr::Object(fields) => {
2915                let entries: Vec<CompiledObjEntry> = fields.iter().map(|f| match f {
2916                    ObjField::Short(name) =>
2917                        CompiledObjEntry::Short {
2918                            name: Arc::from(name.as_str()),
2919                            ic: Arc::new(AtomicU64::new(0)),
2920                        },
2921                    ObjField::Kv { key, val, optional, cond }
2922                        if cond.is_none()
2923                            && Self::try_kv_path_steps(val).is_some()
2924                    => {
2925                        let steps: Vec<KvStep> = Self::try_kv_path_steps(val).unwrap();
2926                        let n = steps.len();
2927                        let mut ics_vec: Vec<AtomicU64> = Vec::with_capacity(n);
2928                        for _ in 0..n { ics_vec.push(AtomicU64::new(0)); }
2929                        CompiledObjEntry::KvPath {
2930                            key: Arc::from(key.as_str()),
2931                            steps: steps.into(),
2932                            optional: *optional,
2933                            ics: ics_vec.into(),
2934                        }
2935                    }
2936                    ObjField::Kv { key, val, optional, cond } =>
2937                        CompiledObjEntry::Kv {
2938                            key: Arc::from(key.as_str()),
2939                            prog: Arc::new(Self::compile_sub(val, ctx)),
2940                            optional: *optional,
2941                            cond: cond.as_ref().map(|c| Arc::new(Self::compile_sub(c, ctx))),
2942                        },
2943                    ObjField::Dynamic { key, val } =>
2944                        CompiledObjEntry::Dynamic {
2945                            key: Arc::new(Self::compile_sub(key, ctx)),
2946                            val: Arc::new(Self::compile_sub(val, ctx)),
2947                        },
2948                    ObjField::Spread(e) =>
2949                        CompiledObjEntry::Spread(Arc::new(Self::compile_sub(e, ctx))),
2950                    ObjField::SpreadDeep(e) =>
2951                        CompiledObjEntry::SpreadDeep(Arc::new(Self::compile_sub(e, ctx))),
2952                }).collect();
2953                ops.push(Opcode::MakeObj(entries.into()));
2954            }
2955
2956            Expr::Array(elems) => {
2957                // Compile each elem as a sub-program.
2958                // Spread elems are handled by a special marker.
2959                let _progs: Vec<Arc<Program>> = elems.iter().map(|e| match e {
2960                    ArrayElem::Expr(ex)   => Arc::new(Self::compile_sub(ex, ctx)),
2961                    // Spread: compile the inner expr with a spread marker opcode
2962                    ArrayElem::Spread(ex) => {
2963                        let mut sub = Self::emit(ex, ctx);
2964                        // Prepend a sentinel to distinguish spread from normal
2965                        sub.insert(0, Opcode::PushNull); // placeholder
2966                        // Actually, encode spread differently via a wrapper
2967                        Arc::new(Self::compile_array_spread(ex, ctx))
2968                    }
2969                }).collect();
2970                // Simpler: build a mixed elem list
2971                let progs = elems.iter().map(|e| match e {
2972                    ArrayElem::Expr(ex) => {
2973                        Arc::new(Self::compile_sub(ex, ctx))
2974                    }
2975                    ArrayElem::Spread(ex) => {
2976                        Arc::new(Self::compile_sub_spread(ex, ctx))
2977                    }
2978                }).collect::<Vec<_>>();
2979                ops.push(Opcode::MakeArr(progs.into()));
2980            }
2981
2982            Expr::Pipeline { base, steps } => {
2983                Self::emit_pipeline(base, steps, ctx, ops);
2984            }
2985
2986            Expr::ListComp { expr, vars, iter, cond } => {
2987                let inner_ctx = ctx.with_vars(vars);
2988                ops.push(Opcode::ListComp(Arc::new(CompSpec {
2989                    expr: Arc::new(Self::compile_sub(expr, &inner_ctx)),
2990                    vars: vars.iter().map(|v| Arc::from(v.as_str())).collect::<Vec<_>>().into(),
2991                    iter: Arc::new(Self::compile_sub(iter, ctx)),
2992                    cond: cond.as_ref().map(|c| Arc::new(Self::compile_sub(c, &inner_ctx))),
2993                })));
2994            }
2995
2996            Expr::DictComp { key, val, vars, iter, cond } => {
2997                let inner_ctx = ctx.with_vars(vars);
2998                ops.push(Opcode::DictComp(Arc::new(DictCompSpec {
2999                    key:  Arc::new(Self::compile_sub(key, &inner_ctx)),
3000                    val:  Arc::new(Self::compile_sub(val, &inner_ctx)),
3001                    vars: vars.iter().map(|v| Arc::from(v.as_str())).collect::<Vec<_>>().into(),
3002                    iter: Arc::new(Self::compile_sub(iter, ctx)),
3003                    cond: cond.as_ref().map(|c| Arc::new(Self::compile_sub(c, &inner_ctx))),
3004                })));
3005            }
3006
3007            Expr::SetComp { expr, vars, iter, cond } |
3008            Expr::GenComp { expr, vars, iter, cond } => {
3009                let inner_ctx = ctx.with_vars(vars);
3010                ops.push(Opcode::SetComp(Arc::new(CompSpec {
3011                    expr: Arc::new(Self::compile_sub(expr, &inner_ctx)),
3012                    vars: vars.iter().map(|v| Arc::from(v.as_str())).collect::<Vec<_>>().into(),
3013                    iter: Arc::new(Self::compile_sub(iter, ctx)),
3014                    cond: cond.as_ref().map(|c| Arc::new(Self::compile_sub(c, &inner_ctx))),
3015                })));
3016            }
3017
3018            Expr::Lambda { .. } => {
3019                // Lambdas as standalone values are errors; they only appear as args
3020                ops.push(Opcode::PushNull);
3021            }
3022
3023            Expr::Let { name, init, body } => {
3024                // Dead-let: if body never references `name` and init is pure,
3025                // drop the binding entirely and emit body only.
3026                if super::analysis::expr_is_pure(init)
3027                    && !super::analysis::expr_uses_ident(body, name) {
3028                    Self::emit_into(body, ctx, ops);
3029                } else {
3030                    Self::emit_into(init, ctx, ops);
3031                    let body_ctx = ctx.with_var(name);
3032                    let body_prog = Arc::new(Self::compile_sub(body, &body_ctx));
3033                    ops.push(Opcode::LetExpr { name: Arc::from(name.as_str()), body: body_prog });
3034                }
3035            }
3036
3037            Expr::IfElse { cond, then_, else_ } => {
3038                // Compile-time fold when cond is a literal bool.
3039                match cond.as_ref() {
3040                    Expr::Bool(true)  => { Self::emit_into(then_, ctx, ops); }
3041                    Expr::Bool(false) => { Self::emit_into(else_, ctx, ops); }
3042                    _ => {
3043                        Self::emit_into(cond, ctx, ops);
3044                        let then_prog = Arc::new(Self::compile_sub(then_, ctx));
3045                        let else_prog = Arc::new(Self::compile_sub(else_, ctx));
3046                        ops.push(Opcode::IfElse { then_: then_prog, else_: else_prog });
3047                    }
3048                }
3049            }
3050
3051            Expr::GlobalCall { name, args } => {
3052                // Compile as a sequence of sub-progs + a special dispatch
3053                let sub_progs: Vec<Arc<Program>> = args.iter().map(|a| match a {
3054                    Arg::Pos(e) | Arg::Named(_, e) => Arc::new(Self::compile_sub(e, ctx)),
3055                }).collect();
3056                let call = Arc::new(CompiledCall {
3057                    method:    BuiltinMethod::Unknown,
3058                    name:      Arc::from(name.as_str()),
3059                    sub_progs: sub_progs.into(),
3060                    orig_args: args.iter().cloned().collect::<Vec<_>>().into(),
3061                });
3062                ops.push(Opcode::PushRoot); // global calls need root pushed first
3063                ops.push(Opcode::CallMethod(call));
3064            }
3065
3066            Expr::Cast { expr, ty } => {
3067                Self::emit_into(expr, ctx, ops);
3068                ops.push(Opcode::CastOp(*ty));
3069            }
3070
3071            Expr::Patch { .. } => {
3072                // Patch block: structural transform with COW writes, conditional
3073                // leaves, DELETE sentinel.  Emit opaque opcode; the VM delegates
3074                // to the tree-walker at runtime (patch is rare enough that the
3075                // opcode compile pays no dividend here).
3076                ops.push(Opcode::PatchEval(Arc::new(expr.clone())));
3077            }
3078
3079            Expr::DeleteMark => {
3080                // DELETE outside a patch-field value is a static error; the
3081                // tree-walker raises it at runtime, so emit a sentinel that
3082                // triggers the same path.
3083                ops.push(Opcode::PatchEval(Arc::new(Expr::DeleteMark)));
3084            }
3085        }
3086    }
3087
3088    fn emit_step(step: &Step, ctx: &VarCtx, ops: &mut Vec<Opcode>) {
3089        match step {
3090            Step::Field(name)    => ops.push(Opcode::GetField(Arc::from(name.as_str()))),
3091            Step::OptField(name) => ops.push(Opcode::OptField(Arc::from(name.as_str()))),
3092            Step::Descendant(n)  => ops.push(Opcode::Descendant(Arc::from(n.as_str()))),
3093            Step::DescendAll     => ops.push(Opcode::DescendAll),
3094            Step::Index(i)       => ops.push(Opcode::GetIndex(*i)),
3095            Step::DynIndex(e)    => ops.push(Opcode::DynIndex(Arc::new(Self::compile_sub(e, ctx)))),
3096            Step::Slice(a, b)    => ops.push(Opcode::GetSlice(*a, *b)),
3097            Step::Method(name, method_args) => {
3098                let call = Self::compile_call(name, method_args, ctx);
3099                ops.push(Opcode::CallMethod(Arc::new(call)));
3100            }
3101            Step::OptMethod(name, method_args) => {
3102                let call = Self::compile_call(name, method_args, ctx);
3103                ops.push(Opcode::CallOptMethod(Arc::new(call)));
3104            }
3105            Step::InlineFilter(pred) => {
3106                ops.push(Opcode::InlineFilter(Arc::new(Self::compile_sub(pred, ctx))));
3107            }
3108            Step::Quantifier(k) => ops.push(Opcode::Quantifier(*k)),
3109        }
3110    }
3111
3112    fn compile_call(name: &str, args: &[Arg], ctx: &VarCtx) -> CompiledCall {
3113        let method = BuiltinMethod::from_name(name);
3114        let sub_progs: Vec<Arc<Program>> = args.iter().map(|a| match a {
3115            Arg::Pos(e) | Arg::Named(_, e) => Arc::new(Self::compile_lambda_or_expr(e, ctx)),
3116        }).collect();
3117        CompiledCall {
3118            method,
3119            name: Arc::from(name),
3120            sub_progs: sub_progs.into(),
3121            orig_args: args.iter().cloned().collect::<Vec<_>>().into(),
3122        }
3123    }
3124
3125    /// Compile an argument expression; for lambdas, the lambda param becomes a
3126    /// known var in the inner context so `Ident(param)` emits `LoadIdent`.
3127    fn compile_lambda_or_expr(expr: &Expr, ctx: &VarCtx) -> Program {
3128        match expr {
3129            Expr::Lambda { params, body } => {
3130                let inner = ctx.with_vars(params);
3131                Self::compile_sub(body, &inner)
3132            }
3133            other => Self::compile_sub(other, ctx),
3134        }
3135    }
3136
3137    fn emit_binop(l: &Expr, op: BinOp, r: &Expr, ctx: &VarCtx, ops: &mut Vec<Opcode>) {
3138        match op {
3139            BinOp::And => {
3140                Self::emit_into(l, ctx, ops);
3141                let rhs_prog = Arc::new(Self::compile_sub(r, ctx));
3142                ops.push(Opcode::AndOp(rhs_prog));
3143            }
3144            BinOp::Or => {
3145                Self::emit_into(l, ctx, ops);
3146                let rhs_prog = Arc::new(Self::compile_sub(r, ctx));
3147                ops.push(Opcode::OrOp(rhs_prog));
3148            }
3149            BinOp::Add => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Add); }
3150            BinOp::Sub => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Sub); }
3151            BinOp::Mul => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Mul); }
3152            BinOp::Div => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Div); }
3153            BinOp::Mod => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Mod); }
3154            BinOp::Eq  => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Eq); }
3155            BinOp::Neq => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Neq); }
3156            BinOp::Lt  => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Lt); }
3157            BinOp::Lte => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Lte); }
3158            BinOp::Gt  => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Gt); }
3159            BinOp::Gte => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Gte); }
3160            BinOp::Fuzzy => { Self::emit_into(l, ctx, ops); Self::emit_into(r, ctx, ops); ops.push(Opcode::Fuzzy); }
3161        }
3162    }
3163
3164    fn emit_pipeline(base: &Expr, steps: &[PipeStep], ctx: &VarCtx, ops: &mut Vec<Opcode>) {
3165        Self::emit_into(base, ctx, ops);
3166        let mut cur_ctx = ctx.clone();
3167        for step in steps {
3168            match step {
3169                PipeStep::Forward(rhs) => {
3170                    Self::emit_pipe_forward(rhs, &cur_ctx, ops);
3171                }
3172                PipeStep::Bind(target) => {
3173                    Self::emit_bind(target, &mut cur_ctx, ops);
3174                }
3175            }
3176        }
3177    }
3178
3179    fn emit_pipe_forward(rhs: &Expr, ctx: &VarCtx, ops: &mut Vec<Opcode>) {
3180        match rhs {
3181            Expr::Ident(name) if !ctx.has(name) => {
3182                // No-arg method call on TOS
3183                let call = CompiledCall {
3184                    method:    BuiltinMethod::from_name(name),
3185                    name:      Arc::from(name.as_str()),
3186                    sub_progs: Arc::from(&[] as &[Arc<Program>]),
3187                    orig_args: Arc::from(&[] as &[Arg]),
3188                };
3189                ops.push(Opcode::CallMethod(Arc::new(call)));
3190            }
3191            Expr::Chain(base, steps) if !steps.is_empty() => {
3192                if let Expr::Ident(name) = base.as_ref() {
3193                    if !ctx.has(name) {
3194                        // method(args...) — base is method, steps are chained
3195                        let call = CompiledCall {
3196                            method:    BuiltinMethod::from_name(name),
3197                            name:      Arc::from(name.as_str()),
3198                            sub_progs: Arc::from(&[] as &[Arc<Program>]),
3199                            orig_args: Arc::from(&[] as &[Arg]),
3200                        };
3201                        ops.push(Opcode::CallMethod(Arc::new(call)));
3202                        for step in steps { Self::emit_step(step, ctx, ops); }
3203                        return;
3204                    }
3205                }
3206                ops.push(Opcode::SetCurrent);
3207                Self::emit_into(rhs, ctx, ops);
3208            }
3209            _ => {
3210                // Arbitrary expression; set current to pipe input, then eval
3211                ops.push(Opcode::SetCurrent);
3212                Self::emit_into(rhs, ctx, ops);
3213            }
3214        }
3215    }
3216
3217    fn emit_bind(target: &BindTarget, ctx: &mut VarCtx, ops: &mut Vec<Opcode>) {
3218        match target {
3219            BindTarget::Name(name) => {
3220                ops.push(Opcode::BindVar(Arc::from(name.as_str())));
3221                *ctx = ctx.with_var(name);
3222            }
3223            BindTarget::Obj { fields, rest } => {
3224                let spec = BindObjSpec {
3225                    fields: fields.iter().map(|f| Arc::from(f.as_str())).collect::<Vec<_>>().into(),
3226                    rest: rest.as_ref().map(|r| Arc::from(r.as_str())),
3227                };
3228                ops.push(Opcode::BindObjDestructure(Arc::new(spec)));
3229                for f in fields { *ctx = ctx.with_var(f); }
3230                if let Some(r) = rest { *ctx = ctx.with_var(r); }
3231            }
3232            BindTarget::Arr(names) => {
3233                let ns: Vec<Arc<str>> = names.iter().map(|n| Arc::from(n.as_str())).collect();
3234                ops.push(Opcode::BindArrDestructure(ns.into()));
3235                for n in names { *ctx = ctx.with_var(n); }
3236            }
3237        }
3238    }
3239
3240    fn compile_sub(expr: &Expr, ctx: &VarCtx) -> Program {
3241        let ops = Self::optimize(Self::emit(expr, ctx));
3242        Program::new(ops, "<sub>")
3243    }
3244
3245    /// Classify an object-value expression as a pure path on `current`:
3246    /// a chain of `Field(name)` / `Index(i)` steps rooted at `Expr::Current`.
3247    /// Returns `None` for anything else — the caller falls back to full
3248    /// sub-program compilation.
3249    fn try_kv_path_steps(expr: &Expr) -> Option<Vec<KvStep>> {
3250        use super::ast::Step;
3251        let (base, steps) = match expr {
3252            Expr::Chain(b, s) => (&**b, s.as_slice()),
3253            _ => return None,
3254        };
3255        if !matches!(base, Expr::Current) { return None; }
3256        if steps.is_empty() { return None; }
3257        let mut out = Vec::with_capacity(steps.len());
3258        for s in steps {
3259            match s {
3260                Step::Field(name) => out.push(KvStep::Field(Arc::from(name.as_str()))),
3261                Step::Index(i)    => out.push(KvStep::Index(*i)),
3262                _ => return None,
3263            }
3264        }
3265        Some(out)
3266    }
3267
3268    fn compile_array_spread(_expr: &Expr, _ctx: &VarCtx) -> Program {
3269        // Not reached — handled in MakeArr execution
3270        Program::new(vec![], "<spread>")
3271    }
3272
3273    /// Compile a spread array element — wrapped with a special marker.
3274    fn compile_sub_spread(expr: &Expr, ctx: &VarCtx) -> Program {
3275        let mut ops = Self::emit(expr, ctx);
3276        // Prefix with a sentinel bool to mark this as a spread
3277        ops.insert(0, Opcode::PushBool(true));
3278        // Append a sentinel for reading: bool(true) + actual val
3279        // Actually use a dedicated approach: GetSlice-like marker
3280        // For simplicity, just compile the expr normally;
3281        // MakeArr handles spread by checking if the result is an array
3282        // when the corresponding ArrayElem is Spread.
3283        // Re-do: just compile normally, MakeArr knows which slots are spreads.
3284        Self::compile_sub(expr, ctx) // caller has separate spread tracking
3285    }
3286}
3287
3288// ── Path cache ────────────────────────────────────────────────────────────────
3289//
3290// Key: (doc_hash, json_pointer) → Val
3291//
3292// Doc-scoped (no program_id): any program resolving the same path on the same
3293// document gets a hit.  Intermediate nodes are cached so a prefix of a longer
3294// path can be reused without re-traversal.
3295
3296struct PathCache {
3297    /// doc_hash → (pointer_string → Val)
3298    docs:     HashMap<u64, HashMap<Arc<str>, Val>>,
3299    /// FIFO eviction order
3300    order:    VecDeque<(u64, Arc<str>)>,
3301    capacity: usize,
3302}
3303
3304impl PathCache {
3305    fn new(cap: usize) -> Self {
3306        Self {
3307            docs:     HashMap::new(),
3308            order:    VecDeque::with_capacity(cap),
3309            capacity: cap,
3310        }
3311    }
3312
3313    /// O(1) immutable lookup — returns cloned Val (Val::clone is O(1)).
3314    #[inline]
3315    fn get(&self, doc_hash: u64, ptr: &str) -> Option<Val> {
3316        self.docs.get(&doc_hash)?.get(ptr).cloned()
3317    }
3318
3319    fn insert(&mut self, doc_hash: u64, ptr: Arc<str>, val: Val) {
3320        if self.order.len() >= self.capacity {
3321            if let Some((old_hash, old_ptr)) = self.order.pop_front() {
3322                if let Some(inner) = self.docs.get_mut(&old_hash) {
3323                    inner.remove(old_ptr.as_ref());
3324                    if inner.is_empty() { self.docs.remove(&old_hash); }
3325                }
3326            }
3327        }
3328        self.order.push_back((doc_hash, ptr.clone()));
3329        self.docs.entry(doc_hash).or_insert_with(HashMap::new).insert(ptr, val);
3330    }
3331
3332    fn len(&self) -> usize { self.order.len() }
3333}
3334
3335// ── VM ────────────────────────────────────────────────────────────────────────
3336
3337/// High-performance v2 virtual machine.
3338///
3339/// Maintains:
3340/// - **Compile cache** — expression string → `Program` (parse + compile once).
3341/// - **Path cache** — `(doc_hash, json_pointer)` → `Val`; doc-scoped so any
3342///   program navigating the same path on the same document shares cached nodes.
3343///   Intermediate nodes are populated as a side-effect of every traversal,
3344///   enabling prefix reuse without re-traversal.
3345///
3346/// One VM per thread; wrap in `Mutex` for shared use.
3347/// Toggle each optimiser pass independently.  Default enables every
3348/// pass.  Disabling a pass invalidates the compile cache for the next
3349/// compilation by changing `hash()`.
3350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3351pub struct PassConfig {
3352    pub root_chain:      bool,
3353    pub field_chain:     bool,
3354    pub filter_count:    bool,
3355    pub filter_fusion:   bool,
3356    pub find_quantifier: bool,
3357    pub strength_reduce: bool,
3358    pub redundant_ops:   bool,
3359    pub kind_check_fold: bool,
3360    pub method_const:    bool,
3361    pub const_fold:      bool,
3362    pub nullness:        bool,
3363    pub equi_join:       bool,
3364    pub reorder_and:     bool,
3365    pub dedup_subprogs:  bool,
3366}
3367
3368impl Default for PassConfig {
3369    fn default() -> Self {
3370        Self {
3371            root_chain: true, field_chain: true, filter_count: true, filter_fusion: true,
3372            find_quantifier: true, strength_reduce: true, redundant_ops: true,
3373            kind_check_fold: true, method_const: true, const_fold: true,
3374            nullness: true, equi_join: true,
3375            reorder_and: true, dedup_subprogs: true,
3376        }
3377    }
3378}
3379
3380impl PassConfig {
3381    /// Disable every pass — emit raw opcodes.
3382    pub fn none() -> Self {
3383        Self {
3384            root_chain: false, field_chain: false, filter_count: false, filter_fusion: false,
3385            find_quantifier: false, strength_reduce: false, redundant_ops: false,
3386            kind_check_fold: false, method_const: false, const_fold: false,
3387            nullness: false, equi_join: false,
3388            reorder_and: false, dedup_subprogs: false,
3389        }
3390    }
3391
3392    pub fn hash(&self) -> u64 {
3393        let mut bits: u64 = 0;
3394        for (i, b) in [self.root_chain, self.field_chain, self.filter_count, self.filter_fusion,
3395                       self.find_quantifier, self.strength_reduce, self.redundant_ops,
3396                       self.kind_check_fold, self.method_const, self.const_fold,
3397                       self.nullness, self.equi_join,
3398                       self.reorder_and, self.dedup_subprogs].iter().enumerate() {
3399            if *b { bits |= 1u64 << i; }
3400        }
3401        bits
3402    }
3403}
3404
3405pub struct VM {
3406    registry:      Arc<MethodRegistry>,
3407    /// Cache key = (pass_config_hash, expr_string).  Changing `config`
3408    /// invalidates prior entries automatically via key divergence.
3409    compile_cache: HashMap<(u64, String), Arc<Program>>,
3410    /// LRU ordering for `compile_cache`; front = least recently used.
3411    /// Entries are moved to back on hit; oldest evicted when over cap.
3412    compile_lru:   std::collections::VecDeque<(u64, String)>,
3413    compile_cap:   usize,
3414    path_cache:    PathCache,
3415    /// Per-exec RootChain resolution cache.  Key = raw address of the
3416    /// `chain` Arc slice; value = resolved Val.  Cleared on every top-level
3417    /// `execute()` call so stale entries never outlive the doc they
3418    /// reference.  Avoids rebuilding the `/a/b/c` pointer string and
3419    /// consulting `path_cache` when the same RootChain opcode fires
3420    /// repeatedly inside a loop.
3421    root_chain_cache: HashMap<usize, Val>,
3422    /// Hash of the document currently being executed — set once by `execute()`,
3423    /// reused by all recursive `exec()` calls within the same top-level call.
3424    doc_hash:      u64,
3425    /// Cache of root Arc pointer → structural hash.  Lets repeated calls
3426    /// against the same cached root (e.g. via `Jetro::collect`) skip the
3427    /// O(doc) structural walk entirely.  Keyed on the inner Arc ptr of
3428    /// the root `Val::Obj`/`Val::Arr`.
3429    root_hash_cache: Option<(usize, u64)>,
3430    /// Optimiser pass toggles.  Default: all on.
3431    config:        PassConfig,
3432}
3433
3434impl Default for VM {
3435    fn default() -> Self { Self::new() }
3436}
3437
3438impl VM {
3439    pub fn new() -> Self { Self::with_capacity(512, 4096) }
3440
3441    pub fn with_capacity(compile_cap: usize, path_cap: usize) -> Self {
3442        Self {
3443            registry:      Arc::new(MethodRegistry::new()),
3444            compile_cache: HashMap::with_capacity(compile_cap),
3445            compile_lru:   std::collections::VecDeque::with_capacity(compile_cap),
3446            compile_cap,
3447            path_cache:    PathCache::new(path_cap),
3448            root_chain_cache: HashMap::new(),
3449            doc_hash:      0,
3450            root_hash_cache: None,
3451            config:        PassConfig::default(),
3452        }
3453    }
3454
3455    /// Build a VM that shares an existing method registry.
3456    pub fn with_registry(registry: Arc<MethodRegistry>) -> Self {
3457        let mut vm = Self::new();
3458        vm.registry = registry;
3459        vm
3460    }
3461
3462    /// Register a method already wrapped in `Arc`.
3463    pub fn register_arc(&mut self, name: &str, method: Arc<dyn crate::eval::methods::Method>) {
3464        Arc::make_mut(&mut self.registry).register_arc(name, method);
3465    }
3466
3467    /// Replace the pass configuration.  The compile cache is not purged,
3468    /// but future lookups key off the new config hash so old entries
3469    /// are effectively invalidated for the new regime.
3470    pub fn set_pass_config(&mut self, config: PassConfig) { self.config = config; }
3471
3472    pub fn pass_config(&self) -> PassConfig { self.config }
3473
3474    /// Register a custom method (callable via `.method_name(...)` in expressions).
3475    pub fn register(&mut self, name: impl Into<String>, method: impl super::eval::methods::Method + 'static) {
3476        Arc::make_mut(&mut self.registry).register(name, method);
3477    }
3478
3479    // ── Public entry-points ───────────────────────────────────────────────────
3480
3481    /// Parse, compile (cached), and execute `expr` against `doc`.
3482    pub fn run_str(&mut self, expr: &str, doc: &serde_json::Value) -> Result<serde_json::Value, EvalError> {
3483        let prog = self.get_or_compile(expr)?;
3484        self.execute(&prog, doc)
3485    }
3486
3487    /// Parse, compile, and execute with raw JSON source bytes retained so
3488    /// that SIMD byte-scan can short-circuit `Opcode::Descendant` at the
3489    /// document root.
3490    pub fn run_str_with_raw(
3491        &mut self,
3492        expr: &str,
3493        doc: &serde_json::Value,
3494        raw_bytes: Arc<[u8]>,
3495    ) -> Result<serde_json::Value, EvalError> {
3496        let prog = self.get_or_compile(expr)?;
3497        self.execute_with_raw(&prog, doc, raw_bytes)
3498    }
3499
3500    /// Execute a pre-compiled `Program` against `doc`.
3501    pub fn execute(&mut self, program: &Program, doc: &serde_json::Value) -> Result<serde_json::Value, EvalError> {
3502        let root = Val::from(doc);
3503        self.doc_hash = self.compute_or_cache_root_hash(&root);
3504        // Per-exec RootChain cache: entries key off raw Arc addresses that
3505        // must not outlive this document.  Clear before every run.
3506        self.root_chain_cache.clear();
3507        let env = self.make_env(root);
3508        let result = self.exec(program, &env)?;
3509        Ok(result.into())
3510    }
3511
3512    /// Reuse the cached structural hash if `root`'s inner Arc pointer
3513    /// matches a prior call; otherwise walk the tree once and cache.
3514    /// Primitive roots bypass the cache (hashing is already O(1)).
3515    fn compute_or_cache_root_hash(&mut self, root: &Val) -> u64 {
3516        let ptr: Option<usize> = match root {
3517            Val::Obj(m)      => Some(Arc::as_ptr(m) as *const () as usize),
3518            Val::Arr(a)      => Some(Arc::as_ptr(a) as *const () as usize),
3519            Val::IntVec(a)   => Some(Arc::as_ptr(a) as *const () as usize),
3520            Val::FloatVec(a) => Some(Arc::as_ptr(a) as *const () as usize),
3521            _ => None,
3522        };
3523        if let Some(p) = ptr {
3524            if let Some((cp, h)) = self.root_hash_cache {
3525                if cp == p { return h; }
3526            }
3527            let h = hash_val_structure(root);
3528            self.root_hash_cache = Some((p, h));
3529            h
3530        } else {
3531            hash_val_structure(root)
3532        }
3533    }
3534
3535    /// Execute with raw JSON source bytes retained on the environment so
3536    /// that descendant opcodes at document root can take the SIMD byte-scan
3537    /// fast path instead of walking the tree.
3538    pub fn execute_with_raw(
3539        &mut self,
3540        program: &Program,
3541        doc: &serde_json::Value,
3542        raw_bytes: Arc<[u8]>,
3543    ) -> Result<serde_json::Value, EvalError> {
3544        let root = Val::from(doc);
3545        self.execute_val_with_raw(program, root, raw_bytes)
3546    }
3547
3548    /// Execute against a pre-built `Val` root (skips the `Val::from(&Value)`
3549    /// conversion on every call).  With raw bytes, the `doc_hash` pointer-
3550    /// cache path is also skipped — byte-scan handles descendants directly
3551    /// and `RootChain` reads are O(chain length) against the already-built
3552    /// tree.
3553    pub fn execute_val_with_raw(
3554        &mut self,
3555        program: &Program,
3556        root: Val,
3557        raw_bytes: Arc<[u8]>,
3558    ) -> Result<serde_json::Value, EvalError> {
3559        // doc_hash seeds the path cache; on the scan fast path we bypass the
3560        // cache entirely, so skip the O(doc) structural hash walk.
3561        self.doc_hash = 0;
3562        self.root_chain_cache.clear();
3563        let env = Env::new_with_raw(root, Arc::clone(&self.registry), raw_bytes);
3564        let result = self.exec(program, &env)?;
3565        Ok(result.into())
3566    }
3567
3568    /// Execute against a pre-built `Val` root without raw bytes.  Skips the
3569    /// `Val::from` conversion only — path cache and doc hash still behave
3570    /// as in `execute()`.
3571    pub fn execute_val(
3572        &mut self,
3573        program: &Program,
3574        root: Val,
3575    ) -> Result<serde_json::Value, EvalError> {
3576        Ok(self.execute_val_raw(program, root)?.into())
3577    }
3578
3579    /// Execute against a pre-built `Val` root and return the raw `Val` —
3580    /// no `serde_json::Value` materialisation.  Use when the caller will
3581    /// consume results structurally (further queries, custom walk) and
3582    /// wants to skip a potentially expensive `Val → Value` conversion.
3583    pub fn execute_val_raw(
3584        &mut self,
3585        program: &Program,
3586        root: Val,
3587    ) -> Result<Val, EvalError> {
3588        self.doc_hash = self.compute_or_cache_root_hash(&root);
3589        self.root_chain_cache.clear();
3590        let env = self.make_env(root);
3591        self.exec(program, &env)
3592    }
3593
3594    /// Execute a compiled program against a document, first specialising
3595    /// against the given shape (turns `OptField` → `GetField` where safe,
3596    /// folds `KindCheck` where type is known, etc.).
3597    pub fn execute_with_schema(
3598        &mut self,
3599        program: &Program,
3600        doc: &serde_json::Value,
3601        shape: &super::schema::Shape,
3602    ) -> Result<serde_json::Value, EvalError> {
3603        let specialized = super::schema::specialize(program, shape);
3604        self.execute(&specialized, doc)
3605    }
3606
3607    /// Execute a program; infer the shape from the document itself.  Costs
3608    /// an O(doc) shape walk before execution; useful when the same
3609    /// compiled program is reused across many docs with similar shapes.
3610    pub fn execute_with_inferred_schema(
3611        &mut self,
3612        program: &Program,
3613        doc: &serde_json::Value,
3614    ) -> Result<serde_json::Value, EvalError> {
3615        let shape = super::schema::Shape::of(doc);
3616        self.execute_with_schema(program, doc, &shape)
3617    }
3618
3619    /// Get or compile an expression string (compile cache).
3620    /// Cache key is (pass_config_hash, expr) so that different pass
3621    /// configurations yield different compiled programs.
3622    pub fn get_or_compile(&mut self, expr: &str) -> Result<Arc<Program>, EvalError> {
3623        let key = (self.config.hash(), expr.to_string());
3624        if let Some(p) = self.compile_cache.get(&key) {
3625            let arc = Arc::clone(p);
3626            self.touch_lru(&key);
3627            return Ok(arc);
3628        }
3629        let prog = Compiler::compile_str_with_config(expr, self.config)?;
3630        let arc = Arc::new(prog);
3631        self.insert_compile(key, Arc::clone(&arc));
3632        Ok(arc)
3633    }
3634
3635    /// Move `key` to the back of the LRU queue (most recently used).
3636    fn touch_lru(&mut self, key: &(u64, String)) {
3637        if let Some(pos) = self.compile_lru.iter().position(|k| k == key) {
3638            let k = self.compile_lru.remove(pos).unwrap();
3639            self.compile_lru.push_back(k);
3640        }
3641    }
3642
3643    /// Insert into compile cache with LRU eviction at `compile_cap`.
3644    fn insert_compile(&mut self, key: (u64, String), prog: Arc<Program>) {
3645        while self.compile_cache.len() >= self.compile_cap && self.compile_cap > 0 {
3646            if let Some(old) = self.compile_lru.pop_front() {
3647                self.compile_cache.remove(&old);
3648            } else {
3649                break;
3650            }
3651        }
3652        self.compile_lru.push_back(key.clone());
3653        self.compile_cache.insert(key, prog);
3654    }
3655
3656    /// Cache statistics: `(compile_entries, path_entries)`.
3657    pub fn cache_stats(&self) -> (usize, usize) {
3658        (self.compile_cache.len(), self.path_cache.len())
3659    }
3660
3661    // ── Internal helpers ──────────────────────────────────────────────────────
3662
3663    fn make_env(&self, root: Val) -> Env {
3664        Env::new_with_registry(root, Arc::clone(&self.registry))
3665    }
3666
3667
3668    // ── Core execution loop ───────────────────────────────────────────────────
3669
3670    /// Execute `program` in environment `env`, returning the top-of-stack value.
3671    pub fn exec(&mut self, program: &Program, env: &Env) -> Result<Val, EvalError> {
3672        let mut stack: SmallVec<[Val; 16]> = SmallVec::new();
3673        let ops_slice: &[Opcode] = &program.ops;
3674        let mut skip_ahead: usize = 0;
3675
3676        for (op_idx, op) in ops_slice.iter().enumerate() {
3677            if skip_ahead > 0 { skip_ahead -= 1; continue; }
3678            match op {
3679                // ── Literals ──────────────────────────────────────────────────
3680                Opcode::PushNull        => stack.push(Val::Null),
3681                Opcode::PushBool(b)     => stack.push(Val::Bool(*b)),
3682                Opcode::PushInt(n)      => stack.push(Val::Int(*n)),
3683                Opcode::PushFloat(f)    => stack.push(Val::Float(*f)),
3684                Opcode::PushStr(s)      => stack.push(Val::Str(s.clone())),
3685
3686                // ── Context ───────────────────────────────────────────────────
3687                Opcode::PushRoot        => stack.push(env.root.clone()),
3688                Opcode::PushCurrent     => stack.push(env.current.clone()),
3689
3690                // ── Navigation ────────────────────────────────────────────────
3691                Opcode::GetField(k) => {
3692                    let v = pop!(stack);
3693                    let out = match &v {
3694                        Val::Obj(m) => ic_get_field(m, k.as_ref(), &program.ics[op_idx]),
3695                        _ => Val::Null,
3696                    };
3697                    stack.push(out);
3698                }
3699                Opcode::FieldChain(chain) => {
3700                    let mut cur = pop!(stack);
3701                    for (i, k) in chain.keys.iter().enumerate() {
3702                        cur = if let Val::Obj(m) = &cur {
3703                            ic_get_field(m, k.as_ref(), &chain.ics[i])
3704                        } else {
3705                            cur.get_field(k.as_ref())
3706                        };
3707                    }
3708                    stack.push(cur);
3709                }
3710                Opcode::GetIndex(i) => {
3711                    let v = pop!(stack);
3712                    stack.push(v.get_index(*i));
3713                }
3714                Opcode::DynIndex(prog) => {
3715                    let v = pop!(stack);
3716                    let key = self.exec(prog, env)?;
3717                    stack.push(match key {
3718                        Val::Int(i) => v.get_index(i),
3719                        Val::Str(s) => v.get_field(s.as_ref()),
3720                        _ => Val::Null,
3721                    });
3722                }
3723                Opcode::GetSlice(from, to) => {
3724                    let v = pop!(stack);
3725                    stack.push(exec_slice(v, *from, *to));
3726                }
3727                Opcode::OptField(k) => {
3728                    let v = pop!(stack);
3729                    let out = match &v {
3730                        Val::Null => Val::Null,
3731                        Val::Obj(m) => ic_get_field(m, k.as_ref(), &program.ics[op_idx]),
3732                        _ => v.get_field(k.as_ref()),
3733                    };
3734                    stack.push(out);
3735                }
3736                Opcode::Descendant(k) => {
3737                    let v = pop!(stack);
3738                    // (D) When descending from root, track pointer paths and
3739                    // cache each discovered node for future RootChain lookups.
3740                    let from_root = match (&v, &env.root) {
3741                        (Val::Obj(a), Val::Obj(b)) => Arc::ptr_eq(a, b),
3742                        (Val::Arr(a), Val::Arr(b)) => Arc::ptr_eq(a, b),
3743                        _ => matches!((&v, &env.root), (Val::Null, Val::Null)),
3744                    };
3745                    // SIMD fast path: descending from root with raw JSON bytes
3746                    // retained → byte-scan instead of walking the tree.  Path
3747                    // cache is skipped on this path (cost/benefit unfavorable
3748                    // vs avoiding the tree walk entirely).
3749                    if from_root {
3750                        if let Some(bytes) = env.raw_bytes.as_ref() {
3751                            let (val, extra) = byte_chain_exec(
3752                                bytes, k.as_ref(), &ops_slice[op_idx + 1..]
3753                            );
3754                            stack.push(val);
3755                            skip_ahead = extra;
3756                            continue;
3757                        }
3758                    }
3759                    // Tree-walker early exit: `$..k.first()` / `$..k!` materialises
3760                    // only the first self-first DFS hit.  SIMD byte_chain_exec
3761                    // already covers the raw-bytes case; this catches tree-only
3762                    // receivers.  Skips pointer-cache population (single-hit
3763                    // caller doesn't benefit from storing siblings).
3764                    if let Some(next) = ops_slice.get(op_idx + 1) {
3765                        if is_first_selector_op(next) {
3766                            let hit = find_desc_first(&v, k.as_ref()).unwrap_or(Val::Null);
3767                            stack.push(hit);
3768                            skip_ahead = 1;
3769                            continue;
3770                        }
3771                    }
3772                    let mut found = Vec::new();
3773                    if from_root {
3774                        let mut prefix = String::new();
3775                        let mut cached: Vec<(Arc<str>, Val)> = Vec::new();
3776                        collect_desc_with_paths(&v, k.as_ref(), &mut prefix, &mut found, &mut cached);
3777                        let doc_hash = self.doc_hash;
3778                        for (ptr, val) in cached {
3779                            self.path_cache.insert(doc_hash, ptr, val);
3780                        }
3781                    } else {
3782                        collect_desc(&v, k.as_ref(), &mut found);
3783                    }
3784                    stack.push(Val::arr(found));
3785                }
3786                Opcode::DescendAll => {
3787                    let v = pop!(stack);
3788                    let mut found = Vec::new();
3789                    collect_all(&v, &mut found);
3790                    stack.push(Val::arr(found));
3791                }
3792                Opcode::InlineFilter(pred) => {
3793                    let val = pop!(stack);
3794                    let items = match val {
3795                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
3796                        other => vec![other],
3797                    };
3798                    let mut out = Vec::with_capacity(items.len());
3799                    let mut scratch = env.clone();
3800                    for item in items {
3801                        let prev = scratch.swap_current(item.clone());
3802                        let keep = is_truthy(&self.exec(pred, &scratch)?);
3803                        scratch.restore_current(prev);
3804                        if keep { out.push(item); }
3805                    }
3806                    stack.push(Val::arr(out));
3807                }
3808                Opcode::Quantifier(kind) => {
3809                    let val = pop!(stack);
3810                    stack.push(match kind {
3811                        QuantifierKind::First => match val {
3812                            Val::Arr(a) => a.first().cloned().unwrap_or(Val::Null),
3813                            other => other,
3814                        },
3815                        QuantifierKind::One => match val {
3816                            Val::Arr(a) if a.len() == 1 => a[0].clone(),
3817                            Val::Arr(a) => return err!("quantifier !: expected exactly one element, got {}", a.len()),
3818                            other => other,
3819                        },
3820                    });
3821                }
3822
3823                // ── Peephole fusions ──────────────────────────────────────────
3824                Opcode::RootChain(chain) => {
3825                    // Fast path — same RootChain opcode fired earlier in this
3826                    // execute() call on this same doc.  Arc pointer identity
3827                    // is stable; cache is cleared per top-level execute().
3828                    let key = Arc::as_ptr(chain) as *const () as usize;
3829                    if let Some(v) = self.root_chain_cache.get(&key) {
3830                        stack.push(v.clone());
3831                        continue;
3832                    }
3833
3834                    let doc_hash = self.doc_hash;
3835                    let mut current = env.root.clone();
3836                    let mut ptr = String::new();
3837                    let mut resumed_from_cache = false;
3838                    for k in chain.iter() {
3839                        ptr.push('/');
3840                        ptr.push_str(k.as_ref());
3841                        // Try resuming from a longer cached prefix before
3842                        // stepping through get_field.
3843                        if !resumed_from_cache {
3844                            if let Some(cached) = self.path_cache.get(doc_hash, &ptr) {
3845                                current = cached;
3846                                continue;
3847                            }
3848                            resumed_from_cache = true;
3849                        }
3850                        current = current.get_field(k.as_ref());
3851                        self.path_cache.insert(doc_hash, Arc::from(ptr.as_str()), current.clone());
3852                    }
3853
3854                    self.root_chain_cache.insert(key, current.clone());
3855                    stack.push(current);
3856                }
3857                Opcode::FilterCount(pred) => {
3858                    let recv = pop!(stack);
3859                    let n = match &recv {
3860                        Val::Arr(a) => {
3861                            let mut count = 0u64;
3862                            let mut scratch = env.clone();
3863                            for item in a.iter() {
3864                                let prev = scratch.swap_current(item.clone());
3865                                let t = is_truthy(&self.exec(pred, &scratch)?);
3866                                scratch.restore_current(prev);
3867                                if t { count += 1; }
3868                            }
3869                            count
3870                        }
3871                        Val::IntVec(a) => {
3872                            let mut count = 0u64;
3873                            let mut scratch = env.clone();
3874                            for &n in a.iter() {
3875                                let prev = scratch.swap_current(Val::Int(n));
3876                                let t = is_truthy(&self.exec(pred, &scratch)?);
3877                                scratch.restore_current(prev);
3878                                if t { count += 1; }
3879                            }
3880                            count
3881                        }
3882                        Val::FloatVec(a) => {
3883                            let mut count = 0u64;
3884                            let mut scratch = env.clone();
3885                            for &f in a.iter() {
3886                                let prev = scratch.swap_current(Val::Float(f));
3887                                let t = is_truthy(&self.exec(pred, &scratch)?);
3888                                scratch.restore_current(prev);
3889                                if t { count += 1; }
3890                            }
3891                            count
3892                        }
3893                        _ => 0,
3894                    };
3895                    stack.push(Val::Int(n as i64));
3896                }
3897                Opcode::FindFirst(pred) => {
3898                    let recv = pop!(stack);
3899                    let mut found = Val::Null;
3900                    if let Val::Arr(a) = &recv {
3901                        let mut scratch = env.clone();
3902                        for item in a.iter() {
3903                            let prev = scratch.swap_current(item.clone());
3904                            let t = is_truthy(&self.exec(pred, &scratch)?);
3905                            scratch.restore_current(prev);
3906                            if t { found = item.clone(); break; }
3907                        }
3908                    } else if !recv.is_null() {
3909                        let sub_env = env.with_current(recv.clone());
3910                        if is_truthy(&self.exec(pred, &sub_env)?) { found = recv; }
3911                    }
3912                    stack.push(found);
3913                }
3914                Opcode::FilterMap { pred, map } => {
3915                    let recv = pop!(stack);
3916                    let recv = match recv {
3917                        Val::StrVec(_) | Val::IntVec(_) | Val::FloatVec(_) => recv.into_arr(),
3918                        v => v,
3919                    };
3920                    if let Val::Arr(a) = recv {
3921                        let mut out = Vec::with_capacity(a.len());
3922                        let mut scratch = env.clone();
3923                        for item in a.iter() {
3924                            let prev = scratch.swap_current(item.clone());
3925                            if is_truthy(&self.exec(pred, &scratch)?) {
3926                                out.push(self.exec(map, &scratch)?);
3927                            }
3928                            scratch.restore_current(prev);
3929                        }
3930                        stack.push(Val::arr(out));
3931                    } else {
3932                        stack.push(Val::arr(Vec::new()));
3933                    }
3934                }
3935                Opcode::MapFilter { map, pred } => {
3936                    let recv = pop!(stack);
3937                    let recv = match recv {
3938                        Val::StrVec(_) | Val::IntVec(_) | Val::FloatVec(_) => recv.into_arr(),
3939                        v => v,
3940                    };
3941                    if let Val::Arr(a) = recv {
3942                        let mut out = Vec::with_capacity(a.len());
3943                        let mut scratch = env.clone();
3944                        for item in a.iter() {
3945                            let prev = scratch.swap_current(item.clone());
3946                            let mapped = self.exec(map, &scratch)?;
3947                            let pscratch = scratch.with_current(mapped.clone());
3948                            if is_truthy(&self.exec(pred, &pscratch)?) {
3949                                out.push(mapped);
3950                            }
3951                            scratch.restore_current(prev);
3952                        }
3953                        stack.push(Val::arr(out));
3954                    } else {
3955                        stack.push(Val::arr(Vec::new()));
3956                    }
3957                }
3958                Opcode::FilterFilter { p1, p2 } => {
3959                    let recv = pop!(stack);
3960                    let recv = match recv {
3961                        Val::StrVec(_) | Val::IntVec(_) | Val::FloatVec(_) => recv.into_arr(),
3962                        v => v,
3963                    };
3964                    if let Val::Arr(a) = recv {
3965                        let mut out = Vec::with_capacity(a.len());
3966                        let mut scratch = env.clone();
3967                        for item in a.iter() {
3968                            let prev = scratch.swap_current(item.clone());
3969                            let keep = is_truthy(&self.exec(p1, &scratch)?)
3970                                    && is_truthy(&self.exec(p2, &scratch)?);
3971                            scratch.restore_current(prev);
3972                            if keep { out.push(item.clone()); }
3973                        }
3974                        stack.push(Val::arr(out));
3975                    } else {
3976                        stack.push(Val::arr(Vec::new()));
3977                    }
3978                }
3979                Opcode::MapToJsonJoin { sep_prog } => {
3980                    use std::fmt::Write as _;
3981                    let recv = pop!(stack);
3982                    let sep_val = self.exec(sep_prog, env)?;
3983                    let sep: &str = match &sep_val {
3984                        Val::Str(s) => s.as_ref(),
3985                        _ => "",
3986                    };
3987                    // Columnar shortcut: IntVec / FloatVec -> tight
3988                    // number-format loop without per-item Val alloc.
3989                    match &recv {
3990                        Val::IntVec(a) => {
3991                            let mut out = String::with_capacity(a.len() * 6);
3992                            let mut first = true;
3993                            for n in a.iter() {
3994                                if !first { out.push_str(sep); } first = false;
3995                                let _ = write!(out, "{}", n);
3996                            }
3997                            stack.push(Val::Str(Arc::<str>::from(out)));
3998                        }
3999                        Val::FloatVec(a) => {
4000                            let mut out = String::with_capacity(a.len() * 8);
4001                            let mut first = true;
4002                            for f in a.iter() {
4003                                if !first { out.push_str(sep); } first = false;
4004                                if f.is_finite() {
4005                                    let v = serde_json::Value::from(*f);
4006                                    out.push_str(&serde_json::to_string(&v).unwrap_or_default());
4007                                } else {
4008                                    out.push_str("null");
4009                                }
4010                            }
4011                            stack.push(Val::Str(Arc::<str>::from(out)));
4012                        }
4013                        Val::Arr(a) => {
4014                            let mut out = String::with_capacity(a.len() * 8);
4015                            let mut first = true;
4016                            for item in a.iter() {
4017                                if !first { out.push_str(sep); } first = false;
4018                                match item {
4019                                    Val::Int(n)  => { let _ = write!(out, "{}", n); }
4020                                    Val::Float(f) => {
4021                                        if f.is_finite() {
4022                                            let v = serde_json::Value::from(*f);
4023                                            out.push_str(&serde_json::to_string(&v).unwrap_or_default());
4024                                        } else { out.push_str("null"); }
4025                                    }
4026                                    Val::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
4027                                    Val::Null    => out.push_str("null"),
4028                                    Val::Str(s)  => {
4029                                        let src = s.as_ref();
4030                                        let mut needs_escape = false;
4031                                        for &b in src.as_bytes() {
4032                                            if b < 0x20 || b == b'"' || b == b'\\' { needs_escape = true; break; }
4033                                        }
4034                                        if !needs_escape {
4035                                            out.push('"'); out.push_str(src); out.push('"');
4036                                        } else {
4037                                            let v = serde_json::Value::String(s.to_string());
4038                                            out.push_str(&serde_json::to_string(&v).unwrap_or_default());
4039                                        }
4040                                    }
4041                                    _ => {
4042                                        let sv: serde_json::Value = item.clone().into();
4043                                        out.push_str(&serde_json::to_string(&sv).unwrap_or_default());
4044                                    }
4045                                }
4046                            }
4047                            stack.push(Val::Str(Arc::<str>::from(out)));
4048                        }
4049                        _ => stack.push(Val::Str(Arc::<str>::from(""))),
4050                    }
4051                }
4052                // ── Fused string-method chains ────────────────────────────────
4053                Opcode::StrTrimUpper | Opcode::StrTrimLower => {
4054                    let v = pop!(stack);
4055                    let out = if let Val::Str(s) = &v {
4056                        let t = s.trim();
4057                        let bytes = t.as_bytes();
4058                        if bytes.is_ascii() {
4059                            let upper = matches!(op, Opcode::StrTrimUpper);
4060                            Val::Str(ascii_fold_to_arc_str(bytes, upper))
4061                        } else {
4062                            let s2 = match op {
4063                                Opcode::StrTrimUpper => t.to_uppercase(),
4064                                _                    => t.to_lowercase(),
4065                            };
4066                            Val::Str(Arc::<str>::from(s2))
4067                        }
4068                    } else {
4069                        return Err(EvalError(format!("{:?}: expected string", op)));
4070                    };
4071                    stack.push(out);
4072                }
4073                Opcode::StrUpperTrim | Opcode::StrLowerTrim => {
4074                    let v = pop!(stack);
4075                    let out = if let Val::Str(s) = &v {
4076                        let bytes = s.as_bytes();
4077                        let t = s.trim();
4078                        let tb = t.as_bytes();
4079                        if bytes.is_ascii() {
4080                            let upper = matches!(op, Opcode::StrUpperTrim);
4081                            Val::Str(ascii_fold_to_arc_str(tb, upper))
4082                        } else {
4083                            let s2 = match op {
4084                                Opcode::StrUpperTrim => t.to_uppercase(),
4085                                _                    => t.to_lowercase(),
4086                            };
4087                            Val::Str(Arc::<str>::from(s2))
4088                        }
4089                    } else {
4090                        return Err(EvalError(format!("{:?}: expected string", op)));
4091                    };
4092                    stack.push(out);
4093                }
4094                Opcode::StrSplitReverseJoin { sep } => {
4095                    let v = pop!(stack);
4096                    let out = if let Val::Str(s) = &v {
4097                        let src = s.as_ref();
4098                        let sep_s = sep.as_ref();
4099                        // Collect segment (start, end) pairs via one byte-scan.
4100                        let mut spans: SmallVec<[(usize, usize); 8]> = SmallVec::new();
4101                        if sep_s.is_empty() {
4102                            // Mirror str::split("") behaviour: every char boundary.
4103                            let mut prev = 0usize;
4104                            for (i, _) in src.char_indices() {
4105                                if i > 0 { spans.push((prev, i)); prev = i; }
4106                            }
4107                            spans.push((prev, src.len()));
4108                        } else {
4109                            let mut prev = 0usize;
4110                            let sb = sep_s.as_bytes();
4111                            let slen = sb.len();
4112                            let bytes = src.as_bytes();
4113                            let mut i = 0usize;
4114                            while i + slen <= bytes.len() {
4115                                if &bytes[i..i + slen] == sb {
4116                                    spans.push((prev, i));
4117                                    i += slen; prev = i;
4118                                } else { i += 1; }
4119                            }
4120                            spans.push((prev, src.len()));
4121                        }
4122                        // SAFETY plan for the write-loop below:
4123                        // - `out_len == src.len()`: reverse join over the
4124                        //   same segments + same separator count produces
4125                        //   exactly the input byte count.
4126                        // - `arc` just returned from `new_uninit_slice`, no
4127                        //   other refs: `get_mut().unwrap()` is sound.
4128                        // - Segment spans `(a, b)` came from `str::char_indices`
4129                        //   or ASCII byte-scan of valid UTF-8 → on UTF-8
4130                        //   boundaries.
4131                        // - Final cast to `*const str`: payload is valid
4132                        //   UTF-8 because it is a permutation of the source
4133                        //   bytes joined by the same `sep` (both already
4134                        //   valid UTF-8 at known boundaries).
4135                        let out_len = src.len();
4136                        let mut arc = Arc::<[u8]>::new_uninit_slice(out_len);
4137                        let slot = Arc::get_mut(&mut arc).unwrap();
4138                        let src_b = src.as_bytes();
4139                        let sep_b = sep_s.as_bytes();
4140                        let slen = sep_b.len();
4141                        let n = spans.len();
4142                        // SAFETY: see plan above.
4143                        unsafe {
4144                            let dst = slot.as_mut_ptr() as *mut u8;
4145                            let mut widx = 0usize;
4146                            for idx in 0..n {
4147                                let (a, b) = spans[n - 1 - idx];
4148                                if idx > 0 && slen > 0 {
4149                                    std::ptr::copy_nonoverlapping(sep_b.as_ptr(), dst.add(widx), slen);
4150                                    widx += slen;
4151                                }
4152                                let seg_len = b - a;
4153                                std::ptr::copy_nonoverlapping(src_b.as_ptr().add(a), dst.add(widx), seg_len);
4154                                widx += seg_len;
4155                            }
4156                            debug_assert_eq!(widx, out_len);
4157                        }
4158                        // SAFETY: all `out_len` bytes initialised by the
4159                        // write-loop above (asserted).
4160                        let arc_bytes: Arc<[u8]> = unsafe { arc.assume_init() };
4161                        // SAFETY: `Arc<[u8]>` and `Arc<str>` share layout;
4162                        // payload is valid UTF-8 as argued above.
4163                        let arc_str: Arc<str> = unsafe {
4164                            Arc::from_raw(Arc::into_raw(arc_bytes) as *const str)
4165                        };
4166                        Val::Str(arc_str)
4167                    } else {
4168                        return Err(EvalError("split_reverse_join: expected string".into()));
4169                    };
4170                    stack.push(out);
4171                }
4172                Opcode::MapReplaceLit { needle, with, all } => {
4173                    let v = pop!(stack);
4174                    let v = if matches!(&v, Val::StrVec(_)) { v.into_arr() } else { v };
4175                    let n: &str = needle.as_ref();
4176                    let w: &str = with.as_ref();
4177                    let nlen = n.len();
4178                    let wlen = w.len();
4179                    let mut out_vec: Vec<Val> = match &v {
4180                        Val::Arr(a) => Vec::with_capacity(a.len()),
4181                        _ => Vec::new(),
4182                    };
4183                    if let Val::Arr(a) = &v {
4184                        for item in a.iter() {
4185                            if let Val::Str(s) = item {
4186                                let src = s.as_ref();
4187                                let Some(first_idx) = src.find(n) else {
4188                                    out_vec.push(Val::Str(s.clone()));
4189                                    continue;
4190                                };
4191                                // Two-pass: (1) count hits to compute exact
4192                                // size, (2) allocate Arc<str> directly with
4193                                // new_uninit_slice and write bytes once —
4194                                // avoids intermediate String alloc + copy
4195                                // that Arc::<str>::from(String) would do.
4196                                let hit_count = if *all {
4197                                    let mut c: usize = 1;
4198                                    let mut pos = first_idx + nlen;
4199                                    while let Some(i) = src[pos..].find(n) {
4200                                        c += 1;
4201                                        pos += i + nlen;
4202                                    }
4203                                    c
4204                                } else { 1 };
4205                                let out_len = src.len() + hit_count * wlen - hit_count * nlen;
4206                                let mut arc = Arc::<[u8]>::new_uninit_slice(out_len);
4207                                // SAFETY: unique new allocation; no other refs exist yet.
4208                                let slot = Arc::get_mut(&mut arc).unwrap();
4209                                let src_b = src.as_bytes();
4210                                let w_b = w.as_bytes();
4211                                let mut widx = 0usize;
4212                                // Write prefix up to first hit.
4213                                // SAFETY: MaybeUninit<u8> has same layout as u8.
4214                                unsafe {
4215                                    let dst = slot.as_mut_ptr() as *mut u8;
4216                                    std::ptr::copy_nonoverlapping(src_b.as_ptr(), dst, first_idx);
4217                                    widx += first_idx;
4218                                    std::ptr::copy_nonoverlapping(w_b.as_ptr(), dst.add(widx), wlen);
4219                                    widx += wlen;
4220                                    let mut last_end = first_idx + nlen;
4221                                    if *all {
4222                                        while let Some(i) = src[last_end..].find(n) {
4223                                            let abs = last_end + i;
4224                                            let len = abs - last_end;
4225                                            std::ptr::copy_nonoverlapping(src_b.as_ptr().add(last_end), dst.add(widx), len);
4226                                            widx += len;
4227                                            std::ptr::copy_nonoverlapping(w_b.as_ptr(), dst.add(widx), wlen);
4228                                            widx += wlen;
4229                                            last_end = abs + nlen;
4230                                        }
4231                                    }
4232                                    let tail = src_b.len() - last_end;
4233                                    std::ptr::copy_nonoverlapping(src_b.as_ptr().add(last_end), dst.add(widx), tail);
4234                                    debug_assert_eq!(widx + tail, out_len,
4235                                        "MapReplaceLit: hit-count predicted {} bytes, wrote {}",
4236                                        out_len, widx + tail);
4237                                }
4238                                // SAFETY: all `out_len` bytes are initialised above; the
4239                                // bytes are valid UTF-8 because src is valid UTF-8 and
4240                                // every substitution inserts a valid UTF-8 `w`.
4241                                let arc_bytes: Arc<[u8]> = unsafe { arc.assume_init() };
4242                                let arc_str: Arc<str> = unsafe {
4243                                    Arc::from_raw(Arc::into_raw(arc_bytes) as *const str)
4244                                };
4245                                out_vec.push(Val::Str(arc_str));
4246                            } else {
4247                                out_vec.push(item.clone());
4248                            }
4249                        }
4250                    }
4251                    stack.push(Val::arr(out_vec));
4252                }
4253                Opcode::MapUpperReplaceLit { needle, with, all }
4254                | Opcode::MapLowerReplaceLit { needle, with, all } => {
4255                    let to_upper = matches!(op, Opcode::MapUpperReplaceLit { .. });
4256                    let v = pop!(stack);
4257                    let v = if matches!(&v, Val::StrVec(_)) { v.into_arr() } else { v };
4258                    let n: &str = needle.as_ref();
4259                    let w: &str = with.as_ref();
4260                    let nlen = n.len();
4261                    let wlen = w.len();
4262                    let mut out_vec: Vec<Val> = match &v {
4263                        Val::Arr(a) => Vec::with_capacity(a.len()),
4264                        _ => Vec::new(),
4265                    };
4266                    if let Val::Arr(a) = &v {
4267                        for item in a.iter() {
4268                            if let Val::Str(s) = item {
4269                                let src = s.as_ref();
4270                                if src.is_ascii() {
4271                                    // ASCII fast path: compute needle hits against
4272                                    // cased source (upper needle vs upper haystack /
4273                                    // lower vs lower) so match semantics equal
4274                                    // `s.upper().replace(n, w)`.
4275                                    let src_b = src.as_bytes();
4276                                    let n_b = n.as_bytes();
4277                                    let w_b = w.as_bytes();
4278                                    let mut hits: Vec<usize> = Vec::new();
4279                                    if nlen > 0 && nlen <= src_b.len() {
4280                                        let mut i = 0usize;
4281                                        'outer: while i + nlen <= src_b.len() {
4282                                            for j in 0..nlen {
4283                                                let c = src_b[i + j];
4284                                                let cased = if to_upper {
4285                                                    if c.is_ascii_lowercase() { c - 32 } else { c }
4286                                                } else if c.is_ascii_uppercase() { c + 32 } else { c };
4287                                                if cased != n_b[j] {
4288                                                    i += 1;
4289                                                    continue 'outer;
4290                                                }
4291                                            }
4292                                            hits.push(i);
4293                                            if !*all { break; }
4294                                            i += nlen;
4295                                        }
4296                                    }
4297                                    let out_len = src_b.len() + hits.len() * wlen - hits.len() * nlen;
4298                                    let mut arc = Arc::<[u8]>::new_uninit_slice(out_len);
4299                                    let slot = Arc::get_mut(&mut arc).unwrap();
4300                                    unsafe {
4301                                        let dst = slot.as_mut_ptr() as *mut u8;
4302                                        let mut widx = 0usize;
4303                                        let mut last_end = 0usize;
4304                                        for &hit in &hits {
4305                                            // Copy [last_end..hit), case-transformed.
4306                                            for k in last_end..hit {
4307                                                let c = src_b[k];
4308                                                let cased = if to_upper {
4309                                                    if c.is_ascii_lowercase() { c - 32 } else { c }
4310                                                } else if c.is_ascii_uppercase() { c + 32 } else { c };
4311                                                *dst.add(widx) = cased;
4312                                                widx += 1;
4313                                            }
4314                                            std::ptr::copy_nonoverlapping(w_b.as_ptr(), dst.add(widx), wlen);
4315                                            widx += wlen;
4316                                            last_end = hit + nlen;
4317                                        }
4318                                        for k in last_end..src_b.len() {
4319                                            let c = src_b[k];
4320                                            let cased = if to_upper {
4321                                                if c.is_ascii_lowercase() { c - 32 } else { c }
4322                                            } else if c.is_ascii_uppercase() { c + 32 } else { c };
4323                                            *dst.add(widx) = cased;
4324                                            widx += 1;
4325                                        }
4326                                        debug_assert_eq!(widx, out_len);
4327                                    }
4328                                    let arc_bytes: Arc<[u8]> = unsafe { arc.assume_init() };
4329                                    let arc_str: Arc<str> = unsafe {
4330                                        Arc::from_raw(Arc::into_raw(arc_bytes) as *const str)
4331                                    };
4332                                    out_vec.push(Val::Str(arc_str));
4333                                } else {
4334                                    // Unicode fallback: build cased String, then replace.
4335                                    let cased = if to_upper { src.to_uppercase() } else { src.to_lowercase() };
4336                                    let replaced = if *all {
4337                                        cased.replace(n, w)
4338                                    } else {
4339                                        cased.replacen(n, w, 1)
4340                                    };
4341                                    out_vec.push(Val::Str(Arc::<str>::from(replaced)));
4342                                }
4343                            } else {
4344                                out_vec.push(item.clone());
4345                            }
4346                        }
4347                    }
4348                    stack.push(Val::arr(out_vec));
4349                }
4350                Opcode::MapStrConcat { prefix, suffix } => {
4351                    let v = pop!(stack);
4352                    let v = if matches!(&v, Val::StrVec(_)) { v.into_arr() } else { v };
4353                    let p_b = prefix.as_bytes();
4354                    let s_b = suffix.as_bytes();
4355                    let pl = p_b.len();
4356                    let sl = s_b.len();
4357                    let mut out_vec: Vec<Val> = match &v {
4358                        Val::Arr(a) => Vec::with_capacity(a.len()),
4359                        _ => Vec::new(),
4360                    };
4361                    if let Val::Arr(a) = &v {
4362                        for item in a.iter() {
4363                            if let Val::Str(s) = item {
4364                                let src_b = s.as_bytes();
4365                                let out_len = pl + src_b.len() + sl;
4366                                let mut arc = Arc::<[u8]>::new_uninit_slice(out_len);
4367                                let slot = Arc::get_mut(&mut arc).unwrap();
4368                                unsafe {
4369                                    let dst = slot.as_mut_ptr() as *mut u8;
4370                                    if pl > 0 {
4371                                        std::ptr::copy_nonoverlapping(p_b.as_ptr(), dst, pl);
4372                                    }
4373                                    std::ptr::copy_nonoverlapping(
4374                                        src_b.as_ptr(), dst.add(pl), src_b.len());
4375                                    if sl > 0 {
4376                                        std::ptr::copy_nonoverlapping(
4377                                            s_b.as_ptr(), dst.add(pl + src_b.len()), sl);
4378                                    }
4379                                }
4380                                let arc_bytes: Arc<[u8]> = unsafe { arc.assume_init() };
4381                                let arc_str: Arc<str> = unsafe {
4382                                    Arc::from_raw(Arc::into_raw(arc_bytes) as *const str)
4383                                };
4384                                out_vec.push(Val::Str(arc_str));
4385                            } else {
4386                                // Non-string element: fall back to add_vals semantics.
4387                                let a1 = super::eval::util::add_vals(
4388                                    Val::Str(prefix.clone()), item.clone())
4389                                    .unwrap_or(Val::Null);
4390                                let a2 = super::eval::util::add_vals(
4391                                    a1, Val::Str(suffix.clone()))
4392                                    .unwrap_or(Val::Null);
4393                                out_vec.push(a2);
4394                            }
4395                        }
4396                    }
4397                    stack.push(Val::arr(out_vec));
4398                }
4399                Opcode::MapSplitLenSum { sep } => {
4400                    let v = pop!(stack);
4401                    let v = if matches!(&v, Val::StrVec(_)) { v.into_arr() } else { v };
4402                    let sep_b = sep.as_bytes();
4403                    let slen = sep_b.len();
4404                    let sep_chars = sep.chars().count() as i64;
4405                    let mut out: Vec<i64> = match &v {
4406                        Val::Arr(a) => Vec::with_capacity(a.len()),
4407                        _ => Vec::new(),
4408                    };
4409                    if let Val::Arr(a) = &v {
4410                        for item in a.iter() {
4411                            if let Val::Str(s) = item {
4412                                let src = s.as_ref();
4413                                let row: i64 = if slen == 0 {
4414                                    // split("") yields one segment per char.
4415                                    // Each segment is one char, count = char_count.
4416                                    // Sum of lens = char_count. But split("") also
4417                                    // yields an empty prefix + per char + empty
4418                                    // suffix; match existing split(.).count()
4419                                    // semantics by falling back for empty sep.
4420                                    0
4421                                } else if src.is_ascii() && slen == 1 {
4422                                    // ASCII 1-byte sep: sum(segment_byte_len)
4423                                    //   = src.len() - hits
4424                                    let byte = sep_b[0];
4425                                    let hits = memchr::memchr_iter(byte, src.as_bytes())
4426                                                 .count() as i64;
4427                                    src.len() as i64 - hits
4428                                } else if src.is_ascii() {
4429                                    // Multi-byte ASCII sep.
4430                                    let hits = memchr::memmem::find_iter(src.as_bytes(), sep_b)
4431                                                 .count() as i64;
4432                                    src.len() as i64 - hits * slen as i64
4433                                } else {
4434                                    // Unicode source: count source chars, then
4435                                    // subtract hits * sep_chars.
4436                                    let src_chars = src.chars().count() as i64;
4437                                    let hits = if slen == 1 {
4438                                        memchr::memchr_iter(sep_b[0], src.as_bytes())
4439                                            .count() as i64
4440                                    } else {
4441                                        memchr::memmem::find_iter(src.as_bytes(), sep_b)
4442                                            .count() as i64
4443                                    };
4444                                    src_chars - hits * sep_chars
4445                                };
4446                                out.push(row);
4447                            } else {
4448                                out.push(0);
4449                            }
4450                        }
4451                    }
4452                    // Fallback for empty sep: recompute via generic path.
4453                    if slen == 0 {
4454                        // Classic split("") behavior = char count per seg +
4455                        // extra; match non-fused by computing via char count.
4456                        out.clear();
4457                        if let Val::Arr(a) = &v {
4458                            for item in a.iter() {
4459                                if let Val::Str(s) = item {
4460                                    // `split("").count()` is chars+1; sum of
4461                                    // individual char lens is just char count.
4462                                    out.push(s.chars().count() as i64);
4463                                } else {
4464                                    out.push(0);
4465                                }
4466                            }
4467                        }
4468                    }
4469                    stack.push(Val::int_vec(out));
4470                }
4471                Opcode::MapSplitCountSum { sep } => {
4472                    let v = pop!(stack);
4473                    let v = if matches!(&v, Val::StrVec(_)) { v.into_arr() } else { v };
4474                    let sep_b = sep.as_bytes();
4475                    let slen = sep_b.len();
4476                    let mut total: i64 = 0;
4477                    if let Val::Arr(a) = &v {
4478                        for item in a.iter() {
4479                            if let Val::Str(s) = item {
4480                                let sb = s.as_bytes();
4481                                let c: i64 = if slen == 0 {
4482                                    s.as_ref().chars().count() as i64 + 1
4483                                } else if slen == 1 {
4484                                    let byte = sep_b[0];
4485                                    let mut c: i64 = 1;
4486                                    let mut hay = sb;
4487                                    while let Some(pos) = memchr(byte, hay) {
4488                                        c += 1;
4489                                        hay = &hay[pos + 1..];
4490                                    }
4491                                    c
4492                                } else {
4493                                    let mut c: i64 = 1;
4494                                    let mut i = 0usize;
4495                                    while i + slen <= sb.len() {
4496                                        if &sb[i..i + slen] == sep_b {
4497                                            c += 1; i += slen;
4498                                        } else { i += 1; }
4499                                    }
4500                                    c
4501                                };
4502                                total += c;
4503                            }
4504                        }
4505                    }
4506                    stack.push(Val::Int(total));
4507                }
4508                Opcode::MapSplitCount { sep } => {
4509                    let v = pop!(stack);
4510                    let v = if matches!(&v, Val::StrVec(_)) { v.into_arr() } else { v };
4511                    let sep_b = sep.as_bytes();
4512                    let slen = sep_b.len();
4513                    let out = if let Val::Arr(a) = &v {
4514                        let mut out = Vec::with_capacity(a.len());
4515                        for item in a.iter() {
4516                            if let Val::Str(s) = item {
4517                                let sb = s.as_bytes();
4518                                let c = if slen == 0 {
4519                                    // str::split("") ≡ char boundary count + 1.
4520                                    s.as_ref().chars().count() as i64 + 1
4521                                } else if slen == 1 {
4522                                    // memchr SIMD scan for single-byte sep.
4523                                    let byte = sep_b[0];
4524                                    let mut c: i64 = 1;
4525                                    let mut hay = sb;
4526                                    while let Some(pos) = memchr(byte, hay) {
4527                                        c += 1;
4528                                        hay = &hay[pos + 1..];
4529                                    }
4530                                    c
4531                                } else {
4532                                    let mut c: i64 = 1;
4533                                    let mut i = 0usize;
4534                                    while i + slen <= sb.len() {
4535                                        if &sb[i..i + slen] == sep_b {
4536                                            c += 1; i += slen;
4537                                        } else { i += 1; }
4538                                    }
4539                                    c
4540                                };
4541                                out.push(Val::Int(c));
4542                            } else {
4543                                out.push(Val::Null);
4544                            }
4545                        }
4546                        Val::arr(out)
4547                    } else { Val::arr(Vec::new()) };
4548                    stack.push(out);
4549                }
4550                Opcode::MapSplitFirst { sep } => {
4551                    let v = pop!(stack);
4552                    let sep_s = sep.as_ref();
4553                    // StrVec / homogeneous Arr<Str> input → emit columnar
4554                    // StrSliceVec (Vec<StrRef>), no per-row Val enum tag.
4555                    if let Val::StrVec(a) = &v {
4556                        let mut out: Vec<crate::strref::StrRef> = Vec::with_capacity(a.len());
4557                        for s in a.iter() {
4558                            let src = s.as_ref();
4559                            if sep_s.is_empty() {
4560                                out.push(crate::strref::StrRef::slice(s.clone(), 0, 0));
4561                            } else {
4562                                let end = src.find(sep_s).unwrap_or(src.len());
4563                                out.push(crate::strref::StrRef::slice(s.clone(), 0, end));
4564                            }
4565                        }
4566                        stack.push(Val::StrSliceVec(Arc::new(out)));
4567                        continue;
4568                    }
4569                    let out = if let Val::Arr(a) = &v {
4570                        let all_str = a.iter().all(|it| matches!(it, Val::Str(_)));
4571                        if all_str {
4572                            let mut out: Vec<crate::strref::StrRef> = Vec::with_capacity(a.len());
4573                            for item in a.iter() {
4574                                if let Val::Str(s) = item {
4575                                    let src = s.as_ref();
4576                                    if sep_s.is_empty() {
4577                                        out.push(crate::strref::StrRef::slice(s.clone(), 0, 0));
4578                                    } else {
4579                                        let end = src.find(sep_s).unwrap_or(src.len());
4580                                        out.push(crate::strref::StrRef::slice(s.clone(), 0, end));
4581                                    }
4582                                }
4583                            }
4584                            stack.push(Val::StrSliceVec(Arc::new(out)));
4585                            continue;
4586                        }
4587                        let mut out = Vec::with_capacity(a.len());
4588                        for item in a.iter() {
4589                            if let Val::Str(s) = item {
4590                                let src = s.as_ref();
4591                                if sep_s.is_empty() {
4592                                    out.push(Val::Str(Arc::<str>::from("")));
4593                                    continue;
4594                                }
4595                                match src.find(sep_s) {
4596                                    Some(idx) => {
4597                                        out.push(Val::StrSlice(
4598                                            crate::strref::StrRef::slice(s.clone(), 0, idx)
4599                                        ));
4600                                    }
4601                                    None => out.push(Val::Str(s.clone())),
4602                                }
4603                            } else {
4604                                out.push(Val::Null);
4605                            }
4606                        }
4607                        Val::arr(out)
4608                    } else { Val::arr(Vec::new()) };
4609                    stack.push(out);
4610                }
4611                Opcode::MapSplitNth { sep, n } => {
4612                    let v = pop!(stack);
4613                    let v = if matches!(&v, Val::StrVec(_)) { v.into_arr() } else { v };
4614                    let sep_s = sep.as_ref();
4615                    let want = *n;
4616                    let out = if let Val::Arr(a) = &v {
4617                        let mut out = Vec::with_capacity(a.len());
4618                        for item in a.iter() {
4619                            if let Val::Str(s) = item {
4620                                let src = s.as_ref();
4621                                let mut pushed = false;
4622                                if sep_s.is_empty() {
4623                                    // nth char boundary
4624                                    if let Some((i, _)) = src.char_indices().nth(want) {
4625                                        let end = src[i..].chars().next().map(|c| i + c.len_utf8()).unwrap_or(i);
4626                                        out.push(Val::Str(Arc::<str>::from(&src[i..end])));
4627                                        pushed = true;
4628                                    }
4629                                } else {
4630                                    let mut prev = 0usize;
4631                                    let mut idx = 0usize;
4632                                    let sb = sep_s.as_bytes();
4633                                    let slen = sb.len();
4634                                    let bytes = src.as_bytes();
4635                                    if slen == 1 {
4636                                        let byte = sb[0];
4637                                        let mut cursor = 0usize;
4638                                        let mut hay = bytes;
4639                                        while let Some(off) = memchr(byte, hay) {
4640                                            let i = cursor + off;
4641                                            if idx == want {
4642                                                out.push(Val::Str(Arc::<str>::from(&src[prev..i])));
4643                                                pushed = true;
4644                                                break;
4645                                            }
4646                                            idx += 1;
4647                                            cursor = i + 1;
4648                                            prev = cursor;
4649                                            hay = &bytes[cursor..];
4650                                        }
4651                                    } else {
4652                                        let mut i = 0usize;
4653                                        while i + slen <= bytes.len() {
4654                                            if &bytes[i..i + slen] == sb {
4655                                                if idx == want {
4656                                                    out.push(Val::Str(Arc::<str>::from(&src[prev..i])));
4657                                                    pushed = true;
4658                                                    break;
4659                                                }
4660                                                idx += 1;
4661                                                i += slen;
4662                                                prev = i;
4663                                            } else { i += 1; }
4664                                        }
4665                                    }
4666                                    if !pushed && idx == want {
4667                                        let arc: Arc<str> = if prev == 0 { s.clone() }
4668                                                            else { Arc::<str>::from(&src[prev..]) };
4669                                        out.push(Val::Str(arc));
4670                                        pushed = true;
4671                                    }
4672                                }
4673                                if !pushed { out.push(Val::Null); }
4674                            } else {
4675                                out.push(Val::Null);
4676                            }
4677                        }
4678                        Val::arr(out)
4679                    } else { Val::arr(Vec::new()) };
4680                    stack.push(out);
4681                }
4682                Opcode::MapSum(f) => {
4683                    let recv = pop!(stack);
4684                    let mut acc_i: i64 = 0;
4685                    let mut acc_f: f64 = 0.0;
4686                    let mut is_float = false;
4687                    if let Val::Arr(a) = &recv {
4688                        if let Some(k) = trivial_field(&f.ops) {
4689                            let mut idx: Option<usize> = None;
4690                            for item in a.iter() {
4691                                if let Val::Obj(m) = item {
4692                                    match lookup_field_cached(m, &k, &mut idx) {
4693                                        Some(Val::Int(n))   => { if is_float { acc_f += *n as f64; } else { acc_i += *n; } }
4694                                        Some(Val::Float(x)) => { if !is_float { acc_f = acc_i as f64; is_float = true; } acc_f += *x; }
4695                                        Some(Val::Null) | None => {}
4696                                        _ => return err!("map(..).sum(): non-numeric mapped value"),
4697                                    }
4698                                }
4699                            }
4700                        } else {
4701                            let mut scratch = env.clone();
4702                            for item in a.iter() {
4703                                let prev = scratch.swap_current(item.clone());
4704                                let v = self.exec(f, &scratch)?;
4705                                scratch.restore_current(prev);
4706                                match v {
4707                                    Val::Int(n) => {
4708                                        if is_float { acc_f += n as f64; } else { acc_i += n; }
4709                                    }
4710                                    Val::Float(x) => {
4711                                        if !is_float { acc_f = acc_i as f64; is_float = true; }
4712                                        acc_f += x;
4713                                    }
4714                                    Val::Null => {}
4715                                    _ => return err!("map(..).sum(): non-numeric mapped value"),
4716                                }
4717                            }
4718                        }
4719                    }
4720                    stack.push(if is_float { Val::Float(acc_f) } else { Val::Int(acc_i) });
4721                }
4722                Opcode::MapAvg(f) => {
4723                    let recv = pop!(stack);
4724                    let mut sum: f64 = 0.0;
4725                    let mut n: usize = 0;
4726                    if let Val::Arr(a) = &recv {
4727                        if let Some(k) = trivial_field(&f.ops) {
4728                            let mut idx: Option<usize> = None;
4729                            for item in a.iter() {
4730                                if let Val::Obj(m) = item {
4731                                    match lookup_field_cached(m, &k, &mut idx) {
4732                                        Some(Val::Int(x))   => { sum += *x as f64; n += 1; }
4733                                        Some(Val::Float(x)) => { sum += *x;        n += 1; }
4734                                        Some(Val::Null) | None => {}
4735                                        _ => return err!("map(..).avg(): non-numeric mapped value"),
4736                                    }
4737                                }
4738                            }
4739                        } else {
4740                            let mut scratch = env.clone();
4741                            for item in a.iter() {
4742                                let prev = scratch.swap_current(item.clone());
4743                                let v = self.exec(f, &scratch)?;
4744                                scratch.restore_current(prev);
4745                                match v {
4746                                    Val::Int(x)   => { sum += x as f64; n += 1; }
4747                                    Val::Float(x) => { sum += x;        n += 1; }
4748                                    Val::Null => {}
4749                                    _ => return err!("map(..).avg(): non-numeric mapped value"),
4750                                }
4751                            }
4752                        }
4753                    }
4754                    stack.push(if n == 0 { Val::Null } else { Val::Float(sum / n as f64) });
4755                }
4756                Opcode::MapMin(f) => {
4757                    let recv = pop!(stack);
4758                    let mut best_i: Option<i64> = None;
4759                    let mut best_f: Option<f64> = None;
4760                    macro_rules! fold_min {
4761                        ($v:expr) => { match $v {
4762                            Val::Int(n) => {
4763                                let n = *n;
4764                                if let Some(bf) = best_f { if (n as f64) < bf { best_f = Some(n as f64); } }
4765                                else if let Some(bi) = best_i { if n < bi { best_i = Some(n); } }
4766                                else { best_i = Some(n); }
4767                            }
4768                            Val::Float(x) => {
4769                                let x = *x;
4770                                if best_f.is_none() { best_f = Some(best_i.map(|i| i as f64).unwrap_or(x)); best_i = None; }
4771                                if x < best_f.unwrap() { best_f = Some(x); }
4772                            }
4773                            Val::Null => {}
4774                            _ => return err!("map(..).min(): non-numeric mapped value"),
4775                        } }
4776                    }
4777                    if let Val::Arr(a) = &recv {
4778                        if let Some(k) = trivial_field(&f.ops) {
4779                            let mut idx: Option<usize> = None;
4780                            for item in a.iter() {
4781                                if let Val::Obj(m) = item {
4782                                    if let Some(v) = lookup_field_cached(m, &k, &mut idx) { fold_min!(v); }
4783                                }
4784                            }
4785                        } else {
4786                            let mut scratch = env.clone();
4787                            for item in a.iter() {
4788                                let prev = scratch.swap_current(item.clone());
4789                                let v = self.exec(f, &scratch)?;
4790                                scratch.restore_current(prev);
4791                                fold_min!(&v);
4792                            }
4793                        }
4794                    }
4795                    stack.push(match (best_i, best_f) {
4796                        (_, Some(x)) => Val::Float(x),
4797                        (Some(i), _) => Val::Int(i),
4798                        _ => Val::Null,
4799                    });
4800                }
4801                Opcode::MapMax(f) => {
4802                    let recv = pop!(stack);
4803                    let mut best_i: Option<i64> = None;
4804                    let mut best_f: Option<f64> = None;
4805                    macro_rules! fold_max {
4806                        ($v:expr) => { match $v {
4807                            Val::Int(n) => {
4808                                let n = *n;
4809                                if let Some(bf) = best_f { if (n as f64) > bf { best_f = Some(n as f64); } }
4810                                else if let Some(bi) = best_i { if n > bi { best_i = Some(n); } }
4811                                else { best_i = Some(n); }
4812                            }
4813                            Val::Float(x) => {
4814                                let x = *x;
4815                                if best_f.is_none() { best_f = Some(best_i.map(|i| i as f64).unwrap_or(x)); best_i = None; }
4816                                if x > best_f.unwrap() { best_f = Some(x); }
4817                            }
4818                            Val::Null => {}
4819                            _ => return err!("map(..).max(): non-numeric mapped value"),
4820                        } }
4821                    }
4822                    if let Val::Arr(a) = &recv {
4823                        if let Some(k) = trivial_field(&f.ops) {
4824                            let mut idx: Option<usize> = None;
4825                            for item in a.iter() {
4826                                if let Val::Obj(m) = item {
4827                                    if let Some(v) = lookup_field_cached(m, &k, &mut idx) { fold_max!(v); }
4828                                }
4829                            }
4830                        } else {
4831                            let mut scratch = env.clone();
4832                            for item in a.iter() {
4833                                let prev = scratch.swap_current(item.clone());
4834                                let v = self.exec(f, &scratch)?;
4835                                scratch.restore_current(prev);
4836                                fold_max!(&v);
4837                            }
4838                        }
4839                    }
4840                    stack.push(match (best_i, best_f) {
4841                        (_, Some(x)) => Val::Float(x),
4842                        (Some(i), _) => Val::Int(i),
4843                        _ => Val::Null,
4844                    });
4845                }
4846
4847                // ── Field-specialised fusions (Tier 3) ────────────────────
4848                Opcode::MapField(k) => {
4849                    let recv = pop!(stack);
4850                    if let Val::Arr(a) = &recv {
4851                        let mut out = Vec::with_capacity(a.len());
4852                        let mut idx: Option<usize> = None;
4853                        for item in a.iter() {
4854                            match item {
4855                                Val::Obj(m) => out.push(
4856                                    lookup_field_cached(m, k, &mut idx)
4857                                        .cloned()
4858                                        .unwrap_or(Val::Null),
4859                                ),
4860                                _ => out.push(Val::Null),
4861                            }
4862                        }
4863                        stack.push(Val::arr(out));
4864                    } else {
4865                        stack.push(Val::arr(Vec::new()));
4866                    }
4867                }
4868                Opcode::MapFieldChain(ks) => {
4869                    let recv = pop!(stack);
4870                    if let Val::Arr(a) = &recv {
4871                        let mut out = Vec::with_capacity(a.len());
4872                        // One IC slot per hop — `lookup_field_cached` keys on
4873                        // slot index + key verify (ptr-independent), so it
4874                        // hits across different Arcs of the same shape.
4875                        let mut ic: SmallVec<[Option<usize>; 4]> = SmallVec::new();
4876                        ic.resize(ks.len(), None);
4877                        for item in a.iter() {
4878                            let mut cur: Val = match item {
4879                                Val::Obj(m) => lookup_field_cached(m, &ks[0], &mut ic[0])
4880                                    .cloned()
4881                                    .unwrap_or(Val::Null),
4882                                _ => Val::Null,
4883                            };
4884                            for (hop, k) in ks[1..].iter().enumerate() {
4885                                cur = match &cur {
4886                                    Val::Obj(m) => lookup_field_cached(m, k, &mut ic[hop + 1])
4887                                        .cloned()
4888                                        .unwrap_or(Val::Null),
4889                                    _ => Val::Null,
4890                                };
4891                                if matches!(cur, Val::Null) { break; }
4892                            }
4893                            out.push(cur);
4894                        }
4895                        stack.push(Val::arr(out));
4896                    } else {
4897                        stack.push(Val::arr(Vec::new()));
4898                    }
4899                }
4900                Opcode::MapFieldSum(k) => {
4901                    let recv = pop!(stack);
4902                    let mut acc_i: i64 = 0;
4903                    let mut acc_f: f64 = 0.0;
4904                    let mut is_float = false;
4905                    let mut idx: Option<usize> = None;
4906                    if let Val::Arr(a) = &recv {
4907                        for item in a.iter() {
4908                            if let Val::Obj(m) = item {
4909                                match lookup_field_cached(m, k, &mut idx) {
4910                                    Some(Val::Int(n))   => { if is_float { acc_f += *n as f64; } else { acc_i += *n; } }
4911                                    Some(Val::Float(x)) => { if !is_float { acc_f = acc_i as f64; is_float = true; } acc_f += *x; }
4912                                    Some(Val::Null) | None => {}
4913                                    _ => return err!("map(k).sum(): non-numeric field"),
4914                                }
4915                            }
4916                        }
4917                    }
4918                    stack.push(if is_float { Val::Float(acc_f) } else { Val::Int(acc_i) });
4919                }
4920                Opcode::MapFieldAvg(k) => {
4921                    let recv = pop!(stack);
4922                    let mut sum: f64 = 0.0;
4923                    let mut n: usize = 0;
4924                    let mut idx: Option<usize> = None;
4925                    if let Val::Arr(a) = &recv {
4926                        for item in a.iter() {
4927                            if let Val::Obj(m) = item {
4928                                match lookup_field_cached(m, k, &mut idx) {
4929                                    Some(Val::Int(x))   => { sum += *x as f64; n += 1; }
4930                                    Some(Val::Float(x)) => { sum += *x;        n += 1; }
4931                                    Some(Val::Null) | None => {}
4932                                    _ => return err!("map(k).avg(): non-numeric field"),
4933                                }
4934                            }
4935                        }
4936                    }
4937                    stack.push(if n == 0 { Val::Null } else { Val::Float(sum / n as f64) });
4938                }
4939                Opcode::MapFieldMin(k) => {
4940                    let recv = pop!(stack);
4941                    let mut best_i: Option<i64> = None;
4942                    let mut best_f: Option<f64> = None;
4943                    let mut idx: Option<usize> = None;
4944                    if let Val::Arr(a) = &recv {
4945                        for item in a.iter() {
4946                            if let Val::Obj(m) = item {
4947                                match lookup_field_cached(m, k, &mut idx) {
4948                                    Some(Val::Int(n)) => {
4949                                        let n = *n;
4950                                        if let Some(bf) = best_f { if (n as f64) < bf { best_f = Some(n as f64); } }
4951                                        else if let Some(bi) = best_i { if n < bi { best_i = Some(n); } }
4952                                        else { best_i = Some(n); }
4953                                    }
4954                                    Some(Val::Float(x)) => {
4955                                        let x = *x;
4956                                        if best_f.is_none() { best_f = Some(best_i.map(|i| i as f64).unwrap_or(x)); best_i = None; }
4957                                        if x < best_f.unwrap() { best_f = Some(x); }
4958                                    }
4959                                    Some(Val::Null) | None => {}
4960                                    _ => return err!("map(k).min(): non-numeric field"),
4961                                }
4962                            }
4963                        }
4964                    }
4965                    stack.push(match (best_i, best_f) {
4966                        (_, Some(x)) => Val::Float(x),
4967                        (Some(i), _) => Val::Int(i),
4968                        _ => Val::Null,
4969                    });
4970                }
4971                Opcode::MapFieldMax(k) => {
4972                    let recv = pop!(stack);
4973                    let mut best_i: Option<i64> = None;
4974                    let mut best_f: Option<f64> = None;
4975                    let mut idx: Option<usize> = None;
4976                    if let Val::Arr(a) = &recv {
4977                        for item in a.iter() {
4978                            if let Val::Obj(m) = item {
4979                                match lookup_field_cached(m, k, &mut idx) {
4980                                    Some(Val::Int(n)) => {
4981                                        let n = *n;
4982                                        if let Some(bf) = best_f { if (n as f64) > bf { best_f = Some(n as f64); } }
4983                                        else if let Some(bi) = best_i { if n > bi { best_i = Some(n); } }
4984                                        else { best_i = Some(n); }
4985                                    }
4986                                    Some(Val::Float(x)) => {
4987                                        let x = *x;
4988                                        if best_f.is_none() { best_f = Some(best_i.map(|i| i as f64).unwrap_or(x)); best_i = None; }
4989                                        if x > best_f.unwrap() { best_f = Some(x); }
4990                                    }
4991                                    Some(Val::Null) | None => {}
4992                                    _ => return err!("map(k).max(): non-numeric field"),
4993                                }
4994                            }
4995                        }
4996                    }
4997                    stack.push(match (best_i, best_f) {
4998                        (_, Some(x)) => Val::Float(x),
4999                        (Some(i), _) => Val::Int(i),
5000                        _ => Val::Null,
5001                    });
5002                }
5003                Opcode::MapFieldUnique(k) => {
5004                    let recv = pop!(stack);
5005                    let mut out: Vec<Val> = Vec::new();
5006                    let mut seen_int: std::collections::HashSet<i64> = std::collections::HashSet::new();
5007                    let mut seen_str: std::collections::HashSet<Arc<str>> = std::collections::HashSet::new();
5008                    let mut seen_other: Vec<Val> = Vec::new();
5009                    let mut idx: Option<usize> = None;
5010                    if let Val::Arr(a) = &recv {
5011                        for item in a.iter() {
5012                            if let Val::Obj(m) = item {
5013                                if let Some(v) = lookup_field_cached(m, k, &mut idx) {
5014                                    match v {
5015                                        Val::Int(n) => {
5016                                            if seen_int.insert(*n) { out.push(v.clone()); }
5017                                        }
5018                                        Val::Str(s) => {
5019                                            if seen_str.insert(s.clone()) { out.push(v.clone()); }
5020                                        }
5021                                        _ => {
5022                                            if !seen_other.iter().any(|o| crate::eval::util::vals_eq(o, v)) {
5023                                                seen_other.push(v.clone());
5024                                                out.push(v.clone());
5025                                            }
5026                                        }
5027                                    }
5028                                }
5029                            }
5030                        }
5031                    }
5032                    stack.push(Val::arr(out));
5033                }
5034                Opcode::MapFieldChainUnique(ks) => {
5035                    let recv = pop!(stack);
5036                    let mut out: Vec<Val> = Vec::new();
5037                    let mut seen_int: std::collections::HashSet<i64> = std::collections::HashSet::new();
5038                    let mut seen_str: std::collections::HashSet<Arc<str>> = std::collections::HashSet::new();
5039                    let mut seen_other: Vec<Val> = Vec::new();
5040                    let mut ic: SmallVec<[Option<usize>; 4]> = SmallVec::new();
5041                    ic.resize(ks.len(), None);
5042                    if let Val::Arr(a) = &recv {
5043                        for item in a.iter() {
5044                            let mut cur: Val = match item {
5045                                Val::Obj(m) => lookup_field_cached(m, &ks[0], &mut ic[0])
5046                                    .cloned()
5047                                    .unwrap_or(Val::Null),
5048                                _ => Val::Null,
5049                            };
5050                            for (hop, k) in ks[1..].iter().enumerate() {
5051                                cur = match &cur {
5052                                    Val::Obj(m) => lookup_field_cached(m, k, &mut ic[hop + 1])
5053                                        .cloned()
5054                                        .unwrap_or(Val::Null),
5055                                    _ => Val::Null,
5056                                };
5057                                if matches!(cur, Val::Null) { break; }
5058                            }
5059                            match &cur {
5060                                Val::Int(n) => {
5061                                    if seen_int.insert(*n) { out.push(cur); }
5062                                }
5063                                Val::Str(s) => {
5064                                    if seen_str.insert(s.clone()) { out.push(cur); }
5065                                }
5066                                _ => {
5067                                    if !seen_other.iter().any(|o| crate::eval::util::vals_eq(o, &cur)) {
5068                                        seen_other.push(cur.clone());
5069                                        out.push(cur);
5070                                    }
5071                                }
5072                            }
5073                        }
5074                    }
5075                    stack.push(Val::arr(out));
5076                }
5077
5078                // ── FlatMapChain (Tier 1) ─────────────────────────────────
5079                Opcode::FlatMapChain(keys) => {
5080                    let recv = pop!(stack);
5081                    let mut cur: Vec<Val> = match recv {
5082                        Val::Arr(a) => a.as_ref().clone(),
5083                        _ => Vec::new(),
5084                    };
5085                    for k in keys.iter() {
5086                        let mut next: Vec<Val> = Vec::with_capacity(cur.len() * 4);
5087                        let mut idx: Option<usize> = None;
5088                        for item in cur.drain(..) {
5089                            if let Val::Obj(m) = item {
5090                                if let Some(Val::Arr(inner)) = lookup_field_cached(&m, k, &mut idx) {
5091                                    for v in inner.iter() { next.push(v.clone()); }
5092                                }
5093                            }
5094                        }
5095                        cur = next;
5096                    }
5097                    stack.push(Val::arr(cur));
5098                }
5099
5100                // ── Predicate specialisation (Tier 4) ─────────────────────
5101                Opcode::FilterFieldEqLit(k, lit) => {
5102                    let recv = pop!(stack);
5103                    let hint = match &recv { Val::Arr(a) => filter_cap_hint(a.len()), _ => 0 };
5104                    let mut out = Vec::with_capacity(hint);
5105                    let mut idx: Option<usize> = None;
5106                    if let Val::Arr(a) = &recv {
5107                        for item in a.iter() {
5108                            if let Val::Obj(m) = item {
5109                                if let Some(v) = lookup_field_cached(m, k, &mut idx) {
5110                                    if crate::eval::util::vals_eq(v, lit) {
5111                                        out.push(item.clone());
5112                                    }
5113                                }
5114                            }
5115                        }
5116                    }
5117                    stack.push(Val::arr(out));
5118                }
5119                Opcode::FilterFieldCmpLit(k, op, lit) => {
5120                    let recv = pop!(stack);
5121                    let hint = match &recv { Val::Arr(a) => filter_cap_hint(a.len()), _ => 0 };
5122                    let mut out = Vec::with_capacity(hint);
5123                    let mut idx: Option<usize> = None;
5124                    if let Val::Arr(a) = &recv {
5125                        for item in a.iter() {
5126                            if let Val::Obj(m) = item {
5127                                if let Some(v) = lookup_field_cached(m, k, &mut idx) {
5128                                    if cmp_val_binop(v, *op, lit) {
5129                                        out.push(item.clone());
5130                                    }
5131                                }
5132                            }
5133                        }
5134                    }
5135                    stack.push(Val::arr(out));
5136                }
5137                Opcode::FilterCurrentCmpLit(op, lit) => {
5138                    use super::ast::BinOp;
5139                    let recv = pop!(stack);
5140                    // Columnar fast paths — IntVec / FloatVec receivers
5141                    // walk the raw slice, produce a typed vec.  This is
5142                    // the autovectoriser-friendly shape (branchless
5143                    // body, one stride, no heap traffic per hit).
5144                    match (&recv, lit) {
5145                        (Val::IntVec(a), Val::Int(rhs)) => {
5146                            let rhs = *rhs;
5147                            let mut out: Vec<i64> = Vec::with_capacity(a.len());
5148                            match op {
5149                                BinOp::Eq  => for &n in a.iter() { if n == rhs { out.push(n); } }
5150                                BinOp::Neq => for &n in a.iter() { if n != rhs { out.push(n); } }
5151                                BinOp::Lt  => for &n in a.iter() { if n <  rhs { out.push(n); } }
5152                                BinOp::Lte => for &n in a.iter() { if n <= rhs { out.push(n); } }
5153                                BinOp::Gt  => for &n in a.iter() { if n >  rhs { out.push(n); } }
5154                                BinOp::Gte => for &n in a.iter() { if n >= rhs { out.push(n); } }
5155                                _ => {
5156                                    stack.push(recv);
5157                                    continue;
5158                                }
5159                            }
5160                            stack.push(Val::int_vec(out));
5161                            continue;
5162                        }
5163                        (Val::IntVec(a), Val::Float(rhs)) => {
5164                            let rhs = *rhs;
5165                            let mut out: Vec<i64> = Vec::with_capacity(a.len());
5166                            match op {
5167                                BinOp::Eq  => for &n in a.iter() { if (n as f64) == rhs { out.push(n); } }
5168                                BinOp::Neq => for &n in a.iter() { if (n as f64) != rhs { out.push(n); } }
5169                                BinOp::Lt  => for &n in a.iter() { if (n as f64) <  rhs { out.push(n); } }
5170                                BinOp::Lte => for &n in a.iter() { if (n as f64) <= rhs { out.push(n); } }
5171                                BinOp::Gt  => for &n in a.iter() { if (n as f64) >  rhs { out.push(n); } }
5172                                BinOp::Gte => for &n in a.iter() { if (n as f64) >= rhs { out.push(n); } }
5173                                _ => { stack.push(recv); continue; }
5174                            }
5175                            stack.push(Val::int_vec(out));
5176                            continue;
5177                        }
5178                        (Val::FloatVec(a), Val::Float(rhs)) => {
5179                            let rhs = *rhs;
5180                            let mut out: Vec<f64> = Vec::with_capacity(a.len());
5181                            match op {
5182                                BinOp::Eq  => for &f in a.iter() { if f == rhs { out.push(f); } }
5183                                BinOp::Neq => for &f in a.iter() { if f != rhs { out.push(f); } }
5184                                BinOp::Lt  => for &f in a.iter() { if f <  rhs { out.push(f); } }
5185                                BinOp::Lte => for &f in a.iter() { if f <= rhs { out.push(f); } }
5186                                BinOp::Gt  => for &f in a.iter() { if f >  rhs { out.push(f); } }
5187                                BinOp::Gte => for &f in a.iter() { if f >= rhs { out.push(f); } }
5188                                _ => { stack.push(recv); continue; }
5189                            }
5190                            stack.push(Val::float_vec(out));
5191                            continue;
5192                        }
5193                        (Val::FloatVec(a), Val::Int(rhs)) => {
5194                            let rhs = *rhs as f64;
5195                            let mut out: Vec<f64> = Vec::with_capacity(a.len());
5196                            match op {
5197                                BinOp::Eq  => for &f in a.iter() { if f == rhs { out.push(f); } }
5198                                BinOp::Neq => for &f in a.iter() { if f != rhs { out.push(f); } }
5199                                BinOp::Lt  => for &f in a.iter() { if f <  rhs { out.push(f); } }
5200                                BinOp::Lte => for &f in a.iter() { if f <= rhs { out.push(f); } }
5201                                BinOp::Gt  => for &f in a.iter() { if f >  rhs { out.push(f); } }
5202                                BinOp::Gte => for &f in a.iter() { if f >= rhs { out.push(f); } }
5203                                _ => { stack.push(recv); continue; }
5204                            }
5205                            stack.push(Val::float_vec(out));
5206                            continue;
5207                        }
5208                        (Val::StrVec(a), Val::Str(rhs)) => {
5209                            let rhs_b = rhs.as_bytes();
5210                            let mut out: Vec<Arc<str>> = Vec::with_capacity(a.len());
5211                            match op {
5212                                BinOp::Eq  => for s in a.iter() { if s.as_bytes() == rhs_b { out.push(s.clone()); } }
5213                                BinOp::Neq => for s in a.iter() { if s.as_bytes() != rhs_b { out.push(s.clone()); } }
5214                                BinOp::Lt  => for s in a.iter() { if s.as_bytes() <  rhs_b { out.push(s.clone()); } }
5215                                BinOp::Lte => for s in a.iter() { if s.as_bytes() <= rhs_b { out.push(s.clone()); } }
5216                                BinOp::Gt  => for s in a.iter() { if s.as_bytes() >  rhs_b { out.push(s.clone()); } }
5217                                BinOp::Gte => for s in a.iter() { if s.as_bytes() >= rhs_b { out.push(s.clone()); } }
5218                                _ => { stack.push(recv); continue; }
5219                            }
5220                            stack.push(Val::str_vec(out));
5221                            continue;
5222                        }
5223                        _ => {}
5224                    }
5225                    // Generic fallback — walk Val::Arr, compare each.
5226                    let hint = match &recv { Val::Arr(a) => filter_cap_hint(a.len()), _ => 0 };
5227                    let mut out = Vec::with_capacity(hint);
5228                    if let Val::Arr(a) = &recv {
5229                        for item in a.iter() {
5230                            if cmp_val_binop(item, *op, lit) { out.push(item.clone()); }
5231                        }
5232                    }
5233                    stack.push(Val::arr(out));
5234                }
5235                Opcode::FilterStrVecStartsWith(needle) => {
5236                    let recv = pop!(stack);
5237                    let n_b = needle.as_bytes();
5238                    match &recv {
5239                        Val::StrVec(a) => {
5240                            let mut out: Vec<Arc<str>> = Vec::with_capacity(a.len());
5241                            for s in a.iter() {
5242                                let b = s.as_bytes();
5243                                if b.len() >= n_b.len() && &b[..n_b.len()] == n_b {
5244                                    out.push(s.clone());
5245                                }
5246                            }
5247                            stack.push(Val::str_vec(out));
5248                        }
5249                        Val::Arr(a) => {
5250                            let mut out: Vec<Val> = Vec::with_capacity(filter_cap_hint(a.len()));
5251                            for item in a.iter() {
5252                                if let Val::Str(s) = item {
5253                                    let b = s.as_bytes();
5254                                    if b.len() >= n_b.len() && &b[..n_b.len()] == n_b {
5255                                        out.push(item.clone());
5256                                    }
5257                                }
5258                            }
5259                            stack.push(Val::arr(out));
5260                        }
5261                        _ => stack.push(Val::arr(Vec::new())),
5262                    }
5263                }
5264                Opcode::FilterStrVecEndsWith(needle) => {
5265                    let recv = pop!(stack);
5266                    let n_b = needle.as_bytes();
5267                    match &recv {
5268                        Val::StrVec(a) => {
5269                            let mut out: Vec<Arc<str>> = Vec::with_capacity(a.len());
5270                            for s in a.iter() {
5271                                let b = s.as_bytes();
5272                                if b.len() >= n_b.len() && &b[b.len() - n_b.len()..] == n_b {
5273                                    out.push(s.clone());
5274                                }
5275                            }
5276                            stack.push(Val::str_vec(out));
5277                        }
5278                        Val::Arr(a) => {
5279                            let mut out: Vec<Val> = Vec::with_capacity(filter_cap_hint(a.len()));
5280                            for item in a.iter() {
5281                                if let Val::Str(s) = item {
5282                                    let b = s.as_bytes();
5283                                    if b.len() >= n_b.len() && &b[b.len() - n_b.len()..] == n_b {
5284                                        out.push(item.clone());
5285                                    }
5286                                }
5287                            }
5288                            stack.push(Val::arr(out));
5289                        }
5290                        _ => stack.push(Val::arr(Vec::new())),
5291                    }
5292                }
5293                Opcode::FilterStrVecContains(needle) => {
5294                    let recv = pop!(stack);
5295                    let n_b = needle.as_bytes();
5296                    // Empty needle → every string matches (std::str::contains semantics).
5297                    match &recv {
5298                        Val::StrVec(a) => {
5299                            if n_b.is_empty() {
5300                                stack.push(recv);
5301                            } else {
5302                                let finder = memmem::Finder::new(n_b);
5303                                let mut out: Vec<Arc<str>> = Vec::with_capacity(a.len());
5304                                for s in a.iter() {
5305                                    if finder.find(s.as_bytes()).is_some() {
5306                                        out.push(s.clone());
5307                                    }
5308                                }
5309                                stack.push(Val::str_vec(out));
5310                            }
5311                        }
5312                        Val::Arr(a) => {
5313                            let finder = memmem::Finder::new(n_b);
5314                            let mut out: Vec<Val> = Vec::with_capacity(filter_cap_hint(a.len()));
5315                            for item in a.iter() {
5316                                if let Val::Str(s) = item {
5317                                    if n_b.is_empty() || finder.find(s.as_bytes()).is_some() {
5318                                        out.push(item.clone());
5319                                    }
5320                                }
5321                            }
5322                            stack.push(Val::arr(out));
5323                        }
5324                        _ => stack.push(Val::arr(Vec::new())),
5325                    }
5326                }
5327                Opcode::MapStrVecUpper => {
5328                    let recv = pop!(stack);
5329                    match &recv {
5330                        Val::StrVec(a) => {
5331                            let mut out: Vec<Arc<str>> = Vec::with_capacity(a.len());
5332                            for s in a.iter() {
5333                                // ASCII fast-path: scan for any lowercase; if none,
5334                                // reuse input Arc (no-alloc). Otherwise clone+mutate
5335                                // in a Vec<u8>, hand ownership to Arc<str> via String.
5336                                let b = s.as_bytes();
5337                                if b.is_ascii() {
5338                                    if !b.iter().any(|c| c.is_ascii_lowercase()) {
5339                                        out.push(s.clone());
5340                                    } else {
5341                                        let mut v = b.to_vec();
5342                                        for c in v.iter_mut() {
5343                                            if c.is_ascii_lowercase() { *c -= 32; }
5344                                        }
5345                                        // SAFETY: v was ASCII in, byte-shifted within ASCII → still UTF-8.
5346                                        let owned = unsafe { String::from_utf8_unchecked(v) };
5347                                        out.push(Arc::<str>::from(owned));
5348                                    }
5349                                } else {
5350                                    out.push(Arc::<str>::from(s.to_uppercase()));
5351                                }
5352                            }
5353                            stack.push(Val::str_vec(out));
5354                        }
5355                        Val::Arr(a) => {
5356                            let mut out: Vec<Val> = Vec::with_capacity(a.len());
5357                            for item in a.iter() {
5358                                if let Val::Str(s) = item {
5359                                    out.push(Val::Str(Arc::<str>::from(s.to_uppercase())));
5360                                } else {
5361                                    out.push(Val::Null);
5362                                }
5363                            }
5364                            stack.push(Val::arr(out));
5365                        }
5366                        Val::Str(s) => stack.push(Val::Str(Arc::<str>::from(s.to_uppercase()))),
5367                        _ => stack.push(recv),
5368                    }
5369                }
5370                Opcode::MapStrVecLower => {
5371                    let recv = pop!(stack);
5372                    match &recv {
5373                        Val::StrVec(a) => {
5374                            let mut out: Vec<Arc<str>> = Vec::with_capacity(a.len());
5375                            for s in a.iter() {
5376                                let b = s.as_bytes();
5377                                if b.is_ascii() {
5378                                    if !b.iter().any(|c| c.is_ascii_uppercase()) {
5379                                        out.push(s.clone());
5380                                    } else {
5381                                        let mut v = b.to_vec();
5382                                        for c in v.iter_mut() {
5383                                            if c.is_ascii_uppercase() { *c += 32; }
5384                                        }
5385                                        let owned = unsafe { String::from_utf8_unchecked(v) };
5386                                        out.push(Arc::<str>::from(owned));
5387                                    }
5388                                } else {
5389                                    out.push(Arc::<str>::from(s.to_lowercase()));
5390                                }
5391                            }
5392                            stack.push(Val::str_vec(out));
5393                        }
5394                        Val::Arr(a) => {
5395                            let mut out: Vec<Val> = Vec::with_capacity(a.len());
5396                            for item in a.iter() {
5397                                if let Val::Str(s) = item {
5398                                    out.push(Val::Str(Arc::<str>::from(s.to_lowercase())));
5399                                } else {
5400                                    out.push(Val::Null);
5401                                }
5402                            }
5403                            stack.push(Val::arr(out));
5404                        }
5405                        Val::Str(s) => stack.push(Val::Str(Arc::<str>::from(s.to_lowercase()))),
5406                        _ => stack.push(recv),
5407                    }
5408                }
5409                Opcode::MapStrVecTrim => {
5410                    let recv = pop!(stack);
5411                    match &recv {
5412                        Val::StrVec(a) => {
5413                            let mut out: Vec<Arc<str>> = Vec::with_capacity(a.len());
5414                            for s in a.iter() {
5415                                let t = s.trim();
5416                                if t.len() == s.len() {
5417                                    out.push(s.clone());
5418                                } else {
5419                                    out.push(Arc::from(t));
5420                                }
5421                            }
5422                            stack.push(Val::str_vec(out));
5423                        }
5424                        Val::Arr(a) => {
5425                            let mut out: Vec<Val> = Vec::with_capacity(a.len());
5426                            for item in a.iter() {
5427                                if let Val::Str(s) = item {
5428                                    let t = s.trim();
5429                                    if t.len() == s.len() {
5430                                        out.push(Val::Str(s.clone()));
5431                                    } else {
5432                                        out.push(Val::Str(Arc::from(t)));
5433                                    }
5434                                } else {
5435                                    out.push(Val::Null);
5436                                }
5437                            }
5438                            stack.push(Val::arr(out));
5439                        }
5440                        Val::Str(s) => {
5441                            let t = s.trim();
5442                            if t.len() == s.len() {
5443                                stack.push(Val::Str(s.clone()));
5444                            } else {
5445                                stack.push(Val::Str(Arc::from(t)));
5446                            }
5447                        }
5448                        _ => stack.push(recv),
5449                    }
5450                }
5451                Opcode::MapNumVecArith { op, lit, flipped } => {
5452                    use super::ast::BinOp;
5453                    let recv = pop!(stack);
5454                    // Int literal or Float literal — determine output lane.
5455                    let (lit_is_float, lit_i, lit_f) = match lit {
5456                        Val::Int(n) => (false, *n, *n as f64),
5457                        Val::Float(f) => (true, *f as i64, *f),
5458                        _ => { stack.push(recv); continue; }
5459                    };
5460                    match (&recv, *op, lit_is_float, *flipped) {
5461                        // IntVec × Int × {Add,Sub,Mul,Mod} → IntVec
5462                        (Val::IntVec(a), BinOp::Add, false, _) => {
5463                            let mut out: Vec<i64> = Vec::with_capacity(a.len());
5464                            for &n in a.iter() { out.push(n + lit_i); }
5465                            stack.push(Val::int_vec(out));
5466                        }
5467                        (Val::IntVec(a), BinOp::Sub, false, false) => {
5468                            let mut out: Vec<i64> = Vec::with_capacity(a.len());
5469                            for &n in a.iter() { out.push(n - lit_i); }
5470                            stack.push(Val::int_vec(out));
5471                        }
5472                        (Val::IntVec(a), BinOp::Sub, false, true) => {
5473                            let mut out: Vec<i64> = Vec::with_capacity(a.len());
5474                            for &n in a.iter() { out.push(lit_i - n); }
5475                            stack.push(Val::int_vec(out));
5476                        }
5477                        (Val::IntVec(a), BinOp::Mul, false, _) => {
5478                            let mut out: Vec<i64> = Vec::with_capacity(a.len());
5479                            for &n in a.iter() { out.push(n * lit_i); }
5480                            stack.push(Val::int_vec(out));
5481                        }
5482                        (Val::IntVec(a), BinOp::Mod, false, false) if lit_i != 0 => {
5483                            let mut out: Vec<i64> = Vec::with_capacity(a.len());
5484                            for &n in a.iter() { out.push(n % lit_i); }
5485                            stack.push(Val::int_vec(out));
5486                        }
5487                        // IntVec × Int × Div → FloatVec (matches Val::Div semantics)
5488                        (Val::IntVec(a), BinOp::Div, false, false) if lit_i != 0 => {
5489                            let mut out: Vec<f64> = Vec::with_capacity(a.len());
5490                            let div = lit_i as f64;
5491                            for &n in a.iter() { out.push(n as f64 / div); }
5492                            stack.push(Val::float_vec(out));
5493                        }
5494                        (Val::IntVec(a), BinOp::Div, false, true) => {
5495                            let mut out: Vec<f64> = Vec::with_capacity(a.len());
5496                            let num = lit_i as f64;
5497                            for &n in a.iter() {
5498                                out.push(if n != 0 { num / n as f64 } else { f64::INFINITY });
5499                            }
5500                            stack.push(Val::float_vec(out));
5501                        }
5502                        // IntVec × Float → FloatVec
5503                        (Val::IntVec(a), _, true, _) => {
5504                            let mut out: Vec<f64> = Vec::with_capacity(a.len());
5505                            for &n in a.iter() {
5506                                let x = n as f64;
5507                                let r = match (op, flipped) {
5508                                    (BinOp::Add, _) => x + lit_f,
5509                                    (BinOp::Sub, false) => x - lit_f,
5510                                    (BinOp::Sub, true)  => lit_f - x,
5511                                    (BinOp::Mul, _) => x * lit_f,
5512                                    (BinOp::Div, false) => x / lit_f,
5513                                    (BinOp::Div, true)  => lit_f / x,
5514                                    (BinOp::Mod, false) => x % lit_f,
5515                                    (BinOp::Mod, true)  => lit_f % x,
5516                                    _ => { out.clear(); break; }
5517                                };
5518                                out.push(r);
5519                            }
5520                            if out.len() == a.len() { stack.push(Val::float_vec(out)); }
5521                            else { stack.push(recv); }
5522                        }
5523                        // FloatVec × (Int|Float) → FloatVec
5524                        (Val::FloatVec(a), _, _, _) => {
5525                            let mut out: Vec<f64> = Vec::with_capacity(a.len());
5526                            for &x in a.iter() {
5527                                let r = match (op, flipped) {
5528                                    (BinOp::Add, _) => x + lit_f,
5529                                    (BinOp::Sub, false) => x - lit_f,
5530                                    (BinOp::Sub, true)  => lit_f - x,
5531                                    (BinOp::Mul, _) => x * lit_f,
5532                                    (BinOp::Div, false) => x / lit_f,
5533                                    (BinOp::Div, true)  => lit_f / x,
5534                                    (BinOp::Mod, false) => x % lit_f,
5535                                    (BinOp::Mod, true)  => lit_f % x,
5536                                    _ => { out.clear(); break; }
5537                                };
5538                                out.push(r);
5539                            }
5540                            if out.len() == a.len() { stack.push(Val::float_vec(out)); }
5541                            else { stack.push(recv); }
5542                        }
5543                        // Arr fallback — per-item numeric arithmetic.
5544                        (Val::Arr(a), _, _, _) => {
5545                            let a = Arc::clone(a);
5546                            drop(recv);
5547                            let mut out: Vec<Val> = Vec::with_capacity(a.len());
5548                            for item in a.iter() {
5549                                let (ix, ifl, is_flt) = match item {
5550                                    Val::Int(n) => (*n, *n as f64, false),
5551                                    Val::Float(f) => (*f as i64, *f, true),
5552                                    _ => { out.push(Val::Null); continue; }
5553                                };
5554                                let r = if is_flt || lit_is_float {
5555                                    let (a, b) = if *flipped { (lit_f, ifl) } else { (ifl, lit_f) };
5556                                    match op {
5557                                        BinOp::Add => Val::Float(a + b),
5558                                        BinOp::Sub => Val::Float(a - b),
5559                                        BinOp::Mul => Val::Float(a * b),
5560                                        BinOp::Div => Val::Float(a / b),
5561                                        BinOp::Mod => Val::Float(a % b),
5562                                        _ => Val::Null,
5563                                    }
5564                                } else {
5565                                    let (a, b) = if *flipped { (lit_i, ix) } else { (ix, lit_i) };
5566                                    match op {
5567                                        BinOp::Add => Val::Int(a + b),
5568                                        BinOp::Sub => Val::Int(a - b),
5569                                        BinOp::Mul => Val::Int(a * b),
5570                                        BinOp::Div if b != 0 => Val::Float(a as f64 / b as f64),
5571                                        BinOp::Mod if b != 0 => Val::Int(a % b),
5572                                        _ => Val::Null,
5573                                    }
5574                                };
5575                                out.push(r);
5576                            }
5577                            stack.push(Val::arr(out));
5578                        }
5579                        _ => stack.push(recv),
5580                    }
5581                }
5582                Opcode::MapNumVecNeg => {
5583                    let recv = pop!(stack);
5584                    match &recv {
5585                        Val::IntVec(a) => {
5586                            let mut out: Vec<i64> = Vec::with_capacity(a.len());
5587                            for &n in a.iter() { out.push(-n); }
5588                            stack.push(Val::int_vec(out));
5589                        }
5590                        Val::FloatVec(a) => {
5591                            let mut out: Vec<f64> = Vec::with_capacity(a.len());
5592                            for &f in a.iter() { out.push(-f); }
5593                            stack.push(Val::float_vec(out));
5594                        }
5595                        Val::Arr(a) => {
5596                            let mut out: Vec<Val> = Vec::with_capacity(a.len());
5597                            for item in a.iter() {
5598                                out.push(match item {
5599                                    Val::Int(n) => Val::Int(-n),
5600                                    Val::Float(f) => Val::Float(-f),
5601                                    _ => Val::Null,
5602                                });
5603                            }
5604                            stack.push(Val::arr(out));
5605                        }
5606                        Val::Int(n) => stack.push(Val::Int(-n)),
5607                        Val::Float(f) => stack.push(Val::Float(-f)),
5608                        _ => stack.push(recv),
5609                    }
5610                }
5611                Opcode::FilterFieldEqLitMapField(kp, lit, kproj) => {
5612                    let recv = pop!(stack);
5613                    let hint = match &recv { Val::Arr(a) => filter_cap_hint(a.len()), _ => 0 };
5614                    let mut out = Vec::with_capacity(hint);
5615                    let mut ip: Option<usize> = None;
5616                    let mut iq: Option<usize> = None;
5617                    if let Val::Arr(a) = &recv {
5618                        for item in a.iter() {
5619                            if let Val::Obj(m) = item {
5620                                if let Some(v) = lookup_field_cached(m, kp, &mut ip) {
5621                                    if crate::eval::util::vals_eq(v, lit) {
5622                                        out.push(
5623                                            lookup_field_cached(m, kproj, &mut iq)
5624                                                .cloned()
5625                                                .unwrap_or(Val::Null),
5626                                        );
5627                                    }
5628                                }
5629                            }
5630                        }
5631                    }
5632                    stack.push(Val::arr(out));
5633                }
5634                Opcode::FilterFieldCmpLitMapField(kp, op, lit, kproj) => {
5635                    let recv = pop!(stack);
5636                    let hint = match &recv { Val::Arr(a) => filter_cap_hint(a.len()), _ => 0 };
5637                    let mut out = Vec::with_capacity(hint);
5638                    let mut ip: Option<usize> = None;
5639                    let mut iq: Option<usize> = None;
5640                    if let Val::Arr(a) = &recv {
5641                        for item in a.iter() {
5642                            if let Val::Obj(m) = item {
5643                                if let Some(v) = lookup_field_cached(m, kp, &mut ip) {
5644                                    if cmp_val_binop(v, *op, lit) {
5645                                        out.push(
5646                                            lookup_field_cached(m, kproj, &mut iq)
5647                                                .cloned()
5648                                                .unwrap_or(Val::Null),
5649                                        );
5650                                    }
5651                                }
5652                            }
5653                        }
5654                    }
5655                    stack.push(Val::arr(out));
5656                }
5657                Opcode::FilterFieldCmpField(k1, op, k2) => {
5658                    let recv = pop!(stack);
5659                    let hint = match &recv { Val::Arr(a) => filter_cap_hint(a.len()), _ => 0 };
5660                    let mut out = Vec::with_capacity(hint);
5661                    let mut i1: Option<usize> = None;
5662                    let mut i2: Option<usize> = None;
5663                    if let Val::Arr(a) = &recv {
5664                        for item in a.iter() {
5665                            if let Val::Obj(m) = item {
5666                                let v1 = lookup_field_cached(m, k1, &mut i1);
5667                                let v2 = lookup_field_cached(m, k2, &mut i2);
5668                                if let (Some(v1), Some(v2)) = (v1, v2) {
5669                                    if cmp_val_binop(v1, *op, v2) {
5670                                        out.push(item.clone());
5671                                    }
5672                                }
5673                            }
5674                        }
5675                    }
5676                    stack.push(Val::arr(out));
5677                }
5678                Opcode::FilterFieldsAllEqLitCount(pairs) => {
5679                    let recv = pop!(stack);
5680                    let mut n: i64 = 0;
5681                    let mut ics: SmallVec<[Option<usize>; 4]> = SmallVec::new();
5682                    ics.resize(pairs.len(), None);
5683                    if let Val::Arr(a) = &recv {
5684                        'item: for item in a.iter() {
5685                            if let Val::Obj(m) = item {
5686                                for (i, (k, lit)) in pairs.iter().enumerate() {
5687                                    match lookup_field_cached(m, k, &mut ics[i]) {
5688                                        Some(v) if crate::eval::util::vals_eq(v, lit) => {}
5689                                        _ => continue 'item,
5690                                    }
5691                                }
5692                                n += 1;
5693                            }
5694                        }
5695                    }
5696                    stack.push(Val::Int(n));
5697                }
5698                Opcode::FilterFieldsAllCmpLitCount(triples) => {
5699                    let recv = pop!(stack);
5700                    let mut n: i64 = 0;
5701                    let mut ics: SmallVec<[Option<usize>; 4]> = SmallVec::new();
5702                    ics.resize(triples.len(), None);
5703                    if let Val::Arr(a) = &recv {
5704                        'item: for item in a.iter() {
5705                            if let Val::Obj(m) = item {
5706                                for (i, (k, cop, lit)) in triples.iter().enumerate() {
5707                                    match lookup_field_cached(m, k, &mut ics[i]) {
5708                                        Some(v) if cmp_val_binop(v, *cop, lit) => {}
5709                                        _ => continue 'item,
5710                                    }
5711                                }
5712                                n += 1;
5713                            }
5714                        }
5715                    }
5716                    stack.push(Val::Int(n));
5717                }
5718                Opcode::FilterFieldEqLitCount(k, lit) => {
5719                    let recv = pop!(stack);
5720                    let mut n: i64 = 0;
5721                    let mut idx: Option<usize> = None;
5722                    if let Val::Arr(a) = &recv {
5723                        for item in a.iter() {
5724                            if let Val::Obj(m) = item {
5725                                if let Some(v) = lookup_field_cached(m, k, &mut idx) {
5726                                    if crate::eval::util::vals_eq(v, lit) { n += 1; }
5727                                }
5728                            }
5729                        }
5730                    }
5731                    stack.push(Val::Int(n));
5732                }
5733                Opcode::FilterFieldCmpLitCount(k, op, lit) => {
5734                    let recv = pop!(stack);
5735                    let mut n: i64 = 0;
5736                    let mut idx: Option<usize> = None;
5737                    if let Val::Arr(a) = &recv {
5738                        for item in a.iter() {
5739                            if let Val::Obj(m) = item {
5740                                if let Some(v) = lookup_field_cached(m, k, &mut idx) {
5741                                    if cmp_val_binop(v, *op, lit) { n += 1; }
5742                                }
5743                            }
5744                        }
5745                    }
5746                    stack.push(Val::Int(n));
5747                }
5748                Opcode::FilterFieldCmpFieldCount(k1, op, k2) => {
5749                    let recv = pop!(stack);
5750                    let mut n: i64 = 0;
5751                    let mut i1: Option<usize> = None;
5752                    let mut i2: Option<usize> = None;
5753                    if let Val::Arr(a) = &recv {
5754                        for item in a.iter() {
5755                            if let Val::Obj(m) = item {
5756                                let v1 = lookup_field_cached(m, k1, &mut i1);
5757                                let v2 = lookup_field_cached(m, k2, &mut i2);
5758                                if let (Some(v1), Some(v2)) = (v1, v2) {
5759                                    if cmp_val_binop(v1, *op, v2) { n += 1; }
5760                                }
5761                            }
5762                        }
5763                    }
5764                    stack.push(Val::Int(n));
5765                }
5766
5767                // ── GroupByField (Tier 2) ─────────────────────────────────
5768                Opcode::GroupByField(k) => {
5769                    let recv = pop!(stack);
5770                    stack.push(group_by_field(&recv, k.as_ref()));
5771                }
5772                Opcode::CountByField(k) => {
5773                    let recv = pop!(stack);
5774                    stack.push(count_by_field(&recv, k.as_ref()));
5775                }
5776                Opcode::UniqueByField(k) => {
5777                    let recv = pop!(stack);
5778                    stack.push(unique_by_field(&recv, k.as_ref()));
5779                }
5780                Opcode::FilterMapSum { pred, map } => {
5781                    let recv = pop!(stack);
5782                    let mut acc_i: i64 = 0;
5783                    let mut acc_f: f64 = 0.0;
5784                    let mut is_float = false;
5785                    let run = |this: &mut Self, item: Val, scratch: &mut Env,
5786                               acc_i: &mut i64, acc_f: &mut f64, is_float: &mut bool|
5787                        -> Result<(), EvalError>
5788                    {
5789                        let prev = scratch.swap_current(item);
5790                        if !is_truthy(&this.exec(pred, scratch)?) {
5791                            scratch.restore_current(prev);
5792                            return Ok(());
5793                        }
5794                        let v = this.exec(map, scratch)?;
5795                        scratch.restore_current(prev);
5796                        match v {
5797                            Val::Int(n) => {
5798                                if *is_float { *acc_f += n as f64; } else { *acc_i += n; }
5799                            }
5800                            Val::Float(x) => {
5801                                if !*is_float { *acc_f = *acc_i as f64; *is_float = true; }
5802                                *acc_f += x;
5803                            }
5804                            Val::Null => {}
5805                            _ => return Err(EvalError("filter(..).map(..).sum(): non-numeric mapped value".into())),
5806                        }
5807                        Ok(())
5808                    };
5809                    match &recv {
5810                        Val::Arr(a) => {
5811                            let mut scratch = env.clone();
5812                            for item in a.iter() {
5813                                run(self, item.clone(), &mut scratch, &mut acc_i, &mut acc_f, &mut is_float)?;
5814                            }
5815                        }
5816                        Val::IntVec(a) => {
5817                            let mut scratch = env.clone();
5818                            for &n in a.iter() {
5819                                run(self, Val::Int(n), &mut scratch, &mut acc_i, &mut acc_f, &mut is_float)?;
5820                            }
5821                        }
5822                        Val::FloatVec(a) => {
5823                            let mut scratch = env.clone();
5824                            for &f in a.iter() {
5825                                run(self, Val::Float(f), &mut scratch, &mut acc_i, &mut acc_f, &mut is_float)?;
5826                            }
5827                        }
5828                        _ => {}
5829                    }
5830                    stack.push(if is_float { Val::Float(acc_f) } else { Val::Int(acc_i) });
5831                }
5832                Opcode::FilterMapAvg { pred, map } => {
5833                    let recv = pop!(stack);
5834                    let mut sum: f64 = 0.0;
5835                    let mut n: usize = 0;
5836                    if let Val::Arr(a) = &recv {
5837                        let mut scratch = env.clone();
5838                        for item in a.iter() {
5839                            let prev = scratch.swap_current(item.clone());
5840                            if !is_truthy(&self.exec(pred, &scratch)?) {
5841                                scratch.restore_current(prev);
5842                                continue;
5843                            }
5844                            let v = self.exec(map, &scratch)?;
5845                            scratch.restore_current(prev);
5846                            match v {
5847                                Val::Int(x)   => { sum += x as f64; n += 1; }
5848                                Val::Float(x) => { sum += x;        n += 1; }
5849                                Val::Null => {}
5850                                _ => return err!("filter(..).map(..).avg(): non-numeric mapped value"),
5851                            }
5852                        }
5853                    }
5854                    stack.push(if n == 0 { Val::Null } else { Val::Float(sum / n as f64) });
5855                }
5856                Opcode::FilterMapFirst { pred, map } => {
5857                    let recv = pop!(stack);
5858                    let mut out = Val::Null;
5859                    if let Val::Arr(a) = &recv {
5860                        let mut scratch = env.clone();
5861                        for item in a.iter() {
5862                            let prev = scratch.swap_current(item.clone());
5863                            if is_truthy(&self.exec(pred, &scratch)?) {
5864                                let mapped = self.exec(map, &scratch)?;
5865                                scratch.restore_current(prev);
5866                                out = mapped;
5867                                break;
5868                            }
5869                            scratch.restore_current(prev);
5870                        }
5871                    } else if !recv.is_null() {
5872                        let sub = env.with_current(recv.clone());
5873                        if is_truthy(&self.exec(pred, &sub)?) {
5874                            out = self.exec(map, &sub)?;
5875                        }
5876                    }
5877                    stack.push(out);
5878                }
5879                Opcode::FilterMapMin { pred, map } => {
5880                    let recv = pop!(stack);
5881                    stack.push(self.filter_map_minmax(recv, pred, map, env, true)?);
5882                }
5883                Opcode::FilterMapMax { pred, map } => {
5884                    let recv = pop!(stack);
5885                    stack.push(self.filter_map_minmax(recv, pred, map, env, false)?);
5886                }
5887                Opcode::FilterLast { pred } => {
5888                    let recv = pop!(stack);
5889                    let mut out = Val::Null;
5890                    if let Val::Arr(a) = &recv {
5891                        let mut scratch = env.clone();
5892                        for item in a.iter().rev() {
5893                            let prev = scratch.swap_current(item.clone());
5894                            if is_truthy(&self.exec(pred, &scratch)?) {
5895                                scratch.restore_current(prev);
5896                                out = item.clone();
5897                                break;
5898                            }
5899                            scratch.restore_current(prev);
5900                        }
5901                    } else if !recv.is_null() {
5902                        let sub = env.with_current(recv.clone());
5903                        if is_truthy(&self.exec(pred, &sub)?) {
5904                            out = recv;
5905                        }
5906                    }
5907                    stack.push(out);
5908                }
5909                Opcode::MapFirst(f) => {
5910                    let recv = pop!(stack);
5911                    let first = match recv {
5912                        Val::Arr(a) => match Arc::try_unwrap(a) {
5913                            Ok(mut v) if !v.is_empty() => Some(v.swap_remove(0)),
5914                            Ok(_) => None,
5915                            Err(a) => a.first().cloned(),
5916                        },
5917                        Val::Null => None,
5918                        other => Some(other),
5919                    };
5920                    let out = match first {
5921                        None => Val::Null,
5922                        Some(item) => {
5923                            let sub = env.with_current(item);
5924                            self.exec(f, &sub)?
5925                        }
5926                    };
5927                    stack.push(out);
5928                }
5929                Opcode::MapLast(f) => {
5930                    let recv = pop!(stack);
5931                    let last = match recv {
5932                        Val::Arr(a) => match Arc::try_unwrap(a) {
5933                            Ok(mut v) => v.pop(),
5934                            Err(a) => a.last().cloned(),
5935                        },
5936                        Val::Null => None,
5937                        other => Some(other),
5938                    };
5939                    let out = match last {
5940                        None => Val::Null,
5941                        Some(item) => {
5942                            let sub = env.with_current(item);
5943                            self.exec(f, &sub)?
5944                        }
5945                    };
5946                    stack.push(out);
5947                }
5948                Opcode::MapFlatten(f) => {
5949                    let recv = pop!(stack);
5950                    let items = match recv {
5951                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
5952                        _ => Vec::new(),
5953                    };
5954                    let mut out = Vec::with_capacity(items.len());
5955                    let mut scratch = env.clone();
5956                    for item in items {
5957                        let prev = scratch.swap_current(item);
5958                        let mapped = self.exec(f, &scratch)?;
5959                        scratch.restore_current(prev);
5960                        match mapped {
5961                            Val::Arr(a) => {
5962                                let v = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
5963                                out.extend(v);
5964                            }
5965                            other => out.push(other),
5966                        }
5967                    }
5968                    stack.push(Val::arr(out));
5969                }
5970                Opcode::FilterTakeWhile { pred, stop } => {
5971                    let recv = pop!(stack);
5972                    let items = match recv {
5973                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
5974                        _ => Vec::new(),
5975                    };
5976                    let mut out = Vec::with_capacity(items.len());
5977                    let mut scratch = env.clone();
5978                    for item in items {
5979                        let prev = scratch.swap_current(item.clone());
5980                        let pass_pred = is_truthy(&self.exec(pred, &scratch)?);
5981                        if !pass_pred { scratch.restore_current(prev); continue; }
5982                        let stop_ok = is_truthy(&self.exec(stop, &scratch)?);
5983                        scratch.restore_current(prev);
5984                        if !stop_ok { break; }
5985                        out.push(item);
5986                    }
5987                    stack.push(Val::arr(out));
5988                }
5989                Opcode::FilterDropWhile { pred, drop } => {
5990                    let recv = pop!(stack);
5991                    let items = match recv {
5992                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
5993                        _ => Vec::new(),
5994                    };
5995                    let mut out = Vec::with_capacity(items.len());
5996                    let mut dropping = true;
5997                    let mut scratch = env.clone();
5998                    for item in items {
5999                        let prev = scratch.swap_current(item.clone());
6000                        let pass_pred = is_truthy(&self.exec(pred, &scratch)?);
6001                        if !pass_pred { scratch.restore_current(prev); continue; }
6002                        if dropping {
6003                            let still_drop = is_truthy(&self.exec(drop, &scratch)?);
6004                            scratch.restore_current(prev);
6005                            if still_drop { continue; }
6006                            dropping = false;
6007                            out.push(item);
6008                            continue;
6009                        }
6010                        scratch.restore_current(prev);
6011                        out.push(item);
6012                    }
6013                    stack.push(Val::arr(out));
6014                }
6015                Opcode::EquiJoin { rhs, lhs_key, rhs_key } => {
6016                    use std::collections::HashMap;
6017                    let left_val = pop!(stack);
6018                    let right_val = self.exec(rhs, env)?;
6019                    let left = match left_val {
6020                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
6021                        _ => Vec::new(),
6022                    };
6023                    let right = match right_val {
6024                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
6025                        _ => Vec::new(),
6026                    };
6027                    let mut idx: HashMap<String, Vec<Val>> = HashMap::with_capacity(right.len());
6028                    let mut r_slot: Option<usize> = None;
6029                    for r in right {
6030                        let key = match &r {
6031                            Val::Obj(o) => lookup_field_cached(o, rhs_key, &mut r_slot)
6032                                .map(super::eval::util::val_to_key),
6033                            _ => None,
6034                        };
6035                        if let Some(k) = key { idx.entry(k).or_default().push(r); }
6036                    }
6037                    let mut out = Vec::with_capacity(left.len());
6038                    let mut l_slot: Option<usize> = None;
6039                    for l in left {
6040                        let key = match &l {
6041                            Val::Obj(o) => lookup_field_cached(o, lhs_key, &mut l_slot)
6042                                .map(super::eval::util::val_to_key),
6043                            _ => None,
6044                        };
6045                        let Some(k) = key else { continue };
6046                        let Some(matches) = idx.get(&k) else { continue };
6047                        for r in matches {
6048                            match (&l, r) {
6049                                (Val::Obj(lo), Val::Obj(ro)) => {
6050                                    let mut m = (**lo).clone();
6051                                    for (k, v) in ro.iter() { m.insert(k.clone(), v.clone()); }
6052                                    out.push(Val::obj(m));
6053                                }
6054                                _ => out.push(l.clone()),
6055                            }
6056                        }
6057                    }
6058                    stack.push(Val::arr(out));
6059                }
6060                Opcode::MapUnique(f) => {
6061                    let recv = pop!(stack);
6062                    let items = match recv {
6063                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
6064                        _ => Vec::new(),
6065                    };
6066                    let mut seen: std::collections::HashSet<String> =
6067                        std::collections::HashSet::with_capacity(items.len());
6068                    let mut out = Vec::with_capacity(items.len());
6069                    let mut scratch = env.clone();
6070                    for item in items {
6071                        let prev = scratch.swap_current(item);
6072                        let mapped = self.exec(f, &scratch)?;
6073                        scratch.restore_current(prev);
6074                        if seen.insert(super::eval::util::val_to_key(&mapped)) {
6075                            out.push(mapped);
6076                        }
6077                    }
6078                    stack.push(Val::arr(out));
6079                }
6080                Opcode::TopN { n, asc } => {
6081                    use std::collections::BinaryHeap;
6082                    use std::cmp::Reverse;
6083                    let recv = pop!(stack);
6084                    let items = match recv {
6085                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
6086                        Val::IntVec(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone())
6087                            .into_iter().map(Val::Int).collect(),
6088                        Val::FloatVec(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone())
6089                            .into_iter().map(Val::Float).collect(),
6090                        _ => Vec::new(),
6091                    };
6092                    if *n >= items.len() {
6093                        let mut v = items;
6094                        v.sort_by(|x, y| super::eval::util::cmp_vals(x, y));
6095                        if !*asc { v.reverse(); }
6096                        stack.push(Val::arr(v));
6097                    } else if *asc {
6098                        // Max-heap of size n; pop largest to keep smallest n.
6099                        let mut heap: BinaryHeap<WrapVal> = BinaryHeap::with_capacity(*n);
6100                        for item in items {
6101                            if heap.len() < *n {
6102                                heap.push(WrapVal(item));
6103                            } else if super::eval::util::cmp_vals(&item, &heap.peek().unwrap().0)
6104                                      == std::cmp::Ordering::Less {
6105                                heap.pop();
6106                                heap.push(WrapVal(item));
6107                            }
6108                        }
6109                        let mut v: Vec<Val> = heap.into_iter().map(|w| w.0).collect();
6110                        v.sort_by(|x, y| super::eval::util::cmp_vals(x, y));
6111                        stack.push(Val::arr(v));
6112                    } else {
6113                        // Min-heap via Reverse; keep largest n.
6114                        let mut heap: BinaryHeap<Reverse<WrapVal>> = BinaryHeap::with_capacity(*n);
6115                        for item in items {
6116                            if heap.len() < *n {
6117                                heap.push(Reverse(WrapVal(item)));
6118                            } else if super::eval::util::cmp_vals(&item, &heap.peek().unwrap().0.0)
6119                                      == std::cmp::Ordering::Greater {
6120                                heap.pop();
6121                                heap.push(Reverse(WrapVal(item)));
6122                            }
6123                        }
6124                        let mut v: Vec<Val> = heap.into_iter().map(|w| w.0.0).collect();
6125                        v.sort_by(|x, y| super::eval::util::cmp_vals(y, x));
6126                        stack.push(Val::arr(v));
6127                    }
6128                }
6129                Opcode::UniqueCount => {
6130                    use super::eval::util::val_to_key;
6131                    let recv = pop!(stack);
6132                    let items = match recv {
6133                        Val::Arr(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
6134                        Val::Null   => { stack.push(Val::Int(0)); continue; }
6135                        other       => vec![other],
6136                    };
6137                    let mut seen: std::collections::HashSet<String> =
6138                        std::collections::HashSet::with_capacity(items.len());
6139                    let mut n: i64 = 0;
6140                    for it in &items {
6141                        if seen.insert(val_to_key(it)) { n += 1; }
6142                    }
6143                    stack.push(Val::Int(n));
6144                }
6145                Opcode::ArgExtreme { key, lam_param, max } => {
6146                    let recv = pop!(stack);
6147                    let items = match recv {
6148                        Val::Arr(a) => a,
6149                        _ => { stack.push(Val::Null); continue; }
6150                    };
6151                    if items.is_empty() { stack.push(Val::Null); continue; }
6152                    let mut scratch = env.clone();
6153                    let param = lam_param.as_deref();
6154                    let mut best_idx: usize = 0;
6155                    let mut best_key = self.exec_lam_body_scratch(
6156                        key, &items[0], param, &mut scratch)?;
6157                    for (i, item) in items.iter().enumerate().skip(1) {
6158                        let k = self.exec_lam_body_scratch(
6159                            key, item, param, &mut scratch)?;
6160                        let ord = super::eval::util::cmp_vals(&k, &best_key);
6161                        let take = if *max {
6162                            // .last() on sorted asc → last occurrence of max;
6163                            // ties update to later index.
6164                            ord != std::cmp::Ordering::Less
6165                        } else {
6166                            // .first() → earliest occurrence of min;
6167                            // strict less only (keep earliest on ties).
6168                            ord == std::cmp::Ordering::Less
6169                        };
6170                        if take { best_idx = i; best_key = k; }
6171                    }
6172                    let mut items_vec = Arc::try_unwrap(items).unwrap_or_else(|a| (*a).clone());
6173                    let winner = std::mem::replace(&mut items_vec[best_idx], Val::Null);
6174                    stack.push(winner);
6175                }
6176                Opcode::MapMap { f1, f2 } => {
6177                    let recv = pop!(stack);
6178                    let recv = match recv {
6179                        Val::StrVec(_) | Val::IntVec(_) | Val::FloatVec(_) => recv.into_arr(),
6180                        v => v,
6181                    };
6182                    if let Val::Arr(a) = recv {
6183                        // COW fast-path: if the Arc is unique, reuse the Vec
6184                        // storage (writing mapped values back in place).
6185                        let mut scratch = env.clone();
6186                        match Arc::try_unwrap(a) {
6187                            Ok(mut v) => {
6188                                for slot in v.iter_mut() {
6189                                    let prev = scratch.swap_current(std::mem::replace(slot, Val::Null));
6190                                    let mid = self.exec(f1, &scratch)?;
6191                                    scratch.swap_current(mid);
6192                                    let res = self.exec(f2, &scratch)?;
6193                                    scratch.restore_current(prev);
6194                                    *slot = res;
6195                                }
6196                                stack.push(Val::arr(v));
6197                            }
6198                            Err(a) => {
6199                                let mut out = Vec::with_capacity(a.len());
6200                                for item in a.iter() {
6201                                    let prev = scratch.swap_current(item.clone());
6202                                    let mid = self.exec(f1, &scratch)?;
6203                                    scratch.swap_current(mid);
6204                                    let res = self.exec(f2, &scratch)?;
6205                                    scratch.restore_current(prev);
6206                                    out.push(res);
6207                                }
6208                                stack.push(Val::arr(out));
6209                            }
6210                        }
6211                    } else {
6212                        stack.push(Val::arr(Vec::new()));
6213                    }
6214                }
6215                Opcode::FindOne(pred) => {
6216                    let recv = pop!(stack);
6217                    let mut found: Option<Val> = None;
6218                    if let Val::Arr(a) = &recv {
6219                        let mut scratch = env.clone();
6220                        for item in a.iter() {
6221                            let prev = scratch.swap_current(item.clone());
6222                            let keep = is_truthy(&self.exec(pred, &scratch)?);
6223                            scratch.restore_current(prev);
6224                            if keep {
6225                                if found.is_some() {
6226                                    return err!("quantifier !: expected exactly one match, found multiple");
6227                                }
6228                                found = Some(item.clone());
6229                            }
6230                        }
6231                    } else if !recv.is_null() {
6232                        let sub_env = env.with_current(recv.clone());
6233                        if is_truthy(&self.exec(pred, &sub_env)?) { found = Some(recv); }
6234                    }
6235                    match found {
6236                        Some(v) => stack.push(v),
6237                        None => return err!("quantifier !: expected exactly one match, found none"),
6238                    }
6239                }
6240
6241                // ── Ident ─────────────────────────────────────────────────────
6242                Opcode::LoadIdent(name) => {
6243                    let v = if let Some(v) = env.get_var(name.as_ref()) {
6244                        v.clone()
6245                    } else {
6246                        env.current.get_field(name.as_ref())
6247                    };
6248                    stack.push(v);
6249                }
6250
6251                // ── Operators ─────────────────────────────────────────────────
6252                Opcode::Add  => { let r = pop!(stack); let l = pop!(stack); stack.push(add_vals(l, r)?); }
6253                Opcode::Sub  => { let r = pop!(stack); let l = pop!(stack); stack.push(num_op(l, r, |a,b|a-b, |a,b|a-b)?); }
6254                Opcode::Mul  => { let r = pop!(stack); let l = pop!(stack); stack.push(num_op(l, r, |a,b|a*b, |a,b|a*b)?); }
6255                Opcode::Div  => {
6256                    let r = pop!(stack); let l = pop!(stack);
6257                    let b = r.as_f64().unwrap_or(0.0);
6258                    if b == 0.0 { return err!("division by zero"); }
6259                    stack.push(Val::Float(l.as_f64().unwrap_or(0.0) / b));
6260                }
6261                Opcode::Mod  => { let r = pop!(stack); let l = pop!(stack); stack.push(num_op(l, r, |a,b|a%b, |a,b|a%b)?); }
6262                Opcode::Eq   => { let r = pop!(stack); let l = pop!(stack); stack.push(Val::Bool(vals_eq(&l,&r))); }
6263                Opcode::Neq  => { let r = pop!(stack); let l = pop!(stack); stack.push(Val::Bool(!vals_eq(&l,&r))); }
6264                Opcode::Lt   => { let r = pop!(stack); let l = pop!(stack); stack.push(Val::Bool(cmp_vals(&l,&r) == std::cmp::Ordering::Less)); }
6265                Opcode::Lte  => { let r = pop!(stack); let l = pop!(stack); stack.push(Val::Bool(cmp_vals(&l,&r) != std::cmp::Ordering::Greater)); }
6266                Opcode::Gt   => { let r = pop!(stack); let l = pop!(stack); stack.push(Val::Bool(cmp_vals(&l,&r) == std::cmp::Ordering::Greater)); }
6267                Opcode::Gte  => { let r = pop!(stack); let l = pop!(stack); stack.push(Val::Bool(cmp_vals(&l,&r) != std::cmp::Ordering::Less)); }
6268                Opcode::Fuzzy => {
6269                    let r = pop!(stack); let l = pop!(stack);
6270                    let ls = match &l { Val::Str(s) => s.to_lowercase(), _ => val_to_string(&l).to_lowercase() };
6271                    let rs = match &r { Val::Str(s) => s.to_lowercase(), _ => val_to_string(&r).to_lowercase() };
6272                    stack.push(Val::Bool(ls.contains(&rs) || rs.contains(&ls)));
6273                }
6274                Opcode::Not  => { let v = pop!(stack); stack.push(Val::Bool(!is_truthy(&v))); }
6275                Opcode::Neg  => {
6276                    let v = pop!(stack);
6277                    stack.push(match v {
6278                        Val::Int(n) => Val::Int(-n),
6279                        Val::Float(f) => Val::Float(-f),
6280                        _ => return err!("unary minus requires a number"),
6281                    });
6282                }
6283                Opcode::CastOp(ty) => {
6284                    let v = pop!(stack);
6285                    stack.push(exec_cast(&v, *ty)?);
6286                }
6287
6288                // ── Short-circuit ops ─────────────────────────────────────────
6289                Opcode::AndOp(rhs) => {
6290                    let lv = pop!(stack);
6291                    if !is_truthy(&lv) {
6292                        stack.push(Val::Bool(false));
6293                    } else {
6294                        let rv = self.exec(rhs, env)?;
6295                        stack.push(Val::Bool(is_truthy(&rv)));
6296                    }
6297                }
6298                Opcode::OrOp(rhs) => {
6299                    let lv = pop!(stack);
6300                    if is_truthy(&lv) {
6301                        stack.push(lv);
6302                    } else {
6303                        stack.push(self.exec(rhs, env)?);
6304                    }
6305                }
6306                Opcode::CoalesceOp(rhs) => {
6307                    let lv = pop!(stack);
6308                    if !lv.is_null() {
6309                        stack.push(lv);
6310                    } else {
6311                        stack.push(self.exec(rhs, env)?);
6312                    }
6313                }
6314                Opcode::IfElse { then_, else_ } => {
6315                    let cv = pop!(stack);
6316                    let branch = if is_truthy(&cv) { then_ } else { else_ };
6317                    stack.push(self.exec(branch, env)?);
6318                }
6319
6320                // ── Method calls ──────────────────────────────────────────────
6321                Opcode::CallMethod(call) => {
6322                    let recv = pop!(stack);
6323                    // SIMD fast path: `$..find(@.k == lit)` on root with raw
6324                    // bytes → scan enclosing objects, skip tree walk.
6325                    if call.method == BuiltinMethod::Unknown
6326                        && call.name.as_ref() == "deep_find"
6327                        && !call.orig_args.is_empty()
6328                    {
6329                        if let Some(bytes) = env.raw_bytes.as_ref() {
6330                            let recv_is_root = match (&recv, &env.root) {
6331                                (Val::Obj(a), Val::Obj(b)) => Arc::ptr_eq(a, b),
6332                                (Val::Arr(a), Val::Arr(b)) => Arc::ptr_eq(a, b),
6333                                _ => false,
6334                            };
6335                            if recv_is_root {
6336                                let tail = &ops_slice[op_idx + 1..];
6337                                if let Some(conjuncts) =
6338                                    super::eval::canonical_field_eq_literals(&call.orig_args)
6339                                {
6340                                    let spans = if conjuncts.len() == 1 {
6341                                        super::scan::find_enclosing_objects_eq(
6342                                            bytes, &conjuncts[0].0, &conjuncts[0].1,
6343                                        )
6344                                    } else {
6345                                        super::scan::find_enclosing_objects_eq_multi(
6346                                            bytes, &conjuncts,
6347                                        )
6348                                    };
6349                                    let (arr, extra) = materialise_find_scan_spans(
6350                                        bytes, &spans, tail,
6351                                    );
6352                                    stack.push(arr);
6353                                    skip_ahead = extra;
6354                                    continue;
6355                                }
6356                                // Single-conjunct numeric-range scan.
6357                                if call.orig_args.len() == 1 {
6358                                    let e = match &call.orig_args[0] {
6359                                        super::ast::Arg::Pos(e)
6360                                        | super::ast::Arg::Named(_, e) => e,
6361                                    };
6362                                    if let Some((field, op, thresh)) =
6363                                        super::eval::canonical_field_cmp_literal(e)
6364                                    {
6365                                        let spans = super::scan::find_enclosing_objects_cmp(
6366                                            bytes, &field, op, thresh,
6367                                        );
6368                                        let (arr, extra) = materialise_find_scan_spans(
6369                                            bytes, &spans, tail,
6370                                        );
6371                                        stack.push(arr);
6372                                        skip_ahead = extra;
6373                                        continue;
6374                                    }
6375                                }
6376                                // Mixed multi-conjunct (eq + numeric-range).
6377                                if let Some(conjuncts) =
6378                                    super::eval::canonical_field_mixed_predicates(&call.orig_args)
6379                                {
6380                                    let spans = super::scan::find_enclosing_objects_mixed(
6381                                        bytes, &conjuncts,
6382                                    );
6383                                    let (arr, extra) = materialise_find_scan_spans(
6384                                        bytes, &spans, tail,
6385                                    );
6386                                    stack.push(arr);
6387                                    skip_ahead = extra;
6388                                    continue;
6389                                }
6390                            }
6391                        }
6392                    }
6393                    let result = self.exec_call(recv, call, env)?;
6394                    stack.push(result);
6395                }
6396                Opcode::CallOptMethod(call) => {
6397                    let recv = pop!(stack);
6398                    if recv.is_null() {
6399                        stack.push(Val::Null);
6400                    } else {
6401                        stack.push(self.exec_call(recv, call, env)?);
6402                    }
6403                }
6404
6405                Opcode::MapFString(parts) => {
6406                    let parts = Arc::clone(parts);
6407                    let recv = pop!(stack);
6408                    let recv = if matches!(&recv, Val::IntVec(_) | Val::FloatVec(_)) {
6409                        recv.into_arr()
6410                    } else { recv };
6411                    // Output is all-strings by construction — emit Val::StrVec
6412                    // (Arc<Vec<Arc<str>>>) to drop the per-row Val enum tag
6413                    // and keep the columnar lane through downstream methods.
6414                    let out_strs: Vec<Arc<str>> = match &recv {
6415                        Val::Arr(a) => {
6416                            let mut out = Vec::with_capacity(a.len());
6417                            let mut scratch = env.clone();
6418                            for item in a.iter() {
6419                                let prev = scratch.swap_current(item.clone());
6420                                let result = self.exec_fstring(&parts, &scratch)?;
6421                                scratch.restore_current(prev);
6422                                if let Val::Str(s) = result { out.push(s); }
6423                                else { out.push(Arc::<str>::from("")); }
6424                            }
6425                            out
6426                        }
6427                        Val::StrVec(a) => {
6428                            let mut out = Vec::with_capacity(a.len());
6429                            let mut scratch = env.clone();
6430                            for s in a.iter() {
6431                                let prev = scratch.swap_current(Val::Str(s.clone()));
6432                                let result = self.exec_fstring(&parts, &scratch)?;
6433                                scratch.restore_current(prev);
6434                                if let Val::Str(s) = result { out.push(s); }
6435                                else { out.push(Arc::<str>::from("")); }
6436                            }
6437                            out
6438                        }
6439                        _ => Vec::new(),
6440                    };
6441                    stack.push(Val::StrVec(Arc::new(out_strs)));
6442                }
6443                Opcode::MapStrSlice { start, end } => {
6444                    let v = pop!(stack);
6445                    let start = *start;
6446                    let end_opt = *end;
6447                    // StrVec input: emit columnar StrSliceVec — single Arc
6448                    // wrapping Vec<StrRef>, no per-row Val enum tag.
6449                    if let Val::StrVec(a) = &v {
6450                        let mut out: Vec<crate::strref::StrRef> = Vec::with_capacity(a.len());
6451                        for s in a.iter() {
6452                            let src = s.as_ref();
6453                            if src.is_ascii() {
6454                                let blen = src.len();
6455                                let start_u = if start < 0 {
6456                                    blen.saturating_sub((-start) as usize)
6457                                } else { start as usize };
6458                                let end_u = match end_opt {
6459                                    Some(e) if e < 0 =>
6460                                        blen.saturating_sub((-e) as usize),
6461                                    Some(e) => (e as usize).min(blen),
6462                                    None    => blen,
6463                                };
6464                                let start_u = start_u.min(end_u).min(blen);
6465                                out.push(crate::strref::StrRef::slice(s.clone(), start_u, end_u));
6466                            } else {
6467                                // Unicode path — char boundaries.
6468                                let (start_b, end_b) = slice_unicode_bounds(src, start, end_opt);
6469                                out.push(crate::strref::StrRef::slice(s.clone(), start_b, end_b));
6470                            }
6471                        }
6472                        stack.push(Val::StrSliceVec(Arc::new(out)));
6473                        continue;
6474                    }
6475                    let out_vec: Vec<Val> = if let Val::Arr(a) = &v {
6476                        // Homogeneous Str input → emit columnar StrSliceVec.
6477                        let all_str = a.iter().all(|it| matches!(it, Val::Str(_)));
6478                        if all_str {
6479                            let mut out: Vec<crate::strref::StrRef> = Vec::with_capacity(a.len());
6480                            for item in a.iter() {
6481                                if let Val::Str(s) = item {
6482                                    let src = s.as_ref();
6483                                    if src.is_ascii() {
6484                                        let blen = src.len();
6485                                        let start_u = if start < 0 {
6486                                            blen.saturating_sub((-start) as usize)
6487                                        } else { start as usize };
6488                                        let end_u = match end_opt {
6489                                            Some(e) if e < 0 =>
6490                                                blen.saturating_sub((-e) as usize),
6491                                            Some(e) => (e as usize).min(blen),
6492                                            None    => blen,
6493                                        };
6494                                        let start_u = start_u.min(end_u).min(blen);
6495                                        out.push(crate::strref::StrRef::slice(s.clone(), start_u, end_u));
6496                                    } else {
6497                                        let (start_b, end_b) = slice_unicode_bounds(src, start, end_opt);
6498                                        out.push(crate::strref::StrRef::slice(s.clone(), start_b, end_b));
6499                                    }
6500                                }
6501                            }
6502                            stack.push(Val::StrSliceVec(Arc::new(out)));
6503                            continue;
6504                        }
6505                        let mut out = Vec::with_capacity(a.len());
6506                        for item in a.iter() {
6507                            if let Val::Str(s) = item {
6508                                let src = s.as_ref();
6509                                if src.is_ascii() {
6510                                    let blen = src.len();
6511                                    let start_u = if start < 0 {
6512                                        blen.saturating_sub((-start) as usize)
6513                                    } else { start as usize };
6514                                    let end_u = match end_opt {
6515                                        Some(e) if e < 0 =>
6516                                            blen.saturating_sub((-e) as usize),
6517                                        Some(e) => (e as usize).min(blen),
6518                                        None    => blen,
6519                                    };
6520                                    let start_u = start_u.min(end_u).min(blen);
6521                                    if start_u == 0 && end_u == blen {
6522                                        out.push(Val::Str(s.clone()));
6523                                    } else {
6524                                        out.push(Val::StrSlice(
6525                                            crate::strref::StrRef::slice(
6526                                                s.clone(), start_u, end_u)
6527                                        ));
6528                                    }
6529                                } else {
6530                                    // Unicode fallback: char-indices walk.
6531                                    let mut start_b = src.len();
6532                                    let mut end_b = src.len();
6533                                    let mut found_start = false;
6534                                    let start_want = if start < 0 { 0 } else { start as usize };
6535                                    let end_want = end_opt.and_then(|e|
6536                                        if e < 0 { None } else { Some(e as usize) });
6537                                    for (ci, (bi, _)) in src.char_indices().enumerate() {
6538                                        if !found_start && ci == start_want {
6539                                            start_b = bi;
6540                                            found_start = true;
6541                                        }
6542                                        if let Some(ew) = end_want {
6543                                            if ci == ew { end_b = bi; break; }
6544                                        }
6545                                    }
6546                                    if !found_start { start_b = src.len(); }
6547                                    if end_want.is_none() { end_b = src.len(); }
6548                                    if start_b > end_b { start_b = end_b; }
6549                                    if start_b == 0 && end_b == src.len() {
6550                                        out.push(Val::Str(s.clone()));
6551                                    } else {
6552                                        out.push(Val::StrSlice(
6553                                            crate::strref::StrRef::slice(
6554                                                s.clone(), start_b, end_b)
6555                                        ));
6556                                    }
6557                                }
6558                            } else {
6559                                out.push(Val::Null);
6560                            }
6561                        }
6562                        out
6563                    } else { Vec::new() };
6564                    stack.push(Val::arr(out_vec));
6565                }
6566                Opcode::MapProject { keys, ics } => {
6567                    let recv = pop!(stack);
6568                    let recv = if matches!(&recv, Val::StrVec(_) | Val::IntVec(_) | Val::FloatVec(_)) {
6569                        recv.into_arr()
6570                    } else { recv };
6571                    // Emit Val::ObjVec — columnar struct-of-arrays with one
6572                    // shared keys schema and a `Vec<Vec<Val>>` of rows.
6573                    // No per-row Arc wrapping, no per-row hashtable.
6574                    if let Val::Arr(a) = &recv {
6575                        let mut rows: Vec<Vec<Val>> = Vec::with_capacity(a.len());
6576                        for item in a.iter() {
6577                            if let Val::Obj(m) = item {
6578                                let mut row: Vec<Val> = Vec::with_capacity(keys.len());
6579                                for (i, k) in keys.iter().enumerate() {
6580                                    row.push(ic_get_field(m, k.as_ref(), &ics[i]));
6581                                }
6582                                rows.push(row);
6583                            } else if let Val::ObjSmall(ps) = item {
6584                                let mut row: Vec<Val> = Vec::with_capacity(keys.len());
6585                                for k in keys.iter() {
6586                                    let mut v = Val::Null;
6587                                    for (kk, vv) in ps.iter() {
6588                                        if kk.as_ref() == k.as_ref() {
6589                                            v = vv.clone();
6590                                            break;
6591                                        }
6592                                    }
6593                                    row.push(v);
6594                                }
6595                                rows.push(row);
6596                            } else {
6597                                rows.push(vec![Val::Null; keys.len()]);
6598                            }
6599                        }
6600                        stack.push(Val::ObjVec(Arc::new(super::eval::value::ObjVecData {
6601                            keys: Arc::clone(keys),
6602                            rows,
6603                        })));
6604                    } else {
6605                        stack.push(Val::arr(Vec::new()));
6606                    }
6607                }
6608
6609                // ── Construction ──────────────────────────────────────────────
6610                Opcode::MakeObj(entries) => {
6611                    let entries = Arc::clone(entries);
6612                    let result = self.exec_make_obj(&entries, env)?;
6613                    stack.push(result);
6614                }
6615                Opcode::MakeArr(progs) => {
6616                    let progs = Arc::clone(progs);
6617                    let mut out = Vec::with_capacity(progs.len());
6618                    for p in progs.iter() {
6619                        let v = self.exec(p, env)?;
6620                        // If the program produces an array from a spread,
6621                        // check if it was tagged; for simplicity, just push.
6622                        out.push(v);
6623                    }
6624                    stack.push(Val::arr(out));
6625                }
6626
6627                // ── F-string ──────────────────────────────────────────────────
6628                Opcode::FString(parts) => {
6629                    let parts = Arc::clone(parts);
6630                    let result = self.exec_fstring(&parts, env)?;
6631                    stack.push(result);
6632                }
6633
6634                // ── Kind check ────────────────────────────────────────────────
6635                Opcode::KindCheck { ty, negate } => {
6636                    let v = pop!(stack);
6637                    let m = kind_matches(&v, *ty);
6638                    stack.push(Val::Bool(if *negate { !m } else { m }));
6639                }
6640
6641                // ── Pipeline helpers ──────────────────────────────────────────
6642                Opcode::SetCurrent => {
6643                    // This is emitted before an arbitrary pipe-forward expression.
6644                    // The value flowing through the pipe is now on the stack as TOS.
6645                    // However we can't mutate env here since env is immutable.
6646                    // The actual "set current" happens by the caller preparing a new env.
6647                    // In practice, SetCurrent should not appear in isolation in the
6648                    // flat opcode stream because pipeline steps that need SetCurrent
6649                    // are compiled as sub-programs. Skip.
6650                }
6651                Opcode::BindVar(name) => {
6652                    // TOS becomes a named var; TOS remains (pass-through for ->) .
6653                    // We can't mutate env here. This is handled at the pipeline level.
6654                    // For now, just keep TOS.
6655                    let _ = name;
6656                }
6657                Opcode::StoreVar(name) => {
6658                    // Pop and discard (the LetExpr opcode handles binding properly).
6659                    let _ = name;
6660                    pop!(stack);
6661                }
6662                Opcode::BindObjDestructure(_) | Opcode::BindArrDestructure(_) => {
6663                    // Pipeline bind destructure — handled at pipeline level.
6664                }
6665
6666                // ── Complex recursive ops ─────────────────────────────────────
6667                Opcode::LetExpr { name, body } => {
6668                    let init_val = pop!(stack);
6669                    let body_env = env.with_var(name.as_ref(), init_val);
6670                    stack.push(self.exec(body, &body_env)?);
6671                }
6672
6673                Opcode::ListComp(spec) => {
6674                    let items = self.exec_iter_vals(&spec.iter, env)?;
6675                    let mut out = Vec::with_capacity(items.len());
6676                    // Fast path: single-var `for x in iter` — reuse one
6677                    // scratch Env via push_lam / pop_lam instead of
6678                    // cloning per iteration.
6679                    if spec.vars.len() == 1 {
6680                        let vname = spec.vars[0].clone();
6681                        let mut scratch = env.clone();
6682                        for item in items {
6683                            let frame = scratch.push_lam(Some(vname.as_ref()), item);
6684                            let keep = match &spec.cond {
6685                                Some(c) => is_truthy(&self.exec(c, &scratch)?),
6686                                None => true,
6687                            };
6688                            if keep {
6689                                let v = self.exec(&spec.expr, &scratch)?;
6690                                out.push(v);
6691                            }
6692                            scratch.pop_lam(frame);
6693                        }
6694                    } else {
6695                        for item in items {
6696                            let ie = bind_comp_vars(env, &spec.vars, item);
6697                            if let Some(cond) = &spec.cond {
6698                                if !is_truthy(&self.exec(cond, &ie)?) { continue; }
6699                            }
6700                            out.push(self.exec(&spec.expr, &ie)?);
6701                        }
6702                    }
6703                    stack.push(Val::arr(out));
6704                }
6705
6706                Opcode::DictComp(spec) => {
6707                    let items = self.exec_iter_vals(&spec.iter, env)?;
6708                    let mut map: IndexMap<Arc<str>, Val> = IndexMap::with_capacity(items.len());
6709                    // Single-var fast path: reuse scratch Env via push_lam/pop_lam
6710                    // instead of cloning per iteration.
6711                    if spec.vars.len() == 1 {
6712                        let vname = spec.vars[0].clone();
6713                        // Hot pattern: `{ f(x): x for x in iter }` with no
6714                        // condition.  Both key and val depend only on `x`,
6715                        // which is also `current`, so we can elide the
6716                        // env rebind and the two `self.exec` calls by
6717                        // dispatching the common shapes inline.
6718                        let val_is_ident = matches!(
6719                            spec.val.ops.as_ref(),
6720                            [Opcode::LoadIdent(v)] if v.as_ref() == vname.as_ref()
6721                        );
6722                        let key_shape = classify_dict_key(&spec.key, vname.as_ref());
6723                        if spec.cond.is_none() && val_is_ident && key_shape.is_some() {
6724                            let shape = key_shape.unwrap();
6725                            for item in items {
6726                                let k: Arc<str> = match shape {
6727                                    DictKeyShape::Ident => match &item {
6728                                        Val::Str(s) => s.clone(),
6729                                        other       => Arc::<str>::from(val_to_key(other)),
6730                                    },
6731                                    DictKeyShape::IdentToString => match &item {
6732                                        Val::Str(s) => s.clone(),
6733                                        Val::Int(n)   => Arc::<str>::from(n.to_string()),
6734                                        Val::Float(f) => Arc::<str>::from(f.to_string()),
6735                                        Val::Bool(b)  => Arc::<str>::from(if *b { "true" } else { "false" }),
6736                                        Val::Null     => Arc::<str>::from("null"),
6737                                        other         => Arc::<str>::from(val_to_key(other)),
6738                                    },
6739                                };
6740                                map.insert(k, item);
6741                            }
6742                            stack.push(Val::obj(map));
6743                            continue;
6744                        }
6745                        let mut scratch = env.clone();
6746                        for item in items {
6747                            let frame = scratch.push_lam(Some(vname.as_ref()), item);
6748                            let keep = match &spec.cond {
6749                                Some(c) => is_truthy(&self.exec(c, &scratch)?),
6750                                None    => true,
6751                            };
6752                            if keep {
6753                                let k: Arc<str> = match self.exec(&spec.key, &scratch)? {
6754                                    Val::Str(s) => s,
6755                                    other       => Arc::<str>::from(val_to_key(&other)),
6756                                };
6757                                let v = self.exec(&spec.val, &scratch)?;
6758                                map.insert(k, v);
6759                            }
6760                            scratch.pop_lam(frame);
6761                        }
6762                    } else {
6763                        for item in items {
6764                            let ie = bind_comp_vars(env, &spec.vars, item);
6765                            if let Some(cond) = &spec.cond {
6766                                if !is_truthy(&self.exec(cond, &ie)?) { continue; }
6767                            }
6768                            let k: Arc<str> = match self.exec(&spec.key, &ie)? {
6769                                Val::Str(s) => s,
6770                                other       => Arc::<str>::from(val_to_key(&other)),
6771                            };
6772                            let v = self.exec(&spec.val, &ie)?;
6773                            map.insert(k, v);
6774                        }
6775                    }
6776                    stack.push(Val::obj(map));
6777                }
6778
6779                Opcode::SetComp(spec) => {
6780                    let items = self.exec_iter_vals(&spec.iter, env)?;
6781                    let mut seen: std::collections::HashSet<String> =
6782                        std::collections::HashSet::with_capacity(items.len());
6783                    let mut out = Vec::with_capacity(items.len());
6784                    if spec.vars.len() == 1 {
6785                        let vname = spec.vars[0].clone();
6786                        let mut scratch = env.clone();
6787                        for item in items {
6788                            let frame = scratch.push_lam(Some(vname.as_ref()), item);
6789                            let keep = match &spec.cond {
6790                                Some(c) => is_truthy(&self.exec(c, &scratch)?),
6791                                None    => true,
6792                            };
6793                            if keep {
6794                                let v = self.exec(&spec.expr, &scratch)?;
6795                                let k = val_to_key(&v);
6796                                if seen.insert(k) { out.push(v); }
6797                            }
6798                            scratch.pop_lam(frame);
6799                        }
6800                    } else {
6801                        for item in items {
6802                            let ie = bind_comp_vars(env, &spec.vars, item);
6803                            if let Some(cond) = &spec.cond {
6804                                if !is_truthy(&self.exec(cond, &ie)?) { continue; }
6805                            }
6806                            let v = self.exec(&spec.expr, &ie)?;
6807                            let k = val_to_key(&v);
6808                            if seen.insert(k) { out.push(v); }
6809                        }
6810                    }
6811                    stack.push(Val::arr(out));
6812                }
6813
6814                // ── Path cache lookup ─────────────────────────────────────────
6815                Opcode::GetPointer(ptr) => {
6816                    let doc_hash = self.doc_hash;
6817                    let v = if let Some(cached) = self.path_cache.get(doc_hash, ptr.as_ref()) {
6818                        cached
6819                    } else {
6820                        let v = resolve_pointer(&env.root, ptr.as_ref());
6821                        self.path_cache.insert(doc_hash, ptr.clone(), v.clone());
6822                        v
6823                    };
6824                    stack.push(v);
6825                }
6826
6827                // ── Patch block ───────────────────────────────────────────────
6828                Opcode::PatchEval(e) => {
6829                    stack.push(eval(e, env)?);
6830                }
6831            }
6832        }
6833
6834        stack.pop().ok_or_else(|| EvalError("program produced no value".into()))
6835    }
6836
6837    // ── Helper: single-pass numeric min/max over filter.map ───────────────────
6838    /// Shared body for `FilterMapMin` / `FilterMapMax`.  `is_min=true` keeps the
6839    /// smallest value, `false` keeps the largest.  Factored out of the main
6840    /// exec match so the hot dispatch loop stays lean.
6841    #[cold]
6842    #[inline(never)]
6843    fn filter_map_minmax(
6844        &mut self,
6845        recv: Val,
6846        pred: &Program,
6847        map:  &Program,
6848        env:  &Env,
6849        is_min: bool,
6850    ) -> Result<Val, EvalError> {
6851        let mut best_i: Option<i64> = None;
6852        let mut best_f: Option<f64> = None;
6853        let better = |new: f64, old: f64| if is_min { new < old } else { new > old };
6854        let better_i = |new: i64, old: i64| if is_min { new < old } else { new > old };
6855        let label = if is_min { "min" } else { "max" };
6856        if let Val::Arr(a) = &recv {
6857            let mut scratch = env.clone();
6858            for item in a.iter() {
6859                let prev = scratch.swap_current(item.clone());
6860                if !is_truthy(&self.exec(pred, &scratch)?) {
6861                    scratch.restore_current(prev);
6862                    continue;
6863                }
6864                let v = self.exec(map, &scratch)?;
6865                scratch.restore_current(prev);
6866                match v {
6867                    Val::Int(n) => {
6868                        if let Some(bf) = best_f {
6869                            if better(n as f64, bf) { best_f = Some(n as f64); }
6870                        } else if let Some(bi) = best_i {
6871                            if better_i(n, bi) { best_i = Some(n); }
6872                        } else { best_i = Some(n); }
6873                    }
6874                    Val::Float(x) => {
6875                        if best_f.is_none() {
6876                            best_f = Some(best_i.map(|i| i as f64).unwrap_or(x));
6877                            best_i = None;
6878                        }
6879                        if better(x, best_f.unwrap()) { best_f = Some(x); }
6880                    }
6881                    Val::Null => {}
6882                    _ => return Err(EvalError(format!(
6883                        "filter(..).map(..).{}(): non-numeric mapped value", label))),
6884                }
6885            }
6886        }
6887        Ok(match (best_i, best_f) {
6888            (_, Some(x)) => Val::Float(x),
6889            (Some(i), _) => Val::Int(i),
6890            _ => Val::Null,
6891        })
6892    }
6893
6894    // ── Method call dispatch ──────────────────────────────────────────────────
6895
6896    fn exec_call(&mut self, recv: Val, call: &CompiledCall, env: &Env) -> Result<Val, EvalError> {
6897        // Global-call opcodes push Root before calling; handle them
6898        if call.method == BuiltinMethod::Unknown {
6899            // Custom registry or global function
6900            if !env.registry_is_empty() {
6901                if let Some(method) = env.registry_get(call.name.as_ref()) {
6902                    let evaled: Result<Vec<Val>, _> = call.sub_progs.iter()
6903                        .map(|p| self.exec(p, env)).collect();
6904                    return method.call(recv, &evaled?);
6905                }
6906            }
6907            // Global function (coalesce, zip, etc.) or fallback to dispatch_method
6908            return dispatch_method(recv, call.name.as_ref(), &call.orig_args, env);
6909        }
6910
6911        // Lambda methods — VM handles iteration, running sub-programs per item
6912        if call.method.is_lambda_method() {
6913            return self.exec_lambda_method(recv, call, env);
6914        }
6915
6916        // Typed-numeric aggregate fast-path: bare `.sum()/.min()/.max()/.avg()`
6917        // on an array.  Skips registry dispatch + `collect_nums` extra Vec.
6918        if call.sub_progs.is_empty() && call.orig_args.is_empty() {
6919            if let Val::Arr(a) = &recv {
6920                match call.method {
6921                    BuiltinMethod::Sum => return Ok(agg_sum_typed(a)),
6922                    BuiltinMethod::Avg => return Ok(agg_avg_typed(a)),
6923                    BuiltinMethod::Min => return Ok(agg_minmax_typed(a, false)),
6924                    BuiltinMethod::Max => return Ok(agg_minmax_typed(a, true)),
6925                    _ => {}
6926                }
6927            }
6928            // Columnar IntVec — pure i64 tight loops, native parity.
6929            if let Val::IntVec(a) = &recv {
6930                match call.method {
6931                    BuiltinMethod::Sum => {
6932                        let s: i64 = a.iter().fold(0i64, |a, b| a.wrapping_add(*b));
6933                        return Ok(Val::Int(s));
6934                    }
6935                    BuiltinMethod::Avg => {
6936                        if a.is_empty() { return Ok(Val::Null); }
6937                        let s: f64 = a.iter().map(|n| *n as f64).sum();
6938                        return Ok(Val::Float(s / a.len() as f64));
6939                    }
6940                    BuiltinMethod::Min => {
6941                        return Ok(a.iter().copied().min().map(Val::Int).unwrap_or(Val::Null));
6942                    }
6943                    BuiltinMethod::Max => {
6944                        return Ok(a.iter().copied().max().map(Val::Int).unwrap_or(Val::Null));
6945                    }
6946                    BuiltinMethod::Count | BuiltinMethod::Len => {
6947                        return Ok(Val::Int(a.len() as i64));
6948                    }
6949                    _ => {}
6950                }
6951            }
6952            // Homogeneous-Int `Val::Arr` receivers get routed through
6953            // columnar reverse/sort — 3x less memory bandwidth than the
6954            // Vec<Val> path.  Clone cost is identical (O(N) in both cases
6955            // because the Arr is typically shared); the win is on write.
6956            if let Val::Arr(a) = &recv {
6957                let is_all_int = a.iter().all(|v| matches!(v, Val::Int(_)));
6958                if is_all_int && !a.is_empty() {
6959                    match call.method {
6960                        BuiltinMethod::Reverse => {
6961                            let mut v: Vec<i64> = a.iter().map(|x|
6962                                if let Val::Int(n) = x { *n } else { 0 }
6963                            ).collect();
6964                            v.reverse();
6965                            return Ok(Val::int_vec(v));
6966                        }
6967                        BuiltinMethod::Sort => {
6968                            let mut v: Vec<i64> = a.iter().map(|x|
6969                                if let Val::Int(n) = x { *n } else { 0 }
6970                            ).collect();
6971                            v.sort_unstable();
6972                            return Ok(Val::int_vec(v));
6973                        }
6974                        BuiltinMethod::Sum => {
6975                            let s: i64 = a.iter().fold(0i64, |acc, v|
6976                                if let Val::Int(n) = v { acc.wrapping_add(*n) } else { acc });
6977                            return Ok(Val::Int(s));
6978                        }
6979                        _ => {}
6980                    }
6981                }
6982            }
6983            // Columnar IntVec receiver — reverse/sort in-place on Vec<i64>.
6984            if let Val::IntVec(a) = &recv {
6985                match call.method {
6986                    BuiltinMethod::Reverse => {
6987                        let mut v: Vec<i64> = Arc::try_unwrap(a.clone()).unwrap_or_else(|a| (*a).clone());
6988                        v.reverse();
6989                        return Ok(Val::int_vec(v));
6990                    }
6991                    BuiltinMethod::Sort => {
6992                        let mut v: Vec<i64> = Arc::try_unwrap(a.clone()).unwrap_or_else(|a| (*a).clone());
6993                        v.sort_unstable();
6994                        return Ok(Val::int_vec(v));
6995                    }
6996                    _ => {}
6997                }
6998            }
6999            if let Val::FloatVec(a) = &recv {
7000                match call.method {
7001                    BuiltinMethod::Sum => {
7002                        let s: f64 = a.iter().sum();
7003                        return Ok(Val::Float(s));
7004                    }
7005                    BuiltinMethod::Avg => {
7006                        if a.is_empty() { return Ok(Val::Null); }
7007                        let s: f64 = a.iter().sum();
7008                        return Ok(Val::Float(s / a.len() as f64));
7009                    }
7010                    BuiltinMethod::Min => {
7011                        let mut best: Option<f64> = None;
7012                        for &f in a.iter() {
7013                            best = Some(match best { Some(b) => if f < b { f } else { b }, None => f });
7014                        }
7015                        return Ok(best.map(Val::Float).unwrap_or(Val::Null));
7016                    }
7017                    BuiltinMethod::Max => {
7018                        let mut best: Option<f64> = None;
7019                        for &f in a.iter() {
7020                            best = Some(match best { Some(b) => if f > b { f } else { b }, None => f });
7021                        }
7022                        return Ok(best.map(Val::Float).unwrap_or(Val::Null));
7023                    }
7024                    BuiltinMethod::Count | BuiltinMethod::Len => {
7025                        return Ok(Val::Int(a.len() as i64));
7026                    }
7027                    _ => {}
7028                }
7029            }
7030            // Bare `.flatten()` (depth=1) — inline depth-1 flatten with exact
7031            // preallocation.  Skips `dispatch_method` + its arg-parse path.
7032            if call.method == BuiltinMethod::Flatten {
7033                if let Val::Arr(a) = &recv {
7034                    // Columnar fast-path: all inners Int-only → emit Val::IntVec.
7035                    let all_int_inner = a.iter().all(|it| match it {
7036                        Val::IntVec(_) => true,
7037                        Val::Arr(inner) => inner.iter().all(|v| matches!(v, Val::Int(_))),
7038                        Val::Int(_) => true,
7039                        _ => false,
7040                    });
7041                    if all_int_inner {
7042                        let cap: usize = a.iter().map(|it| match it {
7043                            Val::IntVec(inner) => inner.len(),
7044                            Val::Arr(inner)    => inner.len(),
7045                            _ => 1,
7046                        }).sum();
7047                        let mut out: Vec<i64> = Vec::with_capacity(cap);
7048                        for item in a.iter() {
7049                            match item {
7050                                Val::IntVec(inner) => out.extend(inner.iter().copied()),
7051                                Val::Arr(inner)    => out.extend(inner.iter().filter_map(|v| v.as_i64())),
7052                                Val::Int(n)        => out.push(*n),
7053                                _ => {}
7054                            }
7055                        }
7056                        return Ok(Val::int_vec(out));
7057                    }
7058                    let cap: usize = a.iter().map(|it| match it {
7059                        Val::Arr(inner) => inner.len(),
7060                        Val::IntVec(inner) => inner.len(),
7061                        Val::FloatVec(inner) => inner.len(),
7062                        _ => 1,
7063                    }).sum();
7064                    let mut out = Vec::with_capacity(cap);
7065                    for item in a.iter() {
7066                        match item {
7067                            Val::Arr(inner) => out.extend(inner.iter().cloned()),
7068                            Val::IntVec(inner) => out.extend(inner.iter().map(|n| Val::Int(*n))),
7069                            Val::FloatVec(inner) => out.extend(inner.iter().map(|f| Val::Float(*f))),
7070                            other => out.push(other.clone()),
7071                        }
7072                    }
7073                    return Ok(Val::arr(out));
7074                }
7075                // Columnar receiver itself — already flat; return as-is.
7076                if matches!(&recv, Val::IntVec(_) | Val::FloatVec(_)) {
7077                    return Ok(recv);
7078                }
7079            }
7080            // Scalar `.to_string()` — inline the conversion.  The registry
7081            // path allocates a fresh `Val::Str` via `val_to_string`; this
7082            // does the same work without the dispatch + argslice copy.
7083            if call.method == BuiltinMethod::ToString {
7084                let s: Arc<str> = match &recv {
7085                    Val::Str(s)   => return Ok(Val::Str(s.clone())),
7086                    Val::Int(n)   => Arc::from(n.to_string()),
7087                    Val::Float(f) => Arc::from(f.to_string()),
7088                    Val::Bool(b)  => Arc::from(b.to_string()),
7089                    Val::Null     => Arc::from("null"),
7090                    other         => Arc::from(super::eval::util::val_to_string(other).as_str()),
7091                };
7092                return Ok(Val::Str(s));
7093            }
7094            // Scalar `.to_json()` — skip the `Val -> serde_json::Value ->
7095            // String` round-trip for primitives.  Big-ticket users are
7096            // `.map(@.to_json())` in hot pipelines.
7097            if call.method == BuiltinMethod::ToJson {
7098                match &recv {
7099                    Val::Int(n)   => return Ok(Val::Str(Arc::from(n.to_string()))),
7100                    Val::Float(f) => {
7101                        let s = if f.is_finite() { f.to_string() } else { "null".to_string() };
7102                        return Ok(Val::Str(Arc::from(s)));
7103                    }
7104                    Val::Bool(b)  => return Ok(Val::Str(Arc::from(if *b { "true" } else { "false" }))),
7105                    Val::Null     => return Ok(Val::Str(Arc::from("null"))),
7106                    Val::Str(s) => {
7107                        // JSON-escape — fall through for now; cheap escape
7108                        // added here to keep the fast-path.  Handles the
7109                        // common no-escape case with a single scan.
7110                        let src = s.as_ref();
7111                        let mut needs_escape = false;
7112                        for &b in src.as_bytes() {
7113                            if b < 0x20 || b == b'"' || b == b'\\' { needs_escape = true; break; }
7114                        }
7115                        if !needs_escape {
7116                            let mut out = String::with_capacity(src.len() + 2);
7117                            out.push('"'); out.push_str(src); out.push('"');
7118                            return Ok(Val::Str(Arc::from(out)));
7119                        }
7120                        // Fall through to serde path for escape handling.
7121                    }
7122                    _ => {}
7123                }
7124            }
7125        }
7126
7127        // Value methods — delegate to the existing dispatch with orig_args
7128        dispatch_method(recv, call.name.as_ref(), &call.orig_args, env)
7129    }
7130
7131    fn exec_lambda_method(&mut self, recv: Val, call: &CompiledCall, env: &Env) -> Result<Val, EvalError> {
7132        let sub = call.sub_progs.first();
7133        // Hoist the lambda param name out of the per-item loop — otherwise
7134        // each iteration would re-scan `orig_args` for the Lambda pattern.
7135        let lam_param: Option<&str> = match call.orig_args.first() {
7136            Some(Arg::Pos(Expr::Lambda { params, .. })) if !params.is_empty() =>
7137                Some(params[0].as_str()),
7138            _ => None,
7139        };
7140        // Single scratch env per call — reused across every item iteration
7141        // below via `push_lam` / `pop_lam` instead of a fresh clone per item.
7142        let mut scratch = env.clone();
7143
7144        match call.method {
7145            BuiltinMethod::Filter => {
7146                let pred = sub.ok_or_else(|| EvalError("filter: requires predicate".into()))?;
7147                let items = recv.into_vec().ok_or_else(|| EvalError("filter: expected array".into()))?;
7148                let mut out = Vec::with_capacity(items.len());
7149                for item in items {
7150                    if is_truthy(&self.exec_lam_body_scratch(pred, &item, lam_param, &mut scratch)?) {
7151                        out.push(item);
7152                    }
7153                }
7154                Ok(Val::arr(out))
7155            }
7156            BuiltinMethod::Map => {
7157                let mapper = sub.ok_or_else(|| EvalError("map: requires mapper".into()))?;
7158                let items = recv.into_vec().ok_or_else(|| EvalError("map: expected array".into()))?;
7159                let mut out = Vec::with_capacity(items.len());
7160                for item in items {
7161                    out.push(self.exec_lam_body_scratch(mapper, &item, lam_param, &mut scratch)?);
7162                }
7163                Ok(Val::arr(out))
7164            }
7165            BuiltinMethod::FlatMap => {
7166                let mapper = sub.ok_or_else(|| EvalError("flatMap: requires mapper".into()))?;
7167                let items = recv.into_vec().ok_or_else(|| EvalError("flatMap: expected array".into()))?;
7168                let mut out = Vec::with_capacity(items.len());
7169                for item in items {
7170                    match self.exec_lam_body_scratch(mapper, &item, lam_param, &mut scratch)? {
7171                        Val::Arr(a) => out.extend(Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone())),
7172                        v => out.push(v),
7173                    }
7174                }
7175                Ok(Val::arr(out))
7176            }
7177            BuiltinMethod::Sort => {
7178                // Delegate to func_arrays::sort which already handles lambda args
7179                dispatch_method(recv, call.name.as_ref(), &call.orig_args, env)
7180            }
7181            BuiltinMethod::Any => {
7182                if let Val::Arr(a) = &recv {
7183                    let pred = sub.ok_or_else(|| EvalError("any: requires predicate".into()))?;
7184                    for item in a.iter() {
7185                        if is_truthy(&self.exec_lam_body_scratch(pred, item, lam_param, &mut scratch)?) {
7186                            return Ok(Val::Bool(true));
7187                        }
7188                    }
7189                    Ok(Val::Bool(false))
7190                } else { Ok(Val::Bool(false)) }
7191            }
7192            BuiltinMethod::All => {
7193                if let Val::Arr(a) = &recv {
7194                    if a.is_empty() { return Ok(Val::Bool(true)); }
7195                    let pred = sub.ok_or_else(|| EvalError("all: requires predicate".into()))?;
7196                    for item in a.iter() {
7197                        if !is_truthy(&self.exec_lam_body_scratch(pred, item, lam_param, &mut scratch)?) {
7198                            return Ok(Val::Bool(false));
7199                        }
7200                    }
7201                    Ok(Val::Bool(true))
7202                } else { Ok(Val::Bool(false)) }
7203            }
7204            BuiltinMethod::Count if !call.sub_progs.is_empty() => {
7205                if let Val::Arr(a) = &recv {
7206                    let pred = &call.sub_progs[0];
7207                    let mut n: i64 = 0;
7208                    for item in a.iter() {
7209                        if is_truthy(&self.exec_lam_body_scratch(pred, item, lam_param, &mut scratch)?) {
7210                            n += 1;
7211                        }
7212                    }
7213                    Ok(Val::Int(n))
7214                } else { Ok(Val::Int(0)) }
7215            }
7216            BuiltinMethod::GroupBy => {
7217                let key_prog = sub.ok_or_else(|| EvalError("groupBy: requires key".into()))?;
7218                let items = recv.into_vec().ok_or_else(|| EvalError("groupBy: expected array".into()))?;
7219                // Pattern specialisation: `lambda x: x % K` where K is a small
7220                // positive Int.  Skips per-item exec + string-keying.  Uses a
7221                // dense Vec<Vec<Val>> indexed by (n % K).rem_euclid — collapsed
7222                // into an IndexMap<Arc<str>, Val> once at the end.
7223                if let Some(param) = lam_param {
7224                    if let [Opcode::LoadIdent(p), Opcode::PushInt(k_lit), Opcode::Mod]
7225                        = key_prog.ops.as_ref()
7226                    {
7227                        if p.as_ref() == param && *k_lit > 0 && *k_lit <= 4096 {
7228                            let k_lit = *k_lit;
7229                            let k_u = k_lit as usize;
7230                            let mut buckets: Vec<Vec<Val>> = vec![Vec::new(); k_u];
7231                            let mut seen: Vec<bool> = vec![false; k_u];
7232                            let mut order: Vec<usize> = Vec::new();
7233                            // All-numeric fast path; error on non-numeric
7234                            // (matches tree-walker `x % K` dispatch).
7235                            for item in items {
7236                                let idx = match &item {
7237                                    Val::Int(n)   => n.rem_euclid(k_lit) as usize,
7238                                    Val::Float(x) => (x.trunc() as i64).rem_euclid(k_lit) as usize,
7239                                    _ => return err!("group_by(x % K): non-numeric item"),
7240                                };
7241                                if !seen[idx] { seen[idx] = true; order.push(idx); }
7242                                buckets[idx].push(item);
7243                            }
7244                            let mut map: IndexMap<Arc<str>, Val> = IndexMap::with_capacity(order.len());
7245                            for idx in order {
7246                                let k: Arc<str> = Arc::from(idx.to_string());
7247                                let bucket = std::mem::take(&mut buckets[idx]);
7248                                map.insert(k, Val::arr(bucket));
7249                            }
7250                            return Ok(Val::obj(map));
7251                        }
7252                    }
7253                }
7254                // General compiled-bytecode path.
7255                let mut map: IndexMap<Arc<str>, Val> = IndexMap::new();
7256                for item in items {
7257                    let k: Arc<str> = Arc::from(val_to_key(&self.exec_lam_body_scratch(key_prog, &item, lam_param, &mut scratch)?).as_str());
7258                    let bucket = map.entry(k).or_insert_with(|| Val::arr(Vec::new()));
7259                    bucket.as_array_mut().unwrap().push(item);
7260                }
7261                Ok(Val::obj(map))
7262            }
7263            BuiltinMethod::CountBy => {
7264                let key_prog = sub.ok_or_else(|| EvalError("countBy: requires key".into()))?;
7265                let items = recv.into_vec().ok_or_else(|| EvalError("countBy: expected array".into()))?;
7266                let mut map: IndexMap<Arc<str>, Val> = IndexMap::new();
7267                for item in items {
7268                    let k: Arc<str> = Arc::from(val_to_key(&self.exec_lam_body_scratch(key_prog, &item, lam_param, &mut scratch)?).as_str());
7269                    let counter = map.entry(k).or_insert(Val::Int(0));
7270                    if let Val::Int(n) = counter { *n += 1; }
7271                }
7272                Ok(Val::obj(map))
7273            }
7274            BuiltinMethod::IndexBy => {
7275                let key_prog = sub.ok_or_else(|| EvalError("indexBy: requires key".into()))?;
7276                let items = recv.into_vec().ok_or_else(|| EvalError("indexBy: expected array".into()))?;
7277                let mut map: IndexMap<Arc<str>, Val> = IndexMap::new();
7278                for item in items {
7279                    let k: Arc<str> = Arc::from(val_to_key(&self.exec_lam_body_scratch(key_prog, &item, lam_param, &mut scratch)?).as_str());
7280                    map.insert(k, item);
7281                }
7282                Ok(Val::obj(map))
7283            }
7284            BuiltinMethod::TakeWhile => {
7285                let pred = sub.ok_or_else(|| EvalError("takeWhile: requires predicate".into()))?;
7286                let items = recv.into_vec().ok_or_else(|| EvalError("takeWhile: expected array".into()))?;
7287                let mut out = Vec::with_capacity(items.len());
7288                for item in items {
7289                    if !is_truthy(&self.exec_lam_body_scratch(pred, &item, lam_param, &mut scratch)?) { break; }
7290                    out.push(item);
7291                }
7292                Ok(Val::arr(out))
7293            }
7294            BuiltinMethod::DropWhile => {
7295                let pred = sub.ok_or_else(|| EvalError("dropWhile: requires predicate".into()))?;
7296                let items = recv.into_vec().ok_or_else(|| EvalError("dropWhile: expected array".into()))?;
7297                let mut dropping = true;
7298                let mut out = Vec::with_capacity(items.len());
7299                for item in items {
7300                    if dropping {
7301                        let still_drop = is_truthy(&self.exec_lam_body_scratch(pred, &item, lam_param, &mut scratch)?);
7302                        if still_drop { continue; }
7303                        dropping = false;
7304                    }
7305                    out.push(item);
7306                }
7307                Ok(Val::arr(out))
7308            }
7309            BuiltinMethod::Accumulate => {
7310                // VM-accelerate the common 2-param form `accumulate(lambda a, x: …)`
7311                // with no `start:` named arg.  Pattern-specialise for a handful
7312                // of tight-loop shapes (`a + x`, `a - x`, `a * x`, `max/min`)
7313                // that the compiled bytecode would otherwise re-dispatch per
7314                // iteration.  Other shapes fall through to a compiled-bytecode
7315                // VM loop; unsupported shapes fall back to the tree-walker.
7316                let lam_body = sub.ok_or_else(|| EvalError("accumulate: requires lambda".into()))?;
7317                let (p1, p2) = match call.orig_args.first() {
7318                    Some(Arg::Pos(Expr::Lambda { params, .. })) if params.len() >= 2 =>
7319                        (params[0].as_str(), params[1].as_str()),
7320                    _ => return dispatch_method(recv, call.name.as_ref(), &call.orig_args, env),
7321                };
7322                if call.orig_args.iter().any(|a|
7323                    matches!(a, Arg::Named(n, _) if n.as_str() == "start"))
7324                {
7325                    return dispatch_method(recv, call.name.as_ref(), &call.orig_args, env);
7326                }
7327                // Try pattern specialisation: `LoadIdent(p1), LoadIdent(p2), <BinOp>`.
7328                let specialised_binop = match lam_body.ops.as_ref() {
7329                    [Opcode::LoadIdent(a), Opcode::LoadIdent(b), op]
7330                        if a.as_ref() == p1 && b.as_ref() == p2 =>
7331                    {
7332                        match op {
7333                            Opcode::Add => Some(AccumOp::Add),
7334                            Opcode::Sub => Some(AccumOp::Sub),
7335                            Opcode::Mul => Some(AccumOp::Mul),
7336                            _           => None,
7337                        }
7338                    }
7339                    _ => None,
7340                };
7341                // Columnar IntVec input → IntVec output (native-parity).
7342                if let (Val::IntVec(a), Some(bop)) = (&recv, specialised_binop.as_ref().copied()) {
7343                    let mut out: Vec<i64> = Vec::with_capacity(a.len());
7344                    let mut acc: i64 = 0;
7345                    let mut first = true;
7346                    for &n in a.iter() {
7347                        if first { acc = n; first = false; }
7348                        else { acc = match bop {
7349                            AccumOp::Add => acc.wrapping_add(n),
7350                            AccumOp::Sub => acc.wrapping_sub(n),
7351                            AccumOp::Mul => acc.wrapping_mul(n),
7352                        }; }
7353                        out.push(acc);
7354                    }
7355                    return Ok(Val::int_vec(out));
7356                }
7357                if let (Val::FloatVec(a), Some(bop)) = (&recv, specialised_binop.as_ref().copied()) {
7358                    let mut out: Vec<f64> = Vec::with_capacity(a.len());
7359                    let mut acc: f64 = 0.0;
7360                    let mut first = true;
7361                    for &n in a.iter() {
7362                        if first { acc = n; first = false; }
7363                        else { acc = match bop {
7364                            AccumOp::Add => acc + n,
7365                            AccumOp::Sub => acc - n,
7366                            AccumOp::Mul => acc * n,
7367                        }; }
7368                        out.push(acc);
7369                    }
7370                    return Ok(Val::float_vec(out));
7371                }
7372                let items = recv.into_vec()
7373                    .ok_or_else(|| EvalError("accumulate: expected array".into()))?;
7374                let mut out = Vec::with_capacity(items.len());
7375                if let Some(bop) = specialised_binop {
7376                    // Typed i64 tight-loop when every item is Int.  No Val
7377                    // match, no add_vals dispatch, no per-step Val clone —
7378                    // native parity.
7379                    if items.iter().all(|v| matches!(v, Val::Int(_))) {
7380                        let mut acc_out: Vec<i64> = Vec::with_capacity(items.len());
7381                        let mut acc: i64 = 0;
7382                        let mut first = true;
7383                        for item in &items {
7384                            let n = if let Val::Int(n) = item { *n } else { unreachable!() };
7385                            if first { acc = n; first = false; }
7386                            else { acc = match bop {
7387                                AccumOp::Add => acc.wrapping_add(n),
7388                                AccumOp::Sub => acc.wrapping_sub(n),
7389                                AccumOp::Mul => acc.wrapping_mul(n),
7390                            }; }
7391                            acc_out.push(acc);
7392                        }
7393                        return Ok(Val::int_vec(acc_out));
7394                    }
7395                    // Typed f64 tight-loop when every item is Float.
7396                    if items.iter().all(|v| matches!(v, Val::Float(_))) {
7397                        let mut acc_out: Vec<f64> = Vec::with_capacity(items.len());
7398                        let mut acc: f64 = 0.0;
7399                        let mut first = true;
7400                        for item in &items {
7401                            let n = if let Val::Float(n) = item { *n } else { unreachable!() };
7402                            if first { acc = n; first = false; }
7403                            else { acc = match bop {
7404                                AccumOp::Add => acc + n,
7405                                AccumOp::Sub => acc - n,
7406                                AccumOp::Mul => acc * n,
7407                            }; }
7408                            acc_out.push(acc);
7409                        }
7410                        return Ok(Val::float_vec(acc_out));
7411                    }
7412                    // Mixed / non-numeric — inline fold via add_vals/num_op.
7413                    let mut running: Option<Val> = None;
7414                    for item in items {
7415                        let next = match running.take() {
7416                            Some(acc) => match bop {
7417                                AccumOp::Add => add_vals(acc, item)?,
7418                                AccumOp::Sub => num_op(acc, item, |a,b| a-b, |a,b| a-b)?,
7419                                AccumOp::Mul => num_op(acc, item, |a,b| a*b, |a,b| a*b)?,
7420                            },
7421                            None => item,
7422                        };
7423                        out.push(next.clone());
7424                        running = Some(next);
7425                    }
7426                    return Ok(Val::arr(out));
7427                }
7428                // General path: compiled-bytecode VM loop.
7429                let mut running: Option<Val> = None;
7430                for item in items {
7431                    let next = if let Some(acc) = running.take() {
7432                        let f1 = scratch.push_lam(Some(p1), acc);
7433                        let f2 = scratch.push_lam(Some(p2), item.clone());
7434                        let r = self.exec(lam_body, &scratch)?;
7435                        scratch.pop_lam(f2);
7436                        scratch.pop_lam(f1);
7437                        r
7438                    } else {
7439                        item
7440                    };
7441                    out.push(next.clone());
7442                    running = Some(next);
7443                }
7444                Ok(Val::arr(out))
7445            }
7446            BuiltinMethod::Partition => {
7447                let pred = sub.ok_or_else(|| EvalError("partition: requires predicate".into()))?;
7448                let items = recv.into_vec().ok_or_else(|| EvalError("partition: expected array".into()))?;
7449                let (mut yes, mut no) = (Vec::with_capacity(items.len()), Vec::with_capacity(items.len()));
7450                for item in items {
7451                    if is_truthy(&self.exec_lam_body_scratch(pred, &item, lam_param, &mut scratch)?) {
7452                        yes.push(item);
7453                    } else {
7454                        no.push(item);
7455                    }
7456                }
7457                Ok(Val::arr(vec![Val::arr(yes), Val::arr(no)]))
7458            }
7459            BuiltinMethod::TransformKeys => {
7460                let lam = sub.ok_or_else(|| EvalError("transformKeys: requires lambda".into()))?;
7461                let map = recv.into_map().ok_or_else(|| EvalError("transformKeys: expected object".into()))?;
7462                let mut out: IndexMap<Arc<str>, Val> = IndexMap::new();
7463                for (k, v) in map {
7464                    let new_key = Arc::from(val_to_key(&self.exec_lam_body_scratch(lam, &Val::Str(k), lam_param, &mut scratch)?).as_str());
7465                    out.insert(new_key, v);
7466                }
7467                Ok(Val::obj(out))
7468            }
7469            BuiltinMethod::TransformValues => {
7470                let lam = sub.ok_or_else(|| EvalError("transformValues: requires lambda".into()))?;
7471                // COW: if the receiver's Arc is unique we mutate in place,
7472                // otherwise deep-clone once and mutate the clone.  Either
7473                // way, no fresh IndexMap allocation and no key-Arc clone
7474                // per entry (IndexMap::values_mut preserves slot identity).
7475                let mut map = recv.into_map().ok_or_else(|| EvalError("transformValues: expected object".into()))?;
7476                // Pattern-specialise `[PushCurrent, PushInt(K), <BinOp>]` —
7477                // the body of `transform_values(@ + K)` / `(@ - K)` / `(@ * K)`.
7478                let pat = match lam.ops.as_ref() {
7479                    [Opcode::PushCurrent, Opcode::PushInt(k), op] => match op {
7480                        Opcode::Add => Some((AccumOp::Add, *k)),
7481                        Opcode::Sub => Some((AccumOp::Sub, *k)),
7482                        Opcode::Mul => Some((AccumOp::Mul, *k)),
7483                        _ => None,
7484                    },
7485                    _ => None,
7486                };
7487                if let Some((op, k_lit)) = pat {
7488                    let kf = k_lit as f64;
7489                    for v in map.values_mut() {
7490                        match v {
7491                            Val::Int(n) => *n = match op {
7492                                AccumOp::Add => n.wrapping_add(k_lit),
7493                                AccumOp::Sub => n.wrapping_sub(k_lit),
7494                                AccumOp::Mul => n.wrapping_mul(k_lit),
7495                            },
7496                            Val::Float(x) => *x = match op {
7497                                AccumOp::Add => *x + kf,
7498                                AccumOp::Sub => *x - kf,
7499                                AccumOp::Mul => *x * kf,
7500                            },
7501                            _ => {
7502                                let new = self.exec_lam_body_scratch(lam, v, lam_param, &mut scratch)?;
7503                                *v = new;
7504                            }
7505                        }
7506                    }
7507                    return Ok(Val::obj(map));
7508                }
7509                // General path — mutate in place via values_mut; no new map,
7510                // no key reinsertion.
7511                for v in map.values_mut() {
7512                    let new = self.exec_lam_body_scratch(lam, v, lam_param, &mut scratch)?;
7513                    *v = new;
7514                }
7515                Ok(Val::obj(map))
7516            }
7517            BuiltinMethod::FilterKeys => {
7518                let lam = sub.ok_or_else(|| EvalError("filterKeys: requires predicate".into()))?;
7519                let map = recv.into_map().ok_or_else(|| EvalError("filterKeys: expected object".into()))?;
7520                let mut out: IndexMap<Arc<str>, Val> = IndexMap::new();
7521                for (k, v) in map {
7522                    if is_truthy(&self.exec_lam_body_scratch(lam, &Val::Str(k.clone()), lam_param, &mut scratch)?) {
7523                        out.insert(k, v);
7524                    }
7525                }
7526                Ok(Val::obj(out))
7527            }
7528            BuiltinMethod::FilterValues => {
7529                let lam = sub.ok_or_else(|| EvalError("filterValues: requires predicate".into()))?;
7530                let map = recv.into_map().ok_or_else(|| EvalError("filterValues: expected object".into()))?;
7531                let mut out: IndexMap<Arc<str>, Val> = IndexMap::new();
7532                for (k, v) in map {
7533                    if is_truthy(&self.exec_lam_body_scratch(lam, &v, lam_param, &mut scratch)?) {
7534                        out.insert(k, v);
7535                    }
7536                }
7537                Ok(Val::obj(out))
7538            }
7539            BuiltinMethod::Pivot => {
7540                dispatch_method(recv, call.name.as_ref(), &call.orig_args, env)
7541            }
7542            BuiltinMethod::Update => {
7543                let lam = sub.ok_or_else(|| EvalError("update: requires lambda".into()))?;
7544                self.exec_lam_body(lam, &recv, lam_param, env)
7545            }
7546            _ => dispatch_method(recv, call.name.as_ref(), &call.orig_args, env),
7547        }
7548    }
7549
7550    /// Convenience wrapper: clones `env` once, runs the prog, discards
7551    /// the scratch.  Hot loops should use `exec_lam_body_scratch` to
7552    /// reuse a single scratch env instead.
7553    fn exec_lam_body(&mut self, prog: &Program, item: &Val, lam_param: Option<&str>, env: &Env)
7554        -> Result<Val, EvalError>
7555    {
7556        let mut scratch = env.clone();
7557        self.exec_lam_body_scratch(prog, item, lam_param, &mut scratch)
7558    }
7559
7560    /// Scratch-reusing variant: mutates `scratch` in place via
7561    /// `Env::push_lam` / `Env::pop_lam` instead of cloning per item.
7562    /// The caller provides (and reuses) the scratch env across loop
7563    /// iterations.
7564    fn exec_lam_body_scratch(&mut self, prog: &Program, item: &Val,
7565                              lam_param: Option<&str>, scratch: &mut Env)
7566        -> Result<Val, EvalError>
7567    {
7568        let frame = scratch.push_lam(lam_param, item.clone());
7569        let r = self.exec(prog, scratch);
7570        scratch.pop_lam(frame);
7571        r
7572    }
7573
7574    // ── Object construction ───────────────────────────────────────────────────
7575
7576    fn exec_make_obj(&mut self, entries: &[CompiledObjEntry], env: &Env) -> Result<Val, EvalError> {
7577        let mut map: IndexMap<Arc<str>, Val> = IndexMap::with_capacity(entries.len());
7578        for entry in entries {
7579            match entry {
7580                CompiledObjEntry::Short { name, ic } => {
7581                    let v = if let Some(v) = env.get_var(name.as_ref()) {
7582                        v.clone()
7583                    } else if let Val::Obj(m) = &env.current {
7584                        ic_get_field(m, name.as_ref(), ic)
7585                    } else {
7586                        env.current.get_field(name.as_ref())
7587                    };
7588                    if !v.is_null() { map.insert(name.clone(), v); }
7589                }
7590                CompiledObjEntry::Kv { key, prog, optional, cond } => {
7591                    if let Some(c) = cond {
7592                        if !super::eval::util::is_truthy(&self.exec(c, env)?) { continue; }
7593                    }
7594                    let v = self.exec(prog, env)?;
7595                    if *optional && v.is_null() { continue; }
7596                    map.insert(key.clone(), v);
7597                }
7598                CompiledObjEntry::KvPath { key, steps, optional, ics } => {
7599                    let mut v = env.current.clone();
7600                    for (i, st) in steps.iter().enumerate() {
7601                        v = match st {
7602                            KvStep::Field(f) => {
7603                                if let Val::Obj(m) = &v {
7604                                    ic_get_field(m, f.as_ref(), &ics[i])
7605                                } else {
7606                                    v.get_field(f.as_ref())
7607                                }
7608                            }
7609                            KvStep::Index(i) => v.get_index(*i),
7610                        };
7611                        if v.is_null() { break; }
7612                    }
7613                    if *optional && v.is_null() { continue; }
7614                    map.insert(key.clone(), v);
7615                }
7616                CompiledObjEntry::Dynamic { key, val } => {
7617                    let k: Arc<str> = Arc::from(val_to_key(&self.exec(key, env)?).as_str());
7618                    let v = self.exec(val, env)?;
7619                    map.insert(k, v);
7620                }
7621                CompiledObjEntry::Spread(prog) => {
7622                    if let Val::Obj(other) = self.exec(prog, env)? {
7623                        let entries = Arc::try_unwrap(other).unwrap_or_else(|m| (*m).clone());
7624                        for (k, v) in entries { map.insert(k, v); }
7625                    }
7626                }
7627                CompiledObjEntry::SpreadDeep(prog) => {
7628                    if let Val::Obj(other) = self.exec(prog, env)? {
7629                        let base = std::mem::take(&mut map);
7630                        let merged = super::eval::util::deep_merge_concat(
7631                            Val::obj(base), Val::Obj(other));
7632                        if let Val::Obj(m) = merged {
7633                            map = Arc::try_unwrap(m).unwrap_or_else(|m| (*m).clone());
7634                        }
7635                    }
7636                }
7637            }
7638        }
7639        Ok(Val::obj(map))
7640    }
7641
7642    // ── F-string ──────────────────────────────────────────────────────────────
7643
7644    fn exec_fstring(&mut self, parts: &[CompiledFSPart], env: &Env) -> Result<Val, EvalError> {
7645        use std::fmt::Write as _;
7646        // Pre-size by summing literal lengths + rough 8 bytes per interp.
7647        let lit_len: usize = parts.iter().map(|p| match p {
7648            CompiledFSPart::Lit(s) => s.len(),
7649            CompiledFSPart::Interp { .. } => 8,
7650        }).sum();
7651        let mut out = String::with_capacity(lit_len);
7652        for part in parts {
7653            match part {
7654                CompiledFSPart::Lit(s) => out.push_str(s.as_ref()),
7655                CompiledFSPart::Interp { prog, fmt } => {
7656                    // Fast path: very small interp programs that just extract
7657                    // a field / index / current — avoid full self.exec() recursion
7658                    // (stack alloc, ic vec, etc.) per row.
7659                    let val: Val = match &prog.ops[..] {
7660                        [Opcode::PushCurrent] => env.current.clone(),
7661                        [Opcode::PushCurrent, Opcode::GetIndex(n)] => {
7662                            match &env.current {
7663                                Val::Arr(a) => {
7664                                    let idx = if *n >= 0 { *n as usize }
7665                                              else { a.len().saturating_sub((-*n) as usize) };
7666                                    a.get(idx).cloned().unwrap_or(Val::Null)
7667                                }
7668                                _ => self.exec(prog, env)?,
7669                            }
7670                        }
7671                        [Opcode::PushCurrent, Opcode::GetField(k)] => {
7672                            match &env.current {
7673                                Val::Obj(m) => m.get(k.as_ref()).cloned().unwrap_or(Val::Null),
7674                                _ => self.exec(prog, env)?,
7675                            }
7676                        }
7677                        [Opcode::LoadIdent(name)] => env.get_var(name).cloned().unwrap_or(Val::Null),
7678                        _ => self.exec(prog, env)?,
7679                    };
7680                    match fmt {
7681                        None => match &val {
7682                            // Fast paths: avoid val_to_string's temporary String.
7683                            Val::Str(s)   => out.push_str(s.as_ref()),
7684                            Val::Int(n)   => { let _ = write!(out, "{}", n); }
7685                            Val::Float(f) => { let _ = write!(out, "{}", f); }
7686                            Val::Bool(b)  => { let _ = write!(out, "{}", b); }
7687                            Val::Null     => out.push_str("null"),
7688                            _             => out.push_str(&val_to_string(&val)),
7689                        },
7690                        Some(FmtSpec::Spec(spec)) => {
7691                            out.push_str(&apply_fmt_spec(&val, spec));
7692                        }
7693                        Some(FmtSpec::Pipe(method)) => {
7694                            let piped = dispatch_method(val, method, &[], env)?;
7695                            match &piped {
7696                                Val::Str(s)   => out.push_str(s.as_ref()),
7697                                Val::Int(n)   => { let _ = write!(out, "{}", n); }
7698                                Val::Float(f) => { let _ = write!(out, "{}", f); }
7699                                Val::Bool(b)  => { let _ = write!(out, "{}", b); }
7700                                Val::Null     => out.push_str("null"),
7701                                _             => out.push_str(&val_to_string(&piped)),
7702                            }
7703                        }
7704                    }
7705                }
7706            }
7707        }
7708        // `Arc::<str>::from(String)` transfers the buffer — no realloc.
7709        Ok(Val::Str(Arc::<str>::from(out)))
7710    }
7711
7712    // ── Comprehension helpers ─────────────────────────────────────────────────
7713
7714    fn exec_iter_vals(&mut self, iter_prog: &Program, env: &Env) -> Result<Vec<Val>, EvalError> {
7715        match self.exec(iter_prog, env)? {
7716            Val::Arr(a) => Ok(Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone())),
7717            Val::Obj(m) => {
7718                let entries = Arc::try_unwrap(m).unwrap_or_else(|m| (*m).clone());
7719                Ok(entries.into_iter().map(|(k, v)| obj2("key", Val::Str(k), "value", v)).collect())
7720            }
7721            other => Ok(vec![other]),
7722        }
7723    }
7724}
7725
7726// ── Env extensions for VM ─────────────────────────────────────────────────────
7727
7728impl Env {
7729    fn registry_is_empty(&self) -> bool { self.registry_ref().is_empty() }
7730    fn registry_get(&self, name: &str) -> Option<Arc<dyn super::eval::methods::Method>> {
7731        self.registry_ref().get(name).cloned()
7732    }
7733}
7734
7735// ── Free helpers ──────────────────────────────────────────────────────────────
7736
7737/// Route C — chained-descendant byte chain.
7738///
7739/// Entry: `root_key` is the key of the root `Descendant` that triggered the
7740/// scan.  `tail` is the opcode slice immediately after that `Descendant`.
7741///
7742/// Consumes as many tail opcodes as can be handled on byte spans:
7743///   - `Descendant(k)` — re-scan within each current span.
7744///   - `Quantifier(First)` — keep first span; `Quantifier(One)` when exactly one.
7745///   - `InlineFilter(pred)` / `CallMethod(Filter, [pred])` with a canonical
7746///     equality literal (int/string/bool/null) — bytewise retain.
7747///
7748/// Any other opcode terminates the chain; remaining spans are materialised
7749/// into `Val`s and returned, and the caller resumes normal opcode dispatch.
7750/// A "first-selector" opcode: bare `.first()` / `Quantifier::First`.
7751/// When `Descendant(k)` is followed by one of these, the byte scan
7752/// can stop at the first match per span.
7753fn is_first_selector_op(op: &Opcode) -> bool {
7754    match op {
7755        Opcode::Quantifier(QuantifierKind::First) => true,
7756        Opcode::CallMethod(c)
7757            if c.sub_progs.is_empty() && c.method == BuiltinMethod::First => true,
7758        _ => false,
7759    }
7760}
7761
7762/// Materialise byte-scan `spans` into a `Val::Arr`, peeking at `tail`
7763/// for a trailing `.map(<field>)` (compiled to `Opcode::MapField`).
7764/// When present the direct child is extracted from each span without
7765/// paying a full `serde_json::from_slice` on the enclosing object.
7766/// Returned `skip` tells the caller how many opcodes were consumed
7767/// beyond the current `CallMethod`.
7768///
7769/// Outlined (`#[cold] #[inline(never)]`) to keep the `exec` dispatch
7770/// match compact — inlining the span loop here was enough to cost
7771/// Q4/Q13 icache footprint in `bench_lock`.
7772#[cold]
7773#[inline(never)]
7774fn materialise_find_scan_spans(
7775    bytes: &[u8],
7776    spans: &[super::scan::ValueSpan],
7777    tail: &[Opcode],
7778) -> (Val, usize) {
7779    // Consume any leading span-refiner tail ops — compiled forms of
7780    // `.filter(@.k == lit)` / `.filter(@.k <cmp> lit)` sitting after the
7781    // byte-scan.  Each narrows `spans` in place via `find_direct_field`
7782    // + bytewise / numeric comparison; the remaining tail then feeds the
7783    // existing map/aggregate dispatch below.
7784    let mut consumed_refiners = 0usize;
7785    let mut owned: Option<Vec<super::scan::ValueSpan>>;
7786    let mut spans_view: &[super::scan::ValueSpan] = spans;
7787    loop {
7788        match tail.get(consumed_refiners) {
7789            Some(Opcode::FilterFieldEqLit(k, lit_val)) => {
7790                let Some(lit) = val_to_canonical_lit_bytes(lit_val) else { break };
7791                let next: Vec<_> = spans_view.iter().copied().filter(|s| {
7792                    let obj = &bytes[s.start..s.end];
7793                    match super::scan::find_direct_field(obj, k.as_ref()) {
7794                        Some(vs) => vs.end - vs.start == lit.len()
7795                            && obj[vs.start..vs.end] == lit[..],
7796                        None => false,
7797                    }
7798                }).collect();
7799                owned = Some(next);
7800                spans_view = owned.as_deref().unwrap();
7801                consumed_refiners += 1;
7802            }
7803            Some(Opcode::FilterFieldCmpLit(k, op, lit_val)) => {
7804                let Some(thresh) = lit_val.as_f64() else { break };
7805                let holds: fn(f64, f64) -> bool = match op {
7806                    super::ast::BinOp::Lt  => |a, b| a <  b,
7807                    super::ast::BinOp::Lte => |a, b| a <= b,
7808                    super::ast::BinOp::Gt  => |a, b| a >  b,
7809                    super::ast::BinOp::Gte => |a, b| a >= b,
7810                    _ => break,
7811                };
7812                let next: Vec<_> = spans_view.iter().copied().filter(|s| {
7813                    let obj = &bytes[s.start..s.end];
7814                    let Some(vs) = super::scan::find_direct_field(obj, k.as_ref())
7815                        else { return false };
7816                    match super::scan::parse_num_span(&obj[vs.start..vs.end]) {
7817                        Some((_, f, _)) => holds(f, thresh),
7818                        None => false,
7819                    }
7820                }).collect();
7821                owned = Some(next);
7822                spans_view = owned.as_deref().unwrap();
7823                consumed_refiners += 1;
7824            }
7825            _ => break,
7826        }
7827    }
7828    let spans = spans_view;
7829    let tail = &tail[consumed_refiners..];
7830    let (val, inner) = materialise_find_scan_spans_tail(bytes, spans, tail);
7831    (val, consumed_refiners + inner)
7832}
7833
7834/// Convert a `Val` literal to its canonical JSON byte encoding — the
7835/// same form `find_direct_field` produces when it locates a scalar value
7836/// inside an object.  Returns `None` for non-scalar / non-canonical values.
7837#[inline]
7838fn val_to_canonical_lit_bytes(v: &Val) -> Option<Vec<u8>> {
7839    match v {
7840        Val::Int(n)   => Some(n.to_string().into_bytes()),
7841        Val::Bool(b)  => Some(if *b { b"true".to_vec() } else { b"false".to_vec() }),
7842        Val::Null     => Some(b"null".to_vec()),
7843        Val::Str(s)   => serde_json::to_vec(
7844            &serde_json::Value::String(s.to_string())
7845        ).ok(),
7846        _ => None,
7847    }
7848}
7849
7850#[cold]
7851#[inline(never)]
7852fn materialise_find_scan_spans_tail(
7853    bytes: &[u8],
7854    spans: &[super::scan::ValueSpan],
7855    tail: &[Opcode],
7856) -> (Val, usize) {
7857    // Trailing `.count()` / `.len()` — just the span count, no parse.
7858    if let Some(Opcode::CallMethod(c)) = tail.first() {
7859        if c.sub_progs.is_empty()
7860            && matches!(c.method, BuiltinMethod::Count | BuiltinMethod::Len)
7861        {
7862            return (Val::Int(spans.len() as i64), 1);
7863        }
7864    }
7865    // Trailing fused `.filter(@.kp op lit).map(kproj)` — peephole fused
7866    // forms `FilterFieldEqLitMapField` / `FilterFieldCmpLitMapField`.
7867    // Refine spans by predicate, then project direct-field values; peek
7868    // for a numeric aggregate right after to fold on the projection.
7869    if let Some(op) = tail.first() {
7870        let refined = match op {
7871            Opcode::FilterFieldEqLitMapField(kp, lit_v, kproj) => {
7872                let lit = val_to_canonical_lit_bytes(lit_v);
7873                lit.map(|lit| {
7874                    let spans2: Vec<_> = spans.iter().copied().filter(|s| {
7875                        let obj = &bytes[s.start..s.end];
7876                        match super::scan::find_direct_field(obj, kp.as_ref()) {
7877                            Some(vs) => vs.end - vs.start == lit.len()
7878                                && obj[vs.start..vs.end] == lit[..],
7879                            None => false,
7880                        }
7881                    }).collect();
7882                    (spans2, kproj.clone())
7883                })
7884            }
7885            Opcode::FilterFieldCmpLitMapField(kp, cop, lit_v, kproj) => {
7886                let thresh_opt = lit_v.as_f64();
7887                let holds_opt: Option<fn(f64, f64) -> bool> = match cop {
7888                    super::ast::BinOp::Lt  => Some(|a, b| a <  b),
7889                    super::ast::BinOp::Lte => Some(|a, b| a <= b),
7890                    super::ast::BinOp::Gt  => Some(|a, b| a >  b),
7891                    super::ast::BinOp::Gte => Some(|a, b| a >= b),
7892                    _ => None,
7893                };
7894                match (thresh_opt, holds_opt) {
7895                    (Some(thresh), Some(holds)) => {
7896                        let spans2: Vec<_> = spans.iter().copied().filter(|s| {
7897                            let obj = &bytes[s.start..s.end];
7898                            let Some(vs) = super::scan::find_direct_field(obj, kp.as_ref())
7899                                else { return false };
7900                            match super::scan::parse_num_span(&obj[vs.start..vs.end]) {
7901                                Some((_, f, _)) => holds(f, thresh),
7902                                None => false,
7903                            }
7904                        }).collect();
7905                        Some((spans2, kproj.clone()))
7906                    }
7907                    _ => None,
7908                }
7909            }
7910            _ => None,
7911        };
7912        if let Some((spans2, k)) = refined {
7913            // Aggregate fold on the projection without materialising.
7914            if let Some(Opcode::CallMethod(c)) = tail.get(1) {
7915                if c.sub_progs.is_empty() {
7916                    match c.method {
7917                        BuiltinMethod::Count | BuiltinMethod::Len => {
7918                            let f = super::scan::fold_direct_field_nums(bytes, &spans2, k.as_ref());
7919                            return (Val::Int(f.count as i64), 2);
7920                        }
7921                        BuiltinMethod::Sum => {
7922                            let f = super::scan::fold_direct_field_nums(bytes, &spans2, k.as_ref());
7923                            let v = if f.count == 0 { Val::Int(0) }
7924                                    else if f.is_float { Val::Float(f.float_sum) }
7925                                    else { Val::Int(f.int_sum) };
7926                            return (v, 2);
7927                        }
7928                        BuiltinMethod::Avg => {
7929                            let f = super::scan::fold_direct_field_nums(bytes, &spans2, k.as_ref());
7930                            let v = if f.count == 0 { Val::Null }
7931                                    else { Val::Float(f.float_sum / f.count as f64) };
7932                            return (v, 2);
7933                        }
7934                        BuiltinMethod::Min => {
7935                            let f = super::scan::fold_direct_field_nums(bytes, &spans2, k.as_ref());
7936                            let v = if !f.any { Val::Null }
7937                                    else if f.is_float { Val::Float(f.min_f) }
7938                                    else { Val::Int(f.min_i) };
7939                            return (v, 2);
7940                        }
7941                        BuiltinMethod::Max => {
7942                            let f = super::scan::fold_direct_field_nums(bytes, &spans2, k.as_ref());
7943                            let v = if !f.any { Val::Null }
7944                                    else if f.is_float { Val::Float(f.max_f) }
7945                                    else { Val::Int(f.max_i) };
7946                            return (v, 2);
7947                        }
7948                        _ => {}
7949                    }
7950                }
7951            }
7952            let mut vals: Vec<Val> = Vec::with_capacity(spans2.len());
7953            for s in &spans2 {
7954                let obj_bytes = &bytes[s.start..s.end];
7955                let v = match super::scan::find_direct_field(obj_bytes, k.as_ref()) {
7956                    Some(vs) => serde_json::from_slice::<serde_json::Value>(
7957                        &obj_bytes[vs.start..vs.end],
7958                    ).ok().map(|sv| Val::from(&sv)).unwrap_or(Val::Null),
7959                    None => Val::Null,
7960                };
7961                vals.push(v);
7962            }
7963            return (Val::arr(vals), 1);
7964        }
7965    }
7966    // Trailing `.map(<field>)` — peek once more for a numeric aggregate
7967    // that can fold straight from the per-span direct field, skipping
7968    // both the full-object parse and the Val array construction.
7969    if let Some(Opcode::MapField(k)) = tail.first() {
7970        if let Some(Opcode::CallMethod(c)) = tail.get(1) {
7971            if c.sub_progs.is_empty() {
7972                match c.method {
7973                    BuiltinMethod::Count | BuiltinMethod::Len => {
7974                        // count of extracted fields == count of spans
7975                        // where the key parses successfully.  Fold gives
7976                        // us that as `f.count`.
7977                        let f = super::scan::fold_direct_field_nums(bytes, spans, k.as_ref());
7978                        return (Val::Int(f.count as i64), 2);
7979                    }
7980                    BuiltinMethod::Sum => {
7981                        let f = super::scan::fold_direct_field_nums(bytes, spans, k.as_ref());
7982                        let v = if f.count == 0 { Val::Int(0) }
7983                                else if f.is_float { Val::Float(f.float_sum) }
7984                                else { Val::Int(f.int_sum) };
7985                        return (v, 2);
7986                    }
7987                    BuiltinMethod::Avg => {
7988                        let f = super::scan::fold_direct_field_nums(bytes, spans, k.as_ref());
7989                        let v = if f.count == 0 { Val::Null }
7990                                else { Val::Float(f.float_sum / f.count as f64) };
7991                        return (v, 2);
7992                    }
7993                    BuiltinMethod::Min => {
7994                        let f = super::scan::fold_direct_field_nums(bytes, spans, k.as_ref());
7995                        let v = if !f.any { Val::Null }
7996                                else if f.is_float { Val::Float(f.min_f) }
7997                                else { Val::Int(f.min_i) };
7998                        return (v, 2);
7999                    }
8000                    BuiltinMethod::Max => {
8001                        let f = super::scan::fold_direct_field_nums(bytes, spans, k.as_ref());
8002                        let v = if !f.any { Val::Null }
8003                                else if f.is_float { Val::Float(f.max_f) }
8004                                else { Val::Int(f.max_i) };
8005                        return (v, 2);
8006                    }
8007                    _ => {}
8008                }
8009            }
8010        }
8011        let mut vals: Vec<Val> = Vec::with_capacity(spans.len());
8012        for s in spans {
8013            let obj_bytes = &bytes[s.start..s.end];
8014            let v = match super::scan::find_direct_field(obj_bytes, k.as_ref()) {
8015                Some(vs) => serde_json::from_slice::<serde_json::Value>(
8016                    &obj_bytes[vs.start..vs.end],
8017                ).ok().map(|sv| Val::from(&sv)).unwrap_or(Val::Null),
8018                None => Val::Null,
8019            };
8020            vals.push(v);
8021        }
8022        return (Val::arr(vals), 1);
8023    }
8024    let mut vals: Vec<Val> = Vec::with_capacity(spans.len());
8025    for s in spans {
8026        if let Ok(v) = serde_json::from_slice::<serde_json::Value>(
8027            &bytes[s.start..s.end],
8028        ) {
8029            vals.push(Val::from(&v));
8030        }
8031    }
8032    (Val::arr(vals), 0)
8033}
8034
8035fn byte_chain_exec(
8036    bytes: &[u8],
8037    root_key: &str,
8038    tail: &[Opcode],
8039) -> (Val, usize) {
8040    // Early-exit: `$..key.first()` / `$..key!` needs only the first match.
8041    let first_after_initial = tail.first().map(is_first_selector_op).unwrap_or(false);
8042    let mut spans: Vec<super::scan::ValueSpan> = if first_after_initial {
8043        super::scan::find_first_key_value_span(bytes, root_key)
8044            .into_iter().collect()
8045    } else {
8046        super::scan::find_key_value_spans(bytes, root_key)
8047    };
8048    let mut scalar = false;
8049    let mut consumed = 0usize;
8050
8051    for (idx, op) in tail.iter().enumerate() {
8052        match op {
8053            Opcode::Descendant(k) => {
8054                let next_first = tail.get(idx + 1)
8055                    .map(is_first_selector_op).unwrap_or(false);
8056                let mut next = Vec::with_capacity(spans.len());
8057                for s in &spans {
8058                    let sub = &bytes[s.start..s.end];
8059                    if next_first {
8060                        if let Some(s2) = super::scan::find_first_key_value_span(sub, k.as_ref()) {
8061                            next.push(super::scan::ValueSpan {
8062                                start: s.start + s2.start,
8063                                end:   s.start + s2.end,
8064                            });
8065                        }
8066                    } else {
8067                        for s2 in super::scan::find_key_value_spans(sub, k.as_ref()) {
8068                            next.push(super::scan::ValueSpan {
8069                                start: s.start + s2.start,
8070                                end:   s.start + s2.end,
8071                            });
8072                        }
8073                    }
8074                }
8075                spans = next;
8076                scalar = false;
8077            }
8078            Opcode::Quantifier(QuantifierKind::First) => {
8079                spans.truncate(1);
8080                scalar = true;
8081            }
8082            Opcode::Quantifier(QuantifierKind::One) => {
8083                if spans.len() != 1 { break; }
8084                scalar = true;
8085            }
8086            // `.first()` / `.last()` compile to `CallMethod(First|Last)` not
8087            // `Quantifier`.  Handle them as scalar terminators.
8088            Opcode::CallMethod(call) if call.sub_progs.is_empty() => {
8089                match call.method {
8090                    BuiltinMethod::First => {
8091                        spans.truncate(1);
8092                        scalar = true;
8093                    }
8094                    BuiltinMethod::Last => {
8095                        if let Some(last) = spans.pop() { spans = vec![last]; }
8096                        scalar = true;
8097                    }
8098                    _ => break,
8099                }
8100            }
8101            Opcode::InlineFilter(prog) => {
8102                match canonical_eq_literal_from_program(prog) {
8103                    Some(lit) => {
8104                        spans.retain(|s| {
8105                            s.end - s.start == lit.len()
8106                                && &bytes[s.start..s.end] == &lit[..]
8107                        });
8108                        scalar = false;
8109                    }
8110                    None => break,
8111                }
8112            }
8113            Opcode::CallMethod(call)
8114                if call.method == BuiltinMethod::Filter && call.sub_progs.len() == 1 =>
8115            {
8116                match canonical_eq_literal_from_program(&call.sub_progs[0]) {
8117                    Some(lit) => {
8118                        spans.retain(|s| {
8119                            s.end - s.start == lit.len()
8120                                && &bytes[s.start..s.end] == &lit[..]
8121                        });
8122                        scalar = false;
8123                    }
8124                    None => break,
8125                }
8126            }
8127            _ => break,
8128        }
8129        consumed += 1;
8130    }
8131
8132    // Numeric-fold fast path: trailing `.sum()/.avg()/.min()/.max()/.count()/.len()`
8133    // — skip Val materialisation, parse numbers inline from byte spans.
8134    if !scalar {
8135        if let Some(Opcode::CallMethod(call)) = tail.get(consumed) {
8136            if call.sub_progs.is_empty() && tail.len() == consumed + 1 {
8137                match call.method {
8138                    BuiltinMethod::Count | BuiltinMethod::Len => {
8139                        return (Val::Int(spans.len() as i64), consumed + 1);
8140                    }
8141                    BuiltinMethod::Sum => {
8142                        let f = super::scan::fold_nums(bytes, &spans);
8143                        let v = if f.count == 0 { Val::Int(0) }
8144                            else if f.is_float { Val::Float(f.float_sum) }
8145                            else { Val::Int(f.int_sum) };
8146                        return (v, consumed + 1);
8147                    }
8148                    BuiltinMethod::Avg => {
8149                        let f = super::scan::fold_nums(bytes, &spans);
8150                        let v = if f.count == 0 { Val::Null }
8151                            else { Val::Float(f.float_sum / f.count as f64) };
8152                        return (v, consumed + 1);
8153                    }
8154                    BuiltinMethod::Min => {
8155                        let f = super::scan::fold_nums(bytes, &spans);
8156                        let v = if !f.any { Val::Null }
8157                            else if f.is_float { Val::Float(f.min_f) }
8158                            else { Val::Int(f.min_i) };
8159                        return (v, consumed + 1);
8160                    }
8161                    BuiltinMethod::Max => {
8162                        let f = super::scan::fold_nums(bytes, &spans);
8163                        let v = if !f.any { Val::Null }
8164                            else if f.is_float { Val::Float(f.max_f) }
8165                            else { Val::Int(f.max_i) };
8166                        return (v, consumed + 1);
8167                    }
8168                    _ => {}
8169                }
8170            }
8171        }
8172    }
8173
8174    let mut materialised: Vec<Val> = Vec::with_capacity(spans.len());
8175    for s in &spans {
8176        if let Ok(v) = serde_json::from_slice::<serde_json::Value>(&bytes[s.start..s.end]) {
8177            materialised.push(Val::from(&v));
8178        }
8179    }
8180
8181    let out = if scalar {
8182        materialised.into_iter().next().unwrap_or(Val::Null)
8183    } else {
8184        Val::arr(materialised)
8185    };
8186    (out, consumed)
8187}
8188
8189/// Recognise `@ == lit` / `lit == @` compiled to a sub-program.  Matches
8190/// the shape `[PushCurrent, Push<Lit>, Eq]` or `[Push<Lit>, PushCurrent, Eq]`
8191/// and returns the canonical-serialised literal bytes.  Floats rejected
8192/// (representation variance vs `1` / `1.0`).
8193fn canonical_eq_literal_from_program(prog: &Program) -> Option<Vec<u8>> {
8194    if prog.ops.len() != 3 { return None; }
8195    if !matches!(prog.ops[2], Opcode::Eq) { return None; }
8196    let (lit_op, has_current) = match (&prog.ops[0], &prog.ops[1]) {
8197        (Opcode::PushCurrent, lit) => (lit, true),
8198        (lit, Opcode::PushCurrent) => (lit, true),
8199        _ => (&prog.ops[0], false),
8200    };
8201    if !has_current { return None; }
8202    match lit_op {
8203        Opcode::PushInt(n)  => Some(n.to_string().into_bytes()),
8204        Opcode::PushBool(b) => Some(if *b { b"true".to_vec() } else { b"false".to_vec() }),
8205        Opcode::PushNull    => Some(b"null".to_vec()),
8206        Opcode::PushStr(s)  => serde_json::to_vec(&serde_json::Value::String(s.to_string())).ok(),
8207        _ => None,
8208    }
8209}
8210
8211fn exec_slice(v: Val, from: Option<i64>, to: Option<i64>) -> Val {
8212    match v {
8213        Val::Arr(a) => {
8214            let len = a.len() as i64;
8215            let s = resolve_idx(from.unwrap_or(0), len);
8216            let e = resolve_idx(to.unwrap_or(len), len);
8217            let items = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
8218            let s = s.min(items.len());
8219            let e = e.min(items.len());
8220            Val::arr(items[s..e].to_vec())
8221        }
8222        Val::IntVec(a) => {
8223            let len = a.len() as i64;
8224            let s = resolve_idx(from.unwrap_or(0), len);
8225            let e = resolve_idx(to.unwrap_or(len), len);
8226            let items = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
8227            let s = s.min(items.len());
8228            let e = e.min(items.len());
8229            Val::int_vec(items[s..e].to_vec())
8230        }
8231        Val::FloatVec(a) => {
8232            let len = a.len() as i64;
8233            let s = resolve_idx(from.unwrap_or(0), len);
8234            let e = resolve_idx(to.unwrap_or(len), len);
8235            let items = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
8236            let s = s.min(items.len());
8237            let e = e.min(items.len());
8238            Val::float_vec(items[s..e].to_vec())
8239        }
8240        _ => Val::Null,
8241    }
8242}
8243
8244fn resolve_idx(i: i64, len: i64) -> usize {
8245    (if i < 0 { (len + i).max(0) } else { i }) as usize
8246}
8247
8248fn collect_desc(v: &Val, name: &str, out: &mut Vec<Val>) {
8249    match v {
8250        Val::Obj(m) => {
8251            if let Some(v) = m.get(name) { out.push(v.clone()); }
8252            for v in m.values() { collect_desc(v, name, out); }
8253        }
8254        Val::Arr(a) => { for item in a.as_ref() { collect_desc(item, name, out); } }
8255        _ => {}
8256    }
8257}
8258
8259/// Early-exit variant of `collect_desc`: returns the first self-first DFS
8260/// hit for `name`, matching the order that `collect_desc` would produce.
8261/// Powers the `$..key.first()` fast path when raw JSON bytes aren't
8262/// available (SIMD `byte_chain_exec` handles the raw-bytes case).
8263fn find_desc_first(v: &Val, name: &str) -> Option<Val> {
8264    match v {
8265        Val::Obj(m) => {
8266            if let Some(v) = m.get(name) { return Some(v.clone()); }
8267            for child in m.values() {
8268                if let Some(hit) = find_desc_first(child, name) { return Some(hit); }
8269            }
8270            None
8271        }
8272        Val::Arr(a) => {
8273            for item in a.as_ref() {
8274                if let Some(hit) = find_desc_first(item, name) { return Some(hit); }
8275            }
8276            None
8277        }
8278        _ => None,
8279    }
8280}
8281
8282fn collect_all(v: &Val, out: &mut Vec<Val>) {
8283    match v {
8284        Val::Obj(m) => {
8285            out.push(v.clone());
8286            for child in m.values() { collect_all(child, out); }
8287        }
8288        Val::Arr(a) => {
8289            for item in a.as_ref() { collect_all(item, out); }
8290        }
8291        other => out.push(other.clone()),
8292    }
8293}
8294
8295/// Path-tracking variant — called when descending from root so paths are
8296/// root-relative and can be cached for future `RootChain` lookups.
8297/// `prefix` is mutated in-place (push/truncate) to avoid allocations.
8298fn collect_desc_with_paths(
8299    v: &Val, name: &str, prefix: &mut String,
8300    out: &mut Vec<Val>, cached: &mut Vec<(Arc<str>, Val)>,
8301) {
8302    match v {
8303        Val::Obj(m) => {
8304            if let Some(found) = m.get(name) {
8305                let mut path = prefix.clone();
8306                path.push('/');
8307                path.push_str(name);
8308                out.push(found.clone());
8309                cached.push((Arc::from(path.as_str()), found.clone()));
8310            }
8311            for (k, child) in m.iter() {
8312                let prev = prefix.len();
8313                prefix.push('/');
8314                prefix.push_str(k.as_ref());
8315                collect_desc_with_paths(child, name, prefix, out, cached);
8316                prefix.truncate(prev);
8317            }
8318        }
8319        Val::Arr(a) => {
8320            for (i, item) in a.iter().enumerate() {
8321                let prev = prefix.len();
8322                prefix.push('/');
8323                let idx = i.to_string();
8324                prefix.push_str(&idx);
8325                collect_desc_with_paths(item, name, prefix, out, cached);
8326                prefix.truncate(prev);
8327            }
8328        }
8329        _ => {}
8330    }
8331}
8332
8333fn resolve_pointer(root: &Val, ptr: &str) -> Val {
8334    let mut cur = root.clone();
8335    for seg in ptr.split('/').filter(|s| !s.is_empty()) {
8336        cur = cur.get_field(seg);
8337    }
8338    cur
8339}
8340
8341#[derive(Clone, Copy)]
8342enum DictKeyShape {
8343    /// key prog is `[LoadIdent(v)]` — use item directly (stringify non-Str).
8344    Ident,
8345    /// key prog is `[LoadIdent(v), CallMethod{ToString, no args}]`.
8346    IdentToString,
8347}
8348
8349fn classify_dict_key(prog: &Program, vname: &str) -> Option<DictKeyShape> {
8350    match prog.ops.as_ref() {
8351        [Opcode::LoadIdent(v)] if v.as_ref() == vname => Some(DictKeyShape::Ident),
8352        [Opcode::LoadIdent(v), Opcode::CallMethod(call)]
8353            if v.as_ref() == vname
8354                && call.method == BuiltinMethod::ToString
8355                && call.sub_progs.is_empty()
8356                && call.orig_args.is_empty() =>
8357            Some(DictKeyShape::IdentToString),
8358        _ => None,
8359    }
8360}
8361
8362fn bind_comp_vars(env: &Env, vars: &[Arc<str>], item: Val) -> Env {
8363    match vars {
8364        [] => env.with_current(item),
8365        [v] => { let mut e = env.with_var(v.as_ref(), item.clone()); e.current = item; e }
8366        [v1, v2, ..] => {
8367            let idx = item.get("index").cloned().unwrap_or(Val::Null);
8368            let val = item.get("value").cloned().unwrap_or_else(|| item.clone());
8369            let mut e = env.with_var(v1.as_ref(), idx).with_var(v2.as_ref(), val.clone());
8370            e.current = val;
8371            e
8372        }
8373    }
8374}
8375
8376fn exec_cast(v: &Val, ty: super::ast::CastType) -> Result<Val, EvalError> {
8377    use super::ast::CastType;
8378    match ty {
8379        CastType::Str => Ok(Val::Str(Arc::from(match v {
8380            Val::Null     => "null".to_string(),
8381            Val::Bool(b)  => b.to_string(),
8382            Val::Int(n)   => n.to_string(),
8383            Val::Float(f) => f.to_string(),
8384            Val::Str(s)   => s.to_string(),
8385            other         => super::eval::util::val_to_string(other),
8386        }.as_str()))),
8387        CastType::Bool => Ok(Val::Bool(match v {
8388            Val::Null         => false,
8389            Val::Bool(b)      => *b,
8390            Val::Int(n)       => *n != 0,
8391            Val::Float(f)     => *f != 0.0,
8392            Val::Str(s)       => !s.is_empty(),
8393            Val::StrSlice(r)  => !r.is_empty(),
8394            Val::Arr(a)       => !a.is_empty(),
8395            Val::IntVec(a)    => !a.is_empty(),
8396            Val::FloatVec(a)  => !a.is_empty(),
8397            Val::StrVec(a)       => !a.is_empty(),
8398            Val::StrSliceVec(a)  => !a.is_empty(),
8399            Val::ObjVec(d)       => !d.rows.is_empty(),
8400            Val::Obj(o)       => !o.is_empty(),
8401            Val::ObjSmall(p)  => !p.is_empty(),
8402        })),
8403        CastType::Number | CastType::Float => match v {
8404            Val::Int(n)   => Ok(Val::Float(*n as f64)),
8405            Val::Float(_) => Ok(v.clone()),
8406            Val::Str(s)   => s.parse::<f64>().map(Val::Float)
8407                              .map_err(|e| EvalError(format!("as float: {}", e))),
8408            Val::Bool(b)  => Ok(Val::Float(if *b { 1.0 } else { 0.0 })),
8409            Val::Null     => Ok(Val::Float(0.0)),
8410            _             => err!("as float: cannot convert"),
8411        },
8412        CastType::Int => match v {
8413            Val::Int(_)   => Ok(v.clone()),
8414            Val::Float(f) => Ok(Val::Int(*f as i64)),
8415            Val::Str(s)   => s.parse::<i64>().map(Val::Int)
8416                              .or_else(|_| s.parse::<f64>().map(|f| Val::Int(f as i64)))
8417                              .map_err(|e| EvalError(format!("as int: {}", e))),
8418            Val::Bool(b)  => Ok(Val::Int(if *b { 1 } else { 0 })),
8419            Val::Null     => Ok(Val::Int(0)),
8420            _             => err!("as int: cannot convert"),
8421        },
8422        CastType::Array => match v {
8423            Val::Arr(_)   => Ok(v.clone()),
8424            Val::Null     => Ok(Val::arr(Vec::new())),
8425            other         => Ok(Val::arr(vec![other.clone()])),
8426        },
8427        CastType::Object => match v {
8428            Val::Obj(_)   => Ok(v.clone()),
8429            _             => err!("as object: cannot convert non-object"),
8430        },
8431        CastType::Null => Ok(Val::Null),
8432    }
8433}
8434
8435fn apply_fmt_spec(val: &Val, spec: &str) -> String {
8436    if let Some(rest) = spec.strip_suffix('f') {
8437        if let Some(prec_str) = rest.strip_prefix('.') {
8438            if let Ok(prec) = prec_str.parse::<usize>() {
8439                if let Some(f) = val.as_f64() { return format!("{:.prec$}", f); }
8440            }
8441        }
8442    }
8443    if spec == "d" { if let Some(i) = val.as_i64() { return format!("{}", i); } }
8444    let s = val_to_string(val);
8445    if let Some(w) = spec.strip_prefix('>').and_then(|s| s.parse::<usize>().ok()) { return format!("{:>w$}", s); }
8446    if let Some(w) = spec.strip_prefix('<').and_then(|s| s.parse::<usize>().ok()) { return format!("{:<w$}", s); }
8447    if let Some(w) = spec.strip_prefix('^').and_then(|s| s.parse::<usize>().ok()) { return format!("{:^w$}", s); }
8448    if let Some(w) = spec.strip_prefix('0').and_then(|s| s.parse::<usize>().ok()) {
8449        if let Some(i) = val.as_i64() { return format!("{:0>w$}", i); }
8450    }
8451    s
8452}
8453
8454// ── Opcode helpers ────────────────────────────────────────────────────────────
8455
8456/// Build a no-arg `CallMethod` opcode (used by peephole strength reduction).
8457/// Newtype wrapper giving `Val` an `Ord` derived from `cmp_vals`,
8458/// so it can be used in `BinaryHeap` for TopN.
8459struct WrapVal(Val);
8460impl PartialEq for WrapVal { fn eq(&self, o: &Self) -> bool {
8461    super::eval::util::cmp_vals(&self.0, &o.0) == std::cmp::Ordering::Equal
8462} }
8463impl Eq for WrapVal {}
8464impl PartialOrd for WrapVal { fn partial_cmp(&self, o: &Self) -> Option<std::cmp::Ordering> {
8465    Some(self.cmp(o))
8466} }
8467impl Ord for WrapVal { fn cmp(&self, o: &Self) -> std::cmp::Ordering {
8468    super::eval::util::cmp_vals(&self.0, &o.0)
8469} }
8470
8471/// Extract a literal string from a single-op program `[PushStr(s)]`.
8472fn const_str_program(p: &Arc<Program>) -> Option<Arc<str>> {
8473    match p.ops.as_ref() {
8474        [Opcode::PushStr(s)] => Some(s.clone()),
8475        _ => None,
8476    }
8477}
8478
8479fn make_noarg_call(method: BuiltinMethod, name: &str) -> Opcode {
8480    Opcode::CallMethod(Arc::new(CompiledCall {
8481        method,
8482        name:      Arc::from(name),
8483        sub_progs: Arc::from(&[] as &[Arc<Program>]),
8484        orig_args: Arc::from(&[] as &[Arg]),
8485    }))
8486}
8487
8488// ── Hash helpers ──────────────────────────────────────────────────────────────
8489
8490fn hash_str(s: &str) -> u64 {
8491    let mut h = DefaultHasher::new();
8492    s.hash(&mut h);
8493    h.finish()
8494}
8495
8496/// Hash the *structure* (keys + types) of a Val for the resolution cache key.
8497/// Does NOT hash values, only shape, so structural navigation results are
8498/// stable across different values in the same-shaped document.
8499fn hash_val_structure(v: &Val) -> u64 {
8500    let mut h = DefaultHasher::new();
8501    hash_structure_into(v, &mut h, 0);
8502    h.finish()
8503}
8504
8505fn hash_structure_into(v: &Val, h: &mut DefaultHasher, depth: usize) {
8506    if depth > 8 { return; }
8507    match v {
8508        Val::Null       => 0u8.hash(h),
8509        Val::Bool(b)    => { 1u8.hash(h); b.hash(h); }
8510        Val::Int(n)     => { 2u8.hash(h); n.hash(h); }
8511        Val::Float(f)   => { 3u8.hash(h); f.to_bits().hash(h); }
8512        Val::Str(s)       => { 4u8.hash(h); s.hash(h); }
8513        Val::StrSlice(r)  => { 4u8.hash(h); r.as_str().hash(h); }
8514        Val::Arr(a)     => { 5u8.hash(h); a.len().hash(h); for item in a.iter() { hash_structure_into(item, h, depth+1); } }
8515        Val::IntVec(a)  => { 5u8.hash(h); a.len().hash(h); for n in a.iter() { 2u8.hash(h); n.hash(h); } }
8516        Val::FloatVec(a) => { 5u8.hash(h); a.len().hash(h); for f in a.iter() { 3u8.hash(h); f.to_bits().hash(h); } }
8517        Val::StrVec(a)  => { 5u8.hash(h); a.len().hash(h); for s in a.iter() { 4u8.hash(h); s.hash(h); } }
8518        Val::StrSliceVec(a) => { 5u8.hash(h); a.len().hash(h); for r in a.iter() { 4u8.hash(h); r.as_str().hash(h); } }
8519        Val::ObjVec(d)  => { 6u8.hash(h); d.rows.len().hash(h); for k in d.keys.iter() { k.hash(h); } }
8520        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); } }
8521        Val::ObjSmall(p) => { 6u8.hash(h); p.len().hash(h); for (k, v) in p.iter() { k.hash(h); hash_structure_into(v, h, depth+1); } }
8522    }
8523}