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