Skip to main content

lex_runtime/
builtins.rs

1//! Pure stdlib builtins — string, numeric, list, option, result, json
2//! ops dispatched via the same `EffectHandler` interface as effects, but
3//! without policy gates (they have no observable side effects).
4
5use lex_bytecode::{MapKey, Value};
6use std::collections::{BTreeMap, BTreeSet, HashMap};
7use std::sync::{Mutex, OnceLock};
8
9/// Returns Some(...) if `(kind, op)` names a known pure builtin.
10/// `None` means "not handled here; fall through to effect dispatch".
11pub fn try_pure_builtin(kind: &str, op: &str, args: &[Value]) -> Option<Result<Value, String>> {
12    if !is_pure_module(kind) { return None; }
13    // `crypto.random` is the lone effectful op in an otherwise-pure
14    // module; let the handler dispatch it under the `[random]` effect
15    // kind instead of the pure-builtin bypass.
16    if (kind, op) == ("crypto", "random") { return None; }
17    // Same shape: `datetime.now` is the only effectful op in
18    // `std.datetime` (all the parse/format/arithmetic ops are pure).
19    if (kind, op) == ("datetime", "now") { return None; }
20    // `std.http` is mostly pure (builders + decoders); only the
21    // wire ops `send`/`get`/`post` need the [net] effect handler.
22    if (kind, "send") == (kind, op) && kind == "http" { return None; }
23    if (kind, "get")  == (kind, op) && kind == "http" { return None; }
24    if (kind, "post") == (kind, op) && kind == "http" { return None; }
25    Some(dispatch(kind, op, args))
26}
27
28/// `kind` is one of the known pure module aliases — used by the policy
29/// walk to skip pure builtins that programs reference via imports.
30pub fn is_pure_module(kind: &str) -> bool {
31    matches!(kind, "str" | "int" | "float" | "bool" | "list" | "iter"
32        | "option" | "result" | "tuple" | "json" | "bytes" | "flow" | "math"
33        | "map" | "set" | "crypto" | "regex" | "deque" | "datetime" | "duration" | "http"
34        | "toml" | "yaml" | "dotenv" | "csv" | "test" | "random" | "parser"
35        | "cli")
36}
37
38fn dispatch(kind: &str, op: &str, args: &[Value]) -> Result<Value, String> {
39    match (kind, op) {
40        // -- str --
41        ("str", "is_empty") => Ok(Value::Bool(expect_str(args.first())?.is_empty())),
42        ("str", "len") => Ok(Value::Int(expect_str(args.first())?.len() as i64)),
43        ("str", "concat") => {
44            let a = expect_str(args.first())?;
45            let b = expect_str(args.get(1))?;
46            Ok(Value::Str(format!("{a}{b}")))
47        }
48        ("str", "to_int") => {
49            let s = expect_str(args.first())?;
50            match s.parse::<i64>() {
51                Ok(n) => Ok(some(Value::Int(n))),
52                Err(_) => Ok(none()),
53            }
54        }
55        ("str", "split") => {
56            let s = expect_str(args.first())?;
57            let sep = expect_str(args.get(1))?;
58            let items: Vec<Value> = if sep.is_empty() {
59                s.chars().map(|c| Value::Str(c.to_string())).collect()
60            } else {
61                s.split(sep.as_str()).map(|p| Value::Str(p.to_string())).collect()
62            };
63            Ok(Value::List(items))
64        }
65        ("str", "join") => {
66            let parts = expect_list(args.first())?;
67            let sep = expect_str(args.get(1))?;
68            let mut out = String::new();
69            for (i, p) in parts.iter().enumerate() {
70                if i > 0 { out.push_str(&sep); }
71                match p {
72                    Value::Str(s) => out.push_str(s),
73                    other => return Err(format!("str.join element must be Str, got {other:?}")),
74                }
75            }
76            Ok(Value::Str(out))
77        }
78        ("str", "starts_with") => {
79            let s = expect_str(args.first())?;
80            let prefix = expect_str(args.get(1))?;
81            Ok(Value::Bool(s.starts_with(prefix.as_str())))
82        }
83        ("str", "ends_with") => {
84            let s = expect_str(args.first())?;
85            let suffix = expect_str(args.get(1))?;
86            Ok(Value::Bool(s.ends_with(suffix.as_str())))
87        }
88        ("str", "contains") => {
89            let s = expect_str(args.first())?;
90            let needle = expect_str(args.get(1))?;
91            Ok(Value::Bool(s.contains(needle.as_str())))
92        }
93        ("str", "replace") => {
94            let s = expect_str(args.first())?;
95            let from = expect_str(args.get(1))?;
96            let to = expect_str(args.get(2))?;
97            Ok(Value::Str(s.replace(from.as_str(), to.as_str())))
98        }
99        ("str", "trim") => Ok(Value::Str(expect_str(args.first())?.trim().to_string())),
100        ("str", "to_upper") => Ok(Value::Str(expect_str(args.first())?.to_uppercase())),
101        ("str", "to_lower") => Ok(Value::Str(expect_str(args.first())?.to_lowercase())),
102        ("str", "strip_prefix") => {
103            let s = expect_str(args.first())?;
104            let prefix = expect_str(args.get(1))?;
105            Ok(match s.strip_prefix(prefix.as_str()) {
106                Some(rest) => some(Value::Str(rest.to_string())),
107                None => none(),
108            })
109        }
110        ("str", "strip_suffix") => {
111            let s = expect_str(args.first())?;
112            let suffix = expect_str(args.get(1))?;
113            Ok(match s.strip_suffix(suffix.as_str()) {
114                Some(rest) => some(Value::Str(rest.to_string())),
115                None => none(),
116            })
117        }
118        ("str", "slice") => {
119            // Half-open byte-range slice. `hi` is clamped to `s.len()`
120            // and a negative `lo` / `hi` clamps to `0`, mirroring
121            // Python's `s[lo:hi]` semantics (and matching what
122            // production users expect when slicing fixed sizes off
123            // a possibly-shorter string — e.g. the first 64 chars
124            // of a license header). Reversed ranges (`lo > hi` after
125            // clamping) error since that's a caller logic bug. A
126            // mid-codepoint `lo` after clamping still errors so
127            // silent UTF-8 truncation never sneaks through.
128            let s = expect_str(args.first())?;
129            let lo_i = expect_int(args.get(1))?;
130            let hi_i = expect_int(args.get(2))?;
131            let lo = (lo_i.max(0) as usize).min(s.len());
132            let hi = (hi_i.max(0) as usize).min(s.len());
133            if lo > hi {
134                return Err(format!(
135                    "str.slice: reversed range [{lo}..{hi}] (after clamping to len {})",
136                    s.len()));
137            }
138            if !s.is_char_boundary(lo) || !s.is_char_boundary(hi) {
139                return Err(format!("str.slice: [{lo}..{hi}] not on char boundaries"));
140            }
141            Ok(Value::Str(s[lo..hi].to_string()))
142        }
143
144        // -- int / float --
145        ("int", "to_str") => Ok(Value::Str(expect_int(args.first())?.to_string())),
146        ("int", "to_float") => Ok(Value::Float(expect_int(args.first())? as f64)),
147        ("float", "to_int") => Ok(Value::Int(expect_float(args.first())? as i64)),
148        ("float", "to_str") => Ok(Value::Str(expect_float(args.first())?.to_string())),
149        ("str", "to_float") => {
150            let s = expect_str(args.first())?;
151            match s.parse::<f64>() {
152                Ok(f) => Ok(some(Value::Float(f))),
153                Err(_) => Ok(none()),
154            }
155        }
156
157        // -- list --
158        ("list", "len") => Ok(Value::Int(expect_list(args.first())?.len() as i64)),
159        ("list", "is_empty") => Ok(Value::Bool(expect_list(args.first())?.is_empty())),
160        ("list", "head") => {
161            let xs = expect_list(args.first())?;
162            match xs.first() {
163                Some(v) => Ok(some(v.clone())),
164                None => Ok(none()),
165            }
166        }
167        ("list", "tail") => {
168            let xs = expect_list(args.first())?;
169            if xs.is_empty() { Ok(Value::List(Vec::new())) }
170            else { Ok(Value::List(xs[1..].to_vec())) }
171        }
172        ("list", "range") => {
173            let lo = expect_int(args.first())?;
174            let hi = expect_int(args.get(1))?;
175            Ok(Value::List((lo..hi).map(Value::Int).collect()))
176        }
177        ("list", "concat") => {
178            let mut out = expect_list(args.first())?.clone();
179            out.extend(expect_list(args.get(1))?.iter().cloned());
180            Ok(Value::List(out))
181        }
182        ("list", "reverse") => {
183            let mut out = expect_list(args.first())?.clone();
184            out.reverse();
185            Ok(Value::List(out))
186        }
187        // #334: cons — prepend a single element to a list.
188        ("list", "cons") => {
189            let head = args.first().cloned().unwrap_or(Value::Unit);
190            let mut out = vec![head];
191            out.extend(expect_list(args.get(1))?.iter().cloned());
192            Ok(Value::List(out))
193        }
194        ("list", "enumerate") => {
195            let xs = expect_list(args.first())?;
196            let pairs = xs.iter().cloned().enumerate()
197                .map(|(i, v)| Value::Tuple(vec![Value::Int(i as i64), v]))
198                .collect::<Vec<_>>();
199            Ok(Value::List(pairs))
200        }
201
202        // -- tuple --
203        // Per §11.1: fst, snd, third for 2- and 3-tuples. Index out of
204        // range is an error rather than a panic so calling `tuple.third`
205        // on a 2-tuple is a clean failure instead of a host crash.
206        ("tuple", "fst")   => tuple_index(first_arg(args)?, 0),
207        ("tuple", "snd")   => tuple_index(first_arg(args)?, 1),
208        ("tuple", "third") => tuple_index(first_arg(args)?, 2),
209        ("tuple", "len") => match first_arg(args)? {
210            Value::Tuple(items) => Ok(Value::Int(items.len() as i64)),
211            other => Err(format!("tuple.len: expected Tuple, got {other:?}")),
212        },
213
214        // -- option --
215        ("option", "unwrap_or") => {
216            let opt = first_arg(args)?;
217            let default = args.get(1).cloned().unwrap_or(Value::Unit);
218            match opt {
219                Value::Variant { name, args } if name == "Some" && !args.is_empty() => Ok(args[0].clone()),
220                Value::Variant { name, .. } if name == "None" => Ok(default),
221                other => Err(format!("option.unwrap_or expected Option, got {other:?}")),
222            }
223        }
224        // option.unwrap_or_else: lazy default via thunk — only called when None.
225        // Handled inline by the bytecode compiler; this arm is the interpreter
226        // fallback path (thunk is pre-applied as a Value::Unit default since the
227        // runtime cannot call closures itself — the compiler path is canonical).
228        ("option", "unwrap_or_else") => {
229            let opt = first_arg(args)?;
230            match opt {
231                Value::Variant { name, args } if name == "Some" && !args.is_empty() => Ok(args[0].clone()),
232                Value::Variant { name, .. } if name == "None" => {
233                    // The closure argument cannot be invoked from pure-builtin
234                    // context; callers that reach this path have already
235                    // evaluated the thunk and passed its result as args[1].
236                    Ok(args.get(1).cloned().unwrap_or(Value::Unit))
237                }
238                other => Err(format!("option.unwrap_or_else expected Option, got {other:?}")),
239            }
240        }
241        ("option", "is_some") => match first_arg(args)? {
242            Value::Variant { name, .. } => Ok(Value::Bool(name == "Some")),
243            other => Err(format!("option.is_some expected Option, got {other:?}")),
244        },
245        ("option", "is_none") => match first_arg(args)? {
246            Value::Variant { name, .. } => Ok(Value::Bool(name == "None")),
247            other => Err(format!("option.is_none expected Option, got {other:?}")),
248        },
249
250        // -- result --
251        ("result", "is_ok") => match first_arg(args)? {
252            Value::Variant { name, .. } => Ok(Value::Bool(name == "Ok")),
253            other => Err(format!("result.is_ok expected Result, got {other:?}")),
254        },
255        ("result", "is_err") => match first_arg(args)? {
256            Value::Variant { name, .. } => Ok(Value::Bool(name == "Err")),
257            other => Err(format!("result.is_err expected Result, got {other:?}")),
258        },
259        ("result", "unwrap_or") => {
260            let res = first_arg(args)?;
261            let default = args.get(1).cloned().unwrap_or(Value::Unit);
262            match res {
263                Value::Variant { name, args } if name == "Ok" && !args.is_empty() => Ok(args[0].clone()),
264                Value::Variant { name, .. } if name == "Err" => Ok(default),
265                other => Err(format!("result.unwrap_or expected Result, got {other:?}")),
266            }
267        }
268
269        // -- json --
270        ("json", "stringify") => {
271            let v = first_arg(args)?;
272            Ok(Value::Str(serde_json::to_string(&value_to_json(v)).unwrap_or_default()))
273        }
274        ("json", "parse") => {
275            let s = expect_str(args.first())?;
276            match serde_json::from_str::<serde_json::Value>(&s) {
277                Ok(v) => Ok(ok_v(json_to_value(&v))),
278                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
279            }
280        }
281        // Tactical fix for #168: validate required fields before
282        // returning Ok. #322: also validate field types via schema.
283        ("json", "parse_strict") => {
284            let s = expect_str(args.first())?;
285            let required = required_field_names(args.get(1))?;
286            let schema = extract_type_schema(args.get(2));
287            match serde_json::from_str::<serde_json::Value>(&s) {
288                Ok(v) => {
289                    if let Err(e) = check_required_fields(&v, &required) {
290                        return Ok(err_v(Value::Str(e)));
291                    }
292                    if let Err(e) = validate_field_types(&v, &schema) {
293                        return Ok(err_v(Value::Str(e)));
294                    }
295                    Ok(ok_v(json_to_value(&v)))
296                }
297                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
298            }
299        }
300
301        // -- toml (config parser; routes through serde_json::Value
302        // so the parsed shape composes with the existing json
303        // tooling. Datetimes become RFC 3339 strings — the only
304        // info-losing step) --
305        ("toml", "parse") => {
306            let s = expect_str(args.first())?;
307            match toml::from_str::<serde_json::Value>(&s) {
308                Ok(mut v) => {
309                    unwrap_toml_datetime_markers(&mut v);
310                    Ok(ok_v(json_to_value(&v)))
311                }
312                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
313            }
314        }
315        // Compiler-emitted variant of parse_strict that carries the type
316        // schema injected by the type-checker rewrite pass (#322).
317        // Identical to parse_strict but the 3rd arg (schema) is always present.
318        ("json", "parse_strict_typed") => {
319            let s = expect_str(args.first())?;
320            let required = required_field_names(args.get(1))?;
321            let schema = extract_type_schema(args.get(2));
322            match serde_json::from_str::<serde_json::Value>(&s) {
323                Ok(v) => {
324                    if let Err(e) = check_required_fields(&v, &required) {
325                        return Ok(err_v(Value::Str(e)));
326                    }
327                    if let Err(e) = validate_field_types(&v, &schema) {
328                        return Ok(err_v(Value::Str(e)));
329                    }
330                    Ok(ok_v(json_to_value(&v)))
331                }
332                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
333            }
334        }
335
336        // Tactical fix for #168: validate required fields before
337        // returning Ok. #322: also validate field types via schema.
338        ("toml", "parse_strict") => {
339            let s = expect_str(args.first())?;
340            let required = required_field_names(args.get(1))?;
341            let schema = extract_type_schema(args.get(2));
342            match toml::from_str::<serde_json::Value>(&s) {
343                Ok(mut v) => {
344                    unwrap_toml_datetime_markers(&mut v);
345                    if let Err(e) = check_required_fields(&v, &required) {
346                        return Ok(err_v(Value::Str(e)));
347                    }
348                    if let Err(e) = validate_field_types(&v, &schema) {
349                        return Ok(err_v(Value::Str(e)));
350                    }
351                    Ok(ok_v(json_to_value(&v)))
352                }
353                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
354            }
355        }
356        ("toml", "parse_strict_typed") => {
357            let s = expect_str(args.first())?;
358            let required = required_field_names(args.get(1))?;
359            let schema = extract_type_schema(args.get(2));
360            match toml::from_str::<serde_json::Value>(&s) {
361                Ok(mut v) => {
362                    unwrap_toml_datetime_markers(&mut v);
363                    if let Err(e) = check_required_fields(&v, &required) {
364                        return Ok(err_v(Value::Str(e)));
365                    }
366                    if let Err(e) = validate_field_types(&v, &schema) {
367                        return Ok(err_v(Value::Str(e)));
368                    }
369                    Ok(ok_v(json_to_value(&v)))
370                }
371                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
372            }
373        }
374        ("toml", "stringify") => {
375            let v = first_arg(args)?;
376            // serde_json::Value → toml::Value via its serde impls.
377            // TOML's grammar is stricter than JSON's (top-level
378            // must be a table; no `null`; no mixed-type arrays),
379            // so the conversion can fail — surface as Result::Err
380            // rather than panic.
381            let json = value_to_json(v);
382            match toml::to_string(&json) {
383                Ok(s)  => Ok(ok_v(Value::Str(s))),
384                Err(e) => Ok(err_v(Value::Str(format!("toml.stringify: {e}")))),
385            }
386        }
387
388        // -- yaml -- mirrors std.toml. Wraps serde_yaml so values
389        // map to the same Lex shape as JSON. YAML's Tag/Anchor
390        // features are folded out by serde_yaml's deserialize-to-
391        // Value path; non-representable shapes (e.g. non-string
392        // map keys when stringifying) surface as Result::Err.
393        ("yaml", "parse") => {
394            let s = expect_str(args.first())?;
395            match serde_yaml::from_str::<serde_json::Value>(&s) {
396                Ok(v)  => Ok(ok_v(json_to_value(&v))),
397                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
398            }
399        }
400        // Tactical fix for #168 — same shape as toml.parse_strict.
401        // #322: also validate field types via schema.
402        ("yaml", "parse_strict") => {
403            let s = expect_str(args.first())?;
404            let required = required_field_names(args.get(1))?;
405            let schema = extract_type_schema(args.get(2));
406            match serde_yaml::from_str::<serde_json::Value>(&s) {
407                Ok(v) => {
408                    if let Err(e) = check_required_fields(&v, &required) {
409                        return Ok(err_v(Value::Str(e)));
410                    }
411                    if let Err(e) = validate_field_types(&v, &schema) {
412                        return Ok(err_v(Value::Str(e)));
413                    }
414                    Ok(ok_v(json_to_value(&v)))
415                }
416                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
417            }
418        }
419        ("yaml", "parse_strict_typed") => {
420            let s = expect_str(args.first())?;
421            let required = required_field_names(args.get(1))?;
422            let schema = extract_type_schema(args.get(2));
423            match serde_yaml::from_str::<serde_json::Value>(&s) {
424                Ok(v) => {
425                    if let Err(e) = check_required_fields(&v, &required) {
426                        return Ok(err_v(Value::Str(e)));
427                    }
428                    if let Err(e) = validate_field_types(&v, &schema) {
429                        return Ok(err_v(Value::Str(e)));
430                    }
431                    Ok(ok_v(json_to_value(&v)))
432                }
433                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
434            }
435        }
436        ("yaml", "stringify") => {
437            let v = first_arg(args)?;
438            let json = value_to_json(v);
439            match serde_yaml::to_string(&json) {
440                Ok(s)  => Ok(ok_v(Value::Str(s))),
441                Err(e) => Ok(err_v(Value::Str(format!("yaml.stringify: {e}")))),
442            }
443        }
444
445        // -- dotenv -- KEY=VALUE pair files. Hand-rolled parser
446        // because the dotenvy crate's API is geared at loading
447        // into the process env, not parsing-to-data. The grammar
448        // we accept: blank lines, `# comment` lines, and
449        // `KEY=VALUE` (optional surrounding `"..."` or `'...'`,
450        // unescaped). Simple but covers the .env files in the
451        // wild that aren't trying to be shell.
452        ("dotenv", "parse") => {
453            use std::collections::BTreeMap;
454            use lex_bytecode::MapKey;
455            let s = expect_str(args.first())?;
456            match parse_dotenv(&s) {
457                Ok(map) => {
458                    let mut bt: BTreeMap<MapKey, Value> = BTreeMap::new();
459                    for (k, v) in map {
460                        bt.insert(MapKey::Str(k), Value::Str(v));
461                    }
462                    Ok(ok_v(Value::Map(bt)))
463                }
464                Err(e) => Ok(err_v(Value::Str(e))),
465            }
466        }
467
468        // -- csv -- rows-as-lists; first row is whatever the file
469        // has. The caller decides whether row 0 is a header. We
470        // could ship a `parse_with_headers` later that returns a
471        // List[Map[Str, Str]]; v1 keeps the surface tight.
472        ("csv", "parse") => {
473            let s = expect_str(args.first())?;
474            let mut rdr = csv::ReaderBuilder::new()
475                .has_headers(false)
476                .flexible(true)
477                .from_reader(s.as_bytes());
478            let mut rows: Vec<Value> = Vec::new();
479            for r in rdr.records() {
480                match r {
481                    Ok(rec) => {
482                        let row: Vec<Value> = rec.iter()
483                            .map(|f| Value::Str(f.to_string()))
484                            .collect();
485                        rows.push(Value::List(row));
486                    }
487                    Err(e) => return Ok(err_v(Value::Str(format!("csv.parse: {e}")))),
488                }
489            }
490            Ok(ok_v(Value::List(rows)))
491        }
492        ("csv", "stringify") => {
493            // List[List[Str]] → CSV string. Mixed-type rows are
494            // not allowed (CSV is text-only); non-Str cells get
495            // stringified via to_json since that's already the
496            // convention for `json.stringify` etc.
497            let v = first_arg(args)?;
498            let rows = match v {
499                Value::List(rs) => rs,
500                _ => return Ok(err_v(Value::Str("csv.stringify expects List[List[Str]]".into()))),
501            };
502            let mut out = Vec::new();
503            {
504                let mut wtr = csv::WriterBuilder::new()
505                    .has_headers(false)
506                    .from_writer(&mut out);
507                for row in rows {
508                    let cells = match row {
509                        Value::List(cs) => cs,
510                        _ => return Ok(err_v(Value::Str("csv.stringify row must be List[Str]".into()))),
511                    };
512                    let strs: Vec<String> = cells.iter().map(|c| match c {
513                        Value::Str(s) => s.clone(),
514                        other => serde_json::to_string(&other.to_json())
515                            .unwrap_or_else(|_| String::new()),
516                    }).collect();
517                    if let Err(e) = wtr.write_record(&strs) {
518                        return Ok(err_v(Value::Str(format!("csv.stringify: {e}"))));
519                    }
520                }
521                if let Err(e) = wtr.flush() {
522                    return Ok(err_v(Value::Str(format!("csv.stringify flush: {e}"))));
523                }
524            }
525            match String::from_utf8(out) {
526                Ok(s) => Ok(ok_v(Value::Str(s))),
527                Err(e) => Ok(err_v(Value::Str(format!("csv.stringify utf8: {e}")))),
528            }
529        }
530
531        // -- test -- tiny assertion library. Each helper is pure
532        // and returns `Result[Unit, Str]` so tests are themselves
533        // functions returning a Result. A suite is a List the user
534        // iterates with `list.fold`; no Rust-side Suite/Runner
535        // types in v1, so the whole thing is 4 builtins + a few
536        // Lex-source helpers callers can copy into their tests/.
537        ("test", "assert_eq") => {
538            let a = first_arg(args)?;
539            let b = args.get(1).ok_or("test.assert_eq: missing second arg")?;
540            if a == b {
541                Ok(ok_v(Value::Unit))
542            } else {
543                Ok(err_v(Value::Str(format!("assert_eq: lhs {} != rhs {}",
544                    value_to_json(a), value_to_json(b)))))
545            }
546        }
547        ("test", "assert_ne") => {
548            let a = first_arg(args)?;
549            let b = args.get(1).ok_or("test.assert_ne: missing second arg")?;
550            if a != b {
551                Ok(ok_v(Value::Unit))
552            } else {
553                Ok(err_v(Value::Str(format!("assert_ne: both sides are {}",
554                    value_to_json(a)))))
555            }
556        }
557        ("test", "assert_true") => {
558            match first_arg(args)? {
559                Value::Bool(true) => Ok(ok_v(Value::Unit)),
560                Value::Bool(false) => Ok(err_v(Value::Str("assert_true: was false".into()))),
561                other => Err(format!("test.assert_true expects Bool, got {other:?}")),
562            }
563        }
564        ("test", "assert_false") => {
565            match first_arg(args)? {
566                Value::Bool(false) => Ok(ok_v(Value::Unit)),
567                Value::Bool(true)  => Ok(err_v(Value::Str("assert_false: was true".into()))),
568                other => Err(format!("test.assert_false expects Bool, got {other:?}")),
569            }
570        }
571
572        // -- bytes --
573        ("bytes", "len") => {
574            let b = expect_bytes(args.first())?;
575            Ok(Value::Int(b.len() as i64))
576        }
577        ("bytes", "eq") => {
578            let a = expect_bytes(args.first())?;
579            let b = expect_bytes(args.get(1))?;
580            Ok(Value::Bool(a == b))
581        }
582        ("bytes", "from_str") => {
583            let s = expect_str(args.first())?;
584            Ok(Value::Bytes(s.into_bytes()))
585        }
586        ("bytes", "to_str") => {
587            let b = expect_bytes(args.first())?;
588            match String::from_utf8(b.to_vec()) {
589                Ok(s) => Ok(ok_v(Value::Str(s))),
590                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
591            }
592        }
593        ("bytes", "slice") => {
594            let b = expect_bytes(args.first())?;
595            let lo = expect_int(args.get(1))? as usize;
596            let hi = expect_int(args.get(2))? as usize;
597            if lo > hi || hi > b.len() {
598                return Err(format!("bytes.slice: out of range [{lo}..{hi}] of {}", b.len()));
599            }
600            Ok(Value::Bytes(b[lo..hi].to_vec()))
601        }
602        ("bytes", "is_empty") => {
603            let b = expect_bytes(args.first())?;
604            Ok(Value::Bool(b.is_empty()))
605        }
606
607        // -- math --
608        // Matrices are stored as the F64Array fast-lane variant (a flat
609        // row-major Vec<f64> with shape). Lex code treats them as the
610        // type alias `Matrix = { rows :: Int, cols :: Int, data ::
611        // List[Float] }`; field access is unsupported, so all
612        // introspection happens through these helpers.
613        ("math", "exp")   => Ok(Value::Float(expect_float(args.first())?.exp())),
614        ("math", "log")   => Ok(Value::Float(expect_float(args.first())?.ln())),
615        ("math", "log2")  => Ok(Value::Float(expect_float(args.first())?.log2())),
616        ("math", "log10") => Ok(Value::Float(expect_float(args.first())?.log10())),
617        ("math", "sqrt")  => Ok(Value::Float(expect_float(args.first())?.sqrt())),
618        ("math", "abs")   => Ok(Value::Float(expect_float(args.first())?.abs())),
619        ("math", "sin")   => Ok(Value::Float(expect_float(args.first())?.sin())),
620        ("math", "cos")   => Ok(Value::Float(expect_float(args.first())?.cos())),
621        ("math", "tan")   => Ok(Value::Float(expect_float(args.first())?.tan())),
622        ("math", "asin")  => Ok(Value::Float(expect_float(args.first())?.asin())),
623        ("math", "acos")  => Ok(Value::Float(expect_float(args.first())?.acos())),
624        ("math", "atan")  => Ok(Value::Float(expect_float(args.first())?.atan())),
625        ("math", "floor") => Ok(Value::Float(expect_float(args.first())?.floor())),
626        ("math", "ceil")  => Ok(Value::Float(expect_float(args.first())?.ceil())),
627        ("math", "round") => Ok(Value::Float(expect_float(args.first())?.round())),
628        ("math", "trunc") => Ok(Value::Float(expect_float(args.first())?.trunc())),
629        ("math", "pow") => {
630            let a = expect_float(args.first())?;
631            let b = expect_float(args.get(1))?;
632            Ok(Value::Float(a.powf(b)))
633        }
634        ("math", "atan2") => {
635            let y = expect_float(args.first())?;
636            let x = expect_float(args.get(1))?;
637            Ok(Value::Float(y.atan2(x)))
638        }
639        ("math", "min") => {
640            let a = expect_float(args.first())?;
641            let b = expect_float(args.get(1))?;
642            Ok(Value::Float(a.min(b)))
643        }
644        ("math", "max") => {
645            let a = expect_float(args.first())?;
646            let b = expect_float(args.get(1))?;
647            Ok(Value::Float(a.max(b)))
648        }
649        ("math", "zeros") => {
650            let r = expect_int(args.first())?;
651            let c = expect_int(args.get(1))?;
652            if r < 0 || c < 0 {
653                return Err(format!("math.zeros: negative dim {r}x{c}"));
654            }
655            let r = r as usize; let c = c as usize;
656            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data: vec![0.0; r * c] })
657        }
658        ("math", "ones") => {
659            let r = expect_int(args.first())?;
660            let c = expect_int(args.get(1))?;
661            if r < 0 || c < 0 {
662                return Err(format!("math.ones: negative dim {r}x{c}"));
663            }
664            let r = r as usize; let c = c as usize;
665            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data: vec![1.0; r * c] })
666        }
667        ("math", "from_lists") => {
668            let rows = expect_list(args.first())?;
669            let r = rows.len();
670            if r == 0 {
671                return Ok(Value::F64Array { rows: 0, cols: 0, data: Vec::new() });
672            }
673            let first_row = match &rows[0] {
674                Value::List(xs) => xs,
675                other => return Err(format!("math.from_lists: row 0 not List, got {other:?}")),
676            };
677            let c = first_row.len();
678            let mut data = Vec::with_capacity(r * c);
679            for (i, row) in rows.iter().enumerate() {
680                let row = match row {
681                    Value::List(xs) => xs,
682                    other => return Err(format!("math.from_lists: row {i} not List, got {other:?}")),
683                };
684                if row.len() != c {
685                    return Err(format!("math.from_lists: row {i} has {} cols, expected {c}", row.len()));
686                }
687                for (j, v) in row.iter().enumerate() {
688                    let f = match v {
689                        Value::Float(f) => *f,
690                        Value::Int(n) => *n as f64,
691                        other => return Err(format!("math.from_lists: ({i},{j}) not numeric, got {other:?}")),
692                    };
693                    data.push(f);
694                }
695            }
696            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data })
697        }
698        ("math", "from_flat") => {
699            let r = expect_int(args.first())?;
700            let c = expect_int(args.get(1))?;
701            let xs = expect_list(args.get(2))?;
702            if r < 0 || c < 0 {
703                return Err(format!("math.from_flat: negative dim {r}x{c}"));
704            }
705            let r = r as usize; let c = c as usize;
706            if xs.len() != r * c {
707                return Err(format!("math.from_flat: list len {} != {}*{}", xs.len(), r, c));
708            }
709            let mut data = Vec::with_capacity(r * c);
710            for v in xs {
711                data.push(match v {
712                    Value::Float(f) => *f,
713                    Value::Int(n)   => *n as f64,
714                    other => return Err(format!("math.from_flat: non-numeric element {other:?}")),
715                });
716            }
717            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data })
718        }
719        ("math", "rows") => {
720            let (r, _, _) = unpack_matrix(first_arg(args)?)?;
721            Ok(Value::Int(r as i64))
722        }
723        ("math", "cols") => {
724            let (_, c, _) = unpack_matrix(first_arg(args)?)?;
725            Ok(Value::Int(c as i64))
726        }
727        ("math", "get") => {
728            let (r, c, data) = unpack_matrix(first_arg(args)?)?;
729            let i = expect_int(args.get(1))? as usize;
730            let j = expect_int(args.get(2))? as usize;
731            if i >= r || j >= c {
732                return Err(format!("math.get: ({i},{j}) out of {r}x{c}"));
733            }
734            Ok(Value::Float(data[i * c + j]))
735        }
736        ("math", "to_flat") => {
737            let (_, _, data) = unpack_matrix(first_arg(args)?)?;
738            Ok(Value::List(data.into_iter().map(Value::Float).collect()))
739        }
740        ("math", "transpose") => {
741            let (r, c, data) = unpack_matrix(first_arg(args)?)?;
742            let mut out = vec![0.0; r * c];
743            for i in 0..r {
744                for j in 0..c {
745                    out[j * r + i] = data[i * c + j];
746                }
747            }
748            Ok(Value::F64Array { rows: c as u32, cols: r as u32, data: out })
749        }
750        ("math", "matmul") => {
751            let (m, k1, a) = unpack_matrix(first_arg(args)?)?;
752            let (k2, n, b) = unpack_matrix(args.get(1).ok_or("math.matmul: missing arg 1")?)?;
753            if k1 != k2 {
754                return Err(format!("math.matmul: dim mismatch {m}x{k1} · {k2}x{n}"));
755            }
756            // Plain triple loop. For the small matrices used in the ML
757            // demo (n<200, k<10) this is well under a millisecond and
758            // avoids pulling in matrixmultiply for the runtime crate.
759            let mut c = vec![0.0; m * n];
760            for i in 0..m {
761                for kk in 0..k1 {
762                    let aik = a[i * k1 + kk];
763                    for j in 0..n {
764                        c[i * n + j] += aik * b[kk * n + j];
765                    }
766                }
767            }
768            Ok(Value::F64Array { rows: m as u32, cols: n as u32, data: c })
769        }
770        ("math", "scale") => {
771            let s = expect_float(args.first())?;
772            let (r, c, mut data) = unpack_matrix(args.get(1).ok_or("math.scale: missing arg 1")?)?;
773            for x in &mut data { *x *= s; }
774            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data })
775        }
776        ("math", "add") | ("math", "sub") => {
777            let (ar, ac, a) = unpack_matrix(first_arg(args)?)?;
778            let (br, bc, b) = unpack_matrix(args.get(1).ok_or("math.add/sub: missing arg 1")?)?;
779            if ar != br || ac != bc {
780                return Err(format!("math.{op}: shape mismatch {ar}x{ac} vs {br}x{bc}"));
781            }
782            let neg = op == "sub";
783            let mut out = a;
784            for (i, x) in out.iter_mut().enumerate() {
785                if neg { *x -= b[i] } else { *x += b[i] }
786            }
787            Ok(Value::F64Array { rows: ar as u32, cols: ac as u32, data: out })
788        }
789        ("math", "sigmoid") => {
790            let (r, c, mut data) = unpack_matrix(first_arg(args)?)?;
791            for x in &mut data { *x = 1.0 / (1.0 + (-*x).exp()); }
792            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data })
793        }
794
795        // -- map --
796        ("map", "new") => Ok(Value::Map(BTreeMap::new())),
797        ("map", "size") => Ok(Value::Int(expect_map(args.first())?.len() as i64)),
798        ("map", "has") => {
799            let m = expect_map(args.first())?;
800            let k = MapKey::from_value(args.get(1).ok_or("map.has: missing key")?)?;
801            Ok(Value::Bool(m.contains_key(&k)))
802        }
803        ("map", "get") => {
804            let m = expect_map(args.first())?;
805            let k = MapKey::from_value(args.get(1).ok_or("map.get: missing key")?)?;
806            Ok(match m.get(&k) {
807                Some(v) => some(v.clone()),
808                None    => none(),
809            })
810        }
811        ("map", "set") => {
812            let mut m = expect_map(args.first())?.clone();
813            let k = MapKey::from_value(args.get(1).ok_or("map.set: missing key")?)?;
814            let v = args.get(2).ok_or("map.set: missing value")?.clone();
815            m.insert(k, v);
816            Ok(Value::Map(m))
817        }
818        ("map", "delete") => {
819            let mut m = expect_map(args.first())?.clone();
820            let k = MapKey::from_value(args.get(1).ok_or("map.delete: missing key")?)?;
821            m.remove(&k);
822            Ok(Value::Map(m))
823        }
824        ("map", "keys") => {
825            let m = expect_map(args.first())?;
826            Ok(Value::List(m.keys().cloned().map(MapKey::into_value).collect()))
827        }
828        ("map", "values") => {
829            let m = expect_map(args.first())?;
830            Ok(Value::List(m.values().cloned().collect()))
831        }
832        ("map", "entries") => {
833            let m = expect_map(args.first())?;
834            Ok(Value::List(m.iter()
835                .map(|(k, v)| Value::Tuple(vec![k.as_value(), v.clone()]))
836                .collect()))
837        }
838        ("map", "from_list") => {
839            let pairs = expect_list(args.first())?;
840            let mut m = BTreeMap::new();
841            for p in pairs {
842                let items = match p {
843                    Value::Tuple(items) if items.len() == 2 => items,
844                    other => return Err(format!(
845                        "map.from_list element must be a 2-tuple, got {other:?}")),
846                };
847                let k = MapKey::from_value(&items[0])?;
848                m.insert(k, items[1].clone());
849            }
850            Ok(Value::Map(m))
851        }
852
853        // -- set --
854        ("set", "new") => Ok(Value::Set(BTreeSet::new())),
855        ("set", "size") => Ok(Value::Int(expect_set(args.first())?.len() as i64)),
856        ("set", "has") => {
857            let s = expect_set(args.first())?;
858            let k = MapKey::from_value(args.get(1).ok_or("set.has: missing element")?)?;
859            Ok(Value::Bool(s.contains(&k)))
860        }
861        ("set", "add") => {
862            let mut s = expect_set(args.first())?.clone();
863            let k = MapKey::from_value(args.get(1).ok_or("set.add: missing element")?)?;
864            s.insert(k);
865            Ok(Value::Set(s))
866        }
867        ("set", "delete") => {
868            let mut s = expect_set(args.first())?.clone();
869            let k = MapKey::from_value(args.get(1).ok_or("set.delete: missing element")?)?;
870            s.remove(&k);
871            Ok(Value::Set(s))
872        }
873        ("set", "to_list") => {
874            let s = expect_set(args.first())?;
875            Ok(Value::List(s.iter().cloned().map(MapKey::into_value).collect()))
876        }
877        ("set", "from_list") => {
878            let xs = expect_list(args.first())?;
879            let mut s = BTreeSet::new();
880            for x in xs {
881                s.insert(MapKey::from_value(x)?);
882            }
883            Ok(Value::Set(s))
884        }
885        ("set", "union") => {
886            let a = expect_set(args.first())?;
887            let b = expect_set(args.get(1))?;
888            Ok(Value::Set(a.union(b).cloned().collect()))
889        }
890        ("set", "intersect") => {
891            let a = expect_set(args.first())?;
892            let b = expect_set(args.get(1))?;
893            Ok(Value::Set(a.intersection(b).cloned().collect()))
894        }
895        ("set", "diff") => {
896            let a = expect_set(args.first())?;
897            let b = expect_set(args.get(1))?;
898            Ok(Value::Set(a.difference(b).cloned().collect()))
899        }
900        ("set", "is_empty") => Ok(Value::Bool(expect_set(args.first())?.is_empty())),
901        ("set", "is_subset") => {
902            let a = expect_set(args.first())?;
903            let b = expect_set(args.get(1))?;
904            Ok(Value::Bool(a.is_subset(b)))
905        }
906
907        // -- map helpers --
908        ("map", "merge") => {
909            // b's entries override a's. We construct a new BTreeMap
910            // by extending a with b's pairs.
911            let a = expect_map(args.first())?.clone();
912            let b = expect_map(args.get(1))?;
913            let mut out = a;
914            for (k, v) in b {
915                out.insert(k.clone(), v.clone());
916            }
917            Ok(Value::Map(out))
918        }
919        ("map", "is_empty") => Ok(Value::Bool(expect_map(args.first())?.is_empty())),
920
921        // -- deque --
922        ("deque", "new") => Ok(Value::Deque(std::collections::VecDeque::new())),
923        ("deque", "size") => Ok(Value::Int(expect_deque(args.first())?.len() as i64)),
924        ("deque", "is_empty") => Ok(Value::Bool(expect_deque(args.first())?.is_empty())),
925        ("deque", "push_back") => {
926            let mut d = expect_deque(args.first())?.clone();
927            let x = args.get(1).ok_or("deque.push_back: missing value")?.clone();
928            d.push_back(x);
929            Ok(Value::Deque(d))
930        }
931        ("deque", "push_front") => {
932            let mut d = expect_deque(args.first())?.clone();
933            let x = args.get(1).ok_or("deque.push_front: missing value")?.clone();
934            d.push_front(x);
935            Ok(Value::Deque(d))
936        }
937        ("deque", "pop_back") => {
938            let mut d = expect_deque(args.first())?.clone();
939            match d.pop_back() {
940                Some(x) => Ok(Value::Variant {
941                    name: "Some".into(),
942                    args: vec![Value::Tuple(vec![x, Value::Deque(d)])],
943                }),
944                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
945            }
946        }
947        ("deque", "pop_front") => {
948            let mut d = expect_deque(args.first())?.clone();
949            match d.pop_front() {
950                Some(x) => Ok(Value::Variant {
951                    name: "Some".into(),
952                    args: vec![Value::Tuple(vec![x, Value::Deque(d)])],
953                }),
954                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
955            }
956        }
957        ("deque", "peek_back") => {
958            let d = expect_deque(args.first())?;
959            match d.back() {
960                Some(x) => Ok(Value::Variant {
961                    name: "Some".into(),
962                    args: vec![x.clone()],
963                }),
964                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
965            }
966        }
967        ("deque", "peek_front") => {
968            let d = expect_deque(args.first())?;
969            match d.front() {
970                Some(x) => Ok(Value::Variant {
971                    name: "Some".into(),
972                    args: vec![x.clone()],
973                }),
974                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
975            }
976        }
977        ("deque", "from_list") => {
978            let xs = expect_list(args.first())?;
979            Ok(Value::Deque(xs.iter().cloned().collect()))
980        }
981        ("deque", "to_list") => {
982            let d = expect_deque(args.first())?;
983            Ok(Value::List(d.iter().cloned().collect()))
984        }
985
986        // -- crypto (pure ops; crypto.random is effectful and routes
987        // through the handler under [random], see try_pure_builtin) --
988        ("crypto", "sha256") => {
989            use sha2::{Digest, Sha256};
990            let data = expect_bytes(args.first())?;
991            let mut h = Sha256::new();
992            h.update(data);
993            Ok(Value::Bytes(h.finalize().to_vec()))
994        }
995        ("crypto", "sha512") => {
996            use sha2::{Digest, Sha512};
997            let data = expect_bytes(args.first())?;
998            let mut h = Sha512::new();
999            h.update(data);
1000            Ok(Value::Bytes(h.finalize().to_vec()))
1001        }
1002        ("crypto", "md5") => {
1003            use md5::{Digest, Md5};
1004            let data = expect_bytes(args.first())?;
1005            let mut h = Md5::new();
1006            h.update(data);
1007            Ok(Value::Bytes(h.finalize().to_vec()))
1008        }
1009        ("crypto", "hmac_sha256") => {
1010            use hmac::{Hmac, KeyInit, Mac};
1011            type HmacSha256 = Hmac<sha2::Sha256>;
1012            let key = expect_bytes(args.first())?;
1013            let data = expect_bytes(args.get(1))?;
1014            let mut mac = HmacSha256::new_from_slice(key)
1015                .map_err(|e| format!("hmac_sha256 key: {e}"))?;
1016            mac.update(data);
1017            Ok(Value::Bytes(mac.finalize().into_bytes().to_vec()))
1018        }
1019        ("crypto", "hmac_sha512") => {
1020            use hmac::{Hmac, KeyInit, Mac};
1021            type HmacSha512 = Hmac<sha2::Sha512>;
1022            let key = expect_bytes(args.first())?;
1023            let data = expect_bytes(args.get(1))?;
1024            let mut mac = HmacSha512::new_from_slice(key)
1025                .map_err(|e| format!("hmac_sha512 key: {e}"))?;
1026            mac.update(data);
1027            Ok(Value::Bytes(mac.finalize().into_bytes().to_vec()))
1028        }
1029        ("crypto", "base64_encode") => {
1030            use base64::{Engine, engine::general_purpose::STANDARD};
1031            let data = expect_bytes(args.first())?;
1032            Ok(Value::Str(STANDARD.encode(data)))
1033        }
1034        ("crypto", "base64_decode") => {
1035            use base64::{Engine, engine::general_purpose::STANDARD};
1036            let s = expect_str(args.first())?;
1037            match STANDARD.decode(s) {
1038                Ok(b)  => Ok(ok_v(Value::Bytes(b))),
1039                Err(e) => Ok(err_v(Value::Str(format!("base64: {e}")))),
1040            }
1041        }
1042        ("crypto", "hex_encode") => {
1043            let data = expect_bytes(args.first())?;
1044            Ok(Value::Str(hex::encode(data)))
1045        }
1046        ("crypto", "hex_decode") => {
1047            let s = expect_str(args.first())?;
1048            match hex::decode(s) {
1049                Ok(b)  => Ok(ok_v(Value::Bytes(b))),
1050                Err(e) => Ok(err_v(Value::Str(format!("hex: {e}")))),
1051            }
1052        }
1053        ("crypto", "constant_time_eq") => {
1054            use subtle::ConstantTimeEq;
1055            let a = expect_bytes(args.first())?;
1056            let b = expect_bytes(args.get(1))?;
1057            // `subtle` returns Choice; comparison only meaningful when
1058            // lengths match. For mismatched lengths return false in
1059            // constant time (length itself isn't secret, but we want
1060            // a single comparison shape).
1061            let eq = if a.len() == b.len() {
1062                a.ct_eq(b).into()
1063            } else {
1064                false
1065            };
1066            Ok(Value::Bool(eq))
1067        }
1068
1069        // -- random (#219): pure, seeded RNG. Backed by SplitMix64;
1070        // state is the u64 mixer state stored as a single i64 in
1071        // `Rng = { state :: Int }`. Threading the Rng through the
1072        // call site is the user's responsibility — there is no
1073        // global RNG and therefore no `[random]` effect tag for
1074        // pure-seeded usage. --
1075        ("random", "seed") => {
1076            let s = args.first().ok_or("random.seed: missing arg")?.as_int();
1077            // Hash the user-supplied seed once before installing it.
1078            // SplitMix64 is fine when seeded with any u64, but
1079            // hashing first protects against pathological seeds
1080            // (e.g., 0) that would make the very first draw zero.
1081            let mixed = splitmix64(s as u64).0;
1082            Ok(rng_value(mixed))
1083        }
1084        ("random", "int") => {
1085            let state = rng_decode(args.first())?;
1086            let lo = args.get(1).ok_or("random.int: missing lo")?.as_int();
1087            let hi = args.get(2).ok_or("random.int: missing hi")?.as_int();
1088            if hi < lo {
1089                return Err(format!(
1090                    "random.int: hi ({hi}) must be >= lo ({lo})"));
1091            }
1092            let span = (hi as i128) - (lo as i128) + 1;
1093            let (raw, next_state) = splitmix64(state);
1094            // Reduce uniformly to [lo, hi]. The bias from a plain
1095            // modulo is at most `(u64::MAX % span) / u64::MAX`,
1096            // which for any practical span is invisible. Crypto
1097            // applications should use `crypto.random` instead.
1098            let drawn = lo as i128 + (raw as u128 % span as u128) as i128;
1099            Ok(Value::Tuple(vec![
1100                Value::Int(drawn as i64),
1101                rng_value(next_state),
1102            ]))
1103        }
1104        ("random", "float") => {
1105            let state = rng_decode(args.first())?;
1106            let (raw, next_state) = splitmix64(state);
1107            // Take the top 53 bits and divide by 2^53 to land in
1108            // [0.0, 1.0); this is the standard f64 uniform draw.
1109            let f = ((raw >> 11) as f64) / ((1u64 << 53) as f64);
1110            Ok(Value::Tuple(vec![Value::Float(f), rng_value(next_state)]))
1111        }
1112        ("random", "choose") => {
1113            let state = rng_decode(args.first())?;
1114            let xs = match args.get(1) {
1115                Some(Value::List(xs)) => xs,
1116                _ => return Err("random.choose: expected List".into()),
1117            };
1118            if xs.is_empty() {
1119                return Ok(Value::Variant {
1120                    name: "None".into(), args: vec![],
1121                });
1122            }
1123            let (raw, next_state) = splitmix64(state);
1124            let idx = (raw as usize) % xs.len();
1125            let pick = xs[idx].clone();
1126            Ok(Value::Variant {
1127                name: "Some".into(),
1128                args: vec![Value::Tuple(vec![pick, rng_value(next_state)])],
1129            })
1130        }
1131
1132        // -- parser (#217): parser combinators. Parser values are
1133        // tagged Records — `{ kind: "Char", ch: "x" }` etc. — so
1134        // canonical equality follows from the canonical Record
1135        // encoding. The interpreter is `parser_run_impl`. --
1136        ("parser", "char") => {
1137            let s = expect_str(args.first())?;
1138            if s.chars().count() != 1 {
1139                return Err(format!(
1140                    "parser.char: expected 1-character string, got {s:?}"));
1141            }
1142            Ok(parser_node("Char", &[("ch", Value::Str(s))]))
1143        }
1144        ("parser", "string") => {
1145            let s = expect_str(args.first())?;
1146            Ok(parser_node("String", &[("s", Value::Str(s))]))
1147        }
1148        ("parser", "digit") => Ok(parser_node("Digit", &[])),
1149        ("parser", "alpha") => Ok(parser_node("Alpha", &[])),
1150        ("parser", "whitespace") => Ok(parser_node("Whitespace", &[])),
1151        ("parser", "eof") => Ok(parser_node("Eof", &[])),
1152        ("parser", "seq") => {
1153            let a = args.first().cloned()
1154                .ok_or_else(|| "parser.seq: missing first parser".to_string())?;
1155            let b = args.get(1).cloned()
1156                .ok_or_else(|| "parser.seq: missing second parser".to_string())?;
1157            Ok(parser_node("Seq", &[("a", a), ("b", b)]))
1158        }
1159        ("parser", "alt") => {
1160            let a = args.first().cloned()
1161                .ok_or_else(|| "parser.alt: missing first parser".to_string())?;
1162            let b = args.get(1).cloned()
1163                .ok_or_else(|| "parser.alt: missing second parser".to_string())?;
1164            Ok(parser_node("Alt", &[("a", a), ("b", b)]))
1165        }
1166        ("parser", "many") => {
1167            let p = args.first().cloned()
1168                .ok_or_else(|| "parser.many: missing inner parser".to_string())?;
1169            Ok(parser_node("Many", &[("p", p)]))
1170        }
1171        ("parser", "optional") => {
1172            let p = args.first().cloned()
1173                .ok_or_else(|| "parser.optional: missing inner parser".to_string())?;
1174            Ok(parser_node("Optional", &[("p", p)]))
1175        }
1176        // `parser.map` and `parser.and_then` (#221): closure-bearing
1177        // combinators. Constructors only — actual closure invocation
1178        // happens at parser.run time via the Vm-level interpreter.
1179        ("parser", "map") => {
1180            let p = args.first().cloned()
1181                .ok_or_else(|| "parser.map: missing parser".to_string())?;
1182            let f = args.get(1).cloned()
1183                .ok_or_else(|| "parser.map: missing closure".to_string())?;
1184            Ok(parser_node("Map", &[("p", p), ("f", f)]))
1185        }
1186        ("parser", "and_then") => {
1187            let p = args.first().cloned()
1188                .ok_or_else(|| "parser.and_then: missing parser".to_string())?;
1189            let f = args.get(1).cloned()
1190                .ok_or_else(|| "parser.and_then: missing closure".to_string())?;
1191            Ok(parser_node("AndThen", &[("p", p), ("f", f)]))
1192        }
1193        // `parser.run` is handled at the Vm level (lex-bytecode's
1194        // `Op::EffectCall` intercept) — it needs reentrant Vm access
1195        // to invoke the closures inside `Map` / `AndThen` nodes. The
1196        // pure-builtin path doesn't have that, so we deliberately do
1197        // *not* have a `("parser", "run")` arm here.
1198
1199        // -- regex (the compiled `Regex` is stored as the pattern
1200        // string; the runtime caches the actual `regex::Regex` so
1201        // ops don't re-compile on every call) --
1202        ("regex", "compile") => {
1203            let pat = expect_str(args.first())?;
1204            match get_or_compile_regex(&pat) {
1205                Ok(_) => Ok(ok_v(Value::Str(pat))),
1206                Err(e) => Ok(err_v(Value::Str(e))),
1207            }
1208        }
1209        ("regex", "is_match") => {
1210            let pat = expect_str(args.first())?;
1211            let s = expect_str(args.get(1))?;
1212            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.is_match: {e}"))?;
1213            Ok(Value::Bool(re.is_match(&s)))
1214        }
1215        // is_match_str :: Str, Str -> Bool
1216        // Compiles the first argument as a pattern on the fly (uses the shared
1217        // cache) and matches against the second.  Returns false on invalid
1218        // pattern rather than propagating an error, keeping the pure signature.
1219        ("regex", "is_match_str") => {
1220            let pat = expect_str(args.first())?;
1221            let s = expect_str(args.get(1))?;
1222            match get_or_compile_regex(&pat) {
1223                Ok(re) => Ok(Value::Bool(re.is_match(&s))),
1224                Err(_) => Ok(Value::Bool(false)),
1225            }
1226        }
1227        ("regex", "find") => {
1228            let pat = expect_str(args.first())?;
1229            let s = expect_str(args.get(1))?;
1230            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.find: {e}"))?;
1231            match re.captures(&s) {
1232                Some(caps) => Ok(Value::Variant {
1233                    name: "Some".into(),
1234                    args: vec![match_value(&caps)],
1235                }),
1236                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
1237            }
1238        }
1239        ("regex", "find_all") => {
1240            let pat = expect_str(args.first())?;
1241            let s = expect_str(args.get(1))?;
1242            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.find_all: {e}"))?;
1243            let items: Vec<Value> = re.captures_iter(&s).map(|caps| match_value(&caps)).collect();
1244            Ok(Value::List(items))
1245        }
1246        ("regex", "replace") => {
1247            let pat = expect_str(args.first())?;
1248            let s = expect_str(args.get(1))?;
1249            let rep = expect_str(args.get(2))?;
1250            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.replace: {e}"))?;
1251            Ok(Value::Str(re.replace(&s, rep.as_str()).into_owned()))
1252        }
1253        ("regex", "replace_all") => {
1254            let pat = expect_str(args.first())?;
1255            let s = expect_str(args.get(1))?;
1256            let rep = expect_str(args.get(2))?;
1257            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.replace_all: {e}"))?;
1258            Ok(Value::Str(re.replace_all(&s, rep.as_str()).into_owned()))
1259        }
1260        // -- datetime (pure ops; datetime.now is effectful and routes
1261        // through the handler under [time]) --
1262        ("datetime", "parse_iso") => {
1263            let s = expect_str(args.first())?;
1264            match chrono::DateTime::parse_from_rfc3339(&s) {
1265                Ok(dt) => Ok(ok_v(Value::Int(instant_from_chrono(dt)))),
1266                Err(e) => Ok(err_v(Value::Str(format!("parse_iso: {e}")))),
1267            }
1268        }
1269        ("datetime", "format_iso") => {
1270            let n = expect_int(args.first())?;
1271            Ok(Value::Str(format_iso(n)))
1272        }
1273        ("datetime", "parse") => {
1274            let s = expect_str(args.first())?;
1275            let fmt = expect_str(args.get(1))?;
1276            match chrono::NaiveDateTime::parse_from_str(&s, &fmt) {
1277                Ok(naive) => {
1278                    use chrono::TimeZone;
1279                    match chrono::Utc.from_local_datetime(&naive).single() {
1280                        Some(dt) => Ok(ok_v(Value::Int(instant_from_chrono(dt)))),
1281                        None => Ok(err_v(Value::Str("parse: ambiguous local time".into()))),
1282                    }
1283                }
1284                Err(e) => Ok(err_v(Value::Str(format!("parse: {e}")))),
1285            }
1286        }
1287        ("datetime", "format") => {
1288            let n = expect_int(args.first())?;
1289            let fmt = expect_str(args.get(1))?;
1290            let dt = chrono_from_instant(n);
1291            Ok(Value::Str(dt.format(&fmt).to_string()))
1292        }
1293        ("datetime", "to_components") => {
1294            let n = expect_int(args.first())?;
1295            let tz = match parse_tz_arg(args.get(1)) {
1296                Ok(t) => t,
1297                Err(e) => return Ok(err_v(Value::Str(e))),
1298            };
1299            match resolve_tz_to_components(n, &tz) {
1300                Ok(rec) => Ok(ok_v(rec)),
1301                Err(e) => Ok(err_v(Value::Str(e))),
1302            }
1303        }
1304        ("datetime", "from_components") => {
1305            let rec = match args.first() {
1306                Some(Value::Record(r)) => r.clone(),
1307                _ => return Err("from_components: expected DateTime record".into()),
1308            };
1309            match instant_from_components(&rec) {
1310                Ok(n) => Ok(ok_v(Value::Int(n))),
1311                Err(e) => Ok(err_v(Value::Str(e))),
1312            }
1313        }
1314        ("datetime", "add") => {
1315            let a = expect_int(args.first())?;
1316            let d = expect_int(args.get(1))?;
1317            Ok(Value::Int(a.saturating_add(d)))
1318        }
1319        ("datetime", "diff") => {
1320            let a = expect_int(args.first())?;
1321            let b = expect_int(args.get(1))?;
1322            Ok(Value::Int(a.saturating_sub(b)))
1323        }
1324        ("datetime", "duration_seconds") => {
1325            let s = expect_float(args.first())?;
1326            let nanos = (s * 1_000_000_000.0) as i64;
1327            Ok(Value::Int(nanos))
1328        }
1329        ("datetime", "duration_minutes") => {
1330            let m = expect_int(args.first())?;
1331            Ok(Value::Int(m.saturating_mul(60_000_000_000)))
1332        }
1333        ("datetime", "duration_days") => {
1334            let d = expect_int(args.first())?;
1335            Ok(Value::Int(d.saturating_mul(86_400_000_000_000)))
1336        }
1337        // #331: Instant comparison ops.
1338        ("datetime", "before") => {
1339            let a = expect_int(args.first())?;
1340            let b = expect_int(args.get(1))?;
1341            Ok(Value::Bool(a < b))
1342        }
1343        ("datetime", "after") => {
1344            let a = expect_int(args.first())?;
1345            let b = expect_int(args.get(1))?;
1346            Ok(Value::Bool(a > b))
1347        }
1348        ("datetime", "compare") => {
1349            let a = expect_int(args.first())?;
1350            let b = expect_int(args.get(1))?;
1351            Ok(Value::Int(a.cmp(&b) as i64))
1352        }
1353        // #331: Duration scalar extraction.
1354        ("duration", "seconds") => {
1355            let nanos = expect_int(args.first())?;
1356            Ok(Value::Int(nanos / 1_000_000_000))
1357        }
1358
1359        ("regex", "split") => {
1360            let pat = expect_str(args.first())?;
1361            let s = expect_str(args.get(1))?;
1362            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.split: {e}"))?;
1363            let parts: Vec<Value> = re.split(&s).map(|p| Value::Str(p.to_string())).collect();
1364            Ok(Value::List(parts))
1365        }
1366
1367        // -- http (builders + decoders; wire ops live in the
1368        // effect handler under `[net]`) --
1369        ("http", "with_header") => {
1370            let req = expect_record_pure(args.first())?.clone();
1371            let k = expect_str(args.get(1))?;
1372            let v = expect_str(args.get(2))?;
1373            Ok(Value::Record(http_set_header(req, &k, &v)))
1374        }
1375        ("http", "with_auth") => {
1376            let req = expect_record_pure(args.first())?.clone();
1377            let scheme = expect_str(args.get(1))?;
1378            let token = expect_str(args.get(2))?;
1379            let value = format!("{scheme} {token}");
1380            Ok(Value::Record(http_set_header(req, "Authorization", &value)))
1381        }
1382        ("http", "with_query") => {
1383            let req = expect_record_pure(args.first())?.clone();
1384            let params = match args.get(1) {
1385                Some(Value::Map(m)) => m.clone(),
1386                Some(other) => return Err(format!(
1387                    "http.with_query: params must be Map[Str, Str], got {other:?}")),
1388                None => return Err("http.with_query: missing params argument".into()),
1389            };
1390            Ok(Value::Record(http_append_query(req, &params)))
1391        }
1392        ("http", "with_timeout_ms") => {
1393            let req = expect_record_pure(args.first())?.clone();
1394            let ms = expect_int(args.get(1))?;
1395            let mut out = req;
1396            out.insert("timeout_ms".into(), Value::Variant {
1397                name: "Some".into(),
1398                args: vec![Value::Int(ms)],
1399            });
1400            Ok(Value::Record(out))
1401        }
1402        ("http", "json_body") => {
1403            let resp = expect_record_pure(args.first())?;
1404            let body = match resp.get("body") {
1405                Some(Value::Bytes(b)) => b.clone(),
1406                _ => return Err("http.json_body: HttpResponse.body must be Bytes".into()),
1407            };
1408            let s = match std::str::from_utf8(&body) {
1409                Ok(s) => s,
1410                Err(e) => return Ok(http_decode_err_pure(format!("body not UTF-8: {e}"))),
1411            };
1412            match serde_json::from_str::<serde_json::Value>(s) {
1413                Ok(j) => Ok(ok_v(Value::from_json(&j))),
1414                Err(e) => Ok(http_decode_err_pure(format!("json parse: {e}"))),
1415            }
1416        }
1417        ("http", "text_body") => {
1418            let resp = expect_record_pure(args.first())?;
1419            let body = match resp.get("body") {
1420                Some(Value::Bytes(b)) => b.clone(),
1421                _ => return Err("http.text_body: HttpResponse.body must be Bytes".into()),
1422            };
1423            match String::from_utf8(body) {
1424                Ok(s) => Ok(ok_v(Value::Str(s))),
1425                Err(e) => Ok(http_decode_err_pure(format!("body not UTF-8: {e}"))),
1426            }
1427        }
1428
1429        // -- std.cli (Rubric port): argparse-equivalent for end-user
1430        // programs. Specs are tagged Json values; the parser walks
1431        // argv against the spec and returns a CliParsed Json record.
1432        ("cli", "flag") => {
1433            let name = expect_str(args.first())?;
1434            let short = opt_str(args.get(1));
1435            let help = expect_str(args.get(2))?;
1436            Ok(value_from_json(crate::cli::flag_spec(&name, short.as_deref(), &help)))
1437        }
1438        ("cli", "option") => {
1439            let name = expect_str(args.first())?;
1440            let short = opt_str(args.get(1));
1441            let help = expect_str(args.get(2))?;
1442            let default = opt_str(args.get(3));
1443            Ok(value_from_json(crate::cli::option_spec(&name, short.as_deref(), &help, default.as_deref())))
1444        }
1445        ("cli", "positional") => {
1446            let name = expect_str(args.first())?;
1447            let help = expect_str(args.get(1))?;
1448            let required = expect_bool(args.get(2))?;
1449            Ok(value_from_json(crate::cli::positional_spec(&name, &help, required)))
1450        }
1451        ("cli", "spec") => {
1452            let name = expect_str(args.first())?;
1453            let help = expect_str(args.get(1))?;
1454            let arg_specs: Vec<serde_json::Value> = expect_list(args.get(2))?
1455                .iter().map(value_to_json).collect();
1456            let subs: Vec<serde_json::Value> = expect_list(args.get(3))?
1457                .iter().map(value_to_json).collect();
1458            Ok(value_from_json(crate::cli::build_spec(&name, &help, arg_specs, subs)))
1459        }
1460        ("cli", "parse") => {
1461            let spec = value_to_json(args.first().unwrap_or(&Value::Unit));
1462            let argv: Vec<String> = expect_list(args.get(1))?
1463                .iter().map(|v| match v {
1464                    Value::Str(s) => Ok(s.clone()),
1465                    other => Err(format!("cli.parse: argv must be List[Str], got {other:?}")),
1466                }).collect::<Result<_, _>>()?;
1467            match crate::cli::parse(&spec, &argv) {
1468                Ok(parsed) => Ok(ok_v(value_from_json(parsed))),
1469                Err(msg) => Ok(err_v(Value::Str(msg))),
1470            }
1471        }
1472        ("cli", "envelope") => {
1473            let ok = expect_bool(args.first())?;
1474            let cmd = expect_str(args.get(1))?;
1475            let data = value_to_json(args.get(2).unwrap_or(&Value::Unit));
1476            Ok(value_from_json(crate::cli::envelope(ok, &cmd, data)))
1477        }
1478        ("cli", "describe") => {
1479            let spec = value_to_json(args.first().unwrap_or(&Value::Unit));
1480            Ok(value_from_json(crate::cli::describe(&spec)))
1481        }
1482        ("cli", "help") => {
1483            let spec = value_to_json(args.first().unwrap_or(&Value::Unit));
1484            Ok(Value::Str(crate::cli::help_text(&spec)))
1485        }
1486
1487        _ => Err(format!("unknown pure builtin: {kind}.{op}")),
1488    }
1489}
1490
1491/// Extract `Option[Str]` arg as `Option<String>`. None and missing
1492/// arg both map to `None`. Used by the `cli` builders so callers can
1493/// pass `option.none()` or `Some("v")` interchangeably.
1494fn opt_str(arg: Option<&Value>) -> Option<String> {
1495    match arg {
1496        Some(Value::Variant { name, args }) if name == "Some" => {
1497            args.first().and_then(|v| match v {
1498                Value::Str(s) => Some(s.clone()),
1499                _ => None,
1500            })
1501        }
1502        _ => None,
1503    }
1504}
1505
1506fn value_from_json(v: serde_json::Value) -> Value { Value::from_json(&v) }
1507
1508/// Process-wide cache of compiled regexes, keyed by the pattern
1509/// string. Compilation is the only cost we want to amortize; matching
1510/// the same `Regex` from multiple threads is safe (`regex::Regex` is
1511/// `Send + Sync`).
1512fn regex_cache() -> &'static Mutex<HashMap<String, regex::Regex>> {
1513    static CACHE: OnceLock<Mutex<HashMap<String, regex::Regex>>> = OnceLock::new();
1514    CACHE.get_or_init(|| Mutex::new(HashMap::new()))
1515}
1516
1517fn get_or_compile_regex(pattern: &str) -> Result<regex::Regex, String> {
1518    let cache = regex_cache();
1519    {
1520        let guard = cache.lock().unwrap();
1521        if let Some(re) = guard.get(pattern) {
1522            return Ok(re.clone());
1523        }
1524    }
1525    let re = regex::Regex::new(pattern).map_err(|e| format!("invalid regex: {e}"))?;
1526    let mut guard = cache.lock().unwrap();
1527    guard.insert(pattern.to_string(), re.clone());
1528    Ok(re)
1529}
1530
1531/// Build a `Match` record value: `{ text, start, end, groups }` where
1532/// `groups` is the captured groups in order (group 0 is the full match).
1533/// Missing optional groups become empty strings.
1534fn match_value(caps: &regex::Captures) -> Value {
1535    let m0 = caps.get(0).expect("regex match always has group 0");
1536    let mut rec = indexmap::IndexMap::new();
1537    rec.insert("text".into(), Value::Str(m0.as_str().to_string()));
1538    rec.insert("start".into(), Value::Int(m0.start() as i64));
1539    rec.insert("end".into(), Value::Int(m0.end() as i64));
1540    let groups: Vec<Value> = (1..caps.len())
1541        .map(|i| {
1542            Value::Str(
1543                caps.get(i)
1544                    .map(|m| m.as_str().to_string())
1545                    .unwrap_or_default(),
1546            )
1547        })
1548        .collect();
1549    rec.insert("groups".into(), Value::List(groups));
1550    Value::Record(rec)
1551}
1552
1553fn expect_map(v: Option<&Value>) -> Result<&BTreeMap<MapKey, Value>, String> {
1554    match v {
1555        Some(Value::Map(m)) => Ok(m),
1556        other => Err(format!("expected Map, got {other:?}")),
1557    }
1558}
1559
1560fn expect_set(v: Option<&Value>) -> Result<&BTreeSet<MapKey>, String> {
1561    match v {
1562        Some(Value::Set(s)) => Ok(s),
1563        other => Err(format!("expected Set, got {other:?}")),
1564    }
1565}
1566
1567/// Unpack any matrix-shaped Value into (rows, cols, flat row-major data).
1568/// Accepts the F64Array fast lane and the legacy `Record { rows, cols,
1569/// data: List[Float] }` shape for compatibility with hand-built matrices.
1570fn unpack_matrix(v: &Value) -> Result<(usize, usize, Vec<f64>), String> {
1571    if let Value::F64Array { rows, cols, data } = v {
1572        return Ok((*rows as usize, *cols as usize, data.clone()));
1573    }
1574    let rec = match v {
1575        Value::Record(r) => r,
1576        other => return Err(format!("expected matrix, got {other:?}")),
1577    };
1578    let rows = match rec.get("rows") {
1579        Some(Value::Int(n)) => *n as usize,
1580        _ => return Err("matrix: missing/invalid `rows`".into()),
1581    };
1582    let cols = match rec.get("cols") {
1583        Some(Value::Int(n)) => *n as usize,
1584        _ => return Err("matrix: missing/invalid `cols`".into()),
1585    };
1586    let data = match rec.get("data") {
1587        Some(Value::List(items)) => {
1588            let mut out = Vec::with_capacity(items.len());
1589            for it in items {
1590                out.push(match it {
1591                    Value::Float(f) => *f,
1592                    Value::Int(n) => *n as f64,
1593                    other => return Err(format!("matrix data: not numeric, got {other:?}")),
1594                });
1595            }
1596            out
1597        }
1598        _ => return Err("matrix: missing/invalid `data`".into()),
1599    };
1600    if data.len() != rows * cols {
1601        return Err(format!("matrix: data len {} != {rows}*{cols}", data.len()));
1602    }
1603    Ok((rows, cols, data))
1604}
1605
1606fn expect_bytes(v: Option<&Value>) -> Result<&Vec<u8>, String> {
1607    match v {
1608        Some(Value::Bytes(b)) => Ok(b),
1609        Some(other) => Err(format!("expected Bytes, got {other:?}")),
1610        None => Err("missing argument".into()),
1611    }
1612}
1613
1614fn first_arg(args: &[Value]) -> Result<&Value, String> {
1615    args.first().ok_or_else(|| "missing argument".into())
1616}
1617
1618fn tuple_index(v: &Value, i: usize) -> Result<Value, String> {
1619    match v {
1620        Value::Tuple(items) => items.get(i).cloned()
1621            .ok_or_else(|| format!("tuple index {i} out of range (len={})", items.len())),
1622        other => Err(format!("expected Tuple, got {other:?}")),
1623    }
1624}
1625
1626fn expect_str(v: Option<&Value>) -> Result<String, String> {
1627    match v {
1628        Some(Value::Str(s)) => Ok(s.clone()),
1629        Some(other) => Err(format!("expected Str, got {other:?}")),
1630        None => Err("missing argument".into()),
1631    }
1632}
1633
1634fn expect_int(v: Option<&Value>) -> Result<i64, String> {
1635    match v {
1636        Some(Value::Int(n)) => Ok(*n),
1637        Some(other) => Err(format!("expected Int, got {other:?}")),
1638        None => Err("missing argument".into()),
1639    }
1640}
1641
1642fn expect_float(v: Option<&Value>) -> Result<f64, String> {
1643    match v {
1644        Some(Value::Float(f)) => Ok(*f),
1645        Some(other) => Err(format!("expected Float, got {other:?}")),
1646        None => Err("missing argument".into()),
1647    }
1648}
1649
1650fn expect_list(v: Option<&Value>) -> Result<&Vec<Value>, String> {
1651    match v {
1652        Some(Value::List(xs)) => Ok(xs),
1653        Some(other) => Err(format!("expected List, got {other:?}")),
1654        None => Err("missing argument".into()),
1655    }
1656}
1657
1658fn expect_bool(v: Option<&Value>) -> Result<bool, String> {
1659    match v {
1660        Some(Value::Bool(b)) => Ok(*b),
1661        Some(other) => Err(format!("expected Bool, got {other:?}")),
1662        None => Err("missing argument".into()),
1663    }
1664}
1665
1666fn expect_deque(v: Option<&Value>) -> Result<&std::collections::VecDeque<Value>, String> {
1667    match v {
1668        Some(Value::Deque(d)) => Ok(d),
1669        Some(other) => Err(format!("expected Deque, got {other:?}")),
1670        None => Err("missing argument".into()),
1671    }
1672}
1673
1674fn some(v: Value) -> Value { Value::Variant { name: "Some".into(), args: vec![v] } }
1675fn none() -> Value { Value::Variant { name: "None".into(), args: Vec::new() } }
1676fn ok_v(v: Value) -> Value { Value::Variant { name: "Ok".into(), args: vec![v] } }
1677fn err_v(v: Value) -> Value { Value::Variant { name: "Err".into(), args: vec![v] } }
1678
1679// -- std.parser helpers (#217) ----------------------------------------
1680
1681/// Construct a tagged parser-AST node. The runtime representation is
1682/// `{ kind: "Char" | "Seq" | ..., ...children }`; the type system
1683/// treats these as opaque `Parser[T]` so user code can't poke at the
1684/// fields. Encoding is canonical because `IndexMap` insertion order
1685/// is stable and we always insert `kind` first.
1686fn parser_node(kind: &str, fields: &[(&str, Value)]) -> Value {
1687    let mut r = indexmap::IndexMap::new();
1688    r.insert("kind".into(), Value::Str(kind.into()));
1689    for (k, v) in fields {
1690        r.insert((*k).into(), v.clone());
1691    }
1692    Value::Record(r)
1693}
1694
1695// `parser.run` interpretation lives in `lex-bytecode::parser_runtime`
1696// (#221) — it needs reentrant Vm access to invoke closures inside
1697// `Map` / `AndThen` nodes, which the pure-builtin path doesn't have.
1698
1699// -- std.random helpers (#219) ----------------------------------------
1700
1701/// SplitMix64 — single-`u64` state PRNG that is byte-identical
1702/// across platforms (no float math, no platform-dependent reductions).
1703/// Returns `(drawn, next_state)`. Constants are the canonical
1704/// SplitMix64 mixer from the original 2014 paper.
1705fn splitmix64(state: u64) -> (u64, u64) {
1706    let next = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
1707    let mut z = next;
1708    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1709    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1710    let z = z ^ (z >> 31);
1711    (z, next)
1712}
1713
1714/// Encode a SplitMix64 state as the user-facing `Rng` value.
1715/// `Rng = { state :: Int }`; the type-checker treats `Rng` as
1716/// opaque so users can't poke at the field.
1717fn rng_value(state: u64) -> Value {
1718    let mut fields = indexmap::IndexMap::new();
1719    fields.insert("state".into(), Value::Int(state as i64));
1720    Value::Record(fields)
1721}
1722
1723/// Pull the SplitMix64 state out of a `Value::Record { state }`.
1724fn rng_decode(v: Option<&Value>) -> Result<u64, String> {
1725    let rec = match v {
1726        Some(Value::Record(r)) => r,
1727        Some(other) => return Err(format!("expected Rng, got {other:?}")),
1728        None => return Err("missing Rng arg".into()),
1729    };
1730    match rec.get("state") {
1731        Some(Value::Int(n)) => Ok(*n as u64),
1732        _ => Err("malformed Rng: missing `state :: Int`".into()),
1733    }
1734}
1735
1736// -- helpers for `std.http` builders / decoders --
1737
1738fn expect_record_pure(v: Option<&Value>) -> Result<&indexmap::IndexMap<String, Value>, String> {
1739    match v {
1740        Some(Value::Record(r)) => Ok(r),
1741        Some(other) => Err(format!("expected Record, got {other:?}")),
1742        None => Err("missing Record argument".into()),
1743    }
1744}
1745
1746fn http_decode_err_pure(msg: String) -> Value {
1747    let inner = Value::Variant {
1748        name: "DecodeError".into(),
1749        args: vec![Value::Str(msg)],
1750    };
1751    err_v(inner)
1752}
1753
1754/// Apply or replace a header in an `HttpRequest` record's `headers`
1755/// field. Header names are normalized to lowercase to match HTTP/1.1
1756/// case-insensitivity; an existing entry under any casing is
1757/// overwritten by the new value.
1758fn http_set_header(
1759    mut req: indexmap::IndexMap<String, Value>,
1760    name: &str,
1761    value: &str,
1762) -> indexmap::IndexMap<String, Value> {
1763    use lex_bytecode::MapKey;
1764    let mut headers = match req.shift_remove("headers") {
1765        Some(Value::Map(m)) => m,
1766        _ => std::collections::BTreeMap::new(),
1767    };
1768    let key = MapKey::Str(name.to_lowercase());
1769    // Drop any case variant of the same header name first so casing
1770    // flips don't accumulate duplicates.
1771    let lowered = name.to_lowercase();
1772    headers.retain(|k, _| match k {
1773        MapKey::Str(s) => s.to_lowercase() != lowered,
1774        _ => true,
1775    });
1776    headers.insert(key, Value::Str(value.to_string()));
1777    req.insert("headers".into(), Value::Map(headers));
1778    req
1779}
1780
1781/// Append `?k=v&...` (URL-encoded) to the `url` field of an
1782/// `HttpRequest` record. Existing query string is preserved and
1783/// extended with `&`. Iteration order is the input map's natural
1784/// order (`BTreeMap` → sorted by key) so the produced URL is
1785/// deterministic.
1786fn http_append_query(
1787    mut req: indexmap::IndexMap<String, Value>,
1788    params: &std::collections::BTreeMap<lex_bytecode::MapKey, Value>,
1789) -> indexmap::IndexMap<String, Value> {
1790    use lex_bytecode::MapKey;
1791    let url = match req.get("url") {
1792        Some(Value::Str(s)) => s.clone(),
1793        _ => return req,
1794    };
1795    let mut pieces = Vec::new();
1796    for (k, v) in params {
1797        let kk = match k { MapKey::Str(s) => s.clone(), _ => continue };
1798        let vv = match v { Value::Str(s) => s.clone(), _ => continue };
1799        pieces.push(format!("{}={}", url_encode(&kk), url_encode(&vv)));
1800    }
1801    if pieces.is_empty() { return req; }
1802    let sep = if url.contains('?') { '&' } else { '?' };
1803    let new_url = format!("{url}{sep}{}", pieces.join("&"));
1804    req.insert("url".into(), Value::Str(new_url));
1805    req
1806}
1807
1808/// Minimal RFC-3986 percent-encode for `application/x-www-form-
1809/// urlencoded` query values. Pulling in `urlencoding` for one
1810/// callsite would drag a dep into the runtime; the inline version is
1811/// short and easy to audit.
1812fn url_encode(s: &str) -> String {
1813    let mut out = String::with_capacity(s.len());
1814    for b in s.bytes() {
1815        match b {
1816            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1817                out.push(b as char);
1818            }
1819            _ => out.push_str(&format!("%{:02X}", b)),
1820        }
1821    }
1822    out
1823}
1824
1825fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
1826
1827/// The `toml` crate's serde adapter wraps datetimes in a sentinel
1828/// object `{"$__toml_private_datetime": "<rfc3339>"}` so that the
1829/// `Datetime` type round-trips through `serde::Value`. For Lex's
1830/// purposes a plain RFC-3339 string is what we want — callers can
1831/// then pipe through `datetime.parse_iso` if they need an
1832/// `Instant`. Walk the tree and replace each wrapper with its
1833/// inner string, in-place.
1834fn unwrap_toml_datetime_markers(v: &mut serde_json::Value) {
1835    use serde_json::Value as J;
1836    match v {
1837        J::Object(map) => {
1838            // Detect single-key marker objects and replace them
1839            // with their inner string. We have to take care to
1840            // avoid borrow conflicts.
1841            if map.len() == 1 {
1842                if let Some(J::String(s)) = map.get("$__toml_private_datetime") {
1843                    let s = s.clone();
1844                    *v = J::String(s);
1845                    return;
1846                }
1847            }
1848            for (_, child) in map.iter_mut() {
1849                unwrap_toml_datetime_markers(child);
1850            }
1851        }
1852        J::Array(items) => {
1853            for item in items.iter_mut() {
1854                unwrap_toml_datetime_markers(item);
1855            }
1856        }
1857        _ => {}
1858    }
1859}
1860
1861fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
1862
1863/// Extract the `List[Str]` of required field names from the second
1864/// argument of `*.parse_strict`. The list is allowed to be empty
1865/// (the parse degenerates to plain `parse`); other shapes are a
1866/// caller bug rather than a parse error.
1867fn required_field_names(arg: Option<&Value>) -> Result<Vec<String>, String> {
1868    let list = expect_list(arg)?;
1869    let mut out = Vec::with_capacity(list.len());
1870    for v in list {
1871        match v {
1872            Value::Str(s) => out.push(s.clone()),
1873            other => return Err(format!(
1874                "parse_strict: required-fields list must contain Str, got {other:?}"
1875            )),
1876        }
1877    }
1878    Ok(out)
1879}
1880
1881/// Verify that `value` is an object containing every entry in
1882/// `required`. A required entry may be a plain field name (must
1883/// exist at the top level) or a dotted path (`"project.license"`)
1884/// which descends through nested objects. Returns a stable,
1885/// human-readable error listing every missing path so the agent's
1886/// verifier can surface it directly.
1887///
1888/// Tactical fix for #168 — gives users a way to make `parse[T]`
1889/// errors propagate as `Result::Err` instead of as runtime
1890/// `GetField` errors at access time. The full type-driven fix
1891/// (deriving `required` from `T` at type-check time so plain
1892/// `parse[T]` works, including auto-wrapping `Option[F]` fields
1893/// as not-required) is the cleaner endgame; see #168.
1894///
1895/// Path semantics:
1896/// * `"name"` → top-level `name` must be present (any value).
1897/// * `"a.b.c"` → walk `a`, then `b`, then check `c` exists. Each
1898///   intermediate value must itself be an object.
1899/// * `\\.` is the literal-dot escape (e.g. `"weird\\.key"` for a
1900///   field that genuinely contains a dot in its name).
1901fn check_required_fields(
1902    value: &serde_json::Value,
1903    required: &[String],
1904) -> Result<(), String> {
1905    if required.is_empty() {
1906        return Ok(());
1907    }
1908    if !matches!(value, serde_json::Value::Object(_)) {
1909        return Err(format!(
1910            "parse_strict: expected top-level object with fields {:?}, got {value}",
1911            required
1912        ));
1913    }
1914    let mut missing: Vec<String> = Vec::new();
1915    for path in required {
1916        if !path_exists(value, path) {
1917            missing.push(path.clone());
1918        }
1919    }
1920    if missing.is_empty() {
1921        Ok(())
1922    } else {
1923        Err(format!("missing required field(s): {}", missing.join(", ")))
1924    }
1925}
1926
1927/// Walk `value` along the dotted `path` and report whether the
1928/// terminal segment exists. Intermediate non-object stops surface
1929/// as "missing" — a path can't traverse through a string, list, or
1930/// scalar.
1931fn path_exists(value: &serde_json::Value, path: &str) -> bool {
1932    let mut cursor = value;
1933    let segments = split_dotted_path(path);
1934    for seg in &segments {
1935        match cursor {
1936            serde_json::Value::Object(o) => match o.get(seg.as_str()) {
1937                Some(next) => cursor = next,
1938                None => return false,
1939            },
1940            _ => return false,
1941        }
1942    }
1943    true
1944}
1945
1946/// Split `"a.b.c"` into `["a", "b", "c"]`, with `\.` recognised
1947/// as a literal-dot escape so legitimate dotted field names
1948/// (e.g. `"package\.json"`) don't accidentally start a descent.
1949fn split_dotted_path(path: &str) -> Vec<String> {
1950    let mut out: Vec<String> = Vec::new();
1951    let mut cur = String::new();
1952    let mut iter = path.chars().peekable();
1953    while let Some(c) = iter.next() {
1954        if c == '\\' {
1955            // Backslash at end is preserved; only `\.` is special.
1956            if let Some(&'.') = iter.peek() {
1957                cur.push('.');
1958                iter.next();
1959                continue;
1960            }
1961            cur.push(c);
1962        } else if c == '.' {
1963            out.push(std::mem::take(&mut cur));
1964        } else {
1965            cur.push(c);
1966        }
1967    }
1968    out.push(cur);
1969    out
1970}
1971
1972/// Extract the `List[(Str, Str)]` type schema from the third argument
1973/// of `*.parse_strict` (#322). If the argument is absent or malformed,
1974/// returns an empty vec — callers treat that as "skip type validation".
1975fn extract_type_schema(v: Option<&Value>) -> Vec<(String, String)> {
1976    match v {
1977        Some(Value::List(pairs)) => pairs.iter().filter_map(|p| {
1978            if let Value::Tuple(items) = p {
1979                if items.len() == 2 {
1980                    if let (Value::Str(name), Value::Str(tag)) = (&items[0], &items[1]) {
1981                        return Some((name.clone(), tag.clone()));
1982                    }
1983                }
1984            }
1985            None
1986        }).collect(),
1987        _ => vec![],
1988    }
1989}
1990
1991/// Validate each field in `json` against its declared type tag from
1992/// the schema. Returns `Err` for the first field whose JSON value
1993/// doesn't match its tag. Fields not present in the JSON object are
1994/// silently skipped (presence is enforced separately by
1995/// `check_required_fields`).
1996fn validate_field_types(
1997    json: &serde_json::Value,
1998    schema: &[(String, String)],
1999) -> Result<(), String> {
2000    if schema.is_empty() {
2001        return Ok(());
2002    }
2003    let obj = match json.as_object() {
2004        Some(o) => o,
2005        None => return Ok(()), // not an object — let other validation handle it
2006    };
2007    for (field, tag) in schema {
2008        if let Some(val) = obj.get(field) {
2009            if let Err(e) = check_json_type(val, tag) {
2010                return Err(format!("field `{field}`: {e}"));
2011            }
2012        }
2013    }
2014    Ok(())
2015}
2016
2017/// Recursively check that `val` conforms to the compact type `tag`.
2018fn check_json_type(val: &serde_json::Value, tag: &str) -> Result<(), String> {
2019    use serde_json::Value as J;
2020    match (tag, val) {
2021        ("Int", J::Number(n)) if n.is_i64() || n.is_u64() => Ok(()),
2022        ("Int", other) => Err(format!("expected Int, got {}", json_type_name(other))),
2023        ("Float", J::Number(_)) => Ok(()),
2024        ("Float", other) => Err(format!("expected Float, got {}", json_type_name(other))),
2025        ("Bool", J::Bool(_)) => Ok(()),
2026        ("Bool", other) => Err(format!("expected Bool, got {}", json_type_name(other))),
2027        ("Str", J::String(_)) => Ok(()),
2028        ("Str", other) => Err(format!("expected Str, got {}", json_type_name(other))),
2029        // Option[X]: null maps to None — any null is acceptable
2030        (tag, J::Null) if tag.starts_with("Option[") => Ok(()),
2031        (tag, val) if tag.starts_with("Option[") && tag.ends_with(']') => {
2032            let inner = &tag[7..tag.len() - 1]; // strip "Option[" and "]"
2033            check_json_type(val, inner)
2034        }
2035        // List[X]: validate each element
2036        (tag, J::Array(items)) if tag.starts_with("List[") && tag.ends_with(']') => {
2037            let inner = &tag[5..tag.len() - 1]; // strip "List[" and "]"
2038            for (i, item) in items.iter().enumerate() {
2039                if let Err(e) = check_json_type(item, inner) {
2040                    return Err(format!("[{i}]: {e}"));
2041                }
2042            }
2043            Ok(())
2044        }
2045        ("Record", _) => Ok(()), // opaque nested record — skip deep check
2046        ("Any", _) => Ok(()),    // unknown type — skip
2047        _ => Ok(()),             // unrecognized tag — skip
2048    }
2049}
2050
2051fn json_type_name(v: &serde_json::Value) -> &'static str {
2052    match v {
2053        serde_json::Value::Null => "null",
2054        serde_json::Value::Bool(_) => "Bool",
2055        serde_json::Value::Number(_) => "Number",
2056        serde_json::Value::String(_) => "Str",
2057        serde_json::Value::Array(_) => "Array",
2058        serde_json::Value::Object(_) => "Object",
2059    }
2060}
2061
2062/// Parse a `.env`-style file into key→value pairs. Accepts:
2063///
2064/// * Blank lines and `# comment` lines (ignored).
2065/// * `KEY=VALUE` with no spaces around `=`. Optional surrounding
2066///   `"..."` or `'...'` quotes on the value. No escape sequences,
2067///   no shell expansion — by design; we want this to be a *data*
2068///   parser, not a shell snippet evaluator.
2069///
2070/// Errors carry the offending line number (1-indexed) so the
2071/// agent's verifier can point a human at the right place.
2072fn parse_dotenv(src: &str) -> Result<indexmap::IndexMap<String, String>, String> {
2073    let mut out = indexmap::IndexMap::new();
2074    for (idx, raw) in src.lines().enumerate() {
2075        let line = raw.trim();
2076        if line.is_empty() || line.starts_with('#') {
2077            continue;
2078        }
2079        // Optional `export KEY=VALUE` shell form — accepted for
2080        // compat with files that grew out of `set -a` workflows.
2081        let after_export = line.strip_prefix("export ").unwrap_or(line);
2082        let (k, v) = match after_export.split_once('=') {
2083            Some(kv) => kv,
2084            None => return Err(format!("dotenv.parse line {}: missing `=`", idx + 1)),
2085        };
2086        let key = k.trim();
2087        if key.is_empty() {
2088            return Err(format!("dotenv.parse line {}: empty key", idx + 1));
2089        }
2090        let v_trim = v.trim();
2091        let value = if let Some(q) = v_trim.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
2092            q.to_string()
2093        } else if let Some(q) = v_trim.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
2094            q.to_string()
2095        } else {
2096            v_trim.to_string()
2097        };
2098        out.insert(key.to_string(), value);
2099    }
2100    Ok(out)
2101}
2102
2103// -- datetime helpers (Instant ↔ chrono::DateTime<Utc>) --
2104
2105/// Convert a `chrono::DateTime` (any `TimeZone`) into a Lex `Instant`,
2106/// represented as nanoseconds since the UTC unix epoch. Saturates on
2107/// out-of-range timestamps so the runtime never panics.
2108fn instant_from_chrono<Tz: chrono::TimeZone>(dt: chrono::DateTime<Tz>) -> i64 {
2109    dt.timestamp_nanos_opt().unwrap_or(i64::MAX)
2110}
2111
2112fn chrono_from_instant(n: i64) -> chrono::DateTime<chrono::Utc> {
2113    let secs = n.div_euclid(1_000_000_000);
2114    let nanos = n.rem_euclid(1_000_000_000) as u32;
2115    use chrono::TimeZone;
2116    chrono::Utc
2117        .timestamp_opt(secs, nanos)
2118        .single()
2119        .unwrap_or_else(chrono::Utc::now)
2120}
2121
2122fn format_iso(n: i64) -> String {
2123    chrono_from_instant(n).to_rfc3339()
2124}
2125
2126/// Parsed form of the user-side `Tz` variant. Mirrors the type
2127/// registered in `TypeEnv::new_with_builtins`.
2128enum TzArg {
2129    Utc,
2130    Local,
2131    /// Fixed offset in minutes east of UTC.
2132    Offset(i32),
2133    /// IANA name like `"America/New_York"`.
2134    Iana(String),
2135}
2136
2137fn parse_tz_arg(v: Option<&Value>) -> Result<TzArg, String> {
2138    match v {
2139        Some(Value::Variant { name, args }) => match (name.as_str(), args.as_slice()) {
2140            ("Utc", []) => Ok(TzArg::Utc),
2141            ("Local", []) => Ok(TzArg::Local),
2142            ("Offset", [Value::Int(m)]) => {
2143                let m = i32::try_from(*m).map_err(|_| {
2144                    format!("Tz::Offset: minutes out of range: {m}")
2145                })?;
2146                Ok(TzArg::Offset(m))
2147            }
2148            ("Iana", [Value::Str(s)]) => Ok(TzArg::Iana(s.clone())),
2149            (other, _) => Err(format!(
2150                "expected Tz variant (Utc | Local | Offset(Int) | Iana(Str)), got `{other}` with {} arg(s)",
2151                args.len()
2152            )),
2153        },
2154        Some(other) => Err(format!("expected Tz variant, got {other:?}")),
2155        None => Err("missing Tz argument".into()),
2156    }
2157}
2158
2159fn resolve_tz_to_components(n: i64, tz: &TzArg) -> Result<Value, String> {
2160    use chrono::{TimeZone, Datelike, Timelike, Offset};
2161    let utc_dt = chrono_from_instant(n);
2162    let (y, m, d, hh, mm, ss, ns, off_min) = match tz {
2163        TzArg::Utc => {
2164            let d = utc_dt;
2165            (d.year(), d.month() as i32, d.day() as i32,
2166             d.hour() as i32, d.minute() as i32, d.second() as i32,
2167             d.nanosecond() as i32, 0)
2168        }
2169        TzArg::Local => {
2170            let d = utc_dt.with_timezone(&chrono::Local);
2171            let off = d.offset().fix().local_minus_utc() / 60;
2172            (d.year(), d.month() as i32, d.day() as i32,
2173             d.hour() as i32, d.minute() as i32, d.second() as i32,
2174             d.nanosecond() as i32, off)
2175        }
2176        TzArg::Offset(off_min) => {
2177            let off_secs = off_min.saturating_mul(60);
2178            let fixed = chrono::FixedOffset::east_opt(off_secs)
2179                .ok_or("to_components: offset out of range")?;
2180            let d = utc_dt.with_timezone(&fixed);
2181            (d.year(), d.month() as i32, d.day() as i32,
2182             d.hour() as i32, d.minute() as i32, d.second() as i32,
2183             d.nanosecond() as i32, *off_min)
2184        }
2185        TzArg::Iana(name) => {
2186            let tz: chrono_tz::Tz = name.parse()
2187                .map_err(|e| format!("to_components: unknown timezone `{name}`: {e}"))?;
2188            let d = utc_dt.with_timezone(&tz);
2189            let off = d.offset().fix().local_minus_utc() / 60;
2190            (d.year(), d.month() as i32, d.day() as i32,
2191             d.hour() as i32, d.minute() as i32, d.second() as i32,
2192             d.nanosecond() as i32, off)
2193        }
2194    };
2195    let mut rec = indexmap::IndexMap::new();
2196    rec.insert("year".into(),    Value::Int(y as i64));
2197    rec.insert("month".into(),   Value::Int(m as i64));
2198    rec.insert("day".into(),     Value::Int(d as i64));
2199    rec.insert("hour".into(),    Value::Int(hh as i64));
2200    rec.insert("minute".into(),  Value::Int(mm as i64));
2201    rec.insert("second".into(),  Value::Int(ss as i64));
2202    rec.insert("nano".into(),    Value::Int(ns as i64));
2203    rec.insert("tz_offset_minutes".into(), Value::Int(off_min as i64));
2204    let _ = chrono::Utc.timestamp_opt(0, 0); // touch TimeZone to suppress unused-import lint paths
2205    Ok(Value::Record(rec))
2206}
2207
2208
2209fn instant_from_components(rec: &indexmap::IndexMap<String, Value>) -> Result<i64, String> {
2210    use chrono::TimeZone;
2211    fn get_int(rec: &indexmap::IndexMap<String, Value>, k: &str) -> Result<i64, String> {
2212        match rec.get(k) {
2213            Some(Value::Int(n)) => Ok(*n),
2214            other => Err(format!("from_components: missing or non-int field `{k}`: {other:?}")),
2215        }
2216    }
2217    let y = get_int(rec, "year")? as i32;
2218    let m = get_int(rec, "month")? as u32;
2219    let d = get_int(rec, "day")? as u32;
2220    let hh = get_int(rec, "hour")? as u32;
2221    let mm = get_int(rec, "minute")? as u32;
2222    let ss = get_int(rec, "second")? as u32;
2223    let ns = get_int(rec, "nano")? as u32;
2224    let off_min = get_int(rec, "tz_offset_minutes")? as i32;
2225    let off = chrono::FixedOffset::east_opt(off_min * 60)
2226        .ok_or("from_components: offset out of range")?;
2227    let dt = off
2228        .with_ymd_and_hms(y, m, d, hh, mm, ss)
2229        .single()
2230        .ok_or("from_components: invalid or ambiguous date/time")?;
2231    let dt = dt + chrono::Duration::nanoseconds(ns as i64);
2232    Ok(instant_from_chrono(dt))
2233}