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