Skip to main content

aura_lang/eval/
methods.rs

1//! Method registry (SPEC §4.4). Dispatch by (receiver TypeTag, method name).
2//! Registration via fn pointers: a new method = a function + `register`, the parser stays unchanged.
3
4use indexmap::IndexMap;
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use super::value::{TypeTag, Value};
9use super::Interpreter;
10use crate::error::Diagnostic;
11use crate::span::Span;
12
13pub type MethodFn<'a> =
14    fn(&mut Interpreter<'a>, &Value<'a>, &[Value<'a>], Span) -> Result<Value<'a>, Diagnostic>;
15
16pub struct MethodRegistry<'a> {
17    table: HashMap<(TypeTag, &'static str), MethodFn<'a>>,
18}
19
20impl<'a> MethodRegistry<'a> {
21    pub fn new() -> Self {
22        MethodRegistry {
23            table: HashMap::new(),
24        }
25    }
26
27    pub fn register(&mut self, tag: TypeTag, name: &'static str, f: MethodFn<'a>) {
28        self.table.insert((tag, name), f);
29    }
30
31    pub fn get(&self, tag: TypeTag, name: &str) -> Option<MethodFn<'a>> {
32        self.table.get(&(tag, name)).copied()
33    }
34
35    /// Every registered `(receiver, method)` pair. Lets tooling (the LSP) check
36    /// its stdlib manifest against the real registry so the two cannot drift.
37    pub fn entries(&self) -> Vec<(TypeTag, &'static str)> {
38        self.table.keys().copied().collect()
39    }
40
41    pub fn builtin() -> Self {
42        let mut r = Self::new();
43        r.register(TypeTag::Str, "upper", m_str_upper);
44        r.register(TypeTag::Str, "lower", m_str_lower);
45        r.register(TypeTag::Str, "len", m_len);
46        r.register(TypeTag::Str, "parse_toml", m_parse_toml);
47        r.register(TypeTag::Str, "parse_duration", m_parse_duration);
48        r.register(TypeTag::Str, "parse_datetime", m_parse_datetime);
49        r.register(TypeTag::Int, "format_duration", m_format_duration);
50        r.register(TypeTag::Int, "format_datetime", m_format_datetime);
51        r.register(TypeTag::Str, "parse_json", m_parse_json);
52        r.register(TypeTag::Str, "parse_yaml", m_parse_yaml);
53        r.register(TypeTag::List, "len", m_len);
54        r.register(TypeTag::List, "compact", m_list_compact);
55        r.register(TypeTag::List, "uniq", m_list_uniq);
56        r.register(TypeTag::List, "map", m_list_map);
57        r.register(TypeTag::List, "filter", m_list_filter);
58        r.register(TypeTag::List, "get", m_get);
59        r.register(TypeTag::List, "contains", m_contains);
60        r.register(TypeTag::List, "join", m_list_join);
61        r.register(TypeTag::Str, "contains", m_contains);
62        r.register(TypeTag::Object, "keys", m_obj_keys);
63        r.register(TypeTag::Object, "values", m_obj_values);
64        r.register(TypeTag::Object, "contains", m_contains);
65        r.register(TypeTag::List, "first", m_list_first);
66        r.register(TypeTag::List, "last", m_list_last);
67        r.register(TypeTag::Object, "len", m_len);
68        r.register(TypeTag::Object, "merge", m_obj_merge);
69        r.register(TypeTag::Object, "get", m_get);
70        for tag in [TypeTag::Object, TypeTag::List] {
71            r.register(tag, "to_json", m_to_json);
72            r.register(tag, "to_yaml", m_to_yaml);
73            r.register(tag, "to_toml", m_to_toml);
74        }
75        // stdlib extension: String
76        r.register(TypeTag::Str, "trim", m_str_trim);
77        r.register(TypeTag::Str, "split", m_str_split);
78        r.register(TypeTag::Str, "replace", m_str_replace);
79        r.register(TypeTag::Str, "starts_with", m_str_starts_with);
80        r.register(TypeTag::Str, "ends_with", m_str_ends_with);
81        r.register(TypeTag::Str, "to_int", m_str_to_int);
82        r.register(TypeTag::Str, "to_float", m_str_to_float);
83        // Pure, deterministic digests/codecs (no I/O): safe under D1/D13.
84        r.register(TypeTag::Str, "sha256", m_str_sha256);
85        r.register(TypeTag::Str, "base64", m_str_base64);
86        r.register(TypeTag::Str, "base64_decode", m_str_base64_decode);
87        // stdlib extension: List
88        r.register(TypeTag::List, "sort", m_list_sort);
89        r.register(TypeTag::List, "reverse", m_list_reverse);
90        r.register(TypeTag::List, "sum", m_list_sum);
91        r.register(TypeTag::List, "min", m_list_min);
92        r.register(TypeTag::List, "max", m_list_max);
93        r.register(TypeTag::List, "flatten", m_list_flatten);
94        r.register(TypeTag::List, "slice", m_list_slice);
95        // stdlib extension: numeric + universal to_str
96        r.register(TypeTag::Int, "abs", m_num_abs);
97        r.register(TypeTag::Float, "abs", m_num_abs);
98        for tag in [TypeTag::Int, TypeTag::Float, TypeTag::Bool, TypeTag::Str] {
99            r.register(tag, "to_str", m_to_str);
100        }
101        r
102    }
103}
104
105impl Default for MethodRegistry<'_> {
106    fn default() -> Self {
107        Self::builtin()
108    }
109}
110
111fn rt(code: &'static str, msg: impl Into<String>, span: Span) -> Diagnostic {
112    Diagnostic::error(code, msg, span, "in this method call")
113}
114
115fn m_str_upper<'a>(
116    _it: &mut Interpreter<'a>,
117    recv: &Value<'a>,
118    _args: &[Value<'a>],
119    _sp: Span,
120) -> Result<Value<'a>, Diagnostic> {
121    let Value::Str(s) = recv else { unreachable!() };
122    Ok(Value::str(s.to_uppercase()))
123}
124
125fn m_str_lower<'a>(
126    _it: &mut Interpreter<'a>,
127    recv: &Value<'a>,
128    _args: &[Value<'a>],
129    _sp: Span,
130) -> Result<Value<'a>, Diagnostic> {
131    let Value::Str(s) = recv else { unreachable!() };
132    Ok(Value::str(s.to_lowercase()))
133}
134
135fn m_len<'a>(
136    _it: &mut Interpreter<'a>,
137    recv: &Value<'a>,
138    _args: &[Value<'a>],
139    _sp: Span,
140) -> Result<Value<'a>, Diagnostic> {
141    let n = match recv {
142        Value::Str(s) => s.chars().count(),
143        Value::List(xs) => xs.len(),
144        Value::Object(m) => m.len(),
145        _ => unreachable!(),
146    };
147    Ok(Value::Int(n as i64))
148}
149
150/// `.compact()` — removes Null while preserving order (SPEC §4.4).
151fn m_list_compact<'a>(
152    _it: &mut Interpreter<'a>,
153    recv: &Value<'a>,
154    _args: &[Value<'a>],
155    _sp: Span,
156) -> Result<Value<'a>, Diagnostic> {
157    let Value::List(xs) = recv else {
158        unreachable!()
159    };
160    Ok(Value::list(
161        xs.iter()
162            .filter(|v| !matches!(v, Value::Null))
163            .cloned()
164            .collect(),
165    ))
166}
167
168/// `.uniq()` — deduplication keeping the first occurrence.
169fn m_list_uniq<'a>(
170    _it: &mut Interpreter<'a>,
171    recv: &Value<'a>,
172    _args: &[Value<'a>],
173    _sp: Span,
174) -> Result<Value<'a>, Diagnostic> {
175    let Value::List(xs) = recv else {
176        unreachable!()
177    };
178    let mut out: Vec<Value<'a>> = Vec::with_capacity(xs.len());
179    for v in xs.iter() {
180        if !out.contains(v) {
181            out.push(v.clone());
182        }
183    }
184    Ok(Value::list(out))
185}
186
187fn expect_lambda<'a, 'b>(
188    args: &'b [Value<'a>],
189    name: &str,
190    sp: Span,
191) -> Result<&'b Value<'a>, Diagnostic> {
192    match args.last() {
193        Some(f @ Value::Function(_)) => Ok(f),
194        _ => Err(rt(
195            "E0315",
196            format!("`{name}` requires a lambda argument"),
197            sp,
198        )),
199    }
200}
201
202/// `.map (elem, index) -> ... end` — the callback receives the element and its index.
203fn m_list_map<'a>(
204    it: &mut Interpreter<'a>,
205    recv: &Value<'a>,
206    args: &[Value<'a>],
207    sp: Span,
208) -> Result<Value<'a>, Diagnostic> {
209    let Value::List(xs) = recv else {
210        unreachable!()
211    };
212    let f = expect_lambda(args, "map", sp)?.clone();
213    let mut out = Vec::with_capacity(xs.len());
214    for (i, v) in xs.iter().enumerate() {
215        out.push(it.call_value(&f, &[v.clone(), Value::Int(i as i64)], sp)?);
216    }
217    Ok(Value::list(out))
218}
219
220fn m_list_filter<'a>(
221    it: &mut Interpreter<'a>,
222    recv: &Value<'a>,
223    args: &[Value<'a>],
224    sp: Span,
225) -> Result<Value<'a>, Diagnostic> {
226    let Value::List(xs) = recv else {
227        unreachable!()
228    };
229    let f = expect_lambda(args, "filter", sp)?.clone();
230    let mut out = Vec::new();
231    for (i, v) in xs.iter().enumerate() {
232        match it.call_value(&f, &[v.clone(), Value::Int(i as i64)], sp)? {
233            Value::Bool(true) => out.push(v.clone()),
234            Value::Bool(false) => {}
235            other => {
236                return Err(rt(
237                    "E0306",
238                    format!("filter lambda must return Bool, got {}", other.type_name()),
239                    sp,
240                ))
241            }
242        }
243    }
244    Ok(Value::list(out))
245}
246
247/// `.merge(other)` — the right-hand operand overrides the left-hand keys.
248fn m_obj_merge<'a>(
249    _it: &mut Interpreter<'a>,
250    recv: &Value<'a>,
251    args: &[Value<'a>],
252    sp: Span,
253) -> Result<Value<'a>, Diagnostic> {
254    let Value::Object(base) = recv else {
255        unreachable!()
256    };
257    let Some(Value::Object(other)) = args.first() else {
258        return Err(rt("E0306", "merge expects an Object argument", sp));
259    };
260    let mut out: IndexMap<String, Value<'a>> = (**base).clone();
261    for (k, v) in other.iter() {
262        out.insert(k.clone(), v.clone());
263    }
264    Ok(Value::object(out))
265}
266
267/// `.parse_toml()` — TOML integers → Int (D6).
268fn m_parse_toml<'a>(
269    _it: &mut Interpreter<'a>,
270    recv: &Value<'a>,
271    _args: &[Value<'a>],
272    sp: Span,
273) -> Result<Value<'a>, Diagnostic> {
274    let Value::Str(s) = recv else { unreachable!() };
275    let parsed: toml::Value =
276        toml::from_str(s).map_err(|e| rt("E0314", format!("invalid TOML: {e}"), sp))?;
277    Ok(toml_to_value(parsed))
278}
279
280/// `.get(index_or_key, default)` — safe access: a miss returns default (or Null).
281fn m_get<'a>(
282    _it: &mut Interpreter<'a>,
283    recv: &Value<'a>,
284    args: &[Value<'a>],
285    sp: Span,
286) -> Result<Value<'a>, Diagnostic> {
287    let default = || args.get(1).cloned().unwrap_or(Value::Null);
288    match (recv, args.first()) {
289        (Value::List(xs), Some(Value::Int(i))) => Ok(usize::try_from(*i)
290            .ok()
291            .and_then(|i| xs.get(i))
292            .cloned()
293            .unwrap_or_else(default)),
294        (Value::Object(m), Some(Value::Str(k))) => {
295            Ok(m.get(k.as_ref()).cloned().unwrap_or_else(default))
296        }
297        (Value::List(_), _) => Err(rt("E0306", "List.get expects an Int index", sp)),
298        _ => Err(rt("E0306", "Object.get expects a String key", sp)),
299    }
300}
301
302fn m_list_first<'a>(
303    _it: &mut Interpreter<'a>,
304    recv: &Value<'a>,
305    _args: &[Value<'a>],
306    sp: Span,
307) -> Result<Value<'a>, Diagnostic> {
308    let Value::List(xs) = recv else {
309        unreachable!()
310    };
311    xs.first()
312        .cloned()
313        .ok_or_else(|| rt("E0317", "first() on an empty list", sp))
314}
315
316fn m_list_last<'a>(
317    _it: &mut Interpreter<'a>,
318    recv: &Value<'a>,
319    _args: &[Value<'a>],
320    sp: Span,
321) -> Result<Value<'a>, Diagnostic> {
322    let Value::List(xs) = recv else {
323        unreachable!()
324    };
325    xs.last()
326        .cloned()
327        .ok_or_else(|| rt("E0317", "last() on an empty list", sp))
328}
329
330fn m_parse_json<'a>(
331    _it: &mut Interpreter<'a>,
332    recv: &Value<'a>,
333    _args: &[Value<'a>],
334    sp: Span,
335) -> Result<Value<'a>, Diagnostic> {
336    let Value::Str(s) = recv else { unreachable!() };
337    let parsed: serde_json::Value =
338        serde_json::from_str(s).map_err(|e| rt("E0314", format!("invalid JSON: {e}"), sp))?;
339    Ok(json_to_value(parsed))
340}
341
342fn m_parse_yaml<'a>(
343    _it: &mut Interpreter<'a>,
344    recv: &Value<'a>,
345    _args: &[Value<'a>],
346    sp: Span,
347) -> Result<Value<'a>, Diagnostic> {
348    let Value::Str(s) = recv else { unreachable!() };
349    let docs = yaml_rust2::YamlLoader::load_from_str(s)
350        .map_err(|e| rt("E0314", format!("invalid YAML: {e}"), sp))?;
351    // A stream with several documents is ambiguous as a single value (D13: one
352    // input, one result), so require exactly one.
353    let doc = match docs.len() {
354        1 => &docs[0],
355        0 => return Ok(Value::Null),
356        n => {
357            return Err(rt(
358                "E0314",
359                format!("invalid YAML: expected one document, found {n}"),
360                sp,
361            ))
362        }
363    };
364    yaml_to_value(doc, sp)
365}
366
367/// yaml-rust2's tree into an Aura value. Written out rather than routed through
368/// serde so the type mapping is explicit: YAML's untyped scalars are where a
369/// version string quietly becomes a float.
370fn yaml_to_value<'a>(y: &yaml_rust2::Yaml, sp: Span) -> Result<Value<'a>, Diagnostic> {
371    use yaml_rust2::Yaml as Y;
372    Ok(match y {
373        Y::Null => Value::Null,
374        Y::Boolean(b) => Value::Bool(*b),
375        Y::Integer(n) => Value::Int(*n),
376        // Reals are kept as text by the parser and parsed on demand.
377        Y::Real(r) => match r.parse::<f64>() {
378            Ok(f) => Value::Float(f),
379            Err(_) => Value::str(r.clone()),
380        },
381        Y::String(s) => Value::str(s.clone()),
382        Y::Array(xs) => {
383            let mut out = Vec::with_capacity(xs.len());
384            for x in xs {
385                out.push(yaml_to_value(x, sp)?);
386            }
387            Value::list(out)
388        }
389        Y::Hash(h) => {
390            let mut map = indexmap::IndexMap::with_capacity(h.len());
391            for (k, v) in h {
392                // Only scalar keys map onto an Aura object.
393                let key = match k {
394                    Y::String(s) => s.clone(),
395                    Y::Integer(n) => n.to_string(),
396                    Y::Boolean(b) => b.to_string(),
397                    Y::Real(r) => r.clone(),
398                    _ => {
399                        return Err(rt(
400                            "E0314",
401                            "invalid YAML: only scalar keys are supported".to_string(),
402                            sp,
403                        ))
404                    }
405                };
406                map.insert(key, yaml_to_value(v, sp)?);
407            }
408            Value::object(map)
409        }
410        Y::Alias(_) | Y::BadValue => {
411            return Err(rt(
412                "E0314",
413                "invalid YAML: aliases are not supported".to_string(),
414                sp,
415            ))
416        }
417    })
418}
419
420fn m_to_json<'a>(
421    _it: &mut Interpreter<'a>,
422    recv: &Value<'a>,
423    _args: &[Value<'a>],
424    sp: Span,
425) -> Result<Value<'a>, Diagnostic> {
426    let json = crate::serialize::to_json(recv).map_err(|d| rt(d.code, d.message, sp))?;
427    Ok(Value::str(
428        serde_json::to_string(&json).expect("valid json"),
429    ))
430}
431
432fn m_to_yaml<'a>(
433    _it: &mut Interpreter<'a>,
434    recv: &Value<'a>,
435    _args: &[Value<'a>],
436    sp: Span,
437) -> Result<Value<'a>, Diagnostic> {
438    crate::serialize::to_yaml_string(recv)
439        .map(Value::str)
440        .map_err(|d| rt(d.code, d.message, sp))
441}
442
443fn m_to_toml<'a>(
444    _it: &mut Interpreter<'a>,
445    recv: &Value<'a>,
446    _args: &[Value<'a>],
447    sp: Span,
448) -> Result<Value<'a>, Diagnostic> {
449    crate::serialize::to_toml_string(recv)
450        .map(Value::str)
451        .map_err(|d| rt(d.code, d.message, sp))
452}
453
454/// `.keys()` — object keys in declaration order.
455fn m_obj_keys<'a>(
456    _it: &mut Interpreter<'a>,
457    recv: &Value<'a>,
458    _args: &[Value<'a>],
459    _sp: Span,
460) -> Result<Value<'a>, Diagnostic> {
461    let Value::Object(m) = recv else {
462        unreachable!()
463    };
464    Ok(Value::list(m.keys().map(Value::str).collect()))
465}
466
467fn m_obj_values<'a>(
468    _it: &mut Interpreter<'a>,
469    recv: &Value<'a>,
470    _args: &[Value<'a>],
471    _sp: Span,
472) -> Result<Value<'a>, Diagnostic> {
473    let Value::Object(m) = recv else {
474        unreachable!()
475    };
476    Ok(Value::list(m.values().cloned().collect()))
477}
478
479/// `.contains(x)`: List — element; Object — key; Str — substring.
480fn m_contains<'a>(
481    _it: &mut Interpreter<'a>,
482    recv: &Value<'a>,
483    args: &[Value<'a>],
484    sp: Span,
485) -> Result<Value<'a>, Diagnostic> {
486    let Some(needle) = args.first() else {
487        return Err(rt("E0306", "contains() expects an argument", sp));
488    };
489    let found = match (recv, needle) {
490        (Value::List(xs), n) => xs.contains(n),
491        (Value::Object(m), Value::Str(k)) => m.contains_key(k.as_ref()),
492        (Value::Str(s), Value::Str(sub)) => s.contains(sub.as_ref()),
493        (Value::Object(_), n) => {
494            return Err(rt(
495                "E0306",
496                format!(
497                    "Object.contains expects a String key, got {}",
498                    n.type_name()
499                ),
500                sp,
501            ))
502        }
503        (Value::Str(_), n) => {
504            return Err(rt(
505                "E0306",
506                format!("String.contains expects a String, got {}", n.type_name()),
507                sp,
508            ))
509        }
510        _ => unreachable!(),
511    };
512    Ok(Value::Bool(found))
513}
514
515/// `.join(sep)` — scalar elements joined by a separator (empty string if no argument).
516fn m_list_join<'a>(
517    it: &mut Interpreter<'a>,
518    recv: &Value<'a>,
519    args: &[Value<'a>],
520    sp: Span,
521) -> Result<Value<'a>, Diagnostic> {
522    let Value::List(xs) = recv else {
523        unreachable!()
524    };
525    let sep = match args.first() {
526        Some(Value::Str(s)) => s.to_string(),
527        None => String::new(),
528        Some(other) => {
529            return Err(rt(
530                "E0306",
531                format!(
532                    "join() expects a String separator, got {}",
533                    other.type_name()
534                ),
535                sp,
536            ))
537        }
538    };
539    let parts: Vec<String> = xs
540        .iter()
541        .map(|v| it.display(v, sp))
542        .collect::<Result<_, _>>()?;
543    Ok(Value::str(parts.join(&sep)))
544}
545
546/// `"1h30m".parse_duration()` → seconds (Int). Units: d, h, m, s.
547/// A deterministic alternative to timeout "magic numbers".
548fn m_parse_duration<'a>(
549    _it: &mut Interpreter<'a>,
550    recv: &Value<'a>,
551    _args: &[Value<'a>],
552    sp: Span,
553) -> Result<Value<'a>, Diagnostic> {
554    let Value::Str(s) = recv else { unreachable!() };
555    let err = || {
556        rt(
557            "E0319",
558            format!("invalid duration '{s}': expected e.g. \"1h30m\", units d/h/m/s"),
559            sp,
560        )
561    };
562    let bytes = s.as_bytes();
563    let mut i = 0;
564    let mut total: i64 = 0;
565    let mut components = 0;
566    while i < bytes.len() {
567        let start = i;
568        while i < bytes.len() && bytes[i].is_ascii_digit() {
569            i += 1;
570        }
571        if i == start || i == bytes.len() {
572            return Err(err());
573        }
574        let n: i64 = s[start..i].parse().map_err(|_| err())?;
575        let mult: i64 = match bytes[i] {
576            b'd' => 86400,
577            b'h' => 3600,
578            b'm' => 60,
579            b's' => 1,
580            _ => return Err(err()),
581        };
582        i += 1;
583        total = n
584            .checked_mul(mult)
585            .and_then(|x| total.checked_add(x))
586            .ok_or_else(|| rt("E0304", "duration overflows i64 seconds", sp))?;
587        components += 1;
588    }
589    if components == 0 {
590        return Err(err());
591    }
592    Ok(Value::Int(total))
593}
594
595/// `5400.format_duration()` → "1h30m" (compact, no zero components).
596fn m_format_duration<'a>(
597    _it: &mut Interpreter<'a>,
598    recv: &Value<'a>,
599    _args: &[Value<'a>],
600    sp: Span,
601) -> Result<Value<'a>, Diagnostic> {
602    let Value::Int(total) = recv else {
603        unreachable!()
604    };
605    if *total < 0 {
606        return Err(rt(
607            "E0306",
608            "format_duration expects a non-negative number of seconds",
609            sp,
610        ));
611    }
612    if *total == 0 {
613        return Ok(Value::str("0s"));
614    }
615    let (mut rest, mut out) = (*total, String::new());
616    for (unit, secs) in [("d", 86400), ("h", 3600), ("m", 60), ("s", 1)] {
617        let n = rest / secs;
618        if n > 0 {
619            out.push_str(&format!("{n}{unit}"));
620            rest %= secs;
621        }
622    }
623    Ok(Value::str(out))
624}
625
626/// `"2026-07-18T12:00:00Z".parse_datetime()` → epoch seconds (Int, UTC).
627/// Formats: `YYYY-MM-DD` (midnight UTC) and RFC3339 with `Z` or a `±HH:MM` offset.
628fn m_parse_datetime<'a>(
629    _it: &mut Interpreter<'a>,
630    recv: &Value<'a>,
631    _args: &[Value<'a>],
632    sp: Span,
633) -> Result<Value<'a>, Diagnostic> {
634    let Value::Str(s) = recv else { unreachable!() };
635    parse_rfc3339(s).map(Value::Int).ok_or_else(|| {
636        rt(
637            "E0320",
638            format!("invalid datetime '{s}': expected RFC3339, e.g. \"2026-07-18T12:00:00Z\""),
639            sp,
640        )
641    })
642}
643
644/// `epoch.format_datetime()` → an RFC3339 string in UTC.
645fn m_format_datetime<'a>(
646    _it: &mut Interpreter<'a>,
647    recv: &Value<'a>,
648    _args: &[Value<'a>],
649    _sp: Span,
650) -> Result<Value<'a>, Diagnostic> {
651    let Value::Int(epoch) = recv else {
652        unreachable!()
653    };
654    let days = epoch.div_euclid(86400);
655    let secs = epoch.rem_euclid(86400);
656    let (y, m, d) = civil_from_days(days);
657    Ok(Value::str(format!(
658        "{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z",
659        secs / 3600,
660        (secs % 3600) / 60,
661        secs % 60
662    )))
663}
664
665fn parse_rfc3339(s: &str) -> Option<i64> {
666    let num = |t: &str| -> Option<i64> {
667        if t.bytes().all(|b| b.is_ascii_digit()) && !t.is_empty() {
668            t.parse().ok()
669        } else {
670            None
671        }
672    };
673    let (date, rest) = if s.len() > 10 {
674        // split_at is byte-indexed: a multi-byte char across the boundary is not
675        // valid RFC3339 (which is ASCII), so reject instead of panicking.
676        if !s.is_char_boundary(10) {
677            return None;
678        }
679        s.split_at(10)
680    } else {
681        (s, "")
682    };
683    let mut dp = date.split('-');
684    let (y, m, d) = (num(dp.next()?)?, num(dp.next()?)?, num(dp.next()?)?);
685    if dp.next().is_some() || !(1..=12).contains(&m) || d < 1 || d > days_in_month(y, m) {
686        return None;
687    }
688    let mut epoch = days_from_civil(y, m, d) * 86400;
689    if rest.is_empty() {
690        return Some(epoch);
691    }
692    let rest = rest.strip_prefix('T')?;
693    if rest.len() < 9 || !rest.is_char_boundary(8) {
694        return None;
695    }
696    let (time, zone) = rest.split_at(8);
697    let mut tp = time.split(':');
698    let (hh, mm, ss) = (num(tp.next()?)?, num(tp.next()?)?, num(tp.next()?)?);
699    if tp.next().is_some() || hh > 23 || mm > 59 || ss > 59 {
700        return None;
701    }
702    epoch += hh * 3600 + mm * 60 + ss;
703    match zone {
704        "Z" => Some(epoch),
705        _ => {
706            let sign = match zone.as_bytes().first()? {
707                b'+' => 1,
708                b'-' => -1,
709                _ => return None,
710            };
711            let (oh, om) = zone[1..].split_once(':')?;
712            let (oh, om) = (num(oh)?, num(om)?);
713            if oh > 23 || om > 59 {
714                return None;
715            }
716            Some(epoch - sign * (oh * 3600 + om * 60))
717        }
718    }
719}
720
721fn days_in_month(y: i64, m: i64) -> i64 {
722    match m {
723        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
724        4 | 6 | 9 | 11 => 30,
725        _ => {
726            if y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) {
727                29
728            } else {
729                28
730            }
731        }
732    }
733}
734
735/// Howard Hinnant's algorithms: proleptic Gregorian calendar ↔ days since the epoch.
736fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
737    let y = if m <= 2 { y - 1 } else { y };
738    let era = if y >= 0 { y } else { y - 399 } / 400;
739    let yoe = y - era * 400;
740    let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
741    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
742    era * 146097 + doe - 719468
743}
744
745fn civil_from_days(z: i64) -> (i64, i64, i64) {
746    let z = z + 719468;
747    let era = if z >= 0 { z } else { z - 146096 } / 146097;
748    let doe = z - era * 146097;
749    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
750    let y = yoe + era * 400;
751    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
752    let mp = (5 * doy + 2) / 153;
753    let d = doy - (153 * mp + 2) / 5 + 1;
754    let m = if mp < 10 { mp + 3 } else { mp - 9 };
755    (if m <= 2 { y + 1 } else { y }, m, d)
756}
757
758// ---- stdlib extension: String ----
759
760fn expect_str_arg<'a>(args: &[Value<'a>], method: &str, sp: Span) -> Result<String, Diagnostic> {
761    match args.first() {
762        Some(Value::Str(s)) => Ok(s.to_string()),
763        Some(other) => Err(rt(
764            "E0306",
765            format!(
766                "{method}() expects a String argument, got {}",
767                other.type_name()
768            ),
769            sp,
770        )),
771        None => Err(rt("E0306", format!("{method}() expects one argument"), sp)),
772    }
773}
774
775fn m_str_trim<'a>(
776    _it: &mut Interpreter<'a>,
777    recv: &Value<'a>,
778    _args: &[Value<'a>],
779    _sp: Span,
780) -> Result<Value<'a>, Diagnostic> {
781    let Value::Str(s) = recv else { unreachable!() };
782    Ok(Value::str(s.trim()))
783}
784
785/// `.split(sep)` → List of substrings; the separator must be non-empty.
786fn m_str_split<'a>(
787    _it: &mut Interpreter<'a>,
788    recv: &Value<'a>,
789    args: &[Value<'a>],
790    sp: Span,
791) -> Result<Value<'a>, Diagnostic> {
792    let Value::Str(s) = recv else { unreachable!() };
793    let sep = expect_str_arg(args, "split", sp)?;
794    if sep.is_empty() {
795        return Err(rt("E0306", "split() separator must be non-empty", sp));
796    }
797    Ok(Value::list(s.split(&sep).map(Value::str).collect()))
798}
799
800/// `.replace(from, to)` → String with all occurrences replaced.
801fn m_str_replace<'a>(
802    _it: &mut Interpreter<'a>,
803    recv: &Value<'a>,
804    args: &[Value<'a>],
805    sp: Span,
806) -> Result<Value<'a>, Diagnostic> {
807    let Value::Str(s) = recv else { unreachable!() };
808    let (Some(Value::Str(from)), Some(Value::Str(to))) = (args.first(), args.get(1)) else {
809        return Err(rt(
810            "E0306",
811            "replace(from, to) expects two String arguments",
812            sp,
813        ));
814    };
815    Ok(Value::str(s.replace(from.as_ref(), to)))
816}
817
818fn m_str_starts_with<'a>(
819    _it: &mut Interpreter<'a>,
820    recv: &Value<'a>,
821    args: &[Value<'a>],
822    sp: Span,
823) -> Result<Value<'a>, Diagnostic> {
824    let Value::Str(s) = recv else { unreachable!() };
825    let prefix = expect_str_arg(args, "starts_with", sp)?;
826    Ok(Value::Bool(s.starts_with(&prefix)))
827}
828
829fn m_str_ends_with<'a>(
830    _it: &mut Interpreter<'a>,
831    recv: &Value<'a>,
832    args: &[Value<'a>],
833    sp: Span,
834) -> Result<Value<'a>, Diagnostic> {
835    let Value::Str(s) = recv else { unreachable!() };
836    let suffix = expect_str_arg(args, "ends_with", sp)?;
837    Ok(Value::Bool(s.ends_with(&suffix)))
838}
839
840/// `.to_int()` — parse (trimmed) into Int; E0314 on failure.
841fn m_str_to_int<'a>(
842    _it: &mut Interpreter<'a>,
843    recv: &Value<'a>,
844    _args: &[Value<'a>],
845    sp: Span,
846) -> Result<Value<'a>, Diagnostic> {
847    let Value::Str(s) = recv else { unreachable!() };
848    s.trim()
849        .parse::<i64>()
850        .map(Value::Int)
851        .map_err(|_| rt("E0314", format!("cannot parse '{s}' as Int"), sp))
852}
853
854fn m_str_to_float<'a>(
855    _it: &mut Interpreter<'a>,
856    recv: &Value<'a>,
857    _args: &[Value<'a>],
858    sp: Span,
859) -> Result<Value<'a>, Diagnostic> {
860    let Value::Str(s) = recv else { unreachable!() };
861    s.trim()
862        .parse::<f64>()
863        .map(Value::Float)
864        .map_err(|_| rt("E0314", format!("cannot parse '{s}' as Float"), sp))
865}
866
867// ---- stdlib extension: digests and codecs ----
868//
869// These are pure functions of their input: no I/O, no clock, no randomness, so
870// they keep the capability model (D1) and determinism (D13) intact. That is why
871// they belong in the core rather than in loadable native plugins, which could
872// not offer the same guarantees.
873
874const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
875
876/// `"text".sha256()` → the lowercase hex digest (config-checksum annotations).
877fn m_str_sha256<'a>(
878    _it: &mut Interpreter<'a>,
879    recv: &Value<'a>,
880    _args: &[Value<'a>],
881    _sp: Span,
882) -> Result<Value<'a>, Diagnostic> {
883    let Value::Str(s) = recv else { unreachable!() };
884    use sha2::{Digest, Sha256};
885    let digest = Sha256::digest(s.as_bytes());
886    let mut hex = String::with_capacity(digest.len() * 2);
887    for b in digest {
888        hex.push_str(&format!("{b:02x}"));
889    }
890    Ok(Value::str(hex))
891}
892
893/// `"text".base64()` → standard base64 with padding (e.g. k8s Secret data).
894fn m_str_base64<'a>(
895    _it: &mut Interpreter<'a>,
896    recv: &Value<'a>,
897    _args: &[Value<'a>],
898    _sp: Span,
899) -> Result<Value<'a>, Diagnostic> {
900    let Value::Str(s) = recv else { unreachable!() };
901    let bytes = s.as_bytes();
902    let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
903    for chunk in bytes.chunks(3) {
904        let b = [
905            chunk[0],
906            *chunk.get(1).unwrap_or(&0),
907            *chunk.get(2).unwrap_or(&0),
908        ];
909        let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
910        for i in 0..4 {
911            if i <= chunk.len() {
912                out.push(B64[((n >> (18 - 6 * i)) & 0x3f) as usize] as char);
913            } else {
914                out.push('=');
915            }
916        }
917    }
918    Ok(Value::str(out))
919}
920
921/// `"aGk=".base64_decode()` → the decoded String; E0321 if it is not valid
922/// base64 or does not decode to UTF-8.
923fn m_str_base64_decode<'a>(
924    _it: &mut Interpreter<'a>,
925    recv: &Value<'a>,
926    _args: &[Value<'a>],
927    sp: Span,
928) -> Result<Value<'a>, Diagnostic> {
929    let Value::Str(s) = recv else { unreachable!() };
930    let err = || rt("E0321", format!("'{s}' is not valid base64"), sp);
931    let src: Vec<u8> = s.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
932    if src.len() % 4 != 0 {
933        return Err(err());
934    }
935    let mut out: Vec<u8> = Vec::with_capacity(src.len() / 4 * 3);
936    for chunk in src.chunks(4) {
937        let pad = chunk.iter().filter(|&&b| b == b'=').count();
938        // Padding is only legal at the very end, at most two characters.
939        if pad > 2 || (pad > 0 && chunk[4 - pad..].iter().any(|&b| b != b'=')) {
940            return Err(err());
941        }
942        let mut n = 0u32;
943        for &b in &chunk[..4 - pad] {
944            let v = B64.iter().position(|&c| c == b).ok_or_else(err)? as u32;
945            n = (n << 6) | v;
946        }
947        n <<= 6 * pad as u32;
948        for i in 0..(3 - pad) {
949            out.push(((n >> (16 - 8 * i)) & 0xff) as u8);
950        }
951    }
952    String::from_utf8(out).map(Value::str).map_err(|_| err())
953}
954
955// ---- stdlib extension: List ----
956
957/// Total-ish scalar ordering; None means the two values are incomparable.
958fn scalar_cmp(a: &Value<'_>, b: &Value<'_>) -> Option<std::cmp::Ordering> {
959    use Value::*;
960    match (a, b) {
961        (Int(x), Int(y)) => Some(x.cmp(y)),
962        (Float(x), Float(y)) => x.partial_cmp(y),
963        (Int(x), Float(y)) => (*x as f64).partial_cmp(y),
964        (Float(x), Int(y)) => x.partial_cmp(&(*y as f64)),
965        (Str(x), Str(y)) => Some(x.as_ref().cmp(y.as_ref())),
966        (Bool(x), Bool(y)) => Some(x.cmp(y)),
967        _ => None,
968    }
969}
970
971/// `.sort()` — ascending; all elements must be mutually comparable scalars.
972fn m_list_sort<'a>(
973    _it: &mut Interpreter<'a>,
974    recv: &Value<'a>,
975    _args: &[Value<'a>],
976    sp: Span,
977) -> Result<Value<'a>, Diagnostic> {
978    let Value::List(xs) = recv else {
979        unreachable!()
980    };
981    // Validate up front so sort_by never sees an incomparable pair.
982    if let Some(first) = xs.first() {
983        for v in xs.iter() {
984            if scalar_cmp(first, v).is_none() {
985                return Err(rt(
986                    "E0306",
987                    format!(
988                        "sort() cannot compare {} and {}",
989                        first.type_name(),
990                        v.type_name()
991                    ),
992                    sp,
993                ));
994            }
995        }
996    }
997    let mut out: Vec<Value<'a>> = xs.to_vec();
998    out.sort_by(|a, b| scalar_cmp(a, b).unwrap_or(std::cmp::Ordering::Equal));
999    Ok(Value::list(out))
1000}
1001
1002fn m_list_reverse<'a>(
1003    _it: &mut Interpreter<'a>,
1004    recv: &Value<'a>,
1005    _args: &[Value<'a>],
1006    _sp: Span,
1007) -> Result<Value<'a>, Diagnostic> {
1008    let Value::List(xs) = recv else {
1009        unreachable!()
1010    };
1011    Ok(Value::list(xs.iter().rev().cloned().collect()))
1012}
1013
1014/// `.sum()` — Int if all Int, Float if any Float; E0304 on Int overflow.
1015fn m_list_sum<'a>(
1016    _it: &mut Interpreter<'a>,
1017    recv: &Value<'a>,
1018    args: &[Value<'a>],
1019    sp: Span,
1020) -> Result<Value<'a>, Diagnostic> {
1021    let _ = args;
1022    let Value::List(xs) = recv else {
1023        unreachable!()
1024    };
1025    let any_float = xs.iter().any(|v| matches!(v, Value::Float(_)));
1026    if any_float {
1027        let mut acc = 0.0f64;
1028        for v in xs.iter() {
1029            match v {
1030                Value::Int(n) => acc += *n as f64,
1031                Value::Float(f) => acc += *f,
1032                other => {
1033                    return Err(rt(
1034                        "E0306",
1035                        format!("sum() expects numbers, got {}", other.type_name()),
1036                        sp,
1037                    ))
1038                }
1039            }
1040        }
1041        Ok(Value::Float(acc))
1042    } else {
1043        let mut acc = 0i64;
1044        for v in xs.iter() {
1045            match v {
1046                Value::Int(n) => {
1047                    acc = acc
1048                        .checked_add(*n)
1049                        .ok_or_else(|| rt("E0304", "sum() overflows i64", sp))?
1050                }
1051                other => {
1052                    return Err(rt(
1053                        "E0306",
1054                        format!("sum() expects numbers, got {}", other.type_name()),
1055                        sp,
1056                    ))
1057                }
1058            }
1059        }
1060        Ok(Value::Int(acc))
1061    }
1062}
1063
1064fn list_extreme<'a>(
1065    xs: &[Value<'a>],
1066    want_max: bool,
1067    method: &str,
1068    sp: Span,
1069) -> Result<Value<'a>, Diagnostic> {
1070    let mut best = xs
1071        .first()
1072        .ok_or_else(|| rt("E0317", format!("{method}() on an empty list"), sp))?;
1073    for v in &xs[1..] {
1074        let ord = scalar_cmp(v, best).ok_or_else(|| {
1075            rt(
1076                "E0306",
1077                format!(
1078                    "{method}() cannot compare {} and {}",
1079                    v.type_name(),
1080                    best.type_name()
1081                ),
1082                sp,
1083            )
1084        })?;
1085        if (want_max && ord == std::cmp::Ordering::Greater)
1086            || (!want_max && ord == std::cmp::Ordering::Less)
1087        {
1088            best = v;
1089        }
1090    }
1091    Ok(best.clone())
1092}
1093
1094fn m_list_min<'a>(
1095    _it: &mut Interpreter<'a>,
1096    recv: &Value<'a>,
1097    _args: &[Value<'a>],
1098    sp: Span,
1099) -> Result<Value<'a>, Diagnostic> {
1100    let Value::List(xs) = recv else {
1101        unreachable!()
1102    };
1103    list_extreme(xs, false, "min", sp)
1104}
1105
1106fn m_list_max<'a>(
1107    _it: &mut Interpreter<'a>,
1108    recv: &Value<'a>,
1109    _args: &[Value<'a>],
1110    sp: Span,
1111) -> Result<Value<'a>, Diagnostic> {
1112    let Value::List(xs) = recv else {
1113        unreachable!()
1114    };
1115    list_extreme(xs, true, "max", sp)
1116}
1117
1118/// `.flatten()` — one level: nested lists are spread, scalars kept as-is.
1119fn m_list_flatten<'a>(
1120    _it: &mut Interpreter<'a>,
1121    recv: &Value<'a>,
1122    _args: &[Value<'a>],
1123    _sp: Span,
1124) -> Result<Value<'a>, Diagnostic> {
1125    let Value::List(xs) = recv else {
1126        unreachable!()
1127    };
1128    let mut out: Vec<Value<'a>> = Vec::new();
1129    for v in xs.iter() {
1130        match v {
1131            Value::List(inner) => out.extend(inner.iter().cloned()),
1132            other => out.push(other.clone()),
1133        }
1134    }
1135    Ok(Value::list(out))
1136}
1137
1138/// `.slice(start, end)` — half-open `[start, end)`, indices clamped to bounds.
1139fn m_list_slice<'a>(
1140    _it: &mut Interpreter<'a>,
1141    recv: &Value<'a>,
1142    args: &[Value<'a>],
1143    sp: Span,
1144) -> Result<Value<'a>, Diagnostic> {
1145    let Value::List(xs) = recv else {
1146        unreachable!()
1147    };
1148    let (Some(Value::Int(start)), Some(Value::Int(end))) = (args.first(), args.get(1)) else {
1149        return Err(rt(
1150            "E0306",
1151            "slice(start, end) expects two Int arguments",
1152            sp,
1153        ));
1154    };
1155    if *start < 0 || *end < 0 {
1156        return Err(rt("E0306", "slice() indices must be non-negative", sp));
1157    }
1158    let len = xs.len();
1159    let s = (*start as usize).min(len);
1160    let e = (*end as usize).clamp(s, len);
1161    Ok(Value::list(xs[s..e].to_vec()))
1162}
1163
1164// ---- stdlib extension: numeric + universal to_str ----
1165
1166fn m_num_abs<'a>(
1167    _it: &mut Interpreter<'a>,
1168    recv: &Value<'a>,
1169    _args: &[Value<'a>],
1170    sp: Span,
1171) -> Result<Value<'a>, Diagnostic> {
1172    match recv {
1173        Value::Int(n) => n
1174            .checked_abs()
1175            .map(Value::Int)
1176            .ok_or_else(|| rt("E0304", "abs() overflows i64", sp)),
1177        Value::Float(f) => Ok(Value::Float(f.abs())),
1178        _ => unreachable!(),
1179    }
1180}
1181
1182/// `.to_str()` — scalar → its String rendering (same as string interpolation).
1183fn m_to_str<'a>(
1184    it: &mut Interpreter<'a>,
1185    recv: &Value<'a>,
1186    _args: &[Value<'a>],
1187    sp: Span,
1188) -> Result<Value<'a>, Diagnostic> {
1189    Ok(Value::str(it.display(recv, sp)?))
1190}
1191
1192fn json_to_value<'a>(j: serde_json::Value) -> Value<'a> {
1193    match j {
1194        serde_json::Value::Null => Value::Null,
1195        serde_json::Value::Bool(b) => Value::Bool(b),
1196        serde_json::Value::Number(n) => match n.as_i64() {
1197            Some(i) => Value::Int(i),
1198            None => Value::Float(n.as_f64().unwrap_or(f64::NAN)),
1199        },
1200        serde_json::Value::String(s) => Value::str(s),
1201        serde_json::Value::Array(xs) => Value::list(xs.into_iter().map(json_to_value).collect()),
1202        serde_json::Value::Object(m) => Value::Object(Arc::new(
1203            m.into_iter().map(|(k, v)| (k, json_to_value(v))).collect(),
1204        )),
1205    }
1206}
1207
1208fn toml_to_value<'a>(t: toml::Value) -> Value<'a> {
1209    match t {
1210        toml::Value::String(s) => Value::str(s),
1211        toml::Value::Integer(n) => Value::Int(n),
1212        toml::Value::Float(n) => Value::Float(n),
1213        toml::Value::Boolean(b) => Value::Bool(b),
1214        toml::Value::Datetime(d) => Value::str(d.to_string()),
1215        toml::Value::Array(xs) => Value::list(xs.into_iter().map(toml_to_value).collect()),
1216        toml::Value::Table(t) => Value::Object(Arc::new(
1217            t.into_iter().map(|(k, v)| (k, toml_to_value(v))).collect(),
1218        )),
1219    }
1220}