Skip to main content

jetro_core/builtins/
mod.rs

1//! Builtin method catalog and shared algorithm implementations.
2//!
3//! All three execution backends (VM, pipeline, composed) dispatch here for
4//! algorithm bodies. Each builtin exposes two primitives:
5//! `*_one(item, eval)` for per-row work and `*_apply(items, eval)` for
6//! buffered work. Streaming consumers call `*_one`; barrier consumers call
7//! `*_apply`. This module owns the loop and truthy-check logic exactly once.
8
9use crate::data::context::EvalError;
10use crate::data::value::Val;
11use indexmap::IndexMap;
12use std::sync::Arc;
13
14/// Pre-resolved method identifier. Carried by `CompiledCall` and pipeline
15/// plan nodes so method dispatch is an O(1) integer match, not a string hash.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[repr(u8)]
18pub enum BuiltinMethod {
19    // ── Object / structural inspection ────────────────────────────────────
20    /// Returns the number of elements in an array, object, or string.
21    Len = 0,
22    /// Returns an array of all keys of an object.
23    Keys,
24    /// Returns an array of all values of an object.
25    Values,
26    /// Returns `[[key, value], ...]` pairs for each object entry.
27    Entries,
28    /// Converts an object to `[{key, val}, ...]` form.
29    ToPairs,
30    /// Inverse of `to_pairs`; reconstructs an object from key/value pairs.
31    FromPairs,
32    /// Swaps keys and values of an object.
33    Invert,
34    /// Reverses an array or string.
35    Reverse,
36    /// Returns a string name for the runtime type of a value.
37    Type,
38    /// Converts any value to its display string representation.
39    ToString,
40    /// Serialises a value to a JSON string.
41    ToJson,
42    /// Parses a JSON string back to a value.
43    FromJson,
44    /// Lifts the current input source into a row stream.
45    Rows,
46
47    // ── Numeric aggregates ─────────────────────────────────────────────────
48    /// Sums all numeric elements; accepts an optional projection lambda.
49    Sum,
50    /// Computes the arithmetic mean; accepts an optional projection lambda.
51    Avg,
52    /// Returns the minimum numeric element; accepts an optional projection.
53    Min,
54    /// Returns the maximum numeric element; accepts an optional projection.
55    Max,
56    /// Counts elements, or truthy results of a predicate lambda.
57    Count,
58    /// Returns true if any element satisfies the predicate.
59    Any,
60    /// Returns true only when every element satisfies the predicate.
61    All,
62    /// Returns the index of the first element satisfying the predicate.
63    FindIndex,
64    /// Returns all indices whose elements satisfy the predicate.
65    IndicesWhere,
66    /// Returns the element whose projected key is the greatest.
67    MaxBy,
68    /// Returns the element whose projected key is the smallest.
69    MinBy,
70    /// Groups elements into an object keyed by the lambda result.
71    GroupBy,
72    /// Counts elements per key produced by the lambda.
73    CountBy,
74    /// Indexes elements into a map keyed by the lambda result (last wins).
75    IndexBy,
76    /// Groups elements by a key lambda, then applies a shape lambda to each group.
77    GroupShape,
78    /// Unnests an array field so each nested value becomes its own row.
79    Explode,
80    /// Inverse of `explode`; collapses rows sharing the same non-field keys.
81    Implode,
82
83    // ── Streaming / array transforms ──────────────────────────────────────
84    /// Keeps only elements for which the predicate is truthy.
85    Filter,
86    /// Projects each element through the lambda.
87    Map,
88    /// Maps each element and flattens one level of the resulting arrays.
89    FlatMap,
90    /// Alias of `filter`; keeps elements matching the predicate.
91    Find,
92    /// Alias of `filter`; keeps all elements matching the predicate.
93    FindAll,
94    /// Sorts an array; supports key expressions and comparator lambdas.
95    Sort,
96    /// Removes duplicate values from an array.
97    Unique,
98    /// Removes duplicates by comparing the value of a key lambda.
99    UniqueBy,
100    /// Wraps a scalar in `[scalar]`; passes arrays through unchanged.
101    Collect,
102    /// DFS pre-order search across the entire value tree.
103    DeepFind,
104    /// Collects all objects that contain every key in the shape pattern.
105    DeepShape,
106    /// Collects all objects whose listed keys equal the given literals.
107    DeepLike,
108    /// Post-order recursive tree transform (bottom-up).
109    Walk,
110    /// Pre-order recursive tree transform (top-down).
111    WalkPre,
112    /// Applies a step expression repeatedly until a fixpoint is reached.
113    Rec,
114    /// Walks the tree collecting `{path, value}` rows for matching nodes.
115    TracePath,
116    /// Flattens nested arrays up to a given depth (default 1).
117    Flatten,
118    /// Removes `null` values from an array.
119    Compact,
120    /// Joins array elements into a string with a separator.
121    Join,
122    /// Returns the first element, or the first N elements as an array.
123    First,
124    /// Returns the last element, or the last N elements as an array.
125    Last,
126    /// Returns the element at a given index (supports negative indexing).
127    Nth,
128    /// Keeps at most N elements from the front of the array.
129    Take,
130    /// Drops the first N elements and returns the rest.
131    Skip,
132    /// Appends an element to the end of an array.
133    Append,
134    /// Inserts an element at the front of an array.
135    Prepend,
136    /// Removes occurrences of a value from an array, or items matching a predicate.
137    Remove,
138    /// Returns elements of the receiver not present in the argument array.
139    Diff,
140    /// Returns elements present in both arrays.
141    Intersect,
142    /// Returns the union of two arrays without duplicates.
143    Union,
144    /// Produces `[{index, value}, ...]` pairs for each element.
145    Enumerate,
146    /// Returns consecutive overlapping pairs as `[[a, b], ...]`.
147    Pairwise,
148    /// Slides a window of size N over the array.
149    Window,
150    /// Splits an array into non-overlapping chunks of size N.
151    Chunk,
152    /// Keeps elements from the front as long as the predicate holds.
153    TakeWhile,
154    /// Drops elements from the front while the predicate holds, then keeps the rest.
155    DropWhile,
156    /// Returns the first element satisfying the predicate, or null.
157    FindFirst,
158    /// Returns the only element satisfying the predicate, erroring on zero or multiple matches.
159    FindOne,
160    /// Counts approximate distinct values using a HyperLogLog-style sketch.
161    ApproxCountDistinct,
162    /// Produces a running accumulation using the lambda.
163    Accumulate,
164    /// Folds the array to a single value with `fn(acc, row) -> acc`. Equivalent
165    /// to `accumulate(init, fn).last()` but avoids the intermediate array.
166    Fold,
167    /// Splits an array into two arrays: elements that pass and those that fail the predicate.
168    Partition,
169    /// Zips two arrays element-wise into `[[a0, b0], ...]`.
170    Zip,
171    /// Like `zip` but pads the shorter array with a fill value.
172    ZipLongest,
173    /// Applies multiple expressions to the same receiver and collects results.
174    Fanout,
175    /// Applies named expressions to one value and collects them into an object.
176    ZipShape,
177
178    // ── Object transforms ──────────────────────────────────────────────────
179    /// Selects a named subset of fields from an object or array of objects.
180    Pick,
181    /// Removes named fields from an object or array of objects.
182    Omit,
183    /// Shallow-merges two objects (right wins on collision).
184    Merge,
185    /// Recursively merges two objects.
186    DeepMerge,
187    /// Fills in missing or null fields from a defaults object.
188    Defaults,
189    /// Renames object keys according to a `{old: new}` map.
190    Rename,
191    /// Maps a lambda over each key, replacing the key with the result.
192    TransformKeys,
193    /// Maps a lambda over each value, replacing the value with the result.
194    TransformValues,
195    /// Keeps only the object entries for which the lambda is truthy.
196    FilterKeys,
197    /// Keeps only the object entries whose values satisfy the lambda.
198    FilterValues,
199    /// Pivots an array of objects into a nested object or flat map.
200    Pivot,
201
202    // ── Path operations ────────────────────────────────────────────────────
203    /// Retrieves a value at a dot-notation path.
204    GetPath,
205    /// Sets a value at a dot-notation path, returning the modified document.
206    SetPath,
207    /// Deletes the value at a dot-notation path.
208    DelPath,
209    /// Deletes values at multiple dot-notation paths.
210    DelPaths,
211    /// Returns true if a non-null value exists at the given path.
212    HasPath,
213    /// Flattens a nested object to dot-notation keys with a given separator.
214    FlattenKeys,
215    /// Reconstructs a nested object from dot-notation flat keys.
216    UnflattenKeys,
217
218    // ── Serialisation ──────────────────────────────────────────────────────
219    /// Serialises an array/object to CSV text.
220    ToCsv,
221    /// Serialises an array/object to TSV text.
222    ToTsv,
223
224    // ── Miscellaneous scalar helpers ───────────────────────────────────────
225    /// Returns the receiver if non-null; otherwise returns the argument.
226    Or,
227    /// Returns true if the object contains the given key.
228    Has,
229    /// Returns true if every literal needle is present in the receiver.
230    HasAll,
231    /// Returns true if the object contains the given key.
232    HasKey,
233    /// Returns true if a field path is absent or null in the receiver.
234    Missing,
235    /// Returns true if the array/string/object contains the given item.
236    Includes,
237    /// Returns the first index of a value in an array, or -1.
238    Index,
239    /// Returns all indices where a value occurs in an array.
240    IndicesOf,
241    /// Replaces the receiver with the argument value (chain-write terminal).
242    Set,
243    /// Mutates the receiver in place using a lambda (chain-write terminal).
244    Update,
245
246    // ── Numeric / math ─────────────────────────────────────────────────────
247    /// Rounds up to the nearest integer.
248    Ceil,
249    /// Rounds down to the nearest integer.
250    Floor,
251    /// Rounds to the nearest integer.
252    Round,
253    /// Returns the absolute value.
254    Abs,
255    /// Computes a rolling sum over a sliding window of size N.
256    RollingSum,
257    /// Computes a rolling mean over a sliding window of size N.
258    RollingAvg,
259    /// Computes a rolling minimum over a sliding window of size N.
260    RollingMin,
261    /// Computes a rolling maximum over a sliding window of size N.
262    RollingMax,
263    /// Shifts values backward by N positions (fills leading positions with null).
264    Lag,
265    /// Shifts values forward by N positions (fills trailing positions with null).
266    Lead,
267    /// Computes element-wise first differences.
268    DiffWindow,
269    /// Computes element-wise percentage change from the previous value.
270    PctChange,
271    /// Running maximum up to each position.
272    CumMax,
273    /// Running minimum up to each position.
274    CumMin,
275    /// Normalises each element to its z-score relative to the array mean/std.
276    Zscore,
277
278    // ── String transforms ──────────────────────────────────────────────────
279    /// Converts a string to all-uppercase.
280    Upper,
281    /// Converts a string to all-lowercase.
282    Lower,
283    /// Uppercases the first character and lowercases the rest.
284    Capitalize,
285    /// Title-cases every word in the string.
286    TitleCase,
287    /// Strips leading and trailing ASCII whitespace.
288    Trim,
289    /// Strips leading ASCII whitespace.
290    TrimLeft,
291    /// Strips trailing ASCII whitespace.
292    TrimRight,
293    /// Converts a string to `snake_case`.
294    SnakeCase,
295    /// Converts a string to `kebab-case`.
296    KebabCase,
297    /// Converts a string to `camelCase`.
298    CamelCase,
299    /// Converts a string to `PascalCase`.
300    PascalCase,
301    /// Reverses the characters of a string.
302    ReverseStr,
303    /// Splits a string on newlines and returns an array of lines.
304    Lines,
305    /// Splits a string on whitespace and returns an array of words.
306    Words,
307    /// Returns each Unicode grapheme cluster as a single-element string.
308    Chars,
309    /// Returns each Unicode code point as a UTF-8 encoded string.
310    CharsOf,
311    /// Returns each byte of the string as an integer.
312    Bytes,
313    /// Returns the byte length (not char count) of a string.
314    ByteLen,
315    /// Returns true if the string is empty or contains only whitespace.
316    IsBlank,
317    /// Returns true if the string consists entirely of ASCII digits.
318    IsNumeric,
319    /// Returns true if the string consists entirely of alphabetic characters.
320    IsAlpha,
321    /// Returns true if the string is valid ASCII.
322    IsAscii,
323    /// Parses a string as an integer or float; returns null on failure.
324    ToNumber,
325    /// Parses `"true"` / `"false"` to a boolean; returns null otherwise.
326    ToBool,
327    /// Parses the string as a base-10 integer; returns null on failure.
328    ParseInt,
329    /// Parses the string as a float; returns null on failure.
330    ParseFloat,
331    /// Parses common truthy/falsy string representations to a boolean.
332    ParseBool,
333    /// Encodes a string as standard Base64.
334    ToBase64,
335    /// Decodes a Base64-encoded string.
336    FromBase64,
337    /// Percent-encodes a string for use in a URL.
338    UrlEncode,
339    /// Decodes a percent-encoded URL string.
340    UrlDecode,
341    /// Escapes `<`, `>`, `&`, `"`, `'` to their HTML entities.
342    HtmlEscape,
343    /// Converts HTML entities back to their literal characters.
344    HtmlUnescape,
345    /// Repeats the string N times.
346    Repeat,
347    /// Left-pads the string to the given width with a fill character.
348    PadLeft,
349    /// Right-pads the string to the given width with a fill character.
350    PadRight,
351    /// Centers the string within the given width using a fill character.
352    Center,
353    /// Returns true if the string starts with the given prefix.
354    StartsWith,
355    /// Returns true if the string ends with the given suffix.
356    EndsWith,
357    /// Returns the char index of the first occurrence, or -1.
358    IndexOf,
359    /// Returns the char index of the last occurrence, or -1.
360    LastIndexOf,
361    /// Replaces the first occurrence of `needle` with `replacement`.
362    Replace,
363    /// Replaces all occurrences of `needle` with `replacement`.
364    ReplaceAll,
365    /// Strips the given prefix if present; returns the receiver unchanged otherwise.
366    StripPrefix,
367    /// Strips the given suffix if present; returns the receiver unchanged otherwise.
368    StripSuffix,
369    /// Returns a substring by character indices (supports negative indexing).
370    Slice,
371    /// Splits a string on a separator and returns an array of parts.
372    Split,
373    /// Prepends N spaces to every line of a string.
374    Indent,
375    /// Removes the common leading whitespace from every line.
376    Dedent,
377    /// Returns true if the string contains the given substring.
378    Matches,
379    /// Returns an array of every non-overlapping occurrence of a pattern.
380    Scan,
381    /// Returns true if the regex matches the string.
382    ReMatch,
383    /// Returns the first regex match as a string, or null.
384    ReMatchFirst,
385    /// Returns all non-overlapping regex matches as an array of strings.
386    ReMatchAll,
387    /// Returns capture groups of the first regex match as an array, or null.
388    ReCaptures,
389    /// Returns all capture groups for every match as an array of arrays.
390    ReCapturesAll,
391    /// Splits a string on a regex pattern.
392    ReSplit,
393    /// Replaces the first regex match with a replacement string.
394    ReReplace,
395    /// Replaces all regex matches with a replacement string.
396    ReReplaceAll,
397    /// Returns true if the string contains any of the given substrings.
398    ContainsAny,
399    /// Returns true if the string contains all of the given substrings.
400    ContainsAll,
401    /// Infers a structural schema description from the value.
402    Schema,
403
404    // ── Relational ─────────────────────────────────────────────────────────
405    /// Performs an inner equi-join of two arrays of objects on matching key fields.
406    EquiJoin,
407
408    /// Sentinel returned by `from_name` when the method string is unrecognised.
409    Unknown,
410}
411
412/// Expands `$macro!(...)` once per `BuiltinMethod` variant — the single source of truth for
413/// "all builtin methods" used by name lookup, registry exports, and any future cross-cutting
414/// per-method generation. Variant names match the corresponding `defs::*` struct names.
415#[macro_export]
416macro_rules! for_each_builtin {
417    ($macro:ident) => {
418        $macro! (
419            Abs, Accumulate, All, Any, Append, ApproxCountDistinct, Avg, ByteLen, Bytes,
420            CamelCase, Capitalize, Ceil, Center, Chars, CharsOf, Chunk, Collect, Compact,
421            ContainsAll, ContainsAny, Count, CountBy, CumMax, CumMin, Dedent, DeepFind,
422            DeepLike, DeepMerge, DeepShape, Defaults, DelPath, DelPaths, Diff, DiffWindow,
423            DropWhile, EndsWith, Entries, Enumerate, EquiJoin, Explode, Fanout, Filter,
424            FilterKeys, FilterValues, Find, FindAll, FindFirst, FindIndex, FindOne, First,
425            FlatMap, Flatten, FlattenKeys, Floor, Fold, FromBase64, FromJson, FromPairs, GetPath,
426            GroupBy, GroupShape, Has, HasAll, HasKey, HasPath, HtmlEscape, HtmlUnescape, Implode,
427            Includes, Indent, Index, IndexBy, IndexOf, IndicesOf, IndicesWhere, Intersect, Invert,
428            IsAlpha, IsAscii, IsBlank, IsNumeric, Join, KebabCase, Keys, Lag, Last,
429            LastIndexOf, Lead, Len, Lines, Lower, Map, Matches, Max, MaxBy, Merge, Min,
430            MinBy, Missing, Nth, Omit, Or, PadLeft, PadRight, Pairwise, ParseBool,
431            ParseFloat, ParseInt, Partition, PascalCase, PctChange, Pick, Pivot, Prepend,
432            Rec, ReCaptures, ReCapturesAll, ReMatch, ReMatchAll, ReMatchFirst, Remove,
433            Rename, Repeat, Replace, ReplaceAll, ReReplace, ReReplaceAll, ReSplit, Reverse,
434            ReverseStr, RollingAvg, RollingMax, RollingMin, RollingSum, Round, Rows, Scan, Schema,
435            Set, SetPath, Skip, Slice, SnakeCase, Sort, Split, StartsWith, StripPrefix,
436            StripSuffix, Sum, Take, TakeWhile, TitleCase, ToBase64, ToBool, ToCsv, ToJson,
437            ToNumber, ToPairs, ToString, ToTsv, TracePath, TransformKeys, TransformValues,
438            Trim, TrimLeft, TrimRight, Type, UnflattenKeys, Union, Unique, UniqueBy, Unknown,
439            Update, Upper, UrlDecode, UrlEncode, Values, Walk, WalkPre, Window, Words, Zip,
440            ZipLongest, ZipShape, Zscore
441        )
442    };
443}
444
445impl BuiltinMethod {
446    /// Resolves a method name string to the corresponding `BuiltinMethod` variant.
447    /// Returns [`BuiltinMethod::Unknown`] when the name is not registered.
448    pub fn from_name(name: &str) -> Self {
449        crate::builtins::registry::by_name(name)
450            .and_then(|id| id.method())
451            .unwrap_or(Self::Unknown)
452    }
453
454    /// Returns true when the method requires a lambda expression as its first argument.
455    /// The pipeline planner uses this to distinguish element vs. expression stages.
456    pub(crate) fn is_lambda_method(self) -> bool {
457        matches!(
458            self,
459            Self::Filter
460                | Self::Map
461                | Self::FlatMap
462                | Self::Sort
463                | Self::Any
464                | Self::All
465                | Self::Count
466                | Self::GroupBy
467                | Self::CountBy
468                | Self::IndexBy
469                | Self::TakeWhile
470                | Self::DropWhile
471                | Self::Accumulate
472                | Self::Fold
473                | Self::Partition
474                | Self::TransformKeys
475                | Self::TransformValues
476                | Self::FilterKeys
477                | Self::FilterValues
478                | Self::Pivot
479                | Self::Update
480        )
481    }
482}
483
484/// Statically-typed argument payload stored inside a [`BuiltinCall`].
485/// Each variant corresponds to the argument signature of a group of builtins,
486/// enabling argument decoding without heap allocation at call time.
487#[derive(Debug, Clone)]
488pub enum BuiltinArgs {
489    /// No arguments.
490    None,
491    /// A single string argument (field name, separator, pattern, etc.).
492    Str(Arc<str>),
493    /// A pre-parsed dot/bracket path used by hot path helpers.
494    Path(Arc<[PathSeg]>),
495    /// Two string arguments (needle + replacement, pattern + replacement).
496    StrPair { first: Arc<str>, second: Arc<str> },
497    /// A list of string arguments (field list for `pick`, `omit`, etc.).
498    StrVec(Vec<Arc<str>>),
499    /// A single signed-integer argument (index, count).
500    I64(i64),
501    /// A primary integer plus an optional second integer (start + optional end for `slice`).
502    I64Opt { first: i64, second: Option<i64> },
503    /// A single unsigned-integer argument (window size, chunk size, etc.).
504    Usize(usize),
505    /// A single pre-evaluated `Val` argument.
506    Val(Val),
507    /// A list of pre-evaluated `Val` arguments (`diff`, `intersect`, `union`).
508    ValVec(Vec<Val>),
509    /// Padding width and fill character (`pad_left`, `pad_right`, `center`).
510    Pad { width: usize, fill: char },
511}
512
513/// A pre-compiled builtin call ready for stateless execution.
514/// Stored in pipeline plan nodes and the `CompiledCall` opcode payload.
515#[derive(Debug, Clone)]
516pub struct BuiltinCall {
517    /// Which builtin to invoke.
518    pub method: BuiltinMethod,
519    /// The decoded static arguments for this call.
520    pub args: BuiltinArgs,
521}
522
523/// Internal helper that decodes static (non-lambda) arguments for [`BuiltinCall::from_static_args`].
524/// Wraps the `eval_arg` and `ident_arg` closures with typed accessor methods.
525struct StaticArgDecoder<'a, E, I> {
526    name: &'a str,
527    eval_arg: E,
528    ident_arg: I,
529}
530
531impl<E, I> StaticArgDecoder<'_, E, I>
532where
533    E: FnMut(usize) -> Result<Option<Val>, EvalError>,
534    I: FnMut(usize) -> Option<Arc<str>>,
535{
536    /// Evaluates the argument at `idx`, returning an error if it is absent.
537    fn val(&mut self, idx: usize) -> Result<Val, EvalError> {
538        (self.eval_arg)(idx)?.ok_or_else(|| EvalError(format!("{}: missing argument", self.name)))
539    }
540
541    /// Evaluates the argument at `idx` as a string, accepting bare identifiers.
542    fn str(&mut self, idx: usize) -> Result<Arc<str>, EvalError> {
543        if let Some(value) = (self.ident_arg)(idx) {
544            return Ok(value);
545        }
546        match self.val(idx)? {
547            Val::Str(s) => Ok(s),
548            other => Ok(Arc::from(crate::util::val_to_string(&other).as_str())),
549        }
550    }
551
552    /// Returns `Some(prefix)` only when the argument is a string-typed
553    /// value, leaving non-string arguments untouched. Used to disambiguate
554    /// overloaded scalar-arg builtins (e.g. `indent(2)` vs `indent("> ")`).
555    fn str_lit(&mut self, idx: usize) -> Option<Arc<str>> {
556        match (self.eval_arg)(idx).ok().flatten()? {
557            Val::Str(s) => Some(s),
558            Val::StrSlice(r) => Some(r.to_arc()),
559            _ => None,
560        }
561    }
562
563    /// Evaluates the argument at `idx` as a signed 64-bit integer.
564    fn i64(&mut self, idx: usize) -> Result<i64, EvalError> {
565        match self.val(idx)? {
566            Val::Int(n) => Ok(n),
567            Val::Float(f) => Ok(f as i64),
568            _ => Err(EvalError(format!(
569                "{}: expected number argument",
570                self.name
571            ))),
572        }
573    }
574
575    /// Evaluates the argument at `idx` as a `usize` (clamped to 0 from below).
576    fn usize(&mut self, idx: usize) -> Result<usize, EvalError> {
577        Ok(self.i64(idx)?.max(0) as usize)
578    }
579
580    /// Evaluates the argument at `idx` as a `Vec<Val>`, failing if not an array.
581    fn vec(&mut self, idx: usize) -> Result<Vec<Val>, EvalError> {
582        self.val(idx).and_then(|value| {
583            value
584                .into_vec()
585                .ok_or_else(|| EvalError(format!("{}: expected array arg", self.name)))
586        })
587    }
588
589    /// Evaluates the argument at `idx` as a vector of strings.
590    fn str_vec(&mut self, idx: usize) -> Result<Vec<Arc<str>>, EvalError> {
591        Ok(self
592            .vec(idx)?
593            .iter()
594            .map(|v| match v {
595                Val::Str(s) => s.clone(),
596                other => Arc::from(crate::util::val_to_string(other).as_str()),
597            })
598            .collect())
599    }
600
601    /// Evaluates the argument at `idx` as a single character for padding operations.
602    /// Defaults to `' '` when the argument index is out of range.
603    fn char(&mut self, idx: usize, arg_len: usize) -> Result<char, EvalError> {
604        if idx >= arg_len {
605            return Ok(' ');
606        }
607        match self.str(idx)? {
608            s if s.chars().count() == 1 => Ok(s.chars().next().unwrap()),
609            _ => Err(EvalError(format!(
610                "{}: filler must be a single-char string",
611                self.name
612            ))),
613        }
614    }
615}
616
617/// Capability and cost descriptor for a single builtin method.
618/// The pipeline planner reads these fields to decide how to lower each stage.
619#[derive(Debug, Clone, Copy)]
620pub struct BuiltinSpec {
621    /// Whether the method is pure (no side effects); impure methods are never fused.
622    pub pure: bool,
623    /// Broad classification used for planning and display.
624    pub category: BuiltinCategory,
625    /// Input-to-output row-count relationship.
626    pub cardinality: BuiltinCardinality,
627    /// Whether the builtin may be used as an indexed projection (e.g. inside `map`).
628    pub can_indexed: bool,
629    /// Whether the builtin has a native view-path implementation.
630    pub view_native: bool,
631    /// Whether the builtin can execute directly on a `JsonView` without materialising.
632    pub view_scalar: bool,
633    /// View-stage lowering target, if the builtin maps to one of the view stages.
634    pub view_stage: Option<BuiltinViewStage>,
635    /// Sink (terminal aggregation) descriptor, present for reducing builtins.
636    pub sink: Option<BuiltinSinkSpec>,
637    /// Keyed reducer kind (group/count/index), used for grouped output planning.
638    pub keyed_reducer: Option<BuiltinKeyedReducer>,
639    /// Numeric reducer kind, used by the numeric sink path.
640    pub numeric_reducer: Option<BuiltinNumericReducer>,
641    /// How adjacent stages of the same kind can be merged (e.g. `take(3).take(2)` → `take(2)`).
642    pub stage_merge: Option<BuiltinStageMerge>,
643    /// Algebraic cancellation rule (e.g. `reverse().reverse()` = identity).
644    pub cancellation: Option<BuiltinCancellation>,
645    /// Columnar stage kind for backends that work on typed column vectors.
646    pub columnar_stage: Option<BuiltinColumnarStage>,
647    /// Structural index backend hint (deep search variants).
648    pub structural: Option<BuiltinStructural>,
649    /// Relative cost used by the planner's heuristic optimizer.
650    pub cost: f64,
651    /// Demand-propagation law for pipeline planning (default: `Identity`).
652    pub demand_law: BuiltinDemandLaw,
653    /// Materialisation policy (default: `Streaming`).
654    pub materialization: BuiltinPipelineMaterialization,
655    /// Cardinality/cost shape annotation for the pipeline cost estimator.
656    pub pipeline_shape: Option<BuiltinPipelineShape>,
657    /// How this builtin affects element ordering in the pipeline.
658    pub order_effect: Option<BuiltinPipelineOrderEffect>,
659    /// Physical stage lowering strategy, if registered.
660    pub lowering: Option<BuiltinPipelineLowering>,
661    /// Whether the builtin is element-wise vectorisable.
662    pub is_element: bool,
663    /// Opt out of the path-receiver scalar-unwrap rewrite. When `true`, the
664    /// planner does **not** lower `$.path.method()` directly to `apply_one`,
665    /// even if `category` and `cardinality` would otherwise allow it. Use for
666    /// methods whose pipeline-streaming behavior is the desired semantic on
667    /// path receivers (e.g. per-element serialization).
668    pub never_unwrap: bool,
669    /// Marks this method as a source-lifting stream boundary. Such methods are
670    /// planned by source/stream planners instead of normal row-local dispatch.
671    pub stream_source: bool,
672}
673
674/// How a builtin transforms downstream demand into the demand it places on
675/// its upstream source. Unknown builtins default to `Identity`.
676#[derive(Debug, Clone, Copy, PartialEq, Eq)]
677pub enum BuiltinDemandLaw {
678    /// Pass downstream demand through unchanged (e.g. purely transforming builtins).
679    Identity,
680    /// Like filter: must scan until `n` outputs are produced, so converts `FirstInput(n)` to `UntilOutput(n)`.
681    FilterLike,
682    /// Like `take_while`: stops at the first predicate failure, so `UntilOutput(n)` becomes `FirstInput(n)`.
683    TakeWhile,
684    /// Like `drop_while`: prefix predicate barrier; safe upstream demand is a full ordered scan.
685    DropWhile,
686    /// Like `unique`/`unique_by`: scan until enough distinct outputs are observed.
687    UniqueLike,
688    /// Like map: the output count equals the input count; passes demand through but requires whole values.
689    MapLike,
690    /// Like scalar `slice`: one-to-one and order-preserving, but consumes the whole input value.
691    Slice,
692    /// Like `flat_map`: output count is unbounded relative to input, so always requests all input.
693    FlatMapLike,
694    /// Cap the upstream pull to the provided count argument.
695    Take,
696    /// Shift the upstream pull window by the provided count argument.
697    Skip,
698    /// Fixed-size chunking; bounded output demand maps to a bounded input prefix.
699    Chunk,
700    /// Sliding window; bounded output demand maps to a bounded input prefix.
701    Window,
702    /// Only the first element is needed; translates any downstream demand to `FirstInput(1)`.
703    First,
704    /// The last element is needed; requires all ordered input.
705    Last,
706    /// A specific positional element is needed.
707    Nth,
708    /// Only a count is needed; requires all inputs but no value payloads.
709    Count,
710    /// A numeric aggregate (sum/min/max/avg); requires all inputs with numeric-only payload.
711    NumericReducer,
712    /// Key-only aggregate such as `count_by`; requires all inputs and key evaluation.
713    KeyOnlyReducer,
714    /// Row-retaining keyed aggregate; requires all full input rows.
715    RowKeyedReducer,
716    /// A full-input ordering barrier; downstream limits can choose strategy, but source scan remains all input.
717    OrderBarrier,
718    /// Reverses one-to-one output order, swapping first/last positional demand.
719    Reverse,
720}
721
722/// Marker that a builtin has a structural (index-based) execution backend.
723/// The query planner may choose the structural path over the generic DFS walk.
724#[derive(Debug, Clone, Copy, PartialEq, Eq)]
725pub enum BuiltinStructural {
726    /// Structural backend for `deep_find`.
727    DeepFind,
728    /// Structural backend for `deep_shape`.
729    DeepShape,
730    /// Structural backend for `deep_like`.
731    DeepLike,
732}
733
734/// View-layer stage that a builtin can be lowered into.
735/// Each variant corresponds to a distinct operation in the view execution path.
736#[derive(Debug, Clone, Copy, PartialEq, Eq)]
737pub enum BuiltinViewStage {
738    /// Predicate-driven row filter stage.
739    Filter,
740    /// Null-removal filter stage.
741    Compact,
742    /// Literal equality removal filter stage.
743    RemoveValue,
744    /// Per-row projection stage.
745    Map,
746    /// Per-row expansion stage (one-to-many).
747    FlatMap,
748    /// Prefix filter that stops at the first non-matching row.
749    TakeWhile,
750    /// Skips leading matching rows and passes the rest.
751    DropWhile,
752    /// Deduplication stage (keeps first occurrence of each key).
753    Distinct,
754    /// Keyed reduce stage (groups, counts, or indexes by key).
755    KeyedReduce,
756    /// Positional limit stage.
757    Take,
758    /// Positional skip stage.
759    Skip,
760}
761
762/// Whether a view stage needs to iterate the source view or can skip it entirely.
763#[derive(Debug, Clone, Copy, PartialEq, Eq)]
764pub enum BuiltinViewInputMode {
765    /// Stage reads values from the underlying view one by one.
766    ReadsView,
767    /// Stage does not consult the view at all (e.g. positional `take`/`skip`).
768    SkipsViewRead,
769}
770
771/// How a view stage produces its output relative to the source view.
772#[derive(Debug, Clone, Copy, PartialEq, Eq)]
773pub enum BuiltinViewOutputMode {
774    /// Output is a sub-slice of the input view (filter, take, skip, etc.).
775    PreservesInputView,
776    /// Output is a single borrowed subview derived from one input element (map).
777    BorrowedSubview,
778    /// Output is multiple borrowed subviews derived from one element (flat_map).
779    BorrowedSubviews,
780    /// Output is a freshly constructed owned value (keyed reduce, etc.).
781    EmitsOwnedValue,
782}
783
784/// Describes how a terminal reducing builtin accumulates its final result.
785#[derive(Debug, Clone, Copy, PartialEq, Eq)]
786pub struct BuiltinSinkSpec {
787    /// Which accumulator algorithm to use.
788    pub accumulator: BuiltinSinkAccumulator,
789    /// How many rows the sink needs to see before it can emit a result.
790    pub demand: BuiltinSinkDemand,
791}
792
793/// The accumulation strategy for a terminal reducing builtin.
794#[derive(Debug, Clone, Copy, PartialEq, Eq)]
795pub enum BuiltinSinkAccumulator {
796    /// Counts the number of rows.
797    Count,
798    /// Applies a numeric reduction (sum, avg, min, max).
799    Numeric,
800    /// Counts approximate distinct values using a probabilistic sketch.
801    ApproxDistinct,
802    /// Selects either the first or last observed row.
803    SelectOne(BuiltinSelectionPosition),
804}
805
806/// The keyed-reduction algorithm used by `group_by` / `count_by` / `index_by`.
807#[derive(Debug, Clone, Copy, PartialEq, Eq)]
808pub enum BuiltinKeyedReducer {
809    /// Counts occurrences per key (`count_by`).
810    Count,
811    /// Maps each key to its last value (`index_by`).
812    Index,
813    /// Maps each key to a list of its values (`group_by`).
814    Group,
815}
816
817/// Which end of the stream the `SelectOne` sink picks.
818#[derive(Debug, Clone, Copy, PartialEq, Eq)]
819pub enum BuiltinSelectionPosition {
820    /// Pick the first row seen (short-circuits on `first`).
821    First,
822    /// Pick the last row seen (must consume the whole stream for `last`).
823    Last,
824}
825
826/// How many rows a terminal sink must consume to produce its result.
827#[derive(Debug, Clone, Copy, PartialEq, Eq)]
828pub enum BuiltinSinkDemand {
829    /// Must see every row; `order` indicates whether row order matters.
830    All {
831        /// Which aspect of each row value is needed.
832        value: BuiltinSinkValueNeed,
833        /// Whether the sink is order-sensitive (affects fusion legality).
834        order: bool,
835    },
836    /// Can stop after the first qualifying row.
837    First {
838        /// Which aspect of the first row's value is needed.
839        value: BuiltinSinkValueNeed,
840    },
841    /// Can satisfy selection by reading from the tail of a reversible/indexed source.
842    Last {
843        /// Which aspect of the last row's value is needed.
844        value: BuiltinSinkValueNeed,
845    },
846}
847
848/// Which portion of each row value the sink algorithm actually reads.
849#[derive(Debug, Clone, Copy, PartialEq, Eq)]
850pub enum BuiltinSinkValueNeed {
851    /// The sink counts rows only and never dereferences their values.
852    None,
853    /// The sink needs the complete `Val` (e.g. `first`, `last`).
854    Whole,
855    /// The sink only reads the numeric representation of each value (sum, avg, min, max).
856    Numeric,
857}
858
859/// Which numeric aggregation the `Numeric` sink accumulator performs.
860#[derive(Debug, Clone, Copy, PartialEq, Eq)]
861pub enum BuiltinNumericReducer {
862    /// Accumulate by addition.
863    Sum,
864    /// Accumulate sum and count, emit mean.
865    Avg,
866    /// Track the running minimum.
867    Min,
868    /// Track the running maximum.
869    Max,
870}
871
872/// Describes how two adjacent identical stages can be collapsed into one.
873#[derive(Debug, Clone, Copy, PartialEq, Eq)]
874pub enum BuiltinStageMerge {
875    /// `take(a).take(b)` → `take(min(a, b))`.
876    UsizeMin,
877    /// `skip(a).skip(b)` → `skip(a + b)` (saturating to avoid overflow).
878    UsizeSaturatingAdd,
879}
880
881/// Algebraic cancellation rule for a builtin.
882/// Two adjacent stages cancel when `a.cancels_with(b)` is true.
883#[derive(Debug, Clone, Copy, PartialEq, Eq)]
884pub enum BuiltinCancellation {
885    /// The operation is its own inverse (`reverse().reverse()` = identity).
886    SelfInverse(BuiltinCancelGroup),
887    /// The operation has a paired inverse (encode/decode, escape/unescape).
888    Inverse {
889        /// Which encode/decode group this operation belongs to.
890        group: BuiltinCancelGroup,
891        /// Whether this is the forward (encoding) or backward (decoding) member.
892        side: BuiltinCancelSide,
893    },
894}
895
896/// Identifies which encode/decode pair a cancellation belongs to.
897#[derive(Debug, Clone, Copy, PartialEq, Eq)]
898pub enum BuiltinCancelGroup {
899    /// String reversal (`reverse_str` is self-inverse).
900    Reverse,
901    /// Base64 encode/decode pair.
902    Base64,
903    /// URL percent-encode/decode pair.
904    Url,
905    /// HTML escape/unescape pair.
906    Html,
907}
908
909/// Which side of a forward/backward cancellation pair this builtin occupies.
910#[derive(Debug, Clone, Copy, PartialEq, Eq)]
911pub enum BuiltinCancelSide {
912    /// The encoding or escaping direction.
913    Forward,
914    /// The decoding or unescaping direction.
915    Backward,
916}
917
918impl BuiltinCancellation {
919    /// Returns true if `self` and `other` are algebraically inverse and can be eliminated.
920    #[inline]
921    pub fn cancels_with(self, other: Self) -> bool {
922        match (self, other) {
923            (Self::SelfInverse(a), Self::SelfInverse(b)) => a == b,
924            (Self::Inverse { group: a, side: sa }, Self::Inverse { group: b, side: sb }) => {
925                a == b && sa != sb
926            }
927            _ => false,
928        }
929    }
930}
931
932impl BuiltinStageMerge {
933    /// Combines two stage arguments according to the merge rule.
934    #[inline]
935    pub fn combine_usize(self, a: usize, b: usize) -> usize {
936        match self {
937            Self::UsizeMin => a.min(b),
938            Self::UsizeSaturatingAdd => a.saturating_add(b),
939        }
940    }
941}
942
943impl BuiltinViewStage {
944    /// Returns whether this stage reads values from the source view or can skip it.
945    #[inline]
946    pub fn input_mode(self) -> BuiltinViewInputMode {
947        match self {
948            Self::Filter
949            | Self::Compact
950            | Self::RemoveValue
951            | Self::Map
952            | Self::FlatMap
953            | Self::TakeWhile
954            | Self::DropWhile
955            | Self::Distinct
956            | Self::KeyedReduce => BuiltinViewInputMode::ReadsView,
957            Self::Take | Self::Skip => BuiltinViewInputMode::SkipsViewRead,
958        }
959    }
960
961    /// Returns how this stage relates its output to the source view's memory.
962    #[inline]
963    pub fn output_mode(self) -> BuiltinViewOutputMode {
964        match self {
965            Self::Map => BuiltinViewOutputMode::BorrowedSubview,
966            Self::FlatMap => BuiltinViewOutputMode::BorrowedSubviews,
967            Self::KeyedReduce => BuiltinViewOutputMode::EmitsOwnedValue,
968            Self::Filter
969            | Self::Compact
970            | Self::RemoveValue
971            | Self::TakeWhile
972            | Self::DropWhile
973            | Self::Distinct
974            | Self::Take
975            | Self::Skip => BuiltinViewOutputMode::PreservesInputView,
976        }
977    }
978
979/// Returns the output row-count relationship of this stage.
980    #[inline]
981    pub fn cardinality(self) -> BuiltinCardinality {
982        match self {
983            Self::Filter | Self::Compact | Self::RemoveValue => BuiltinCardinality::Filtering,
984            Self::Map => BuiltinCardinality::OneToOne,
985            Self::FlatMap => BuiltinCardinality::Expanding,
986            Self::TakeWhile | Self::DropWhile => BuiltinCardinality::Filtering,
987            Self::Distinct => BuiltinCardinality::Filtering,
988            Self::KeyedReduce => BuiltinCardinality::Reducing,
989            Self::Take | Self::Skip => BuiltinCardinality::Bounded,
990        }
991    }
992
993    /// Returns whether this stage can participate in indexed (random-access) evaluation.
994    #[inline]
995    pub fn can_indexed(self) -> bool {
996        matches!(self, Self::Map | Self::KeyedReduce)
997    }
998
999    /// Returns the relative per-row cost estimate used by the planner.
1000    #[inline]
1001    pub fn cost(self) -> f64 {
1002        match self {
1003            Self::Filter
1004            | Self::Compact
1005            | Self::RemoveValue
1006            | Self::Map
1007            | Self::FlatMap
1008            | Self::TakeWhile
1009            | Self::DropWhile
1010            | Self::Distinct
1011            | Self::KeyedReduce => 10.0,
1012            Self::Take | Self::Skip => 0.5,
1013        }
1014    }
1015
1016    /// Returns the estimated output-to-input row ratio (1.0 = no change, 0.5 = half the rows).
1017    #[inline]
1018    pub fn selectivity(self) -> f64 {
1019        match self {
1020            Self::Filter | Self::Compact | Self::RemoveValue | Self::TakeWhile | Self::DropWhile => 0.5,
1021            Self::Distinct => 1.0,
1022            Self::Map | Self::FlatMap | Self::KeyedReduce => 1.0,
1023            Self::Take | Self::Skip => 0.5,
1024        }
1025    }
1026}
1027
1028/// Planning metadata for a builtin in the pipeline execution path.
1029/// The planner uses these fields to order and fuse pipeline stages.
1030#[derive(Debug, Clone, Copy, PartialEq)]
1031pub struct BuiltinPipelineShape {
1032    /// Row-count relationship of this stage.
1033    pub cardinality: BuiltinCardinality,
1034    /// Whether the stage supports indexed access.
1035    pub can_indexed: bool,
1036    /// Relative per-row cost used for ordering heuristics.
1037    pub cost: f64,
1038    /// Estimated output/input row ratio.
1039    pub selectivity: f64,
1040}
1041
1042/// When/how a pipeline stage materialises its output into a concrete `Vec<Val>`.
1043#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1044pub enum BuiltinPipelineMaterialization {
1045    /// Stage processes rows one-at-a-time without buffering.
1046    Streaming,
1047    /// Stage buffers all input (barrier), then emits via the composed path.
1048    ComposedBarrier,
1049    /// Stage uses the legacy full-materialisation path.
1050    LegacyMaterialized,
1051}
1052
1053/// Describes how a pipeline stage interacts with the ordering of its input stream.
1054#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1055pub enum BuiltinPipelineOrderEffect {
1056    /// Stage forwards rows in the same order it receives them.
1057    Preserves,
1058    /// Stage emits a contiguous prefix determined by a predicate (take_while, drop_while).
1059    PredicatePrefix,
1060    /// Stage may reorder or buffer all rows (sort, group_by, etc.).
1061    Blocks,
1062}
1063
1064/// Stage variant for columnar (typed-array) execution backends.
1065#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1066pub enum BuiltinColumnarStage {
1067    /// Columnar predicate filter.
1068    Filter,
1069    /// Columnar projection.
1070    Map,
1071    /// Columnar expansion.
1072    FlatMap,
1073    /// Columnar keyed grouping.
1074    GroupBy,
1075}
1076
1077impl BuiltinPipelineShape {
1078    /// Constructs a `BuiltinPipelineShape` from its four planning fields.
1079    #[inline]
1080    pub fn new(
1081        cardinality: BuiltinCardinality,
1082        can_indexed: bool,
1083        cost: f64,
1084        selectivity: f64,
1085    ) -> Self {
1086        Self {
1087            cardinality,
1088            can_indexed,
1089            cost,
1090            selectivity,
1091        }
1092    }
1093}
1094
1095/// Describes the *shape* of a builtin's lowering call (arg count + arg type).
1096///
1097/// The lowering routine dispatches first on this shape to validate args, then on
1098/// `BuiltinMethod` itself to emit the right `Stage` variant. Shape tags do not
1099/// duplicate method identity — the method is already passed alongside the spec.
1100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1101pub enum BuiltinPipelineLowering {
1102    /// One Expr argument (a lambda).
1103    ExprArg,
1104    /// One Expr argument followed by a terminal builtin that collapses the stream when last.
1105    TerminalExprArg {
1106        /// The terminal method applied after the stage.
1107        terminal: BuiltinMethod,
1108    },
1109    /// No arguments.
1110    Nullary,
1111    /// One `usize` argument with a minimum legal value.
1112    UsizeArg {
1113        /// Minimum legal argument value; arguments below this are rejected.
1114        min: usize,
1115    },
1116    /// One string argument.
1117    StringArg,
1118    /// Two string arguments.
1119    StringPairArg,
1120    /// One or two integer arguments (e.g. slice).
1121    IntRangeArg,
1122    /// Sort with optional key expression (zero or one arg).
1123    Sort,
1124    /// Terminal sink (no stage emitted).
1125    TerminalSink,
1126    /// Terminal sink with one `usize` argument (e.g. `nth(i)`).
1127    TerminalUsizeSink {
1128        /// Minimum legal argument value; arguments below this are rejected.
1129        min: usize,
1130    },
1131}
1132
1133/// Broad category for a builtin, used for grouping and display purposes.
1134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1135pub enum BuiltinCategory {
1136    /// Operates on a single scalar value (string transforms, math, type ops).
1137    Scalar,
1138    /// Streaming one-to-one transform over array elements (map, enumerate, etc.).
1139    StreamingOneToOne,
1140    /// Streaming predicate filter (filter, take_while, drop_while, compact, etc.).
1141    StreamingFilter,
1142    /// Streaming expansion (flat_map, flatten, explode, split, etc.).
1143    StreamingExpand,
1144    /// Reduces many rows to one value (sum, count, any, all, group_by, etc.).
1145    Reducer,
1146    /// Positional slice (first, last, nth, take, skip).
1147    Positional,
1148    /// Full barrier: must buffer all input before emitting (sort, reverse, window, etc.).
1149    Barrier,
1150    /// Object-manipulation builtin (pick, omit, merge, keys, values, etc.).
1151    Object,
1152    /// Dot-path navigation and mutation (get_path, set_path, del_path, etc.).
1153    Path,
1154    /// Deep tree traversal (deep_find, deep_shape, walk, rec, etc.).
1155    Deep,
1156    /// Serialisation / deserialisation (to_csv, to_json, from_json, etc.).
1157    Serialization,
1158    /// Set-theory or join operations across multiple collections (equi_join, etc.).
1159    Relational,
1160    /// In-place mutation chain write (set, update).
1161    Mutation,
1162    /// Category is not known at compile time.
1163    Unknown,
1164}
1165
1166/// Row-count relationship between a builtin's input and output.
1167/// Used by the pipeline planner to reason about stream length.
1168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1169pub enum BuiltinCardinality {
1170    /// Every input row produces exactly one output row.
1171    OneToOne,
1172    /// Output has at most as many rows as the input (subset).
1173    Filtering,
1174    /// Output may have more rows than the input (flat_map, flatten, etc.).
1175    Expanding,
1176    /// Output is bounded by a fixed constant regardless of input size.
1177    Bounded,
1178    /// Multiple input rows collapse to one output value.
1179    Reducing,
1180    /// Must buffer the full input stream before emitting; output size may vary.
1181    Barrier,
1182}
1183
1184impl BuiltinSpec {
1185    /// Creates a minimal `BuiltinSpec` with sensible defaults (pure, cost 1.0, no optional features).
1186    fn new(category: BuiltinCategory, cardinality: BuiltinCardinality) -> Self {
1187        Self {
1188            pure: true,
1189            category,
1190            cardinality,
1191            can_indexed: false,
1192            view_native: false,
1193            view_scalar: false,
1194            view_stage: None,
1195            sink: None,
1196            keyed_reducer: None,
1197            numeric_reducer: None,
1198            stage_merge: None,
1199            cancellation: None,
1200            columnar_stage: None,
1201            structural: None,
1202            cost: 1.0,
1203            demand_law: BuiltinDemandLaw::Identity,
1204            materialization: BuiltinPipelineMaterialization::Streaming,
1205            pipeline_shape: None,
1206            order_effect: None,
1207            lowering: None,
1208            is_element: false,
1209            never_unwrap: false,
1210            stream_source: false,
1211        }
1212    }
1213
1214    /// Returns `true` when `$.path.method()` should bypass pipeline streaming
1215    /// and dispatch as a direct `apply_one` (or `apply_args`) call on the
1216    /// single value produced by the chain. Eligibility covers scalar and
1217    /// object one-to-one builtins (e.g. `upper`, `type`, `ceil`, `omit`,
1218    /// `transform_values`) that have not opted out via `never_unwrap`.
1219    /// Streaming and reducing categories keep pipeline lowering — their
1220    /// per-element behavior is the canonical semantic on path receivers.
1221    pub fn dispatches_scalar_direct(&self) -> bool {
1222        matches!(
1223            self.category,
1224            BuiltinCategory::Scalar | BuiltinCategory::Object
1225        ) && matches!(self.cardinality, BuiltinCardinality::OneToOne)
1226            && !self.never_unwrap
1227    }
1228
1229    /// Marks this builtin as safe for indexed (random-access) evaluation.
1230    fn indexed(mut self) -> Self {
1231        self.can_indexed = true;
1232        self
1233    }
1234
1235    /// Marks this builtin as having a native view-path implementation.
1236    fn view_native(mut self) -> Self {
1237        self.view_native = true;
1238        self
1239    }
1240
1241    /// Attaches the view stage lowering target for this builtin.
1242    fn view_stage(mut self, stage: BuiltinViewStage) -> Self {
1243        self.view_stage = Some(stage);
1244        self
1245    }
1246
1247    /// Marks this builtin as a view-scalar method (implies `view_native`).
1248    fn view_scalar(mut self) -> Self {
1249        self.view_scalar = true;
1250        self.view_native = true;
1251        self
1252    }
1253
1254    /// Attaches a columnar stage kind for typed-array execution backends.
1255    fn columnar_stage(mut self, stage: BuiltinColumnarStage) -> Self {
1256        self.columnar_stage = Some(stage);
1257        self
1258    }
1259
1260    /// Configures a counting sink (demand all rows, value not needed, order-insensitive).
1261    fn count_sink(mut self) -> Self {
1262        self.sink = Some(BuiltinSinkSpec {
1263            accumulator: BuiltinSinkAccumulator::Count,
1264            demand: BuiltinSinkDemand::All {
1265                value: BuiltinSinkValueNeed::None,
1266                order: false,
1267            },
1268        });
1269        self
1270    }
1271
1272    /// Configures a select-one sink that picks the first or last row.
1273    fn select_one_sink(mut self, position: BuiltinSelectionPosition) -> Self {
1274        self.sink = Some(BuiltinSinkSpec {
1275            accumulator: BuiltinSinkAccumulator::SelectOne(position),
1276            demand: match position {
1277                BuiltinSelectionPosition::First => BuiltinSinkDemand::First {
1278                    value: BuiltinSinkValueNeed::Whole,
1279                },
1280                BuiltinSelectionPosition::Last => BuiltinSinkDemand::Last {
1281                    value: BuiltinSinkValueNeed::Whole,
1282                },
1283            },
1284        });
1285        self
1286    }
1287
1288    /// Configures a numeric sink (sum, avg, min, max) that needs numeric values from every row.
1289    fn numeric_sink(mut self, reducer: BuiltinNumericReducer) -> Self {
1290        self.sink = Some(BuiltinSinkSpec {
1291            accumulator: BuiltinSinkAccumulator::Numeric,
1292            demand: BuiltinSinkDemand::All {
1293                value: BuiltinSinkValueNeed::Numeric,
1294                order: false,
1295            },
1296        });
1297        self.numeric_reducer = Some(reducer);
1298        self
1299    }
1300
1301    /// Configures an approximate distinct-count sink.
1302    fn approx_distinct_sink(mut self) -> Self {
1303        self.sink = Some(BuiltinSinkSpec {
1304            accumulator: BuiltinSinkAccumulator::ApproxDistinct,
1305            demand: BuiltinSinkDemand::All {
1306                value: BuiltinSinkValueNeed::Whole,
1307                order: false,
1308            },
1309        });
1310        self
1311    }
1312
1313    /// Attaches a keyed reducer kind (group, count, or index).
1314    fn keyed_reducer(mut self, reducer: BuiltinKeyedReducer) -> Self {
1315        self.keyed_reducer = Some(reducer);
1316        self
1317    }
1318
1319    /// Attaches a stage-merge rule so adjacent identical stages can be collapsed.
1320    fn stage_merge(mut self, merge: BuiltinStageMerge) -> Self {
1321        self.stage_merge = Some(merge);
1322        self
1323    }
1324
1325    /// Attaches an algebraic cancellation rule for this builtin.
1326    fn cancellation(mut self, cancellation: BuiltinCancellation) -> Self {
1327        self.cancellation = Some(cancellation);
1328        self
1329    }
1330
1331    /// Marks this builtin as having a structural index backend.
1332    fn structural(mut self, structural: BuiltinStructural) -> Self {
1333        self.structural = Some(structural);
1334        self
1335    }
1336
1337    /// Overrides the default relative cost estimate.
1338    fn cost(mut self, cost: f64) -> Self {
1339        self.cost = cost;
1340        self
1341    }
1342
1343    /// Sets the demand-propagation law for pipeline planning.
1344    fn demand_law(mut self, law: BuiltinDemandLaw) -> Self {
1345        self.demand_law = law;
1346        self
1347    }
1348
1349    /// Sets the materialization policy.
1350    fn materialization(mut self, m: BuiltinPipelineMaterialization) -> Self {
1351        self.materialization = m;
1352        self
1353    }
1354
1355    /// Sets the cardinality/cost pipeline shape annotation.
1356    fn pipeline_shape(mut self, s: BuiltinPipelineShape) -> Self {
1357        self.pipeline_shape = Some(s);
1358        self
1359    }
1360
1361    /// Sets the ordering-effect annotation.
1362    fn order_effect(mut self, o: BuiltinPipelineOrderEffect) -> Self {
1363        self.order_effect = Some(o);
1364        self
1365    }
1366
1367    /// Sets the physical stage lowering strategy.
1368    fn lowering(mut self, l: BuiltinPipelineLowering) -> Self {
1369        self.lowering = Some(l);
1370        self
1371    }
1372
1373    /// Marks this builtin as element-wise vectorisable.
1374    fn element(mut self) -> Self {
1375        self.is_element = true;
1376        self
1377    }
1378
1379    /// Opts this builtin out of the path-receiver scalar-unwrap rewrite. The
1380    /// pipeline-streaming lowering remains the canonical path for
1381    /// `$.path.method()`, even when category/cardinality would otherwise be
1382    /// eligible for direct dispatch.
1383    #[allow(dead_code)]
1384    fn never_unwrap(mut self) -> Self {
1385        self.never_unwrap = true;
1386        self
1387    }
1388
1389    /// Marks this builtin as a stream source boundary.
1390    fn stream_source(mut self) -> Self {
1391        self.stream_source = true;
1392        self
1393    }
1394}
1395
1396impl BuiltinMethod {
1397    /// Returns true for string scalar methods that take a single string argument and
1398    /// can execute directly on a `JsonView` without materialising the receiver.
1399    #[inline]
1400    pub(crate) fn is_string_arg_view_scalar(self) -> bool {
1401        matches!(
1402            self,
1403            Self::StartsWith | Self::EndsWith | Self::Matches | Self::IndexOf | Self::LastIndexOf
1404        )
1405    }
1406
1407    /// Returns true for zero-argument string scalar methods that can execute on a `JsonView`.
1408    #[inline]
1409    pub(crate) fn is_string_no_arg_view_scalar(self) -> bool {
1410        matches!(
1411            self,
1412            Self::Upper
1413                | Self::Lower
1414                | Self::Trim
1415                | Self::TrimLeft
1416                | Self::TrimRight
1417                | Self::ByteLen
1418                | Self::IsBlank
1419                | Self::IsNumeric
1420                | Self::IsAlpha
1421                | Self::IsAscii
1422                | Self::ToNumber
1423                | Self::ToBool
1424        )
1425    }
1426
1427    /// Returns true for zero-argument numeric scalar methods that can execute on a `JsonView`.
1428    #[inline]
1429    pub(crate) fn is_numeric_no_arg_view_scalar(self) -> bool {
1430        matches!(self, Self::Ceil | Self::Floor | Self::Round | Self::Abs)
1431    }
1432
1433    /// Returns true if this method can be evaluated on a raw `JsonView` without materialising.
1434    #[inline]
1435    pub(crate) fn is_view_scalar_method(self) -> bool {
1436        self == Self::Len
1437            || self.is_string_arg_view_scalar()
1438            || self.is_string_no_arg_view_scalar()
1439            || self.is_numeric_no_arg_view_scalar()
1440    }
1441
1442    /// Returns true for object/path membership helpers that can execute on a
1443    /// borrowed `JsonView` without materialising the receiver object.
1444    #[inline]
1445    pub(crate) fn is_view_object_key_method(self) -> bool {
1446        matches!(
1447            self,
1448            Self::Has | Self::HasKey | Self::Missing | Self::GetPath | Self::HasPath
1449        )
1450    }
1451
1452    /// Returns true when this builtin can be composed into a view-native
1453    /// projection kernel without materialising the receiver row.
1454    #[inline]
1455    pub(crate) fn is_view_projection_method(self) -> bool {
1456        self.spec().view_scalar || self.is_view_object_key_method()
1457    }
1458
1459    /// Returns the full capability descriptor for this builtin.
1460    /// Called by the pipeline planner and VM to query cardinality, cost, and feature flags.
1461    #[inline]
1462    pub fn spec(self) -> BuiltinSpec {
1463        macro_rules! spec_arm {
1464            ( $( $variant:ident ),* $(,)? ) => {
1465                match self {
1466                    $( Self::$variant => <defs::$variant as builtin::Builtin>::spec(), )*
1467                }
1468            };
1469        }
1470        let spec = crate::for_each_builtin!(spec_arm);
1471        // Apply per-method cancellation override (defaults to None for most methods).
1472        macro_rules! cancel_arm {
1473            ( $( $variant:ident ),* $(,)? ) => {
1474                match self {
1475                    $( Self::$variant => <defs::$variant as builtin::Builtin>::cancellation(), )*
1476                }
1477            };
1478        }
1479        match crate::for_each_builtin!(cancel_arm) {
1480            Some(c) => spec.cancellation(c),
1481            None => spec,
1482        }
1483    }
1484}
1485
1486impl BuiltinCall {
1487    /// Constructs a `BuiltinCall` from a resolved method and its decoded arguments.
1488    #[inline]
1489    pub fn new(method: BuiltinMethod, args: BuiltinArgs) -> Self {
1490        Self { method, args }
1491    }
1492
1493    /// Returns the capability descriptor for this call, potentially overriding the
1494    /// method-level spec with argument-specific cost or indexability adjustments.
1495    #[inline]
1496    pub fn spec(&self) -> BuiltinSpec {
1497        let mut spec = self.method.spec();
1498        let (cost, can_indexed) = match self.method {
1499            BuiltinMethod::Keys | BuiltinMethod::Values | BuiltinMethod::Entries => (1.0, false),
1500            BuiltinMethod::Repeat
1501            | BuiltinMethod::Indent
1502            | BuiltinMethod::PadLeft
1503            | BuiltinMethod::PadRight
1504            | BuiltinMethod::Center => (2.0, true),
1505            BuiltinMethod::IndexOf
1506            | BuiltinMethod::LastIndexOf
1507            | BuiltinMethod::Scan
1508            | BuiltinMethod::StartsWith
1509            | BuiltinMethod::EndsWith
1510            | BuiltinMethod::StripPrefix
1511            | BuiltinMethod::StripSuffix
1512            | BuiltinMethod::Matches
1513            | BuiltinMethod::ReMatch
1514            | BuiltinMethod::ReMatchFirst
1515            | BuiltinMethod::ReMatchAll
1516            | BuiltinMethod::ReCaptures
1517            | BuiltinMethod::ReCapturesAll
1518            | BuiltinMethod::ReSplit
1519            | BuiltinMethod::ReReplace
1520            | BuiltinMethod::ReReplaceAll
1521            | BuiltinMethod::ContainsAny
1522            | BuiltinMethod::ContainsAll => (2.0, true),
1523            _ => (spec.cost, spec.can_indexed),
1524        };
1525        spec.cost = cost;
1526        spec.can_indexed = can_indexed;
1527        spec
1528    }
1529
1530    /// Returns true if applying this builtin twice is equivalent to applying it once.
1531    /// The pipeline optimizer uses this to eliminate redundant stages.
1532    #[inline]
1533    pub fn is_idempotent(&self) -> bool {
1534        matches!(
1535            self.method,
1536            BuiltinMethod::Upper
1537                | BuiltinMethod::Lower
1538                | BuiltinMethod::Trim
1539                | BuiltinMethod::TrimLeft
1540                | BuiltinMethod::TrimRight
1541                | BuiltinMethod::Capitalize
1542                | BuiltinMethod::TitleCase
1543                | BuiltinMethod::SnakeCase
1544                | BuiltinMethod::KebabCase
1545                | BuiltinMethod::CamelCase
1546                | BuiltinMethod::PascalCase
1547                | BuiltinMethod::Dedent
1548        )
1549    }
1550
1551    /// Executes the builtin against `recv` with its pre-decoded static arguments.
1552    /// Returns `None` when the receiver type is not applicable (caller may fall back).
1553    /// For methods that can return errors, prefer [`BuiltinCall::try_apply`].
1554    pub fn apply(&self, recv: &Val) -> Option<Val> {
1555        macro_rules! apply_or_recv {
1556            ($expr:expr) => {
1557                return Some($expr.unwrap_or_else(|| recv.clone()))
1558            };
1559        }
1560        // Try trait dispatch first. Each migrated builtin overrides `apply_one` (no-arg)
1561        // or `apply_args` (any-args). Both default to `None`, falling through to legacy.
1562        macro_rules! trait_arm {
1563            ( $( $variant:ident ),* $(,)? ) => {
1564                match self.method {
1565                    $( BuiltinMethod::$variant => {
1566                        if matches!(self.args, BuiltinArgs::None) {
1567                            if let Some(v) = <defs::$variant as builtin::Builtin>::apply_one(recv) {
1568                                return Some(v);
1569                            }
1570                        }
1571                        if let Some(v) = <defs::$variant as builtin::Builtin>::apply_args(recv, &self.args) {
1572                            return Some(v);
1573                        }
1574                    } )*
1575                }
1576            };
1577        }
1578        crate::for_each_builtin!(trait_arm);
1579        match (self.method, &self.args) {
1580            (BuiltinMethod::ByteLen, BuiltinArgs::None)
1581            | (BuiltinMethod::IsBlank, BuiltinArgs::None)
1582            | (BuiltinMethod::IsNumeric, BuiltinArgs::None)
1583            | (BuiltinMethod::IsAlpha, BuiltinArgs::None)
1584            | (BuiltinMethod::IsAscii, BuiltinArgs::None)
1585            | (BuiltinMethod::ToNumber, BuiltinArgs::None)
1586            | (BuiltinMethod::ToBool, BuiltinArgs::None) => {
1587                apply_or_recv!(str_no_arg_scalar_val_apply(self.method, recv))
1588            }
1589            (BuiltinMethod::Sum, BuiltinArgs::None)
1590            | (BuiltinMethod::Avg, BuiltinArgs::None)
1591            | (BuiltinMethod::Min, BuiltinArgs::None)
1592            | (BuiltinMethod::Max, BuiltinArgs::None) => {
1593                return Some(numeric_aggregate_apply(recv, self.method));
1594            }
1595            (BuiltinMethod::Len, BuiltinArgs::None) | (BuiltinMethod::Count, BuiltinArgs::None) => {
1596                apply_or_recv!(len_apply(recv))
1597            }
1598            (BuiltinMethod::Keys, BuiltinArgs::None) => return Some(keys_apply(recv)),
1599            (BuiltinMethod::Values, BuiltinArgs::None) => return Some(values_apply(recv)),
1600            (BuiltinMethod::Entries, BuiltinArgs::None) => return Some(entries_apply(recv)),
1601            (BuiltinMethod::Collect, BuiltinArgs::None) => return Some(collect_apply(recv)),
1602            (BuiltinMethod::FromJson, BuiltinArgs::None) => return from_json_apply(recv),
1603            (BuiltinMethod::Ceil, BuiltinArgs::None)
1604            | (BuiltinMethod::Floor, BuiltinArgs::None)
1605            | (BuiltinMethod::Round, BuiltinArgs::None)
1606            | (BuiltinMethod::Abs, BuiltinArgs::None) => {
1607                return numeric_no_arg_scalar_val_apply(self.method, recv)
1608            }
1609            (BuiltinMethod::Or, BuiltinArgs::Val(default)) => return Some(or_apply(recv, default)),
1610            (BuiltinMethod::Missing, BuiltinArgs::Str(k)) => return Some(missing_apply(recv, k)),
1611            (BuiltinMethod::Includes, BuiltinArgs::Val(item)) => {
1612                return Some(includes_apply(recv, item))
1613            }
1614            (BuiltinMethod::Index, BuiltinArgs::Val(item)) => return index_value_apply(recv, item),
1615            (BuiltinMethod::IndicesOf, BuiltinArgs::Val(item)) => {
1616                return indices_of_apply(recv, item)
1617            }
1618            (BuiltinMethod::Set, BuiltinArgs::Val(item)) => return Some(item.clone()),
1619            (BuiltinMethod::Join, BuiltinArgs::Str(sep)) => return join_apply(recv, sep),
1620            (BuiltinMethod::Enumerate, BuiltinArgs::None) => return enumerate_apply(recv),
1621            (BuiltinMethod::Flatten, BuiltinArgs::Usize(depth)) => {
1622                apply_or_recv!(flatten_depth_apply(recv, *depth))
1623            }
1624            (BuiltinMethod::First, BuiltinArgs::I64(n)) => apply_or_recv!(first_apply(recv, *n)),
1625            (BuiltinMethod::Last, BuiltinArgs::I64(n)) => apply_or_recv!(last_apply(recv, *n)),
1626            (BuiltinMethod::Nth, BuiltinArgs::I64(n)) => apply_or_recv!(nth_any_apply(recv, *n)),
1627            (BuiltinMethod::Append, BuiltinArgs::Val(item)) => {
1628                apply_or_recv!(append_apply(recv, item))
1629            }
1630            (BuiltinMethod::Prepend, BuiltinArgs::Val(item)) => {
1631                apply_or_recv!(prepend_apply(recv, item))
1632            }
1633            (BuiltinMethod::Remove, BuiltinArgs::Val(item)) => {
1634                apply_or_recv!(remove_value_apply(recv, item))
1635            }
1636            (BuiltinMethod::Diff, BuiltinArgs::ValVec(other)) => {
1637                let arr_recv = recv.clone().into_vec().map(Val::arr)?;
1638                apply_or_recv!(diff_apply(&arr_recv, other))
1639            }
1640            (BuiltinMethod::Intersect, BuiltinArgs::ValVec(other)) => {
1641                let arr_recv = recv.clone().into_vec().map(Val::arr)?;
1642                apply_or_recv!(intersect_apply(&arr_recv, other))
1643            }
1644            (BuiltinMethod::Union, BuiltinArgs::ValVec(other)) => {
1645                let arr_recv = recv.clone().into_vec().map(Val::arr)?;
1646                apply_or_recv!(union_apply(&arr_recv, other))
1647            }
1648            (BuiltinMethod::Window, BuiltinArgs::Usize(n)) => {
1649                let arr_recv = recv.clone().into_vec().map(Val::arr)?;
1650                apply_or_recv!(window_arr_apply(&arr_recv, *n))
1651            }
1652            (BuiltinMethod::Chunk, BuiltinArgs::Usize(n)) => {
1653                let arr_recv = recv.clone().into_vec().map(Val::arr)?;
1654                apply_or_recv!(chunk_arr_apply(&arr_recv, *n))
1655            }
1656            (BuiltinMethod::RollingSum, BuiltinArgs::Usize(n)) => {
1657                apply_or_recv!(rolling_sum_apply(recv, *n))
1658            }
1659            (BuiltinMethod::RollingAvg, BuiltinArgs::Usize(n)) => {
1660                apply_or_recv!(rolling_avg_apply(recv, *n))
1661            }
1662            (BuiltinMethod::RollingMin, BuiltinArgs::Usize(n)) => {
1663                apply_or_recv!(rolling_min_apply(recv, *n))
1664            }
1665            (BuiltinMethod::RollingMax, BuiltinArgs::Usize(n)) => {
1666                apply_or_recv!(rolling_max_apply(recv, *n))
1667            }
1668            (BuiltinMethod::Lag, BuiltinArgs::Usize(n)) => apply_or_recv!(lag_apply(recv, *n)),
1669            (BuiltinMethod::Lead, BuiltinArgs::Usize(n)) => apply_or_recv!(lead_apply(recv, *n)),
1670            (BuiltinMethod::Merge, BuiltinArgs::Val(other)) => {
1671                apply_or_recv!(merge_apply(recv, other))
1672            }
1673            (BuiltinMethod::DeepMerge, BuiltinArgs::Val(other)) => {
1674                apply_or_recv!(deep_merge_apply(recv, other))
1675            }
1676            (BuiltinMethod::Defaults, BuiltinArgs::Val(other)) => {
1677                apply_or_recv!(defaults_apply(recv, other))
1678            }
1679            (BuiltinMethod::Rename, BuiltinArgs::Val(other)) => {
1680                apply_or_recv!(rename_apply(recv, other))
1681            }
1682            (BuiltinMethod::Explode, BuiltinArgs::Str(field)) => {
1683                apply_or_recv!(explode_apply(recv, field))
1684            }
1685            (BuiltinMethod::Implode, BuiltinArgs::Str(field)) => {
1686                apply_or_recv!(implode_apply(recv, field))
1687            }
1688            (BuiltinMethod::Has, BuiltinArgs::Str(k)) => {
1689                apply_or_recv!(has_apply(recv, k))
1690            }
1691            (BuiltinMethod::HasAll, BuiltinArgs::Val(v)) => {
1692                apply_or_recv!(has_all_apply(recv, v))
1693            }
1694            (BuiltinMethod::HasAll, BuiltinArgs::StrVec(keys)) => {
1695                apply_or_recv!(has_all_keys_apply(recv, keys))
1696            }
1697            (BuiltinMethod::HasKey, BuiltinArgs::Str(k)) => return Some(has_key_apply(recv, k)),
1698            (BuiltinMethod::GetPath, BuiltinArgs::Str(p)) => {
1699                apply_or_recv!(get_path_apply(recv, p))
1700            }
1701            (BuiltinMethod::GetPath, BuiltinArgs::Path(path)) => {
1702                return Some(get_path_impl(recv, path))
1703            }
1704            (BuiltinMethod::HasPath, BuiltinArgs::Str(p)) => {
1705                apply_or_recv!(has_path_apply(recv, p))
1706            }
1707            (BuiltinMethod::HasPath, BuiltinArgs::Path(path)) => {
1708                return Some(Val::Bool(!get_path_impl(recv, path).is_null()))
1709            }
1710            (BuiltinMethod::DelPath, BuiltinArgs::Str(p)) => {
1711                apply_or_recv!(del_path_apply(recv, p))
1712            }
1713            (BuiltinMethod::FlattenKeys, BuiltinArgs::Str(p)) => {
1714                apply_or_recv!(flatten_keys_apply(recv, p))
1715            }
1716            (BuiltinMethod::UnflattenKeys, BuiltinArgs::Str(p)) => {
1717                apply_or_recv!(unflatten_keys_apply(recv, p))
1718            }
1719            (BuiltinMethod::StartsWith, BuiltinArgs::Str(p))
1720            | (BuiltinMethod::EndsWith, BuiltinArgs::Str(p))
1721            | (BuiltinMethod::Matches, BuiltinArgs::Str(p))
1722            | (BuiltinMethod::IndexOf, BuiltinArgs::Str(p))
1723            | (BuiltinMethod::LastIndexOf, BuiltinArgs::Str(p)) => {
1724                apply_or_recv!(str_arg_scalar_val_apply(self.method, recv, p))
1725            }
1726            (BuiltinMethod::StripPrefix, BuiltinArgs::Str(p)) => {
1727                apply_or_recv!(strip_prefix_apply(recv, p))
1728            }
1729            (BuiltinMethod::StripSuffix, BuiltinArgs::Str(p)) => {
1730                apply_or_recv!(strip_suffix_apply(recv, p))
1731            }
1732            (BuiltinMethod::Scan, BuiltinArgs::Str(p)) => apply_or_recv!(scan_apply(recv, p)),
1733            (BuiltinMethod::Split, BuiltinArgs::Str(p)) => apply_or_recv!(split_apply(recv, p)),
1734            (BuiltinMethod::Slice, BuiltinArgs::I64Opt { first, second }) => {
1735                return Some(slice_apply(recv.clone(), *first, *second));
1736            }
1737            (BuiltinMethod::Replace, BuiltinArgs::StrPair { first, second }) => {
1738                apply_or_recv!(replace_apply(recv.clone(), first, second, false))
1739            }
1740            (BuiltinMethod::ReplaceAll, BuiltinArgs::StrPair { first, second }) => {
1741                apply_or_recv!(replace_apply(recv.clone(), first, second, true))
1742            }
1743            (BuiltinMethod::ReMatch, BuiltinArgs::Str(p)) => {
1744                apply_or_recv!(re_match_apply(recv, p))
1745            }
1746            (BuiltinMethod::ReMatchFirst, BuiltinArgs::Str(p)) => {
1747                apply_or_recv!(re_match_first_apply(recv, p))
1748            }
1749            (BuiltinMethod::ReMatchAll, BuiltinArgs::Str(p)) => {
1750                apply_or_recv!(re_match_all_apply(recv, p))
1751            }
1752            (BuiltinMethod::ReCaptures, BuiltinArgs::Str(p)) => {
1753                apply_or_recv!(re_captures_apply(recv, p))
1754            }
1755            (BuiltinMethod::ReCapturesAll, BuiltinArgs::Str(p)) => {
1756                apply_or_recv!(re_captures_all_apply(recv, p))
1757            }
1758            (BuiltinMethod::ReSplit, BuiltinArgs::Str(p)) => {
1759                apply_or_recv!(re_split_apply(recv, p))
1760            }
1761            (BuiltinMethod::ReReplace, BuiltinArgs::StrPair { first, second }) => {
1762                apply_or_recv!(re_replace_apply(recv, first, second))
1763            }
1764            (BuiltinMethod::ReReplaceAll, BuiltinArgs::StrPair { first, second }) => {
1765                apply_or_recv!(re_replace_all_apply(recv, first, second))
1766            }
1767            (BuiltinMethod::ContainsAny, BuiltinArgs::StrVec(ns)) => {
1768                apply_or_recv!(contains_any_apply(recv, ns))
1769            }
1770            (BuiltinMethod::ContainsAll, BuiltinArgs::StrVec(ns)) => {
1771                apply_or_recv!(contains_all_apply(recv, ns))
1772            }
1773            (BuiltinMethod::Pick, BuiltinArgs::StrVec(keys)) => {
1774                apply_or_recv!(pick_apply(recv, keys))
1775            }
1776            (BuiltinMethod::Omit, BuiltinArgs::StrVec(keys)) => {
1777                apply_or_recv!(omit_apply(recv, keys))
1778            }
1779            (BuiltinMethod::Repeat, BuiltinArgs::Usize(n)) => {
1780                apply_or_recv!(repeat_apply(recv, *n))
1781            }
1782            (BuiltinMethod::Indent, BuiltinArgs::Usize(n)) => {
1783                apply_or_recv!(indent_apply(recv, *n))
1784            }
1785            (BuiltinMethod::Indent, BuiltinArgs::Str(prefix)) => {
1786                apply_or_recv!(indent_with_prefix_apply(recv, prefix.as_ref()))
1787            }
1788            (BuiltinMethod::PadLeft, BuiltinArgs::Pad { width, fill }) => {
1789                apply_or_recv!(pad_left_apply(recv, *width, *fill))
1790            }
1791            (BuiltinMethod::PadRight, BuiltinArgs::Pad { width, fill }) => {
1792                apply_or_recv!(pad_right_apply(recv, *width, *fill))
1793            }
1794            (BuiltinMethod::Center, BuiltinArgs::Pad { width, fill }) => {
1795                apply_or_recv!(center_apply(recv, *width, *fill))
1796            }
1797            _ => None,
1798        }
1799    }
1800
1801    /// Like [`BuiltinCall::apply`] but propagates evaluation errors (regex compilation,
1802    /// window-size-zero, JSON parse failures, etc.) as `EvalError`.
1803    pub fn try_apply(&self, recv: &Val) -> Result<Option<Val>, EvalError> {
1804        match (self.method, &self.args) {
1805            (BuiltinMethod::ReMatch, BuiltinArgs::Str(p)) => try_re_match_apply(recv, p),
1806            (BuiltinMethod::ReMatchFirst, BuiltinArgs::Str(p)) => try_re_match_first_apply(recv, p),
1807            (BuiltinMethod::ReMatchAll, BuiltinArgs::Str(p)) => try_re_match_all_apply(recv, p),
1808            (BuiltinMethod::ReCaptures, BuiltinArgs::Str(p)) => try_re_captures_apply(recv, p),
1809            (BuiltinMethod::ReCapturesAll, BuiltinArgs::Str(p)) => {
1810                try_re_captures_all_apply(recv, p)
1811            }
1812            (BuiltinMethod::ReSplit, BuiltinArgs::Str(p)) => try_re_split_apply(recv, p),
1813            (BuiltinMethod::ReReplace, BuiltinArgs::StrPair { first, second }) => {
1814                try_re_replace_apply(recv, first, second)
1815            }
1816            (BuiltinMethod::ReReplaceAll, BuiltinArgs::StrPair { first, second }) => {
1817                try_re_replace_all_apply(recv, first, second)
1818            }
1819            (BuiltinMethod::FromJson, BuiltinArgs::None) => try_from_json_apply(recv),
1820            (BuiltinMethod::Join, BuiltinArgs::Str(sep)) => join_apply(recv, sep)
1821                .map(Some)
1822                .ok_or_else(|| EvalError("join: expected array".into())),
1823            (BuiltinMethod::Enumerate, BuiltinArgs::None) => enumerate_apply(recv)
1824                .map(Some)
1825                .ok_or_else(|| EvalError("enumerate: expected array".into())),
1826            (BuiltinMethod::Sort, BuiltinArgs::None) => sort_apply(recv.clone()).map(Some),
1827            (BuiltinMethod::Index, BuiltinArgs::Val(item)) => index_value_apply(recv, item)
1828                .map(Some)
1829                .ok_or_else(|| EvalError("index: expected array".into())),
1830            (BuiltinMethod::IndicesOf, BuiltinArgs::Val(item)) => indices_of_apply(recv, item)
1831                .map(Some)
1832                .ok_or_else(|| EvalError("indices_of: expected array".into())),
1833            (BuiltinMethod::Ceil, BuiltinArgs::None) => try_ceil_apply(recv),
1834            (BuiltinMethod::Floor, BuiltinArgs::None) => try_floor_apply(recv),
1835            (BuiltinMethod::Round, BuiltinArgs::None) => try_round_apply(recv),
1836            (BuiltinMethod::Abs, BuiltinArgs::None) => try_abs_apply(recv),
1837            (BuiltinMethod::RollingSum, BuiltinArgs::Usize(0)) => {
1838                Err(EvalError("rolling_sum: window must be > 0".into()))
1839            }
1840            (BuiltinMethod::RollingAvg, BuiltinArgs::Usize(0)) => {
1841                Err(EvalError("rolling_avg: window must be > 0".into()))
1842            }
1843            (BuiltinMethod::RollingMin, BuiltinArgs::Usize(0)) => {
1844                Err(EvalError("rolling_min: window must be > 0".into()))
1845            }
1846            (BuiltinMethod::RollingMax, BuiltinArgs::Usize(0)) => {
1847                Err(EvalError("rolling_max: window must be > 0".into()))
1848            }
1849            (BuiltinMethod::RollingSum, BuiltinArgs::Usize(_))
1850            | (BuiltinMethod::RollingAvg, BuiltinArgs::Usize(_))
1851            | (BuiltinMethod::RollingMin, BuiltinArgs::Usize(_))
1852            | (BuiltinMethod::RollingMax, BuiltinArgs::Usize(_))
1853            | (BuiltinMethod::Lag, BuiltinArgs::Usize(_))
1854            | (BuiltinMethod::Lead, BuiltinArgs::Usize(_))
1855            | (BuiltinMethod::DiffWindow, BuiltinArgs::None)
1856            | (BuiltinMethod::PctChange, BuiltinArgs::None)
1857            | (BuiltinMethod::CumMax, BuiltinArgs::None)
1858            | (BuiltinMethod::CumMin, BuiltinArgs::None)
1859            | (BuiltinMethod::Zscore, BuiltinArgs::None) => self
1860                .apply(recv)
1861                .map(Some)
1862                .ok_or_else(|| EvalError("expected numeric array".into())),
1863            _ => Ok(self.apply(recv)),
1864        }
1865    }
1866
1867    /// Decodes static (non-lambda) arguments for `method` and constructs a `BuiltinCall`.
1868    /// `eval_arg` evaluates positional argument expressions; `ident_arg` extracts bare
1869    /// identifier names (used to accept field names without quote syntax).
1870    /// Returns `Ok(None)` for methods that require lambda arguments (handled separately).
1871    pub fn from_static_args<E, I>(
1872        method: BuiltinMethod,
1873        name: &str,
1874        arg_len: usize,
1875        eval_arg: E,
1876        ident_arg: I,
1877    ) -> Result<Option<Self>, EvalError>
1878    where
1879        E: FnMut(usize) -> Result<Option<Val>, EvalError>,
1880        I: FnMut(usize) -> Option<Arc<str>>,
1881    {
1882        if method == BuiltinMethod::Unknown {
1883            return Ok(None);
1884        }
1885
1886        let mut args = StaticArgDecoder {
1887            name,
1888            eval_arg,
1889            ident_arg,
1890        };
1891
1892        let call = match method {
1893            BuiltinMethod::Flatten => {
1894                let depth = if arg_len > 0 { args.usize(0)? } else { 1 };
1895                Self::new(method, BuiltinArgs::Usize(depth))
1896            }
1897            BuiltinMethod::First | BuiltinMethod::Last => {
1898                let n = if arg_len > 0 { args.i64(0)? } else { 1 };
1899                Self::new(method, BuiltinArgs::I64(n))
1900            }
1901            BuiltinMethod::Nth => Self::new(method, BuiltinArgs::I64(args.i64(0)?)),
1902            BuiltinMethod::Take | BuiltinMethod::Skip => {
1903                Self::new(method, BuiltinArgs::Usize(args.usize(0)?))
1904            }
1905            BuiltinMethod::Append | BuiltinMethod::Prepend | BuiltinMethod::Set => {
1906                let item = if arg_len > 0 { args.val(0)? } else { Val::Null };
1907                Self::new(method, BuiltinArgs::Val(item))
1908            }
1909            BuiltinMethod::Or => {
1910                let default = if arg_len > 0 { args.val(0)? } else { Val::Null };
1911                Self::new(method, BuiltinArgs::Val(default))
1912            }
1913            BuiltinMethod::Includes | BuiltinMethod::Index | BuiltinMethod::IndicesOf => {
1914                Self::new(method, BuiltinArgs::Val(args.val(0)?))
1915            }
1916            BuiltinMethod::Diff | BuiltinMethod::Intersect | BuiltinMethod::Union => {
1917                Self::new(method, BuiltinArgs::ValVec(args.vec(0)?))
1918            }
1919            BuiltinMethod::Window
1920            | BuiltinMethod::Chunk
1921            | BuiltinMethod::RollingSum
1922            | BuiltinMethod::RollingAvg
1923            | BuiltinMethod::RollingMin
1924            | BuiltinMethod::RollingMax => Self::new(method, BuiltinArgs::Usize(args.usize(0)?)),
1925            BuiltinMethod::Lag | BuiltinMethod::Lead => {
1926                let n = if arg_len > 0 { args.usize(0)? } else { 1 };
1927                Self::new(method, BuiltinArgs::Usize(n))
1928            }
1929            BuiltinMethod::Merge
1930            | BuiltinMethod::DeepMerge
1931            | BuiltinMethod::Defaults
1932            | BuiltinMethod::Rename => Self::new(method, BuiltinArgs::Val(args.val(0)?)),
1933            BuiltinMethod::Slice => {
1934                let start = args.i64(0)?;
1935                let end = if arg_len > 1 {
1936                    Some(args.i64(1)?)
1937                } else {
1938                    None
1939                };
1940                Self::new(
1941                    method,
1942                    BuiltinArgs::I64Opt {
1943                        first: start,
1944                        second: end,
1945                    },
1946                )
1947            }
1948            // `missing(...keys)`: multi-arg form returns the array of
1949            // absent keys; single-arg keeps the legacy boolean. Listed
1950            // BEFORE the catch-all `Missing` so the multi-arg case wins.
1951            BuiltinMethod::Missing if arg_len >= 2 => {
1952                let mut keys = Vec::with_capacity(arg_len);
1953                for i in 0..arg_len {
1954                    keys.push(args.str(i)?);
1955                }
1956                Self::new(method, BuiltinArgs::StrVec(keys))
1957            }
1958            BuiltinMethod::GetPath | BuiltinMethod::HasPath => {
1959                let path = args.str(0)?;
1960                Self::new(method, BuiltinArgs::Path(parse_path_segs(path.as_ref()).into()))
1961            }
1962            BuiltinMethod::HasAll => Self::new(method, BuiltinArgs::Val(args.val(0)?)),
1963            BuiltinMethod::Has
1964            | BuiltinMethod::HasKey
1965            | BuiltinMethod::Join
1966            | BuiltinMethod::Explode
1967            | BuiltinMethod::Implode
1968            | BuiltinMethod::DelPath
1969            | BuiltinMethod::FlattenKeys
1970            | BuiltinMethod::UnflattenKeys
1971            | BuiltinMethod::Missing
1972            | BuiltinMethod::StartsWith
1973            | BuiltinMethod::EndsWith
1974            | BuiltinMethod::IndexOf
1975            | BuiltinMethod::LastIndexOf
1976            | BuiltinMethod::StripPrefix
1977            | BuiltinMethod::StripSuffix
1978            | BuiltinMethod::Matches
1979            | BuiltinMethod::Scan
1980            | BuiltinMethod::Split
1981            | BuiltinMethod::ReMatch
1982            | BuiltinMethod::ReMatchFirst
1983            | BuiltinMethod::ReMatchAll
1984            | BuiltinMethod::ReCaptures
1985            | BuiltinMethod::ReCapturesAll
1986            | BuiltinMethod::ReSplit => {
1987                let s = if arg_len > 0 {
1988                    args.str(0)?
1989                } else if matches!(method, BuiltinMethod::Join) {
1990                    Arc::from("")
1991                } else if matches!(
1992                    method,
1993                    BuiltinMethod::FlattenKeys | BuiltinMethod::UnflattenKeys
1994                ) {
1995                    Arc::from(".")
1996                } else {
1997                    return Ok(None);
1998                };
1999                Self::new(method, BuiltinArgs::Str(s))
2000            }
2001            BuiltinMethod::Replace
2002            | BuiltinMethod::ReplaceAll
2003            | BuiltinMethod::ReReplace
2004            | BuiltinMethod::ReReplaceAll => Self::new(
2005                method,
2006                BuiltinArgs::StrPair {
2007                    first: args.str(0)?,
2008                    second: args.str(1)?,
2009                },
2010            ),
2011            BuiltinMethod::ContainsAny | BuiltinMethod::ContainsAll => {
2012                Self::new(method, BuiltinArgs::StrVec(args.str_vec(0)?))
2013            }
2014            BuiltinMethod::Omit => {
2015                let mut keys = Vec::with_capacity(arg_len);
2016                for idx in 0..arg_len {
2017                    keys.push(args.str(idx)?);
2018                }
2019                Self::new(method, BuiltinArgs::StrVec(keys))
2020            }
2021            BuiltinMethod::Repeat => Self::new(method, BuiltinArgs::Usize(args.usize(0)?)),
2022            BuiltinMethod::Indent => {
2023                if arg_len > 0 {
2024                    if let Some(prefix) = args.str_lit(0) {
2025                        Self::new(method, BuiltinArgs::Str(prefix))
2026                    } else {
2027                        Self::new(method, BuiltinArgs::Usize(args.usize(0)?))
2028                    }
2029                } else {
2030                    Self::new(method, BuiltinArgs::Usize(2))
2031                }
2032            }
2033            BuiltinMethod::PadLeft | BuiltinMethod::PadRight | BuiltinMethod::Center => Self::new(
2034                method,
2035                BuiltinArgs::Pad {
2036                    width: args.usize(0)?,
2037                    fill: args.char(1, arg_len)?,
2038                },
2039            ),
2040            _ if arg_len == 0 => Self::new(method, BuiltinArgs::None),
2041            _ => return Ok(None),
2042        };
2043        Ok(Some(call))
2044    }
2045
2046    /// Attempts to construct a `BuiltinCall` from AST arguments that are all compile-time
2047    /// literals. Non-literal or lambda arguments cause `None` to be returned, falling back
2048    /// to runtime evaluation.
2049    pub fn from_literal_ast_args(name: &str, args: &[crate::parse::ast::Arg]) -> Option<Self> {
2050        use crate::parse::ast::{Arg, ArrayElem, Expr, ObjField};
2051
2052        let method = BuiltinMethod::from_name(name);
2053        if method == BuiltinMethod::Unknown {
2054            return None;
2055        }
2056
2057        fn literal_val(expr: &Expr) -> Option<Val> {
2058            match expr {
2059                Expr::Null => Some(Val::Null),
2060                Expr::Bool(b) => Some(Val::Bool(*b)),
2061                Expr::Int(n) => Some(Val::Int(*n)),
2062                Expr::Float(f) => Some(Val::Float(*f)),
2063                Expr::Str(s) => Some(Val::Str(Arc::from(s.as_str()))),
2064                Expr::Array(elems) => {
2065                    let mut out = Vec::with_capacity(elems.len());
2066                    for elem in elems {
2067                        match elem {
2068                            ArrayElem::Expr(expr) => out.push(literal_val(expr)?),
2069                            ArrayElem::Spread(_) => return None,
2070                        }
2071                    }
2072                    Some(Val::Arr(Arc::new(out)))
2073                }
2074                Expr::Object(fields) => {
2075                    let mut out = IndexMap::with_capacity(fields.len());
2076                    for field in fields {
2077                        match field {
2078                            ObjField::Kv {
2079                                key,
2080                                val,
2081                                optional: false,
2082                                cond: None,
2083                            } => {
2084                                out.insert(Arc::from(key.as_str()), literal_val(val)?);
2085                            }
2086                            _ => return None,
2087                        }
2088                    }
2089                    Some(Val::Obj(Arc::new(out)))
2090                }
2091                _ => None,
2092            }
2093        }
2094
2095        if method == BuiltinMethod::Remove {
2096            return match args {
2097                [Arg::Pos(expr)] => {
2098                    Some(Self::new(method, BuiltinArgs::Val(literal_val(expr)?)))
2099                }
2100                _ => None,
2101            };
2102        }
2103
2104        if method == BuiltinMethod::HasAll {
2105            return match args {
2106                [Arg::Pos(Expr::Array(elems))] => {
2107                    let mut keys = Vec::with_capacity(elems.len());
2108                    for elem in elems {
2109                        let ArrayElem::Expr(expr) = elem else {
2110                            return None;
2111                        };
2112                        keys.push(Arc::from(crate::util::val_to_key(&literal_val(expr)?)));
2113                    }
2114                    Some(Self::new(method, BuiltinArgs::StrVec(keys)))
2115                }
2116                [Arg::Pos(expr)] => Some(Self::new(method, BuiltinArgs::Val(literal_val(expr)?))),
2117                _ => None,
2118            };
2119        }
2120
2121        Self::from_static_args(
2122            method,
2123            name,
2124            args.len(),
2125            |idx| {
2126                Ok(match args.get(idx) {
2127                    Some(Arg::Pos(expr)) => literal_val(expr),
2128                    _ => None,
2129                })
2130            },
2131            |idx| match args.get(idx) {
2132                Some(Arg::Pos(Expr::Ident(value))) => Some(Arc::from(value.as_str())),
2133                _ => None,
2134            },
2135        )
2136        .ok()
2137        .flatten()
2138    }
2139
2140    /// Like [`BuiltinCall::from_literal_ast_args`] but also requires the method to be a
2141    /// registered pipeline element method, returning `None` otherwise.
2142    pub fn from_pipeline_literal_args(name: &str, args: &[crate::parse::ast::Arg]) -> Option<Self> {
2143        let call = Self::from_literal_ast_args(name, args)?;
2144        call.method.is_pipeline_element_method().then_some(call)
2145    }
2146
2147    /// Evaluates this builtin directly on a zero-copy `JsonView` without materialising a `Val`.
2148    /// Only works for view-scalar methods; returns `None` for all other builtins.
2149    pub fn try_apply_json_view(&self, recv: crate::util::JsonView<'_>) -> Option<Val> {
2150        if !self.spec().view_scalar {
2151            return None;
2152        }
2153        match (self.method, &self.args) {
2154            (BuiltinMethod::Len, BuiltinArgs::None) => json_view_len(recv).map(Val::Int),
2155            (method, BuiltinArgs::None) if method.is_string_no_arg_view_scalar() => {
2156                let value = json_view_str(recv)?;
2157                str_no_arg_scalar_apply(method, value)
2158            }
2159            (method, BuiltinArgs::None) if method.is_numeric_no_arg_view_scalar() => {
2160                numeric_no_arg_scalar_apply(method, recv)
2161            }
2162            (method, BuiltinArgs::Str(arg)) if method.is_string_arg_view_scalar() => {
2163                let value = json_view_str(recv)?;
2164                str_arg_scalar_apply(method, value, arg.as_ref())
2165            }
2166            (BuiltinMethod::Includes, BuiltinArgs::Val(Val::Str(arg))) => {
2167                let value = json_view_str(recv)?;
2168                Some(Val::Bool(value.contains(arg.as_ref())))
2169            }
2170            (BuiltinMethod::Includes, BuiltinArgs::Val(Val::StrSlice(arg))) => {
2171                let value = json_view_str(recv)?;
2172                Some(Val::Bool(value.contains(arg.as_str())))
2173            }
2174            _ => None,
2175        }
2176    }
2177}
2178
2179/// Applies a zero-argument numeric scalar method (`ceil`, `floor`, `round`, `abs`) to a `JsonView`.
2180#[inline]
2181fn numeric_no_arg_scalar_apply(
2182    method: BuiltinMethod,
2183    recv: crate::util::JsonView<'_>,
2184) -> Option<Val> {
2185    match (method, recv) {
2186        (
2187            BuiltinMethod::Ceil | BuiltinMethod::Floor | BuiltinMethod::Round,
2188            crate::util::JsonView::Int(n),
2189        ) => Some(Val::Int(n)),
2190        (
2191            BuiltinMethod::Ceil | BuiltinMethod::Floor | BuiltinMethod::Round,
2192            crate::util::JsonView::UInt(n),
2193        ) => Some(uint_to_val(n)),
2194        (BuiltinMethod::Ceil, crate::util::JsonView::Float(f)) => Some(Val::Int(f.ceil() as i64)),
2195        (BuiltinMethod::Floor, crate::util::JsonView::Float(f)) => Some(Val::Int(f.floor() as i64)),
2196        (BuiltinMethod::Round, crate::util::JsonView::Float(f)) => Some(Val::Int(f.round() as i64)),
2197        (BuiltinMethod::Abs, crate::util::JsonView::Int(n)) => Some(Val::Int(n.wrapping_abs())),
2198        (BuiltinMethod::Abs, crate::util::JsonView::UInt(n)) => Some(uint_to_val(n)),
2199        (BuiltinMethod::Abs, crate::util::JsonView::Float(f)) => Some(Val::Float(f.abs())),
2200        _ => None,
2201    }
2202}
2203
2204/// Applies a zero-argument numeric scalar method to a materialised `Val`.
2205#[inline]
2206fn numeric_no_arg_scalar_val_apply(method: BuiltinMethod, recv: &Val) -> Option<Val> {
2207    numeric_no_arg_scalar_apply(method, crate::util::JsonView::from_val(recv))
2208}
2209
2210/// Converts a `u64` to `Val::Int` if it fits, otherwise `Val::Float`.
2211#[inline]
2212fn uint_to_val(n: u64) -> Val {
2213    if n <= i64::MAX as u64 {
2214        Val::Int(n as i64)
2215    } else {
2216        Val::Float(n as f64)
2217    }
2218}
2219
2220/// Applies a zero-argument string scalar method to a `&str`, returning the result as a `Val`.
2221#[inline]
2222fn str_no_arg_scalar_apply(method: BuiltinMethod, value: &str) -> Option<Val> {
2223    match method {
2224        BuiltinMethod::Upper => {
2225            if value.is_ascii() {
2226                let mut buf = value.to_owned();
2227                buf.make_ascii_uppercase();
2228                Some(Val::Str(Arc::from(buf)))
2229            } else {
2230                Some(Val::Str(Arc::from(value.to_uppercase())))
2231            }
2232        }
2233        BuiltinMethod::Lower => {
2234            if value.is_ascii() {
2235                let mut buf = value.to_owned();
2236                buf.make_ascii_lowercase();
2237                Some(Val::Str(Arc::from(buf)))
2238            } else {
2239                Some(Val::Str(Arc::from(value.to_lowercase())))
2240            }
2241        }
2242        BuiltinMethod::Trim => Some(Val::Str(Arc::from(value.trim()))),
2243        BuiltinMethod::TrimLeft => Some(Val::Str(Arc::from(value.trim_start()))),
2244        BuiltinMethod::TrimRight => Some(Val::Str(Arc::from(value.trim_end()))),
2245        BuiltinMethod::ByteLen => Some(Val::Int(value.len() as i64)),
2246        BuiltinMethod::IsBlank => Some(Val::Bool(value.chars().all(|c| c.is_whitespace()))),
2247        BuiltinMethod::IsNumeric => Some(Val::Bool(
2248            !value.is_empty() && value.chars().all(|c| c.is_ascii_digit()),
2249        )),
2250        BuiltinMethod::IsAlpha => Some(Val::Bool(
2251            !value.is_empty() && value.chars().all(|c| c.is_alphabetic()),
2252        )),
2253        BuiltinMethod::IsAscii => Some(Val::Bool(value.is_ascii())),
2254        BuiltinMethod::ToNumber => {
2255            if let Ok(i) = value.parse::<i64>() {
2256                return Some(Val::Int(i));
2257            }
2258            if let Ok(f) = value.parse::<f64>() {
2259                return Some(Val::Float(f));
2260            }
2261            Some(Val::Null)
2262        }
2263        BuiltinMethod::ToBool => Some(match value {
2264            "true" => Val::Bool(true),
2265            "false" => Val::Bool(false),
2266            _ => Val::Null,
2267        }),
2268        _ => None,
2269    }
2270}
2271
2272/// Applies a zero-argument string scalar method to a `Val`, extracting the string slice first.
2273#[inline]
2274fn str_no_arg_scalar_val_apply(method: BuiltinMethod, recv: &Val) -> Option<Val> {
2275    str_no_arg_scalar_apply(method, recv.as_str_ref()?)
2276}
2277
2278/// Applies a single-string-argument scalar method to a `&str` value with the argument.
2279#[inline]
2280fn str_arg_scalar_apply(method: BuiltinMethod, value: &str, arg: &str) -> Option<Val> {
2281    match method {
2282        BuiltinMethod::StartsWith => Some(Val::Bool(value.starts_with(arg))),
2283        BuiltinMethod::EndsWith => Some(Val::Bool(value.ends_with(arg))),
2284        BuiltinMethod::Matches => Some(Val::Bool(value.contains(arg))),
2285        BuiltinMethod::IndexOf => Some(str_index_of(value, arg, false)),
2286        BuiltinMethod::LastIndexOf => Some(str_index_of(value, arg, true)),
2287        _ => None,
2288    }
2289}
2290
2291/// Applies a single-string-argument scalar method to a `Val` receiver.
2292#[inline]
2293fn str_arg_scalar_val_apply(method: BuiltinMethod, recv: &Val, arg: &str) -> Option<Val> {
2294    str_arg_scalar_apply(method, recv.as_str_ref()?, arg)
2295}
2296
2297/// Returns the character index of `needle` in `value`; uses `rfind` when `last` is true.
2298/// Returns `Val::Int(-1)` when not found.
2299#[inline]
2300fn str_index_of(value: &str, needle: &str, last: bool) -> Val {
2301    let offset = if last {
2302        value.rfind(needle)
2303    } else {
2304        value.find(needle)
2305    };
2306    match offset {
2307        Some(i) => Val::Int(value[..i].chars().count() as i64),
2308        None => Val::Int(-1),
2309    }
2310}
2311
2312/// Extracts the logical length from a `JsonView` (char count for strings, element count for collections).
2313#[inline]
2314fn json_view_len(recv: crate::util::JsonView<'_>) -> Option<i64> {
2315    match recv {
2316        crate::util::JsonView::Str(s) => Some(s.chars().count() as i64),
2317        crate::util::JsonView::ArrayLen(n) | crate::util::JsonView::ObjectLen(n) => Some(n as i64),
2318        _ => None,
2319    }
2320}
2321
2322/// Extracts a `&str` from a `JsonView::Str` variant; returns `None` for other variants.
2323#[inline]
2324fn json_view_str(recv: crate::util::JsonView<'_>) -> Option<&str> {
2325    match recv {
2326        crate::util::JsonView::Str(s) => Some(s),
2327        _ => None,
2328    }
2329}
2330
2331/// Main dispatch entry point called by the tree-walking evaluator.
2332///
2333/// Resolves `name` to a [`BuiltinMethod`], decodes arguments, and invokes the
2334/// appropriate algorithm body. Three evaluator closures supply the backend's
2335/// expression evaluation strategy:
2336/// - `eval_arg`: evaluates a standalone argument expression.
2337/// - `eval_item`: evaluates a lambda body with `@` bound to an array element.
2338/// - `eval_pair`: evaluates a two-parameter comparator lambda (`sort` with a custom comparator).
2339pub(crate) fn eval_builtin_method<F, G, H>(
2340    recv: Val,
2341    name: &str,
2342    args: &[crate::parse::ast::Arg],
2343    mut eval_arg: F,
2344    mut eval_item: G,
2345    mut eval_pair: H,
2346) -> Result<Val, EvalError>
2347where
2348    F: FnMut(&crate::parse::ast::Arg) -> Result<Val, EvalError>,
2349    G: FnMut(&Val, &crate::parse::ast::Arg) -> Result<Val, EvalError>,
2350    H: FnMut(&Val, &Val, &crate::parse::ast::Arg) -> Result<Val, EvalError>,
2351{
2352    use crate::parse::ast::{Arg, Expr, ObjField};
2353
2354    let method = BuiltinMethod::from_name(name);
2355    if method == BuiltinMethod::Unknown {
2356        return Err(EvalError(format!("unknown method '{}'", name)));
2357    }
2358
2359    macro_rules! arg_val {
2360        ($idx:expr) => {{
2361            let arg = args
2362                .get($idx)
2363                .ok_or_else(|| EvalError(format!("{}: missing argument", name)))?;
2364            eval_arg(arg)
2365        }};
2366    }
2367
2368    macro_rules! str_arg {
2369        ($idx:expr) => {{
2370            match args.get($idx) {
2371                Some(Arg::Pos(Expr::Ident(s))) => Ok(Arc::from(s.as_str())),
2372                Some(_) => match arg_val!($idx)? {
2373                    Val::Str(s) => Ok(s),
2374                    other => Ok(Arc::from(crate::util::val_to_string(&other).as_str())),
2375                },
2376                None => Err(EvalError(format!("{}: missing argument", name))),
2377            }
2378        }};
2379    }
2380
2381    macro_rules! i64_arg {
2382        ($idx:expr) => {{
2383            match arg_val!($idx)? {
2384                Val::Int(n) => Ok(n),
2385                Val::Float(f) => Ok(f as i64),
2386                _ => Err(EvalError(format!("{}: expected number argument", name))),
2387            }
2388        }};
2389    }
2390
2391    macro_rules! vec_arg {
2392        ($idx:expr) => {{
2393            arg_val!($idx)?
2394                .into_vec()
2395                .ok_or_else(|| EvalError(format!("{}: expected array arg", name)))
2396        }};
2397    }
2398
2399    macro_rules! str_vec_arg {
2400        ($idx:expr) => {{
2401            Ok(vec_arg!($idx)?
2402                .iter()
2403                .map(|v| match v {
2404                    Val::Str(s) => s.clone(),
2405                    other => Arc::from(crate::util::val_to_string(other).as_str()),
2406                })
2407                .collect())
2408        }};
2409    }
2410
2411    macro_rules! fill_arg {
2412        ($idx:expr) => {{
2413            match args.get($idx) {
2414                None => Ok(' '),
2415                Some(_) => {
2416                    let s = str_arg!($idx)?;
2417                    if s.chars().count() == 1 {
2418                        Ok(s.chars().next().unwrap())
2419                    } else {
2420                        Err(EvalError(format!(
2421                            "{}: filler must be a single-char string",
2422                            name
2423                        )))
2424                    }
2425                }
2426            }
2427        }};
2428    }
2429
2430    let call = match method {
2431        BuiltinMethod::Len
2432        | BuiltinMethod::Count
2433        | BuiltinMethod::Sum
2434        | BuiltinMethod::Avg
2435        | BuiltinMethod::Min
2436        | BuiltinMethod::Max
2437        | BuiltinMethod::Keys
2438        | BuiltinMethod::Values
2439        | BuiltinMethod::Entries
2440        | BuiltinMethod::Reverse
2441        | BuiltinMethod::Unique
2442        | BuiltinMethod::Collect
2443        | BuiltinMethod::Compact
2444        | BuiltinMethod::FromJson
2445        | BuiltinMethod::FromPairs
2446        | BuiltinMethod::ToPairs
2447        | BuiltinMethod::Invert
2448        | BuiltinMethod::Enumerate
2449        | BuiltinMethod::Pairwise
2450        | BuiltinMethod::Ceil
2451        | BuiltinMethod::Floor
2452        | BuiltinMethod::Round
2453        | BuiltinMethod::Abs
2454        | BuiltinMethod::DiffWindow
2455        | BuiltinMethod::PctChange
2456        | BuiltinMethod::CumMax
2457        | BuiltinMethod::CumMin
2458        | BuiltinMethod::Zscore
2459        | BuiltinMethod::Upper
2460        | BuiltinMethod::Lower
2461        | BuiltinMethod::Trim
2462        | BuiltinMethod::TrimLeft
2463        | BuiltinMethod::TrimRight
2464        | BuiltinMethod::Capitalize
2465        | BuiltinMethod::TitleCase
2466        | BuiltinMethod::SnakeCase
2467        | BuiltinMethod::KebabCase
2468        | BuiltinMethod::CamelCase
2469        | BuiltinMethod::PascalCase
2470        | BuiltinMethod::ReverseStr
2471        | BuiltinMethod::HtmlEscape
2472        | BuiltinMethod::HtmlUnescape
2473        | BuiltinMethod::UrlEncode
2474        | BuiltinMethod::UrlDecode
2475        | BuiltinMethod::ToBase64
2476        | BuiltinMethod::FromBase64
2477        | BuiltinMethod::Dedent
2478        | BuiltinMethod::Lines
2479        | BuiltinMethod::Words
2480        | BuiltinMethod::Chars
2481        | BuiltinMethod::CharsOf
2482        | BuiltinMethod::Bytes
2483        | BuiltinMethod::ByteLen
2484        | BuiltinMethod::IsBlank
2485        | BuiltinMethod::IsNumeric
2486        | BuiltinMethod::IsAlpha
2487        | BuiltinMethod::IsAscii
2488        | BuiltinMethod::ToNumber
2489        | BuiltinMethod::ToBool
2490        | BuiltinMethod::ParseInt
2491        | BuiltinMethod::ParseFloat
2492        | BuiltinMethod::ParseBool
2493        | BuiltinMethod::Type
2494        | BuiltinMethod::ToString
2495        | BuiltinMethod::ToJson
2496        | BuiltinMethod::ToCsv
2497        | BuiltinMethod::ToTsv
2498        | BuiltinMethod::Schema
2499        | BuiltinMethod::ApproxCountDistinct
2500        | BuiltinMethod::ZipShape
2501        | BuiltinMethod::GroupShape
2502            if args.is_empty() =>
2503        {
2504            BuiltinCall::new(method, BuiltinArgs::None)
2505        }
2506        BuiltinMethod::Sum | BuiltinMethod::Avg | BuiltinMethod::Min | BuiltinMethod::Max => {
2507            return numeric_aggregate_projected_apply(&recv, method, |item| {
2508                eval_item(item, &args[0])
2509            });
2510        }
2511        BuiltinMethod::Count => {
2512            let items = recv
2513                .as_vals()
2514                .ok_or_else(|| EvalError("count: expected array".into()))?;
2515            let mut n: i64 = 0;
2516            for item in items.iter() {
2517                if crate::util::is_truthy(&eval_item(item, &args[0])?) {
2518                    n += 1;
2519                }
2520            }
2521            return Ok(Val::Int(n));
2522        }
2523        BuiltinMethod::Find | BuiltinMethod::FindFirst => {
2524            return find_first_apply(recv, args.len(), |item, idx| eval_item(item, &args[idx]));
2525        }
2526        BuiltinMethod::FindAll => {
2527            return find_apply(recv, args.len(), |item, idx| eval_item(item, &args[idx]));
2528        }
2529        BuiltinMethod::FindIndex => {
2530            return find_index_apply(recv, args.len(), |item, idx| eval_item(item, &args[idx]));
2531        }
2532        BuiltinMethod::IndicesWhere => {
2533            return indices_where_apply(recv, args.len(), |item, idx| eval_item(item, &args[idx]));
2534        }
2535        BuiltinMethod::UniqueBy => {
2536            let key_arg = args
2537                .first()
2538                .ok_or_else(|| EvalError("unique_by: requires key fn".into()))?;
2539            return unique_by_apply(recv, |item| eval_item(item, key_arg));
2540        }
2541        BuiltinMethod::MaxBy | BuiltinMethod::MinBy => {
2542            let key_arg = args
2543                .first()
2544                .ok_or_else(|| EvalError(format!("{}: requires a key expression", name)))?;
2545            return extreme_by_apply(recv, method == BuiltinMethod::MaxBy, |item| {
2546                eval_item(item, key_arg)
2547            });
2548        }
2549        BuiltinMethod::DeepFind => {
2550            return deep_find_apply(recv, args.len(), |item, idx| eval_item(item, &args[idx]));
2551        }
2552        BuiltinMethod::DeepShape => {
2553            let arg = args
2554                .first()
2555                .ok_or_else(|| EvalError("shape: requires pattern".into()))?;
2556            let expr = match arg {
2557                Arg::Pos(e) | Arg::Named(_, e) => e,
2558            };
2559            let Expr::Object(fields) = expr else {
2560                return Err(EvalError(
2561                    "shape: expected `{k1, k2, ...}` object pattern".into(),
2562                ));
2563            };
2564            let mut keys = Vec::with_capacity(fields.len());
2565            for field in fields {
2566                match field {
2567                    ObjField::Short(k) => keys.push(Arc::from(k.as_str())),
2568                    ObjField::Kv { key, val, .. } if matches!(val, Expr::Ident(n) if n == key) => {
2569                        keys.push(Arc::from(key.as_str()));
2570                    }
2571                    _ => return Err(EvalError("shape: unsupported pattern field".into())),
2572                }
2573            }
2574            return deep_shape_apply(recv, &keys);
2575        }
2576        BuiltinMethod::DeepLike => {
2577            let arg = args
2578                .first()
2579                .ok_or_else(|| EvalError("like: requires pattern".into()))?;
2580            let expr = match arg {
2581                Arg::Pos(e) | Arg::Named(_, e) => e,
2582            };
2583            let Expr::Object(fields) = expr else {
2584                return Err(EvalError(
2585                    "like: expected `{k: lit, ...}` object pattern".into(),
2586                ));
2587            };
2588            let mut pats = Vec::with_capacity(fields.len());
2589            for field in fields {
2590                match field {
2591                    ObjField::Kv { key, val, .. } => {
2592                        pats.push((Arc::from(key.as_str()), eval_arg(&Arg::Pos(val.clone()))?));
2593                    }
2594                    ObjField::Short(k) => {
2595                        pats.push((
2596                            Arc::from(k.as_str()),
2597                            eval_arg(&Arg::Pos(Expr::Ident(k.clone())))?,
2598                        ));
2599                    }
2600                    _ => return Err(EvalError("like: unsupported pattern field".into())),
2601                }
2602            }
2603            return deep_like_apply(recv, &pats);
2604        }
2605        BuiltinMethod::Walk | BuiltinMethod::WalkPre => {
2606            let arg = args
2607                .first()
2608                .ok_or_else(|| EvalError("walk: requires fn".into()))?;
2609            let pre = method == BuiltinMethod::WalkPre;
2610            let mut eval = |value: Val| eval_item(&value, arg);
2611            return walk_apply(recv, pre, &mut eval);
2612        }
2613        BuiltinMethod::Rec => {
2614            let arg = args
2615                .first()
2616                .ok_or_else(|| EvalError("rec: requires step expression".into()))?;
2617            if let Some(cond_arg) = args.get(1) {
2618                let eval_cell = std::cell::RefCell::new(eval_item);
2619                return rec_cond_apply(
2620                    recv,
2621                    |value| eval_cell.borrow_mut()(&value, arg),
2622                    |value| eval_cell.borrow_mut()(value, cond_arg),
2623                );
2624            }
2625            return rec_apply(recv, |value| eval_item(&value, arg));
2626        }
2627        BuiltinMethod::TracePath => {
2628            let arg = args
2629                .first()
2630                .ok_or_else(|| EvalError("trace_path: requires predicate".into()))?;
2631            return trace_path_apply(recv, |value| eval_item(value, arg));
2632        }
2633        BuiltinMethod::Fanout => {
2634            return fanout_apply(&recv, args.len(), |value, idx| eval_item(value, &args[idx]));
2635        }
2636        BuiltinMethod::ZipShape => {
2637            // No-arg form: parallel-array interleave. Receiver is an
2638            // object whose values are arrays of the same length; emit
2639            // one row per index with each key holding the array's
2640            // i-th element. Non-array values are broadcast.
2641            if args.is_empty() {
2642                return zip_shape_obj_apply(&recv)
2643                    .ok_or_else(|| EvalError("zip_shape: expected object receiver".into()));
2644            }
2645            // Object-literal sugar: `zip_shape({a, b})` ≡ `zip_shape(a, b)`.
2646            // The single `{a, b}` arg is evaluated as an object literal
2647            // against the receiver, then dispatched through the no-arg
2648            // parallel-array interleave.
2649            if args.len() == 1 {
2650                if let Arg::Pos(Expr::Object(fields)) = &args[0] {
2651                    let all_short = fields.iter().all(|f| {
2652                        matches!(f, crate::parse::ast::ObjField::Short(_))
2653                    });
2654                    if all_short {
2655                        let obj = eval_arg(&args[0])?;
2656                        return zip_shape_obj_apply(&obj).ok_or_else(|| {
2657                            EvalError("zip_shape: expected object receiver".into())
2658                        });
2659                    }
2660                }
2661            }
2662            let mut names = Vec::with_capacity(args.len());
2663            for arg in args {
2664                let name: Arc<str> = match arg {
2665                    Arg::Named(n, _) => Arc::from(n.as_str()),
2666                    Arg::Pos(Expr::Ident(n)) => Arc::from(n.as_str()),
2667                    _ => {
2668                        return Err(EvalError(
2669                            "zip_shape: args must be `name: expr` or bare identifier".into(),
2670                        ))
2671                    }
2672                };
2673                names.push(name);
2674            }
2675            return zip_shape_apply(&recv, &names, |value, idx| {
2676                eval_item(value, &args[idx])
2677            });
2678        }
2679        BuiltinMethod::GroupShape => {
2680            // No-arg form: group an array of objects by their
2681            // structural key set (the sorted keys joined with `,`).
2682            // Output is `{shape_key: [items]}`. Useful for partitioning
2683            // a heterogeneous collection by which keys each row has.
2684            if args.is_empty() {
2685                return group_shape_by_keys_apply(recv)
2686                    .ok_or_else(|| EvalError("group_shape: expected array".into()));
2687            }
2688            // 1-arg form: `group_shape(key_expr)` keys each element by the
2689            // projected value and emits `{key_value: [items]}` with the
2690            // original element preserved as the bucket value.
2691            if args.len() == 1 {
2692                let key_arg = &args[0];
2693                return group_shape_apply(recv, |value, idx| {
2694                    if idx == 0 {
2695                        eval_item(&value, key_arg)
2696                    } else {
2697                        Ok(value)
2698                    }
2699                });
2700            }
2701            let key_arg = &args[0];
2702            let shape_arg = &args[1];
2703            return group_shape_apply(recv, |value, idx| {
2704                if idx == 0 {
2705                    eval_item(&value, key_arg)
2706                } else {
2707                    eval_item(&value, shape_arg)
2708                }
2709            });
2710        }
2711        BuiltinMethod::Sort => {
2712            if args.is_empty() {
2713                return sort_apply(recv);
2714            }
2715            let mut key_args = Vec::with_capacity(args.len());
2716            let mut desc = Vec::with_capacity(args.len());
2717            for arg in args {
2718                match arg {
2719                    Arg::Pos(Expr::Lambda { params, .. })
2720                    | Arg::Named(_, Expr::Lambda { params, .. })
2721                        if params.len() == 2 =>
2722                    {
2723                        return sort_comparator_apply(recv, |left, right| {
2724                            eval_pair(left, right, arg)
2725                        });
2726                    }
2727                    Arg::Pos(Expr::UnaryNeg(inner)) => {
2728                        desc.push(true);
2729                        key_args.push(Arg::Pos((**inner).clone()));
2730                    }
2731                    Arg::Pos(e) => {
2732                        desc.push(false);
2733                        key_args.push(Arg::Pos(e.clone()));
2734                    }
2735                    Arg::Named(name, Expr::UnaryNeg(inner)) => {
2736                        desc.push(true);
2737                        key_args.push(Arg::Named(name.clone(), (**inner).clone()));
2738                    }
2739                    Arg::Named(name, e) => {
2740                        desc.push(false);
2741                        key_args.push(Arg::Named(name.clone(), e.clone()));
2742                    }
2743                }
2744            }
2745            return sort_by_apply(recv, &desc, |item, idx| eval_item(item, &key_args[idx]));
2746        }
2747        BuiltinMethod::Flatten => {
2748            let depth = if args.is_empty() {
2749                1
2750            } else {
2751                i64_arg!(0)?.max(0) as usize
2752            };
2753            BuiltinCall::new(method, BuiltinArgs::Usize(depth))
2754        }
2755        BuiltinMethod::First | BuiltinMethod::Last => {
2756            let n = if args.is_empty() { 1 } else { i64_arg!(0)? };
2757            BuiltinCall::new(method, BuiltinArgs::I64(n))
2758        }
2759        BuiltinMethod::Nth => BuiltinCall::new(method, BuiltinArgs::I64(i64_arg!(0)?)),
2760        BuiltinMethod::Take | BuiltinMethod::Skip => {
2761            BuiltinCall::new(method, BuiltinArgs::Usize(i64_arg!(0)?.max(0) as usize))
2762        }
2763        BuiltinMethod::Append | BuiltinMethod::Prepend | BuiltinMethod::Set => {
2764            let item = if args.is_empty() {
2765                Val::Null
2766            } else {
2767                arg_val!(0)?
2768            };
2769            BuiltinCall::new(method, BuiltinArgs::Val(item))
2770        }
2771        BuiltinMethod::Or => {
2772            let default = if args.is_empty() {
2773                Val::Null
2774            } else {
2775                arg_val!(0)?
2776            };
2777            BuiltinCall::new(method, BuiltinArgs::Val(default))
2778        }
2779        BuiltinMethod::Includes | BuiltinMethod::Index | BuiltinMethod::IndicesOf => {
2780            BuiltinCall::new(method, BuiltinArgs::Val(arg_val!(0)?))
2781        }
2782        BuiltinMethod::Diff | BuiltinMethod::Intersect | BuiltinMethod::Union => {
2783            BuiltinCall::new(method, BuiltinArgs::ValVec(vec_arg!(0)?))
2784        }
2785        BuiltinMethod::Window
2786        | BuiltinMethod::Chunk
2787        | BuiltinMethod::RollingSum
2788        | BuiltinMethod::RollingAvg
2789        | BuiltinMethod::RollingMin
2790        | BuiltinMethod::RollingMax => {
2791            BuiltinCall::new(method, BuiltinArgs::Usize(i64_arg!(0)?.max(0) as usize))
2792        }
2793        BuiltinMethod::Lag | BuiltinMethod::Lead => {
2794            let n = if args.is_empty() {
2795                1
2796            } else {
2797                i64_arg!(0)?.max(0) as usize
2798            };
2799            BuiltinCall::new(method, BuiltinArgs::Usize(n))
2800        }
2801        BuiltinMethod::Merge
2802        | BuiltinMethod::DeepMerge
2803        | BuiltinMethod::Defaults
2804        | BuiltinMethod::Rename => BuiltinCall::new(method, BuiltinArgs::Val(arg_val!(0)?)),
2805        BuiltinMethod::ParseInt if !args.is_empty() => {
2806            // `parse_int(radix)` — package the radix as a `Usize` so the
2807            // trait dispatch in `BuiltinCall::apply_args` (defs::ParseInt)
2808            // picks it up. Falls through to base-10 no-arg form when the
2809            // arg is missing.
2810            let radix = i64_arg!(0)?;
2811            BuiltinCall::new(method, BuiltinArgs::Usize(radix.max(0) as usize))
2812        }
2813        BuiltinMethod::ToCsv | BuiltinMethod::ToTsv if !args.is_empty() => {
2814            // `to_csv(headers)` / `to_tsv(headers)` — headers must be a
2815            // string array; package as `BuiltinArgs::StrVec`.
2816            let headers = str_vec_arg!(0)?;
2817            BuiltinCall::new(method, BuiltinArgs::StrVec(headers))
2818        }
2819        BuiltinMethod::Remove => match args.first() {
2820            // Treat anything that touches the current item (`@.x > 0`,
2821            // `@ != null`, comparison/binop/chain on `@`) as a per-element
2822            // predicate — same path as an explicit lambda. The original
2823            // dispatch only matched `Expr::Lambda`, so `@`-form predicates
2824            // fell through to the value-equality path and silently kept
2825            // every element.
2826            Some(Arg::Pos(Expr::Lambda { .. })) | Some(Arg::Named(_, Expr::Lambda { .. })) => {
2827                return remove_predicate_apply(recv, |item| eval_item(item, &args[0]));
2828            }
2829            Some(arg) if arg_uses_current(arg) => {
2830                return remove_predicate_apply(recv, |item| eval_item(item, &args[0]));
2831            }
2832            Some(_) => BuiltinCall::new(method, BuiltinArgs::Val(arg_val!(0)?)),
2833            None => return Err(EvalError("remove: requires arg".into())),
2834        },
2835        BuiltinMethod::Zip => {
2836            let other = args
2837                .first()
2838                .map(|arg| eval_arg(arg))
2839                .transpose()?
2840                .unwrap_or_else(|| Val::arr(Vec::new()));
2841            return zip_apply(recv, other);
2842        }
2843        BuiltinMethod::ZipLongest => {
2844            let mut other = Val::arr(Vec::new());
2845            let mut fill = Val::Null;
2846            for arg in args {
2847                match arg {
2848                    Arg::Pos(_) => other = eval_arg(arg)?,
2849                    Arg::Named(n, _) if n == "fill" => fill = eval_arg(arg)?,
2850                    Arg::Named(_, _) => {}
2851                }
2852            }
2853            return zip_longest_apply(recv, other, fill);
2854        }
2855        BuiltinMethod::EquiJoin => {
2856            let other = arg_val!(0)?;
2857            let lhs_key = str_arg!(1)?;
2858            let rhs_key = str_arg!(2)?;
2859            return equi_join_apply(recv, other, &lhs_key, &rhs_key);
2860        }
2861        BuiltinMethod::Pivot => {
2862            return pivot_apply(recv, args.len(), |item, idx| match &args[idx] {
2863                Arg::Pos(Expr::Str(s)) | Arg::Named(_, Expr::Str(s)) => {
2864                    Ok(item.get_field(s.as_str()))
2865                }
2866                arg => eval_item(item, arg),
2867            });
2868        }
2869        BuiltinMethod::Slice => {
2870            let start = i64_arg!(0)?;
2871            let end = if args.len() > 1 {
2872                Some(i64_arg!(1)?)
2873            } else {
2874                None
2875            };
2876            BuiltinCall::new(
2877                method,
2878                BuiltinArgs::I64Opt {
2879                    first: start,
2880                    second: end,
2881                },
2882            )
2883        }
2884        BuiltinMethod::Join => {
2885            let sep = if args.is_empty() {
2886                Arc::from("")
2887            } else {
2888                str_arg!(0)?
2889            };
2890            BuiltinCall::new(method, BuiltinArgs::Str(sep))
2891        }
2892        BuiltinMethod::FlattenKeys | BuiltinMethod::UnflattenKeys if args.is_empty() => {
2893            BuiltinCall::new(method, BuiltinArgs::Str(Arc::from(".")))
2894        }
2895        // `missing(...keys)` — variadic key-existence audit. Multi-key
2896        // form returns the array of absent keys; single-key form keeps the
2897        // legacy boolean.
2898        BuiltinMethod::Missing if args.len() >= 2 => {
2899            let keys = (0..args.len())
2900                .map(|i| str_arg!(i))
2901                .collect::<Result<Vec<_>, _>>()?;
2902            BuiltinCall::new(method, BuiltinArgs::StrVec(keys))
2903        }
2904        BuiltinMethod::GetPath | BuiltinMethod::HasPath => {
2905            BuiltinCall::new(
2906                method,
2907                BuiltinArgs::Path(parse_path_segs(str_arg!(0)?.as_ref()).into()),
2908            )
2909        }
2910        BuiltinMethod::HasAll => BuiltinCall::new(method, BuiltinArgs::Val(arg_val!(0)?)),
2911        BuiltinMethod::Has
2912        | BuiltinMethod::HasKey
2913        | BuiltinMethod::Missing
2914        | BuiltinMethod::Explode
2915        | BuiltinMethod::Implode
2916        | BuiltinMethod::DelPath
2917        | BuiltinMethod::FlattenKeys
2918        | BuiltinMethod::UnflattenKeys
2919        | BuiltinMethod::StartsWith
2920        | BuiltinMethod::EndsWith
2921        | BuiltinMethod::IndexOf
2922        | BuiltinMethod::LastIndexOf
2923        | BuiltinMethod::StripPrefix
2924        | BuiltinMethod::StripSuffix
2925        | BuiltinMethod::Matches
2926        | BuiltinMethod::Scan
2927        | BuiltinMethod::Split
2928        | BuiltinMethod::ReMatch
2929        | BuiltinMethod::ReMatchFirst
2930        | BuiltinMethod::ReMatchAll
2931        | BuiltinMethod::ReCaptures
2932        | BuiltinMethod::ReCapturesAll
2933        | BuiltinMethod::ReSplit => BuiltinCall::new(method, BuiltinArgs::Str(str_arg!(0)?)),
2934        BuiltinMethod::Replace
2935        | BuiltinMethod::ReplaceAll
2936        | BuiltinMethod::ReReplace
2937        | BuiltinMethod::ReReplaceAll => BuiltinCall::new(
2938            method,
2939            BuiltinArgs::StrPair {
2940                first: str_arg!(0)?,
2941                second: str_arg!(1)?,
2942            },
2943        ),
2944        BuiltinMethod::ContainsAny | BuiltinMethod::ContainsAll => {
2945            BuiltinCall::new(method, BuiltinArgs::StrVec(str_vec_arg!(0)?))
2946        }
2947        BuiltinMethod::Pick => {
2948            let mut specs = Vec::with_capacity(args.len());
2949            for arg in args {
2950                let resolved: Option<(Arc<str>, Arc<str>)> = match arg {
2951                    Arg::Pos(Expr::Ident(s)) => {
2952                        let key: Arc<str> = Arc::from(s.as_str());
2953                        Some((key.clone(), key))
2954                    }
2955                    Arg::Pos(_) => match eval_arg(arg)? {
2956                        Val::Str(s) => {
2957                            let out_key: Arc<str> = if s.contains('.') || s.contains('[') {
2958                                match parse_path_segs(&s).first() {
2959                                    Some(PathSeg::Field(f)) => Arc::from(f.as_str()),
2960                                    Some(PathSeg::Index(i)) => Arc::from(i.to_string().as_str()),
2961                                    None => s.clone(),
2962                                }
2963                            } else {
2964                                s.clone()
2965                            };
2966                            Some((out_key, s))
2967                        }
2968                        _ => None,
2969                    },
2970                    Arg::Named(alias, Expr::Ident(src)) => {
2971                        Some((Arc::from(alias.as_str()), Arc::from(src.as_str())))
2972                    }
2973                    Arg::Named(alias, _) => match eval_arg(arg)? {
2974                        Val::Str(s) => Some((Arc::from(alias.as_str()), s)),
2975                        _ => None,
2976                    },
2977                };
2978                let Some((out_key, src)) = resolved else {
2979                    continue;
2980                };
2981                let source = if src.contains('.') || src.contains('[') {
2982                    PickSource::Path(parse_path_segs(&src))
2983                } else {
2984                    PickSource::Field(src)
2985                };
2986                specs.push(PickSpec { out_key, source });
2987            }
2988            return pick_specs_apply(&recv, &specs)
2989                .ok_or_else(|| EvalError("pick: expected object or array of objects".into()));
2990        }
2991        BuiltinMethod::Omit => {
2992            let mut keys = Vec::with_capacity(args.len());
2993            for idx in 0..args.len() {
2994                keys.push(str_arg!(idx)?);
2995            }
2996            BuiltinCall::new(method, BuiltinArgs::StrVec(keys))
2997        }
2998        BuiltinMethod::Repeat | BuiltinMethod::Indent => {
2999            // `indent("> ")` accepts a string prefix argument. Detect the
3000            // string-literal shape before falling through to the integer
3001            // count coercion used by both `repeat` and the spaces-form of
3002            // `indent`.
3003            let prefix_arg = if matches!(method, BuiltinMethod::Indent) && !args.is_empty() {
3004                match &args[0] {
3005                    Arg::Pos(Expr::Str(s)) => Some(Arc::<str>::from(s.as_str())),
3006                    Arg::Named(_, Expr::Str(s)) => Some(Arc::<str>::from(s.as_str())),
3007                    _ => None,
3008                }
3009            } else {
3010                None
3011            };
3012            if let Some(prefix) = prefix_arg {
3013                BuiltinCall::new(method, BuiltinArgs::Str(prefix))
3014            } else {
3015                let n = if args.is_empty() {
3016                    if matches!(method, BuiltinMethod::Indent) {
3017                        2
3018                    } else {
3019                        1
3020                    }
3021                } else {
3022                    i64_arg!(0)?.max(0) as usize
3023                };
3024                BuiltinCall::new(method, BuiltinArgs::Usize(n))
3025            }
3026        }
3027        BuiltinMethod::PadLeft | BuiltinMethod::PadRight | BuiltinMethod::Center => {
3028            BuiltinCall::new(
3029                method,
3030                BuiltinArgs::Pad {
3031                    width: i64_arg!(0)?.max(0) as usize,
3032                    fill: fill_arg!(1)?,
3033                },
3034            )
3035        }
3036        BuiltinMethod::SetPath => {
3037            return set_path_apply(&recv, &str_arg!(0)?, &arg_val!(1)?)
3038                .ok_or_else(|| EvalError("set_path: builtin unsupported".into()));
3039        }
3040        BuiltinMethod::DelPaths => {
3041            let mut paths = Vec::with_capacity(args.len());
3042            for idx in 0..args.len() {
3043                paths.push(str_arg!(idx)?);
3044            }
3045            return del_paths_apply(&recv, &paths)
3046                .ok_or_else(|| EvalError("del_paths: builtin unsupported".into()));
3047        }
3048        _ => {
3049            return Err(EvalError(format!(
3050                "{}: builtin not migrated to builtins.rs AST adapter",
3051                name
3052            )));
3053        }
3054    };
3055
3056    call.try_apply(&recv)?
3057        .ok_or_else(|| EvalError(format!("{}: builtin unsupported", name)))
3058}
3059
3060/// Returns `true` if `arg`'s expression tree references `Expr::Current`
3061/// (the `@` placeholder), indicating it should be treated as a per-element
3062/// predicate / projection rather than a literal value. Used by builtins
3063/// like `remove` whose semantics depend on whether the user supplied a
3064/// per-element predicate or a literal needle.
3065fn arg_uses_current(arg: &crate::parse::ast::Arg) -> bool {
3066    use crate::parse::ast::{Arg, Expr};
3067    fn walk(e: &Expr) -> bool {
3068        match e {
3069            Expr::Current => true,
3070            Expr::Lambda { .. } => false, // lambda introduces its own scope
3071            Expr::Chain(base, _) => walk(base),
3072            Expr::UnaryNeg(x) | Expr::Not(x) => walk(x),
3073            Expr::BinOp(l, _, r) => walk(l) || walk(r),
3074            Expr::Coalesce(l, r) => walk(l) || walk(r),
3075            Expr::IfElse { cond, then_, else_ } => walk(cond) || walk(then_) || walk(else_),
3076            Expr::Try { body, default } => walk(body) || walk(default),
3077            Expr::Cast { expr, .. } => walk(expr),
3078            Expr::FString(parts) => parts.iter().any(|p| match p {
3079                crate::parse::ast::FStringPart::Interp { expr, .. } => walk(expr),
3080                _ => false,
3081            }),
3082            Expr::Let { init, body, .. } => walk(init) || walk(body),
3083            // Comprehensions, match arms, patches, identifiers, literals,
3084            // root, and ident references do not transitively bind `@` in
3085            // the dispatch position we care about.
3086            _ => false,
3087        }
3088    }
3089    match arg {
3090        Arg::Pos(e) | Arg::Named(_, e) => walk(e),
3091    }
3092}
3093
3094/// Convenience wrapper over [`eval_builtin_method`] for zero-argument builtins.
3095/// Panics (via `EvalError`) if any argument evaluation closure is unexpectedly invoked.
3096pub(crate) fn eval_builtin_no_args(recv: Val, name: &str) -> Result<Val, EvalError> {
3097    eval_builtin_method(
3098        recv,
3099        name,
3100        &[],
3101        |_| {
3102            Err(EvalError(format!(
3103                "{}: unexpected argument evaluation",
3104                name
3105            )))
3106        },
3107        |_, _| Err(EvalError(format!("{}: unexpected item evaluation", name))),
3108        |_, _, _| Err(EvalError(format!("{}: unexpected pair evaluation", name))),
3109    )
3110}
3111
3112impl BuiltinMethod {
3113    /// Returns true if this method is registered as a pipeline element method.
3114    /// Pipeline element methods operate on individual values and can run in-stream.
3115    #[inline]
3116    pub fn is_pipeline_element_method(self) -> bool {
3117        crate::builtins::registry::pipeline_element(crate::builtins::registry::BuiltinId::from_method(
3118            self,
3119        ))
3120    }
3121}
3122
3123
3124pub mod ops;
3125
3126pub(crate) mod builtin;
3127pub(crate) mod defs;
3128#[cfg_attr(not(test), allow(dead_code))]
3129pub(crate) mod helpers;
3130pub(crate) mod registry;
3131
3132pub use ops::array::*;
3133pub use ops::collection::*;
3134pub use ops::misc::*;
3135pub use ops::path::*;
3136pub use ops::regex::*;
3137pub use ops::schema::*;
3138pub use ops::string::*;