Skip to main content

edikt_core/
eval.rs

1//! The query evaluator (value calculus) over an in-memory [`Value`].
2//!
3//! jq-style generator semantics: every expression maps one input value to a
4//! *stream* of output values (0, 1, or many), collected here into a `Vec`.
5//! A miss (missing key, out-of-range index) yields an **empty stream**, not
6//! `null` - the CLI renders it as a silent no-op (sed-shaped), and `//`
7//! supplies defaults. An explicit `null` in the document still yields `null`.
8//!
9//! Mutation `=`, `|=`, and `del` are handled here at the value level - this
10//! defines the *semantics* (what value ends up where). The format-preserving CST
11//! *write* path lives in the format modules and mirrors these rules. `+=`
12//! arrives in a later slice.
13
14use crate::ast::{BinOp, Expr, Step};
15use crate::comment::Commented;
16use crate::strings;
17use crate::value::Value;
18use std::cmp::Ordering;
19
20/// An evaluation failure (type error, unknown function, arity mismatch).
21#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
22#[error("{msg}")]
23pub struct EvalError {
24    pub msg: String,
25}
26
27impl EvalError {
28    pub(crate) fn new(msg: impl Into<String>) -> EvalError {
29        EvalError { msg: msg.into() }
30    }
31}
32
33/// Evaluate a query that may address comments (`#`) against the document's
34/// commented projection. Comment-free sub-expressions fall back to the plain
35/// value evaluator; a comment path resolves the comment text of each selected
36/// node. Supported in v0.2 Phase 1 as a **read** surface: a comment path
37/// (`.foo.#`, `.foo.#.inline`, `.items[].#`) optionally piped or defaulted
38/// (`| ascii_upcase`, `// "none"`). Comment access after a value pipe, or as an
39/// assignment target, is not yet served and errors clearly.
40pub fn eval_with_comments(expr: &Expr, root: &Commented) -> Result<Vec<Value>, EvalError> {
41    if !expr.has_comment() {
42        return eval(expr, &root.to_value());
43    }
44    match expr {
45        Expr::Path(steps) => Ok(root.resolve_comment(steps)),
46        // The document-wide `comments` stream: one record per comment.
47        Expr::Call(name, args) if name == "comments" && args.is_empty() => {
48            Ok(comment_records(root))
49        }
50        Expr::Pipe(a, b) => {
51            let mut out = Vec::new();
52            for v in eval_with_comments(a, root)? {
53                // Past the comment, the piped value is an ordinary scalar.
54                out.extend(eval(b, &v)?);
55            }
56            Ok(out)
57        }
58        Expr::Alternative(a, b) => {
59            let truthy: Vec<Value> = eval_with_comments(a, root)?
60                .into_iter()
61                .filter(Value::is_truthy)
62                .collect();
63            if truthy.is_empty() {
64                eval_with_comments(b, root)
65            } else {
66                Ok(truthy)
67            }
68        }
69        Expr::Comma(items) => {
70            let mut out = Vec::new();
71            for it in items {
72                out.extend(eval_with_comments(it, root)?);
73            }
74            Ok(out)
75        }
76        Expr::Collect(inner) => {
77            let items = match inner {
78                Some(e) => eval_with_comments(e, root)?,
79                None => Vec::new(),
80            };
81            Ok(vec![Value::Array(items)])
82        }
83        _ => Err(EvalError::new(
84            "comment access (`#` / `comments`) here isn't supported: use a comment \
85             path (`.foo.#`) or the `comments` stream, optionally piped or collected",
86        )),
87    }
88}
89
90/// The document-wide `comments` stream: one `{ path, kind, text }` record per
91/// comment, in document order. `path` is a rendered path to the annotated node
92/// (`.web.image`), so `comments | select(.text | test("TODO")) | .path` answers
93/// "which keys carry a TODO?".
94fn comment_records(root: &Commented) -> Vec<Value> {
95    root.comment_targets()
96        .into_iter()
97        .map(|(steps, kind, text)| {
98            Value::Object(vec![
99                ("path".into(), Value::Str(crate::render_path(&steps))),
100                ("kind".into(), Value::Str(kind.as_str().to_string())),
101                ("text".into(), Value::Str(text)),
102            ])
103        })
104        .collect()
105}
106
107/// Evaluate `expr` against `input`, returning the output stream.
108pub fn eval(expr: &Expr, input: &Value) -> Result<Vec<Value>, EvalError> {
109    match expr {
110        Expr::Path(steps) => eval_path(steps, input),
111        Expr::Literal(v) => Ok(vec![v.clone()]),
112        Expr::Neg(inner) => {
113            let mut out = Vec::new();
114            for v in eval(inner, input)? {
115                out.push(negate(&v)?);
116            }
117            Ok(out)
118        }
119        Expr::Binary(op, l, r) => {
120            let lefts = eval(l, input)?;
121            let rights = eval(r, input)?;
122            let mut out = Vec::new();
123            for a in &lefts {
124                for b in &rights {
125                    out.push(binary(*op, a, b)?);
126                }
127            }
128            Ok(out)
129        }
130        Expr::Pipe(l, r) => {
131            let mut out = Vec::new();
132            for v in eval(l, input)? {
133                out.extend(eval(r, &v)?);
134            }
135            Ok(out)
136        }
137        Expr::Alternative(l, r) => {
138            // jq's `//`: the left side's truthy outputs; if there are none -
139            // a miss, `null`, or `false` - the right side's. A type *error*
140            // on the left still propagates: a miss falls back, a mistake
141            // doesn't hide.
142            let truthy: Vec<Value> = eval(l, input)?
143                .into_iter()
144                .filter(Value::is_truthy)
145                .collect();
146            if truthy.is_empty() {
147                eval(r, input)
148            } else {
149                Ok(truthy)
150            }
151        }
152        Expr::Comma(items) => {
153            let mut out = Vec::new();
154            for it in items {
155                out.extend(eval(it, input)?);
156            }
157            Ok(out)
158        }
159        Expr::Call(name, args) => eval_call(name, args, input),
160        Expr::Collect(inner) => {
161            let items = match inner {
162                Some(e) => eval(e, input)?,
163                None => Vec::new(),
164            };
165            Ok(vec![Value::Array(items)])
166        }
167        Expr::ObjectConstruct(pairs) => {
168            let mut obj = Vec::with_capacity(pairs.len());
169            for (key, value_expr) in pairs {
170                let v = eval(value_expr, input)?
171                    .into_iter()
172                    .next()
173                    .unwrap_or(Value::Null);
174                obj.push((key.clone(), v));
175            }
176            Ok(vec![Value::Object(obj)])
177        }
178        Expr::Assign(lhs, rhs) => {
179            let steps = assign_path(lhs)?;
180            let mut out = Vec::new();
181            for rv in eval(rhs, input)? {
182                out.push(set_path(input, steps, &rv)?);
183            }
184            Ok(out)
185        }
186        Expr::UpdateAssign(lhs, rhs) => {
187            let steps = assign_path(lhs)?;
188            Ok(vec![update_path(input, steps, rhs)?])
189        }
190        Expr::AddAssign(lhs, rhs) => {
191            let steps = assign_path(lhs)?;
192            let mut out = Vec::new();
193            for rv in eval(rhs, input)? {
194                let current = eval_path(steps, input)?
195                    .into_iter()
196                    .next()
197                    .unwrap_or(Value::Null);
198                let sum = binary(BinOp::Add, &current, &rv)?;
199                out.push(set_path(input, steps, &sum)?);
200            }
201            Ok(out)
202        }
203    }
204}
205
206/// The left side of an assignment must be a plain path.
207fn assign_path(expr: &Expr) -> Result<&[Step], EvalError> {
208    expr.as_path()
209        .ok_or_else(|| EvalError::new("left side of an assignment must be a path"))
210}
211
212/// Return a copy of `v` with `steps` set to `new`. Missing object keys and
213/// array slots are created (arrays extend with nulls), matching jq.
214fn set_path(v: &Value, steps: &[Step], new: &Value) -> Result<Value, EvalError> {
215    let Some((head, rest)) = steps.split_first() else {
216        return Ok(new.clone());
217    };
218    match head {
219        Step::Field(k) => {
220            let mut obj = match v {
221                Value::Object(m) => m.clone(),
222                Value::Null => Vec::new(),
223                other => {
224                    return Err(EvalError::new(format!(
225                        "cannot set field of {}",
226                        other.type_name()
227                    )));
228                }
229            };
230            match obj.iter_mut().find(|(kk, _)| kk == k) {
231                Some(pair) => pair.1 = set_path(&pair.1, rest, new)?,
232                None => obj.push((k.clone(), set_path(&Value::Null, rest, new)?)),
233            }
234            Ok(Value::Object(obj))
235        }
236        Step::Index(i) => {
237            let mut arr = match v {
238                Value::Array(a) => a.clone(),
239                Value::Null => Vec::new(),
240                other => {
241                    return Err(EvalError::new(format!(
242                        "cannot index {} with a number",
243                        other.type_name()
244                    )));
245                }
246            };
247            let idx = if *i < 0 { arr.len() as i64 + i } else { *i };
248            if idx < 0 {
249                return Err(EvalError::new("array index out of range"));
250            }
251            let idx = idx as usize;
252            if idx >= arr.len() {
253                arr.resize(idx + 1, Value::Null);
254            }
255            arr[idx] = set_path(&arr[idx], rest, new)?;
256            Ok(Value::Array(arr))
257        }
258        Step::Iterate => match v {
259            Value::Array(a) => {
260                let mut out = Vec::with_capacity(a.len());
261                for e in a {
262                    out.push(set_path(e, rest, new)?);
263                }
264                Ok(Value::Array(out))
265            }
266            Value::Object(m) => {
267                let mut out = Vec::with_capacity(m.len());
268                for (k, e) in m {
269                    out.push((k.clone(), set_path(e, rest, new)?));
270                }
271                Ok(Value::Object(out))
272            }
273            other => Err(EvalError::new(format!(
274                "cannot iterate over {}",
275                other.type_name()
276            ))),
277        },
278        Step::Comment(_) => Err(EvalError::new(comment_mutation_unsupported())),
279    }
280}
281
282/// Return a copy of `v` with the value at `steps` replaced by `f` applied to it.
283fn update_path(v: &Value, steps: &[Step], f: &Expr) -> Result<Value, EvalError> {
284    let Some((head, rest)) = steps.split_first() else {
285        return Ok(eval(f, v)?.into_iter().next().unwrap_or(Value::Null));
286    };
287    match head {
288        Step::Field(k) => {
289            let mut obj = match v {
290                Value::Object(m) => m.clone(),
291                other => {
292                    return Err(EvalError::new(format!(
293                        "cannot update field of {}",
294                        other.type_name()
295                    )));
296                }
297            };
298            match obj.iter_mut().find(|(kk, _)| kk == k) {
299                Some(pair) => pair.1 = update_path(&pair.1, rest, f)?,
300                None => return Err(EvalError::new(format!("no such key: \"{k}\""))),
301            }
302            Ok(Value::Object(obj))
303        }
304        Step::Index(i) => {
305            let mut arr = match v {
306                Value::Array(a) => a.clone(),
307                other => {
308                    return Err(EvalError::new(format!(
309                        "cannot index {} with a number",
310                        other.type_name()
311                    )));
312                }
313            };
314            let idx = if *i < 0 { arr.len() as i64 + i } else { *i };
315            if idx < 0 || idx as usize >= arr.len() {
316                return Err(EvalError::new("array index out of range"));
317            }
318            let idx = idx as usize;
319            arr[idx] = update_path(&arr[idx], rest, f)?;
320            Ok(Value::Array(arr))
321        }
322        Step::Iterate => match v {
323            Value::Array(a) => {
324                let mut out = Vec::with_capacity(a.len());
325                for e in a {
326                    out.push(update_path(e, rest, f)?);
327                }
328                Ok(Value::Array(out))
329            }
330            Value::Object(m) => {
331                let mut out = Vec::with_capacity(m.len());
332                for (k, e) in m {
333                    out.push((k.clone(), update_path(e, rest, f)?));
334                }
335                Ok(Value::Object(out))
336            }
337            other => Err(EvalError::new(format!(
338                "cannot iterate over {}",
339                other.type_name()
340            ))),
341        },
342        Step::Comment(_) => Err(EvalError::new(comment_mutation_unsupported())),
343    }
344}
345
346fn eval_path(steps: &[Step], input: &Value) -> Result<Vec<Value>, EvalError> {
347    let mut stream = vec![input.clone()];
348    for step in steps {
349        let mut next = Vec::new();
350        for v in &stream {
351            next.extend(apply_step(step, v)?);
352        }
353        stream = next;
354    }
355    Ok(stream)
356}
357
358fn apply_step(step: &Step, v: &Value) -> Result<Vec<Value>, EvalError> {
359    match step {
360        Step::Field(k) => match v {
361            Value::Object(m) => Ok(m
362                .iter()
363                .find(|(kk, _)| kk == k)
364                .map(|(_, val)| vec![val.clone()])
365                .unwrap_or_default()),
366            Value::Null => Ok(vec![]),
367            other => Err(EvalError::new(format!(
368                "cannot index {} with \"{k}\"",
369                other.type_name()
370            ))),
371        },
372        Step::Index(i) => match v {
373            Value::Array(a) => {
374                let idx = if *i < 0 { a.len() as i64 + i } else { *i };
375                if idx >= 0 && (idx as usize) < a.len() {
376                    Ok(vec![a[idx as usize].clone()])
377                } else {
378                    Ok(vec![])
379                }
380            }
381            Value::Null => Ok(vec![]),
382            other => Err(EvalError::new(format!(
383                "cannot index {} with a number",
384                other.type_name()
385            ))),
386        },
387        Step::Iterate => match v {
388            Value::Array(a) => Ok(a.clone()),
389            Value::Object(m) => Ok(m.iter().map(|(_, val)| val.clone()).collect()),
390            other => Err(EvalError::new(format!(
391                "cannot iterate over {}",
392                other.type_name()
393            ))),
394        },
395        // A comment step is resolved against the document's commented
396        // projection, not the value stream - see `eval_with_comments`. Reaching
397        // it here means it was used in a spot the value evaluator can't serve.
398        Step::Comment(_) => Err(EvalError::new(
399            "comment access (`#`) resolves only as a whole path like `.foo.#`, \
400             not after a pipe over a value",
401        )),
402    }
403}
404
405fn negate(v: &Value) -> Result<Value, EvalError> {
406    match v {
407        Value::Int(i) => Ok(Value::Int(-i)),
408        Value::Float(f) => Ok(Value::Float(-f)),
409        other => Err(EvalError::new(format!(
410            "cannot negate {}",
411            other.type_name()
412        ))),
413    }
414}
415
416fn binary(op: BinOp, a: &Value, b: &Value) -> Result<Value, EvalError> {
417    match op {
418        BinOp::Eq => Ok(Value::Bool(a.value_eq(b))),
419        BinOp::Ne => Ok(Value::Bool(!a.value_eq(b))),
420        BinOp::Lt => Ok(Value::Bool(a.order(b) == Ordering::Less)),
421        BinOp::Gt => Ok(Value::Bool(a.order(b) == Ordering::Greater)),
422        BinOp::Le => Ok(Value::Bool(a.order(b) != Ordering::Greater)),
423        BinOp::Ge => Ok(Value::Bool(a.order(b) != Ordering::Less)),
424        BinOp::Add => add(a, b),
425        BinOp::Sub => arith(a, b, |x, y| x - y, i64::checked_sub, "subtract"),
426        BinOp::Mul => arith(a, b, |x, y| x * y, i64::checked_mul, "multiply"),
427        BinOp::Div => divide(a, b),
428        BinOp::Mod => modulo(a, b),
429    }
430}
431
432/// `+` is overloaded: `null` is the identity, plus numeric addition, string
433/// concat, and array concat.
434fn add(a: &Value, b: &Value) -> Result<Value, EvalError> {
435    match (a, b) {
436        (Value::Null, _) => Ok(b.clone()),
437        (_, Value::Null) => Ok(a.clone()),
438        (Value::Str(x), Value::Str(y)) => Ok(Value::Str(format!("{x}{y}"))),
439        (Value::Array(x), Value::Array(y)) => {
440            let mut v = x.clone();
441            v.extend(y.clone());
442            Ok(Value::Array(v))
443        }
444        _ => arith(a, b, |x, y| x + y, i64::checked_add, "add"),
445    }
446}
447
448fn arith(
449    a: &Value,
450    b: &Value,
451    f: impl Fn(f64, f64) -> f64,
452    checked: impl Fn(i64, i64) -> Option<i64>,
453    verb: &str,
454) -> Result<Value, EvalError> {
455    match (a, b) {
456        (Value::Int(x), Value::Int(y)) => match checked(*x, *y) {
457            Some(r) => Ok(Value::Int(r)),
458            None => Ok(Value::Float(f(*x as f64, *y as f64))),
459        },
460        _ => match (a.as_f64(), b.as_f64()) {
461            (Some(x), Some(y)) => Ok(Value::Float(f(x, y))),
462            _ => Err(EvalError::new(format!(
463                "cannot {verb} {} and {}",
464                a.type_name(),
465                b.type_name()
466            ))),
467        },
468    }
469}
470
471fn divide(a: &Value, b: &Value) -> Result<Value, EvalError> {
472    match (a.as_f64(), b.as_f64()) {
473        (Some(x), Some(y)) => {
474            if y == 0.0 {
475                return Err(EvalError::new("division by zero"));
476            }
477            // Keep an integer result when both sides are integers and it divides
478            // evenly; otherwise a float, like most calculators.
479            match (a, b) {
480                (Value::Int(xi), Value::Int(yi)) if *xi % *yi == 0 => Ok(Value::Int(*xi / *yi)),
481                _ => Ok(Value::Float(x / y)),
482            }
483        }
484        _ => Err(EvalError::new(format!(
485            "cannot divide {} and {}",
486            a.type_name(),
487            b.type_name()
488        ))),
489    }
490}
491
492fn modulo(a: &Value, b: &Value) -> Result<Value, EvalError> {
493    match (a.as_f64(), b.as_f64()) {
494        (Some(x), Some(y)) => {
495            if y == 0.0 {
496                return Err(EvalError::new("division by zero"));
497            }
498            if let (Value::Int(xi), Value::Int(yi)) = (a, b) {
499                return Ok(Value::Int(*xi % *yi));
500            }
501            Ok(Value::Float(x % y))
502        }
503        _ => Err(EvalError::new(format!(
504            "cannot mod {} and {}",
505            a.type_name(),
506            b.type_name()
507        ))),
508    }
509}
510
511fn eval_call(name: &str, args: &[Expr], input: &Value) -> Result<Vec<Value>, EvalError> {
512    let arity = |n: usize| -> Result<(), EvalError> {
513        if args.len() == n {
514            Ok(())
515        } else {
516            Err(EvalError::new(format!(
517                "{name} takes {n} argument(s), got {}",
518                args.len()
519            )))
520        }
521    };
522    // For builtins with a trailing optional argument (regex flags).
523    let arity_between = |min: usize, max: usize| -> Result<(), EvalError> {
524        if (min..=max).contains(&args.len()) {
525            Ok(())
526        } else {
527            Err(EvalError::new(format!(
528                "{name} takes {min}-{max} arguments, got {}",
529                args.len()
530            )))
531        }
532    };
533    // The optional flags argument, defaulting to none.
534    let flags_arg = |at: usize| -> Result<String, EvalError> {
535        match args.get(at) {
536            Some(a) => str_arg(a, input, "flags"),
537            None => Ok(String::new()),
538        }
539    };
540
541    match name {
542        "select" => {
543            arity(1)?;
544            let mut out = Vec::new();
545            for cond in eval(&args[0], input)? {
546                if cond.is_truthy() {
547                    out.push(input.clone());
548                }
549            }
550            Ok(out)
551        }
552        "length" => {
553            arity(0)?;
554            Ok(vec![length(input)?])
555        }
556        "keys" => {
557            arity(0)?;
558            Ok(vec![keys(input)?])
559        }
560        "type" => {
561            arity(0)?;
562            Ok(vec![Value::Str(input.type_name().to_string())])
563        }
564        "tostring" => {
565            arity(0)?;
566            Ok(vec![Value::Str(input.to_raw_string())])
567        }
568        "tonumber" => {
569            arity(0)?;
570            Ok(vec![tonumber(input)?])
571        }
572        "ascii_upcase" => {
573            arity(0)?;
574            Ok(vec![map_str(input, |s| s.to_uppercase())?])
575        }
576        "ascii_downcase" => {
577            arity(0)?;
578            Ok(vec![map_str(input, |s| s.to_lowercase())?])
579        }
580        "has" => {
581            arity(1)?;
582            let mut out = Vec::new();
583            for key in eval(&args[0], input)? {
584                out.push(Value::Bool(has(input, &key)?));
585            }
586            Ok(out)
587        }
588        "ltrimstr" => {
589            arity(1)?;
590            trim_str(input, &args[0], true)
591        }
592        "rtrimstr" => {
593            arity(1)?;
594            trim_str(input, &args[0], false)
595        }
596        "startswith" | "endswith" => {
597            arity(1)?;
598            let s = str_input(input, name)?;
599            let affix = str_arg(&args[0], input, "the affix")?;
600            let hit = if name == "startswith" {
601                s.starts_with(&affix)
602            } else {
603                s.ends_with(&affix)
604            };
605            Ok(vec![Value::Bool(hit)])
606        }
607        "test" => {
608            arity_between(1, 2)?;
609            let re = str_arg(&args[0], input, "the regex")?;
610            Ok(vec![strings::test(
611                str_input(input, name)?,
612                &re,
613                &flags_arg(1)?,
614            )?])
615        }
616        "match" => {
617            arity_between(1, 2)?;
618            let re = str_arg(&args[0], input, "the regex")?;
619            strings::find(str_input(input, name)?, &re, &flags_arg(1)?)
620        }
621        "capture" => {
622            arity_between(1, 2)?;
623            let re = str_arg(&args[0], input, "the regex")?;
624            strings::capture(str_input(input, name)?, &re, &flags_arg(1)?)
625        }
626        "sub" | "gsub" => {
627            arity_between(2, 3)?;
628            let re = str_arg(&args[0], input, "the regex")?;
629            let repl = str_arg(&args[1], input, "the replacement")?;
630            let mut flags = flags_arg(2)?;
631            if name == "gsub" {
632                flags.push('g');
633            }
634            Ok(vec![strings::sub(
635                str_input(input, name)?,
636                &re,
637                &repl,
638                &flags,
639            )?])
640        }
641        "split" => {
642            arity_between(1, 2)?;
643            let sep = str_arg(&args[0], input, "the separator")?;
644            // jq's shape: 1-arg splits on a literal, 2-arg on a regex.
645            let regex_flags = if args.len() == 2 {
646                Some(flags_arg(1)?)
647            } else {
648                None
649            };
650            Ok(vec![strings::split(
651                str_input(input, name)?,
652                &sep,
653                regex_flags.as_deref(),
654            )?])
655        }
656        "join" => {
657            arity(1)?;
658            let Value::Array(items) = input else {
659                return Err(EvalError::new(format!(
660                    "join requires an array input, got {}",
661                    input.type_name()
662                )));
663            };
664            let sep = str_arg(&args[0], input, "the separator")?;
665            Ok(vec![strings::join(items, &sep)?])
666        }
667        "del" => {
668            arity(1)?;
669            let steps = args[0]
670                .as_path()
671                .ok_or_else(|| EvalError::new("del(...) takes a path"))?;
672            Ok(vec![delete_path(input, steps)?])
673        }
674        _ => Err(EvalError::new(format!("unknown function `{name}`"))),
675    }
676}
677
678/// Return a copy of `v` with the value at `steps` removed. A missing key or
679/// out-of-range index is a no-op (jq semantics).
680fn delete_path(v: &Value, steps: &[Step]) -> Result<Value, EvalError> {
681    let Some((head, rest)) = steps.split_first() else {
682        return Err(EvalError::new("del(.) is not allowed"));
683    };
684    if rest.is_empty() {
685        return remove_step(v, head);
686    }
687    match head {
688        Step::Field(k) => {
689            let mut obj = match v {
690                Value::Object(m) => m.clone(),
691                Value::Null => return Ok(Value::Null),
692                other => {
693                    return Err(EvalError::new(format!(
694                        "cannot descend into {}",
695                        other.type_name()
696                    )));
697                }
698            };
699            if let Some(pair) = obj.iter_mut().find(|(kk, _)| kk == k) {
700                pair.1 = delete_path(&pair.1, rest)?;
701            }
702            Ok(Value::Object(obj))
703        }
704        Step::Index(i) => {
705            let mut arr = match v {
706                Value::Array(a) => a.clone(),
707                Value::Null => return Ok(Value::Null),
708                other => {
709                    return Err(EvalError::new(format!(
710                        "cannot index {} with a number",
711                        other.type_name()
712                    )));
713                }
714            };
715            let idx = if *i < 0 { arr.len() as i64 + i } else { *i };
716            if idx >= 0 && (idx as usize) < arr.len() {
717                let idx = idx as usize;
718                arr[idx] = delete_path(&arr[idx], rest)?;
719            }
720            Ok(Value::Array(arr))
721        }
722        Step::Iterate => match v {
723            Value::Array(a) => {
724                let mut out = Vec::with_capacity(a.len());
725                for e in a {
726                    out.push(delete_path(e, rest)?);
727                }
728                Ok(Value::Array(out))
729            }
730            Value::Object(m) => {
731                let mut out = Vec::with_capacity(m.len());
732                for (k, e) in m {
733                    out.push((k.clone(), delete_path(e, rest)?));
734                }
735                Ok(Value::Object(out))
736            }
737            other => Err(EvalError::new(format!(
738                "cannot iterate over {}",
739                other.type_name()
740            ))),
741        },
742        Step::Comment(_) => Err(EvalError::new(comment_mutation_unsupported())),
743    }
744}
745
746/// Remove `step` from the container `v` (the leaf of a `del` path).
747fn remove_step(v: &Value, step: &Step) -> Result<Value, EvalError> {
748    match step {
749        Step::Field(k) => match v {
750            Value::Object(m) => {
751                let kept = m.iter().filter(|(kk, _)| kk != k).cloned().collect();
752                Ok(Value::Object(kept))
753            }
754            Value::Null => Ok(Value::Null),
755            other => Err(EvalError::new(format!(
756                "cannot delete a field of {}",
757                other.type_name()
758            ))),
759        },
760        Step::Index(i) => match v {
761            Value::Array(a) => {
762                let mut arr = a.clone();
763                let idx = if *i < 0 { arr.len() as i64 + i } else { *i };
764                if idx >= 0 && (idx as usize) < arr.len() {
765                    arr.remove(idx as usize);
766                }
767                Ok(Value::Array(arr))
768            }
769            Value::Null => Ok(Value::Null),
770            other => Err(EvalError::new(format!(
771                "cannot delete an index of {}",
772                other.type_name()
773            ))),
774        },
775        Step::Iterate => match v {
776            Value::Array(_) => Ok(Value::Array(Vec::new())),
777            Value::Object(_) => Ok(Value::Object(Vec::new())),
778            other => Err(EvalError::new(format!(
779                "cannot iterate over {}",
780                other.type_name()
781            ))),
782        },
783        Step::Comment(_) => Err(EvalError::new(comment_mutation_unsupported())),
784    }
785}
786
787/// The message for a comment edit, which lands in v0.2 Phase 2.
788fn comment_mutation_unsupported() -> &'static str {
789    "editing comments (`#`) is not supported yet (planned for v0.2); reading works - e.g. `edikt '.foo.#' file`"
790}
791
792fn length(v: &Value) -> Result<Value, EvalError> {
793    let n = match v {
794        Value::Null => 0,
795        Value::Str(s) => s.chars().count() as i64,
796        Value::Array(a) => a.len() as i64,
797        Value::Object(m) => m.len() as i64,
798        other => {
799            return Err(EvalError::new(format!(
800                "{} has no length",
801                other.type_name()
802            )));
803        }
804    };
805    Ok(Value::Int(n))
806}
807
808fn keys(v: &Value) -> Result<Value, EvalError> {
809    match v {
810        Value::Object(m) => {
811            let mut ks: Vec<String> = m.iter().map(|(k, _)| k.clone()).collect();
812            ks.sort(); // jq's `keys` is sorted; use `keys_unsorted` later for order
813            Ok(Value::Array(ks.into_iter().map(Value::Str).collect()))
814        }
815        Value::Array(a) => Ok(Value::Array((0..a.len() as i64).map(Value::Int).collect())),
816        other => Err(EvalError::new(format!("{} has no keys", other.type_name()))),
817    }
818}
819
820fn tonumber(v: &Value) -> Result<Value, EvalError> {
821    match v {
822        Value::Int(_) | Value::Float(_) => Ok(v.clone()),
823        Value::Str(s) => {
824            let t = s.trim();
825            if let Ok(i) = t.parse::<i64>() {
826                Ok(Value::Int(i))
827            } else if let Ok(f) = t.parse::<f64>() {
828                Ok(Value::Float(f))
829            } else {
830                Err(EvalError::new(format!("cannot parse \"{s}\" as a number")))
831            }
832        }
833        other => Err(EvalError::new(format!(
834            "cannot parse {} as a number",
835            other.type_name()
836        ))),
837    }
838}
839
840/// The input as a string, for string-only builtins.
841fn str_input<'a>(input: &'a Value, name: &str) -> Result<&'a str, EvalError> {
842    match input {
843        Value::Str(s) => Ok(s),
844        other => Err(EvalError::new(format!(
845            "{name} requires a string input, got {}",
846            other.type_name()
847        ))),
848    }
849}
850
851/// Evaluate an argument expression to a single string (its first value).
852fn str_arg(arg: &Expr, input: &Value, what: &str) -> Result<String, EvalError> {
853    match eval(arg, input)?.into_iter().next() {
854        Some(Value::Str(s)) => Ok(s),
855        Some(other) => Err(EvalError::new(format!(
856            "{what} must be a string, got {}",
857            other.type_name()
858        ))),
859        None => Err(EvalError::new(format!("{what} produced no value"))),
860    }
861}
862
863fn map_str(v: &Value, f: impl Fn(&str) -> String) -> Result<Value, EvalError> {
864    match v {
865        Value::Str(s) => Ok(Value::Str(f(s))),
866        other => Err(EvalError::new(format!(
867            "expected a string, got {}",
868            other.type_name()
869        ))),
870    }
871}
872
873fn has(v: &Value, key: &Value) -> Result<bool, EvalError> {
874    match (v, key) {
875        (Value::Object(m), Value::Str(k)) => Ok(m.iter().any(|(kk, _)| kk == k)),
876        (Value::Array(a), Value::Int(i)) => Ok(*i >= 0 && (*i as usize) < a.len()),
877        _ => Err(EvalError::new(format!(
878            "cannot check membership of {} in {}",
879            key.type_name(),
880            v.type_name()
881        ))),
882    }
883}
884
885fn trim_str(input: &Value, arg: &Expr, left: bool) -> Result<Vec<Value>, EvalError> {
886    let s = match input {
887        Value::Str(s) => s,
888        other => {
889            return Err(EvalError::new(format!(
890                "expected a string, got {}",
891                other.type_name()
892            )));
893        }
894    };
895    let mut out = Vec::new();
896    for prefix in eval(arg, input)? {
897        let p = match &prefix {
898            Value::Str(p) => p,
899            other => {
900                return Err(EvalError::new(format!(
901                    "expected a string argument, got {}",
902                    other.type_name()
903                )));
904            }
905        };
906        let trimmed = if left {
907            s.strip_prefix(p).unwrap_or(s)
908        } else {
909            s.strip_suffix(p).unwrap_or(s)
910        };
911        out.push(Value::Str(trimmed.to_string()));
912    }
913    Ok(out)
914}
915
916#[cfg(test)]
917mod tests {
918    use super::*;
919    use crate::parser::parse;
920
921    fn obj(pairs: &[(&str, Value)]) -> Value {
922        Value::Object(
923            pairs
924                .iter()
925                .map(|(k, v)| (k.to_string(), v.clone()))
926                .collect(),
927        )
928    }
929
930    fn run(expr: &str, input: &Value) -> Vec<Value> {
931        eval(&parse(expr).unwrap(), input).unwrap_or_else(|e| panic!("eval `{expr}`: {e}"))
932    }
933
934    fn one(expr: &str, input: &Value) -> Value {
935        let r = run(expr, input);
936        assert_eq!(r.len(), 1, "`{expr}` should yield one value, got {r:?}");
937        r.into_iter().next().unwrap()
938    }
939
940    #[test]
941    fn navigation() {
942        let doc = obj(&[(
943            "compilerOptions",
944            obj(&[
945                ("strict", Value::Bool(true)),
946                ("target", Value::Str("ES2020".into())),
947            ]),
948        )]);
949        assert_eq!(one(".compilerOptions.strict", &doc), Value::Bool(true));
950        assert_eq!(
951            one(".compilerOptions.target", &doc),
952            Value::Str("ES2020".into())
953        );
954    }
955
956    #[test]
957    fn missing_is_empty_stream() {
958        // A missing key (or OOB index) is a miss -> empty stream. Indexing a
959        // scalar is a *type error*, tested separately in `type_errors`.
960        let doc = obj(&[("a", obj(&[("x", Value::Int(1))]))]);
961        assert!(run(".nope", &doc).is_empty());
962        assert!(run(".a.nope", &doc).is_empty());
963        assert!(run(".a.nope.deeper", &doc).is_empty());
964    }
965
966    #[test]
967    fn explicit_null_is_a_value() {
968        let doc = obj(&[("a", Value::Null)]);
969        assert_eq!(run(".a", &doc), vec![Value::Null]);
970    }
971
972    #[test]
973    fn iterate_and_index() {
974        let doc = obj(&[(
975            "lib",
976            Value::Array(vec![Value::Str("ES2020".into()), Value::Str("DOM".into())]),
977        )]);
978        assert_eq!(
979            run(".lib[]", &doc),
980            vec![Value::Str("ES2020".into()), Value::Str("DOM".into())]
981        );
982        assert_eq!(one(".lib[0]", &doc), Value::Str("ES2020".into()));
983        assert_eq!(one(".lib[-1]", &doc), Value::Str("DOM".into()));
984        assert!(run(".lib[9]", &doc).is_empty());
985    }
986
987    #[test]
988    fn select_filter() {
989        let doc = obj(&[(
990            "items",
991            Value::Array(vec![
992                obj(&[
993                    ("name", Value::Str("keep".into())),
994                    ("on", Value::Bool(true)),
995                ]),
996                obj(&[
997                    ("name", Value::Str("drop".into())),
998                    ("on", Value::Bool(false)),
999                ]),
1000            ]),
1001        )]);
1002        let r = run(".items[] | select(.on == true)", &doc);
1003        assert_eq!(r.len(), 1);
1004        assert_eq!(one(".name", &r[0]), Value::Str("keep".into()));
1005    }
1006
1007    #[test]
1008    fn arithmetic_and_strings() {
1009        let doc = obj(&[
1010            ("count", Value::Int(5)),
1011            ("name", Value::Str("edikt".into())),
1012        ]);
1013        assert_eq!(one(".count + 1", &doc), Value::Int(6));
1014        assert_eq!(one(".count * 2 - 3", &doc), Value::Int(7));
1015        assert_eq!(one(".name + \"!\"", &doc), Value::Str("edikt!".into()));
1016        assert_eq!(
1017            one(".name | ascii_upcase", &doc),
1018            Value::Str("EDIKT".into())
1019        );
1020        assert_eq!(one(".name | length", &doc), Value::Int(5));
1021    }
1022
1023    #[test]
1024    fn multi_output_comma() {
1025        let doc = obj(&[("a", Value::Int(1)), ("b", Value::Int(2))]);
1026        assert_eq!(run(".a, .b", &doc), vec![Value::Int(1), Value::Int(2)]);
1027    }
1028
1029    #[test]
1030    fn object_construction() {
1031        let doc = obj(&[("x", Value::Int(5))]);
1032        assert_eq!(
1033            one("{ a: 1, b: .x }", &doc),
1034            obj(&[("a", Value::Int(1)), ("b", Value::Int(5))])
1035        );
1036        assert_eq!(one("{}", &doc), Value::Object(vec![]));
1037    }
1038
1039    #[test]
1040    fn bracket_string_keys() {
1041        let doc = obj(&[("weird.key", Value::Str("w".into()))]);
1042        assert_eq!(one(r#".["weird.key"]"#, &doc), Value::Str("w".into()));
1043    }
1044
1045    #[test]
1046    fn builtins() {
1047        let doc = obj(&[("a", Value::Int(1)), ("b", Value::Int(2))]);
1048        assert_eq!(
1049            one("keys", &doc),
1050            Value::Array(vec![Value::Str("a".into()), Value::Str("b".into())])
1051        );
1052        assert_eq!(one("has(\"a\")", &doc), Value::Bool(true));
1053        assert_eq!(one("type", &doc), Value::Str("object".into()));
1054        assert_eq!(one("length", &doc), Value::Int(2));
1055        assert_eq!(one("\"12\" | tonumber", &Value::Null), Value::Int(12));
1056        assert_eq!(
1057            one("\"pre-x\" | ltrimstr(\"pre-\")", &Value::Null),
1058            Value::Str("x".into())
1059        );
1060    }
1061
1062    #[test]
1063    fn type_errors() {
1064        assert!(eval(&parse(".a").unwrap(), &Value::Int(3)).is_err());
1065        assert!(eval(&parse(".[]").unwrap(), &Value::Int(3)).is_err());
1066        assert!(eval(&parse("length").unwrap(), &Value::Int(3)).is_err());
1067    }
1068
1069    #[test]
1070    fn value_level_set_paths() {
1071        // Create nested keys through null / missing; extend arrays with nulls.
1072        let r = run(".a.b.c = 1", &Value::Null);
1073        assert_eq!(one(".a.b.c", &r[0]), Value::Int(1));
1074        let arr = run(
1075            ".xs[2] = 9",
1076            &obj(&[("xs", Value::Array(vec![Value::Int(0)]))]),
1077        );
1078        assert_eq!(
1079            one(".xs", &arr[0]),
1080            Value::Array(vec![Value::Int(0), Value::Null, Value::Int(9)])
1081        );
1082        // Iterate-assignment sets every element / value.
1083        let it = run(".[] = 0", &Value::Array(vec![Value::Int(1), Value::Int(2)]));
1084        assert_eq!(it[0], Value::Array(vec![Value::Int(0), Value::Int(0)]));
1085        let ito = run(
1086            ".[] = 0",
1087            &obj(&[("a", Value::Int(1)), ("b", Value::Int(2))]),
1088        );
1089        assert_eq!(ito[0], obj(&[("a", Value::Int(0)), ("b", Value::Int(0))]));
1090        // Negative index out of range, and setting through the wrong type, error.
1091        assert!(
1092            eval(
1093                &parse(".xs[-9] = 1").unwrap(),
1094                &obj(&[("xs", Value::Array(vec![]))])
1095            )
1096            .is_err()
1097        );
1098        assert!(eval(&parse(".a = 1").unwrap(), &Value::Int(3)).is_err());
1099        assert!(eval(&parse(".[0] = 1").unwrap(), &Value::Str("x".into())).is_err());
1100    }
1101
1102    #[test]
1103    fn value_level_update_and_delete() {
1104        // |= over a field, an index, and an iterate.
1105        assert_eq!(
1106            one(".a |= . + 1", &obj(&[("a", Value::Int(1))])),
1107            obj(&[("a", Value::Int(2))])
1108        );
1109        let xs = obj(&[("xs", Value::Array(vec![Value::Int(1), Value::Int(2)]))]);
1110        assert_eq!(
1111            one(".xs[0] |= . * 10", &xs),
1112            obj(&[("xs", Value::Array(vec![Value::Int(10), Value::Int(2)]))])
1113        );
1114        assert_eq!(
1115            one(".xs[] |= . + 1", &xs),
1116            obj(&[("xs", Value::Array(vec![Value::Int(2), Value::Int(3)]))])
1117        );
1118        // del of a nested key, an index, an iterate, and a miss (no-op).
1119        assert_eq!(
1120            one(
1121                "del(.a.b)",
1122                &obj(&[("a", obj(&[("b", Value::Int(1)), ("c", Value::Int(2))]))])
1123            ),
1124            obj(&[("a", obj(&[("c", Value::Int(2))]))])
1125        );
1126        assert_eq!(
1127            one("del(.xs[0])", &xs),
1128            obj(&[("xs", Value::Array(vec![Value::Int(2)]))])
1129        );
1130        assert_eq!(one("del(.xs[])", &xs), obj(&[("xs", Value::Array(vec![]))]));
1131        assert_eq!(
1132            one("del(.nope)", &obj(&[("a", Value::Int(1))])),
1133            obj(&[("a", Value::Int(1))])
1134        );
1135        // Update through the wrong type errors.
1136        assert!(eval(&parse(".a |= .").unwrap(), &Value::Int(3)).is_err());
1137    }
1138
1139    #[test]
1140    fn arithmetic_and_its_errors() {
1141        assert_eq!(one("3 - 1", &Value::Null), Value::Int(2));
1142        assert_eq!(one("3 * 4", &Value::Null), Value::Int(12));
1143        assert_eq!(one("7 % 3", &Value::Null), Value::Int(1));
1144        assert_eq!(one("6 / 2", &Value::Null), Value::Int(3)); // even -> int
1145        assert_eq!(one("7 / 2", &Value::Null), Value::Float(3.5)); // uneven -> float
1146        assert_eq!(one("2.5 + 0.5", &Value::Null), Value::Int(3)); // 3.0 prints as int
1147        // Comparisons.
1148        assert_eq!(one("1 < 2", &Value::Null), Value::Bool(true));
1149        assert_eq!(one("2 <= 2", &Value::Null), Value::Bool(true));
1150        assert_eq!(one("3 >= 4", &Value::Null), Value::Bool(false));
1151        assert_eq!(one("1 != 2", &Value::Null), Value::Bool(true));
1152        // Division / modulo by zero, and non-numeric arithmetic, error.
1153        assert!(eval(&parse("1 / 0").unwrap(), &Value::Null).is_err());
1154        assert!(eval(&parse("1 % 0").unwrap(), &Value::Null).is_err());
1155        assert!(eval(&parse("\"a\" - 1").unwrap(), &Value::Null).is_err());
1156        assert!(eval(&parse("-\"a\"").unwrap(), &Value::Null).is_err());
1157        // Overflow promotes to float rather than panicking.
1158        assert!(matches!(
1159            one("9223372036854775807 + 1", &Value::Null),
1160            Value::Float(_)
1161        ));
1162    }
1163
1164    #[test]
1165    fn add_is_overloaded() {
1166        assert_eq!(one("null + 5", &Value::Null), Value::Int(5));
1167        assert_eq!(one("5 + null", &Value::Null), Value::Int(5));
1168        assert_eq!(one("\"a\" + \"b\"", &Value::Null), Value::Str("ab".into()));
1169        assert_eq!(
1170            one("[1] + [2]", &Value::Null),
1171            Value::Array(vec![Value::Int(1), Value::Int(2)])
1172        );
1173    }
1174
1175    #[test]
1176    fn builtin_error_and_edge_paths() {
1177        // has / keys / length on the wrong type, and ltrimstr/rtrimstr edges.
1178        assert!(eval(&parse("has(\"a\")").unwrap(), &Value::Int(1)).is_err());
1179        assert!(eval(&parse("keys").unwrap(), &Value::Int(1)).is_err());
1180        assert_eq!(
1181            one("has(1)", &Value::Array(vec![Value::Int(0), Value::Int(0)])),
1182            Value::Bool(true)
1183        );
1184        assert_eq!(
1185            one("\"abc\" | ltrimstr(\"x\")", &Value::Null),
1186            Value::Str("abc".into())
1187        );
1188        assert_eq!(
1189            one("\"abc\" | rtrimstr(\"bc\")", &Value::Null),
1190            Value::Str("a".into())
1191        );
1192        assert_eq!(one("\"42\" | tonumber", &Value::Null), Value::Int(42));
1193        assert!(eval(&parse("\"x\" | tonumber").unwrap(), &Value::Null).is_err());
1194        assert_eq!(one("length", &Value::Str("héllo".into())), Value::Int(5));
1195        assert_eq!(one("length", &Value::Null), Value::Int(0));
1196        // Unknown function and wrong arity.
1197        assert!(eval(&parse("nope").unwrap(), &Value::Null).is_err());
1198        assert!(eval(&parse("length(1)").unwrap(), &Value::Null).is_err());
1199    }
1200
1201    #[test]
1202    fn alternative_operator() {
1203        let doc = obj(&[
1204            ("a", Value::Int(1)),
1205            ("z", Value::Null),
1206            ("f", Value::Bool(false)),
1207        ]);
1208        // A present, truthy value wins.
1209        assert_eq!(one(r#".a // "d""#, &doc), Value::Int(1));
1210        // A miss, null, and false all fall back.
1211        assert_eq!(one(r#".nope // "d""#, &doc), Value::Str("d".into()));
1212        assert_eq!(one(r#".z // "d""#, &doc), Value::Str("d".into()));
1213        assert_eq!(one(r#".f // "d""#, &doc), Value::Str("d".into()));
1214        // Right-associative chain.
1215        assert_eq!(
1216            one(r#".x // .y // "last""#, &doc),
1217            Value::Str("last".into())
1218        );
1219        // Binds tighter than `=`: the RHS gets the default.
1220        let r = run(r#".k = .nope // "d""#, &doc);
1221        assert_eq!(one(".k", &r[0]), Value::Str("d".into()));
1222        // Binds looser than comparison: `.a == 2 // "d"` is ((.a == 2)) // "d".
1223        assert_eq!(one(r#".a == 2 // "d""#, &doc), Value::Str("d".into()));
1224        // Filters a stream to its truthy members before falling back.
1225        let items = obj(&[(
1226            "xs",
1227            Value::Array(vec![Value::Bool(false), Value::Int(7), Value::Null]),
1228        )]);
1229        assert_eq!(run(r#".xs[] // "d""#, &items), vec![Value::Int(7)]);
1230        // A type error on the left still propagates - a miss falls back, a
1231        // mistake doesn't hide.
1232        assert!(eval(&parse(r#".a.b // "d""#).unwrap(), &doc).is_err());
1233    }
1234
1235    #[test]
1236    fn comments_stream_records_and_paths() {
1237        use crate::comment::{Commented, CommentedNode, Comments};
1238        // A little commented tree: web (head), web.image (inline), debug (inline).
1239        let img = Commented {
1240            comments: Comments {
1241                head: vec![],
1242                inline: Some("pinned".into()),
1243                foot: vec![],
1244            },
1245            node: CommentedNode::Scalar(Value::Str("nginx".into())),
1246        };
1247        let web = Commented {
1248            comments: Comments {
1249                head: vec!["the service".into()],
1250                inline: None,
1251                foot: vec![],
1252            },
1253            node: CommentedNode::Object(vec![("image".into(), img)]),
1254        };
1255        let debug = Commented {
1256            comments: Comments {
1257                head: vec![],
1258                inline: Some("TODO remove".into()),
1259                foot: vec![],
1260            },
1261            node: CommentedNode::Scalar(Value::Bool(false)),
1262        };
1263        let root = Commented {
1264            comments: Comments::default(),
1265            node: CommentedNode::Object(vec![("web".into(), web), ("debug".into(), debug)]),
1266        };
1267
1268        // The stream yields one record per comment, in document order.
1269        let recs = comment_records(&root);
1270        assert_eq!(recs.len(), 3);
1271        // comment -> key: which paths carry a TODO?
1272        let todos = eval_with_comments(
1273            &parse(r#"comments | select(.text | test("TODO")) | .path"#).unwrap(),
1274            &root,
1275        )
1276        .unwrap();
1277        assert_eq!(todos, vec![Value::Str(".debug".into())]);
1278        // paths render as re-usable expressions.
1279        let paths = eval_with_comments(&parse("comments | .path").unwrap(), &root).unwrap();
1280        assert_eq!(
1281            paths,
1282            vec![
1283                Value::Str(".web".into()),
1284                Value::Str(".web.image".into()),
1285                Value::Str(".debug".into()),
1286            ]
1287        );
1288        // collectable.
1289        assert_eq!(
1290            eval_with_comments(&parse("[comments] | length").unwrap(), &root).unwrap(),
1291            vec![Value::Int(3)]
1292        );
1293    }
1294
1295    #[test]
1296    fn regex_test_match_capture() {
1297        let s = Value::Str("nginx:1.25".into());
1298        assert_eq!(one(r#"test("^nginx")"#, &s), Value::Bool(true));
1299        assert_eq!(one(r#"test("^NGINX")"#, &s), Value::Bool(false));
1300        // The `;`-separated flags argument, jq-style.
1301        assert_eq!(one(r#"test("^NGINX"; "i")"#, &s), Value::Bool(true));
1302
1303        // match: no match -> empty stream (a silent miss at the CLI); `g`
1304        // streams every match.
1305        assert!(run(r#"match("\\d+"; "g")"#, &Value::Str("a1b22".into())).len() == 2);
1306        assert!(run(r#"match("z")"#, &s).is_empty());
1307        let m = one(r#"match(":(\\d+)")"#, &s);
1308        assert_eq!(one(".offset", &m), Value::Int(5));
1309        assert_eq!(one(".string", &m), Value::Str(":1".into()));
1310        assert_eq!(one(".captures[0].string", &m), Value::Str("1".into()));
1311
1312        // capture: named groups as an object.
1313        assert_eq!(
1314            one(r#"capture("(?<img>\\w+):(?<tag>.+)")"#, &s),
1315            obj(&[
1316                ("img", Value::Str("nginx".into())),
1317                ("tag", Value::Str("1.25".into())),
1318            ])
1319        );
1320
1321        // errors: bad regex, bad flag, non-string input
1322        assert!(eval(&parse(r#"test("(")"#).unwrap(), &s).is_err());
1323        assert!(eval(&parse(r#"test("a"; "q")"#).unwrap(), &s).is_err());
1324        assert!(eval(&parse(r#"test("a")"#).unwrap(), &Value::Int(1)).is_err());
1325    }
1326
1327    #[test]
1328    fn regex_sub_and_gsub() {
1329        let v = Value::Str("v1.2.3".into());
1330        assert_eq!(one(r#"sub("^v"; "")"#, &v), Value::Str("1.2.3".into()));
1331        // sub replaces the first; gsub all; `$name` references captures.
1332        let s = Value::Str("a-b-c".into());
1333        assert_eq!(one(r#"sub("-"; "_")"#, &s), Value::Str("a_b-c".into()));
1334        assert_eq!(one(r#"gsub("-"; "_")"#, &s), Value::Str("a_b_c".into()));
1335        assert_eq!(
1336            one(
1337                r#"sub("(?<k>\\w+)=(?<v>\\w+)"; "${v}:${k}")"#,
1338                &Value::Str("port=80".into())
1339            ),
1340            Value::Str("80:port".into())
1341        );
1342    }
1343
1344    #[test]
1345    fn split_join_and_affixes() {
1346        let path = Value::Str("/usr/bin:/bin".into());
1347        assert_eq!(
1348            one(r#"split(":")"#, &path),
1349            Value::Array(vec![
1350                Value::Str("/usr/bin".into()),
1351                Value::Str("/bin".into()),
1352            ])
1353        );
1354        // The round trip real configs want: split, extend, join.
1355        assert_eq!(
1356            one(r#"split(":") + ["/sbin"] | join(":")"#, &path),
1357            Value::Str("/usr/bin:/bin:/sbin".into())
1358        );
1359        // 2-arg split is regex (jq's shape).
1360        assert_eq!(
1361            one(r#""a1b22c" | split("\\d+"; "")"#, &Value::Null),
1362            Value::Array(vec![
1363                Value::Str("a".into()),
1364                Value::Str("b".into()),
1365                Value::Str("c".into()),
1366            ])
1367        );
1368        assert_eq!(
1369            one(r#""VITE_PORT" | startswith("VITE_")"#, &Value::Null),
1370            Value::Bool(true)
1371        );
1372        assert_eq!(
1373            one(r#""app.log" | endswith(".log")"#, &Value::Null),
1374            Value::Bool(true)
1375        );
1376        // join stringifies scalars and rejects containers.
1377        assert!(eval(&parse(r#"join(",")"#).unwrap(), &Value::Str("x".into())).is_err());
1378    }
1379
1380    // --- mutation (Value-level semantics) ---------------------------------
1381
1382    #[test]
1383    fn assign_sets_and_leaves_siblings() {
1384        let doc = obj(&[("a", Value::Int(1)), ("b", Value::Int(2))]);
1385        let r = run(".a = 5", &doc);
1386        assert_eq!(one(".a", &r[0]), Value::Int(5));
1387        assert_eq!(one(".b", &r[0]), Value::Int(2));
1388    }
1389
1390    #[test]
1391    fn assign_creates_missing_key() {
1392        let doc = obj(&[("a", Value::Int(1))]);
1393        let r = run(".c = 9", &doc);
1394        assert_eq!(one(".c", &r[0]), Value::Int(9));
1395    }
1396
1397    #[test]
1398    fn assign_rhs_evaluated_against_input() {
1399        let doc = obj(&[("a", Value::Int(1)), ("b", Value::Int(7))]);
1400        let r = run(".a = .b", &doc);
1401        assert_eq!(one(".a", &r[0]), Value::Int(7));
1402    }
1403
1404    #[test]
1405    fn assign_into_array_index() {
1406        let doc = obj(&[("a", Value::Array(vec![Value::Int(1), Value::Int(2)]))]);
1407        let r = run(".a[0] = 9", &doc);
1408        assert_eq!(one(".a[0]", &r[0]), Value::Int(9));
1409        assert_eq!(one(".a[1]", &r[0]), Value::Int(2));
1410    }
1411
1412    #[test]
1413    fn update_assign_computes_and_maps() {
1414        let doc = obj(&[
1415            ("count", Value::Int(5)),
1416            ("name", Value::Str("edikt".into())),
1417        ]);
1418        let r = run(".count |= . + 1", &doc);
1419        assert_eq!(one(".count", &r[0]), Value::Int(6));
1420        let r2 = run(".name |= ascii_upcase", &doc);
1421        assert_eq!(one(".name", &r2[0]), Value::Str("EDIKT".into()));
1422    }
1423
1424    #[test]
1425    fn mutation_detection() {
1426        assert!(parse(".a = 1").unwrap().is_mutation());
1427        assert!(parse(".a |= . + 1").unwrap().is_mutation());
1428        assert!(parse("del(.a)").unwrap().is_mutation());
1429        assert!(!parse(".a.b").unwrap().is_mutation());
1430        assert!(!parse(".items[] | select(. == 1)").unwrap().is_mutation());
1431    }
1432
1433    #[test]
1434    fn assign_lhs_must_be_path() {
1435        // `1 = 2` - the left side is a literal, not a path.
1436        assert!(eval(&parse("1 = 2").unwrap(), &Value::Null).is_err());
1437    }
1438
1439    #[test]
1440    fn del_removes_key_and_index() {
1441        let doc = obj(&[("a", Value::Int(1)), ("b", Value::Int(2))]);
1442        let r = run("del(.a)", &doc);
1443        assert!(run(".a", &r[0]).is_empty());
1444        assert_eq!(one(".b", &r[0]), Value::Int(2));
1445
1446        let arr = obj(&[(
1447            "x",
1448            Value::Array(vec![Value::Int(10), Value::Int(20), Value::Int(30)]),
1449        )]);
1450        let r2 = run("del(.x[1])", &arr);
1451        assert_eq!(run(".x[]", &r2[0]), vec![Value::Int(10), Value::Int(30)]);
1452    }
1453
1454    #[test]
1455    fn del_missing_is_noop() {
1456        let doc = obj(&[("a", Value::Int(1))]);
1457        let r = run("del(.nope)", &doc);
1458        assert_eq!(one(".a", &r[0]), Value::Int(1));
1459    }
1460
1461    #[test]
1462    fn del_nested() {
1463        let doc = obj(&[("a", obj(&[("b", Value::Int(1)), ("c", Value::Int(2))]))]);
1464        let r = run("del(.a.b)", &doc);
1465        assert!(run(".a.b", &r[0]).is_empty());
1466        assert_eq!(one(".a.c", &r[0]), Value::Int(2));
1467    }
1468
1469    #[test]
1470    fn add_assign_number_string_array() {
1471        let doc = obj(&[
1472            ("count", Value::Int(5)),
1473            ("name", Value::Str("edikt".into())),
1474            ("list", Value::Array(vec![Value::Int(1)])),
1475        ]);
1476        assert_eq!(one(".count", &run(".count += 3", &doc)[0]), Value::Int(8));
1477        assert_eq!(
1478            one(".name", &run(".name += \"!\"", &doc)[0]),
1479            Value::Str("edikt!".into())
1480        );
1481        let appended = run(".list += [2, 3]", &doc);
1482        assert_eq!(
1483            run(".list[]", &appended[0]),
1484            vec![Value::Int(1), Value::Int(2), Value::Int(3)]
1485        );
1486    }
1487
1488    #[test]
1489    fn add_assign_null_identity() {
1490        // A missing key is `null`; `null + [x] == [x]`, so `+=` creates it.
1491        let doc = obj(&[("a", Value::Int(1))]);
1492        let r = run(".tags += [\"x\"]", &doc);
1493        assert_eq!(run(".tags[]", &r[0]), vec![Value::Str("x".into())]);
1494    }
1495}