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")
35}
36
37fn dispatch(kind: &str, op: &str, args: &[Value]) -> Result<Value, String> {
38    match (kind, op) {
39        // -- str --
40        ("str", "is_empty") => Ok(Value::Bool(expect_str(args.first())?.is_empty())),
41        ("str", "len") => Ok(Value::Int(expect_str(args.first())?.len() as i64)),
42        ("str", "concat") => {
43            let a = expect_str(args.first())?;
44            let b = expect_str(args.get(1))?;
45            Ok(Value::Str(format!("{a}{b}")))
46        }
47        ("str", "to_int") => {
48            let s = expect_str(args.first())?;
49            match s.parse::<i64>() {
50                Ok(n) => Ok(some(Value::Int(n))),
51                Err(_) => Ok(none()),
52            }
53        }
54        ("str", "split") => {
55            let s = expect_str(args.first())?;
56            let sep = expect_str(args.get(1))?;
57            let items: Vec<Value> = if sep.is_empty() {
58                s.chars().map(|c| Value::Str(c.to_string())).collect()
59            } else {
60                s.split(sep.as_str()).map(|p| Value::Str(p.to_string())).collect()
61            };
62            Ok(Value::List(items))
63        }
64        ("str", "join") => {
65            let parts = expect_list(args.first())?;
66            let sep = expect_str(args.get(1))?;
67            let mut out = String::new();
68            for (i, p) in parts.iter().enumerate() {
69                if i > 0 { out.push_str(&sep); }
70                match p {
71                    Value::Str(s) => out.push_str(s),
72                    other => return Err(format!("str.join element must be Str, got {other:?}")),
73                }
74            }
75            Ok(Value::Str(out))
76        }
77        ("str", "starts_with") => {
78            let s = expect_str(args.first())?;
79            let prefix = expect_str(args.get(1))?;
80            Ok(Value::Bool(s.starts_with(prefix.as_str())))
81        }
82        ("str", "ends_with") => {
83            let s = expect_str(args.first())?;
84            let suffix = expect_str(args.get(1))?;
85            Ok(Value::Bool(s.ends_with(suffix.as_str())))
86        }
87        ("str", "contains") => {
88            let s = expect_str(args.first())?;
89            let needle = expect_str(args.get(1))?;
90            Ok(Value::Bool(s.contains(needle.as_str())))
91        }
92        ("str", "replace") => {
93            let s = expect_str(args.first())?;
94            let from = expect_str(args.get(1))?;
95            let to = expect_str(args.get(2))?;
96            Ok(Value::Str(s.replace(from.as_str(), to.as_str())))
97        }
98        ("str", "trim") => Ok(Value::Str(expect_str(args.first())?.trim().to_string())),
99        ("str", "to_upper") => Ok(Value::Str(expect_str(args.first())?.to_uppercase())),
100        ("str", "to_lower") => Ok(Value::Str(expect_str(args.first())?.to_lowercase())),
101        ("str", "strip_prefix") => {
102            let s = expect_str(args.first())?;
103            let prefix = expect_str(args.get(1))?;
104            Ok(match s.strip_prefix(prefix.as_str()) {
105                Some(rest) => some(Value::Str(rest.to_string())),
106                None => none(),
107            })
108        }
109        ("str", "strip_suffix") => {
110            let s = expect_str(args.first())?;
111            let suffix = expect_str(args.get(1))?;
112            Ok(match s.strip_suffix(suffix.as_str()) {
113                Some(rest) => some(Value::Str(rest.to_string())),
114                None => none(),
115            })
116        }
117        ("str", "slice") => {
118            // Half-open byte-range slice. Out-of-range or non-UTF-8
119            // boundaries error rather than panic, so caller code can
120            // recover via Result. Spec §11.1 doesn't pin a name; this
121            // matches the bytes.slice helper added earlier.
122            let s = expect_str(args.first())?;
123            let lo = expect_int(args.get(1))? as usize;
124            let hi = expect_int(args.get(2))? as usize;
125            if lo > hi || hi > s.len() {
126                return Err(format!("str.slice: out of range [{lo}..{hi}] of len {}", s.len()));
127            }
128            if !s.is_char_boundary(lo) || !s.is_char_boundary(hi) {
129                return Err(format!("str.slice: [{lo}..{hi}] not on char boundaries"));
130            }
131            Ok(Value::Str(s[lo..hi].to_string()))
132        }
133
134        // -- int / float --
135        ("int", "to_str") => Ok(Value::Str(expect_int(args.first())?.to_string())),
136        ("int", "to_float") => Ok(Value::Float(expect_int(args.first())? as f64)),
137        ("float", "to_int") => Ok(Value::Int(expect_float(args.first())? as i64)),
138        ("float", "to_str") => Ok(Value::Str(expect_float(args.first())?.to_string())),
139        ("str", "to_float") => {
140            let s = expect_str(args.first())?;
141            match s.parse::<f64>() {
142                Ok(f) => Ok(some(Value::Float(f))),
143                Err(_) => Ok(none()),
144            }
145        }
146
147        // -- list --
148        ("list", "len") => Ok(Value::Int(expect_list(args.first())?.len() as i64)),
149        ("list", "is_empty") => Ok(Value::Bool(expect_list(args.first())?.is_empty())),
150        ("list", "head") => {
151            let xs = expect_list(args.first())?;
152            match xs.first() {
153                Some(v) => Ok(some(v.clone())),
154                None => Ok(none()),
155            }
156        }
157        ("list", "tail") => {
158            let xs = expect_list(args.first())?;
159            if xs.is_empty() { Ok(Value::List(Vec::new())) }
160            else { Ok(Value::List(xs[1..].to_vec())) }
161        }
162        ("list", "range") => {
163            let lo = expect_int(args.first())?;
164            let hi = expect_int(args.get(1))?;
165            Ok(Value::List((lo..hi).map(Value::Int).collect()))
166        }
167        ("list", "concat") => {
168            let mut out = expect_list(args.first())?.clone();
169            out.extend(expect_list(args.get(1))?.iter().cloned());
170            Ok(Value::List(out))
171        }
172
173        // -- tuple --
174        // Per §11.1: fst, snd, third for 2- and 3-tuples. Index out of
175        // range is an error rather than a panic so calling `tuple.third`
176        // on a 2-tuple is a clean failure instead of a host crash.
177        ("tuple", "fst")   => tuple_index(first_arg(args)?, 0),
178        ("tuple", "snd")   => tuple_index(first_arg(args)?, 1),
179        ("tuple", "third") => tuple_index(first_arg(args)?, 2),
180        ("tuple", "len") => match first_arg(args)? {
181            Value::Tuple(items) => Ok(Value::Int(items.len() as i64)),
182            other => Err(format!("tuple.len: expected Tuple, got {other:?}")),
183        },
184
185        // -- option --
186        ("option", "unwrap_or") => {
187            let opt = first_arg(args)?;
188            let default = args.get(1).cloned().unwrap_or(Value::Unit);
189            match opt {
190                Value::Variant { name, args } if name == "Some" && !args.is_empty() => Ok(args[0].clone()),
191                Value::Variant { name, .. } if name == "None" => Ok(default),
192                other => Err(format!("option.unwrap_or expected Option, got {other:?}")),
193            }
194        }
195        ("option", "is_some") => match first_arg(args)? {
196            Value::Variant { name, .. } => Ok(Value::Bool(name == "Some")),
197            other => Err(format!("option.is_some expected Option, got {other:?}")),
198        },
199        ("option", "is_none") => match first_arg(args)? {
200            Value::Variant { name, .. } => Ok(Value::Bool(name == "None")),
201            other => Err(format!("option.is_none expected Option, got {other:?}")),
202        },
203
204        // -- result --
205        ("result", "is_ok") => match first_arg(args)? {
206            Value::Variant { name, .. } => Ok(Value::Bool(name == "Ok")),
207            other => Err(format!("result.is_ok expected Result, got {other:?}")),
208        },
209        ("result", "is_err") => match first_arg(args)? {
210            Value::Variant { name, .. } => Ok(Value::Bool(name == "Err")),
211            other => Err(format!("result.is_err expected Result, got {other:?}")),
212        },
213        ("result", "unwrap_or") => {
214            let res = first_arg(args)?;
215            let default = args.get(1).cloned().unwrap_or(Value::Unit);
216            match res {
217                Value::Variant { name, args } if name == "Ok" && !args.is_empty() => Ok(args[0].clone()),
218                Value::Variant { name, .. } if name == "Err" => Ok(default),
219                other => Err(format!("result.unwrap_or expected Result, got {other:?}")),
220            }
221        }
222
223        // -- json --
224        ("json", "stringify") => {
225            let v = first_arg(args)?;
226            Ok(Value::Str(serde_json::to_string(&value_to_json(v)).unwrap_or_default()))
227        }
228        ("json", "parse") => {
229            let s = expect_str(args.first())?;
230            match serde_json::from_str::<serde_json::Value>(&s) {
231                Ok(v) => Ok(ok_v(json_to_value(&v))),
232                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
233            }
234        }
235        // Tactical fix for #168: validate required fields before
236        // returning Ok. The full type-driven fix (derive `required`
237        // from `T` at type-check time) tracked in #168.
238        ("json", "parse_strict") => {
239            let s = expect_str(args.first())?;
240            let required = required_field_names(args.get(1))?;
241            match serde_json::from_str::<serde_json::Value>(&s) {
242                Ok(v) => match check_required_fields(&v, &required) {
243                    Ok(()) => Ok(ok_v(json_to_value(&v))),
244                    Err(e) => Ok(err_v(Value::Str(e))),
245                },
246                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
247            }
248        }
249
250        // -- toml (config parser; routes through serde_json::Value
251        // so the parsed shape composes with the existing json
252        // tooling. Datetimes become RFC 3339 strings — the only
253        // info-losing step) --
254        ("toml", "parse") => {
255            let s = expect_str(args.first())?;
256            match toml::from_str::<serde_json::Value>(&s) {
257                Ok(mut v) => {
258                    unwrap_toml_datetime_markers(&mut v);
259                    Ok(ok_v(json_to_value(&v)))
260                }
261                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
262            }
263        }
264        // Tactical fix for #168: validate required fields before
265        // returning Ok. Caller passes the field names explicitly
266        // so the runtime doesn't need T's shape (which lex-bytecode
267        // doesn't carry today). Full type-driven fix tracked in #168.
268        ("toml", "parse_strict") => {
269            let s = expect_str(args.first())?;
270            let required = required_field_names(args.get(1))?;
271            match toml::from_str::<serde_json::Value>(&s) {
272                Ok(mut v) => {
273                    unwrap_toml_datetime_markers(&mut v);
274                    match check_required_fields(&v, &required) {
275                        Ok(()) => Ok(ok_v(json_to_value(&v))),
276                        Err(e) => Ok(err_v(Value::Str(e))),
277                    }
278                }
279                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
280            }
281        }
282        ("toml", "stringify") => {
283            let v = first_arg(args)?;
284            // serde_json::Value → toml::Value via its serde impls.
285            // TOML's grammar is stricter than JSON's (top-level
286            // must be a table; no `null`; no mixed-type arrays),
287            // so the conversion can fail — surface as Result::Err
288            // rather than panic.
289            let json = value_to_json(v);
290            match toml::to_string(&json) {
291                Ok(s)  => Ok(ok_v(Value::Str(s))),
292                Err(e) => Ok(err_v(Value::Str(format!("toml.stringify: {e}")))),
293            }
294        }
295
296        // -- yaml -- mirrors std.toml. Wraps serde_yaml so values
297        // map to the same Lex shape as JSON. YAML's Tag/Anchor
298        // features are folded out by serde_yaml's deserialize-to-
299        // Value path; non-representable shapes (e.g. non-string
300        // map keys when stringifying) surface as Result::Err.
301        ("yaml", "parse") => {
302            let s = expect_str(args.first())?;
303            match serde_yaml::from_str::<serde_json::Value>(&s) {
304                Ok(v)  => Ok(ok_v(json_to_value(&v))),
305                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
306            }
307        }
308        // Tactical fix for #168 — same shape as toml.parse_strict.
309        ("yaml", "parse_strict") => {
310            let s = expect_str(args.first())?;
311            let required = required_field_names(args.get(1))?;
312            match serde_yaml::from_str::<serde_json::Value>(&s) {
313                Ok(v) => match check_required_fields(&v, &required) {
314                    Ok(()) => Ok(ok_v(json_to_value(&v))),
315                    Err(e) => Ok(err_v(Value::Str(e))),
316                },
317                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
318            }
319        }
320        ("yaml", "stringify") => {
321            let v = first_arg(args)?;
322            let json = value_to_json(v);
323            match serde_yaml::to_string(&json) {
324                Ok(s)  => Ok(ok_v(Value::Str(s))),
325                Err(e) => Ok(err_v(Value::Str(format!("yaml.stringify: {e}")))),
326            }
327        }
328
329        // -- dotenv -- KEY=VALUE pair files. Hand-rolled parser
330        // because the dotenvy crate's API is geared at loading
331        // into the process env, not parsing-to-data. The grammar
332        // we accept: blank lines, `# comment` lines, and
333        // `KEY=VALUE` (optional surrounding `"..."` or `'...'`,
334        // unescaped). Simple but covers the .env files in the
335        // wild that aren't trying to be shell.
336        ("dotenv", "parse") => {
337            use std::collections::BTreeMap;
338            use lex_bytecode::MapKey;
339            let s = expect_str(args.first())?;
340            match parse_dotenv(&s) {
341                Ok(map) => {
342                    let mut bt: BTreeMap<MapKey, Value> = BTreeMap::new();
343                    for (k, v) in map {
344                        bt.insert(MapKey::Str(k), Value::Str(v));
345                    }
346                    Ok(ok_v(Value::Map(bt)))
347                }
348                Err(e) => Ok(err_v(Value::Str(e))),
349            }
350        }
351
352        // -- csv -- rows-as-lists; first row is whatever the file
353        // has. The caller decides whether row 0 is a header. We
354        // could ship a `parse_with_headers` later that returns a
355        // List[Map[Str, Str]]; v1 keeps the surface tight.
356        ("csv", "parse") => {
357            let s = expect_str(args.first())?;
358            let mut rdr = csv::ReaderBuilder::new()
359                .has_headers(false)
360                .flexible(true)
361                .from_reader(s.as_bytes());
362            let mut rows: Vec<Value> = Vec::new();
363            for r in rdr.records() {
364                match r {
365                    Ok(rec) => {
366                        let row: Vec<Value> = rec.iter()
367                            .map(|f| Value::Str(f.to_string()))
368                            .collect();
369                        rows.push(Value::List(row));
370                    }
371                    Err(e) => return Ok(err_v(Value::Str(format!("csv.parse: {e}")))),
372                }
373            }
374            Ok(ok_v(Value::List(rows)))
375        }
376        ("csv", "stringify") => {
377            // List[List[Str]] → CSV string. Mixed-type rows are
378            // not allowed (CSV is text-only); non-Str cells get
379            // stringified via to_json since that's already the
380            // convention for `json.stringify` etc.
381            let v = first_arg(args)?;
382            let rows = match v {
383                Value::List(rs) => rs,
384                _ => return Ok(err_v(Value::Str("csv.stringify expects List[List[Str]]".into()))),
385            };
386            let mut out = Vec::new();
387            {
388                let mut wtr = csv::WriterBuilder::new()
389                    .has_headers(false)
390                    .from_writer(&mut out);
391                for row in rows {
392                    let cells = match row {
393                        Value::List(cs) => cs,
394                        _ => return Ok(err_v(Value::Str("csv.stringify row must be List[Str]".into()))),
395                    };
396                    let strs: Vec<String> = cells.iter().map(|c| match c {
397                        Value::Str(s) => s.clone(),
398                        other => serde_json::to_string(&other.to_json())
399                            .unwrap_or_else(|_| String::new()),
400                    }).collect();
401                    if let Err(e) = wtr.write_record(&strs) {
402                        return Ok(err_v(Value::Str(format!("csv.stringify: {e}"))));
403                    }
404                }
405                if let Err(e) = wtr.flush() {
406                    return Ok(err_v(Value::Str(format!("csv.stringify flush: {e}"))));
407                }
408            }
409            match String::from_utf8(out) {
410                Ok(s) => Ok(ok_v(Value::Str(s))),
411                Err(e) => Ok(err_v(Value::Str(format!("csv.stringify utf8: {e}")))),
412            }
413        }
414
415        // -- test -- tiny assertion library. Each helper is pure
416        // and returns `Result[Unit, Str]` so tests are themselves
417        // functions returning a Result. A suite is a List the user
418        // iterates with `list.fold`; no Rust-side Suite/Runner
419        // types in v1, so the whole thing is 4 builtins + a few
420        // Lex-source helpers callers can copy into their tests/.
421        ("test", "assert_eq") => {
422            let a = first_arg(args)?;
423            let b = args.get(1).ok_or("test.assert_eq: missing second arg")?;
424            if a == b {
425                Ok(ok_v(Value::Unit))
426            } else {
427                Ok(err_v(Value::Str(format!("assert_eq: lhs {} != rhs {}",
428                    value_to_json(a), value_to_json(b)))))
429            }
430        }
431        ("test", "assert_ne") => {
432            let a = first_arg(args)?;
433            let b = args.get(1).ok_or("test.assert_ne: missing second arg")?;
434            if a != b {
435                Ok(ok_v(Value::Unit))
436            } else {
437                Ok(err_v(Value::Str(format!("assert_ne: both sides are {}",
438                    value_to_json(a)))))
439            }
440        }
441        ("test", "assert_true") => {
442            match first_arg(args)? {
443                Value::Bool(true) => Ok(ok_v(Value::Unit)),
444                Value::Bool(false) => Ok(err_v(Value::Str("assert_true: was false".into()))),
445                other => Err(format!("test.assert_true expects Bool, got {other:?}")),
446            }
447        }
448        ("test", "assert_false") => {
449            match first_arg(args)? {
450                Value::Bool(false) => Ok(ok_v(Value::Unit)),
451                Value::Bool(true)  => Ok(err_v(Value::Str("assert_false: was true".into()))),
452                other => Err(format!("test.assert_false expects Bool, got {other:?}")),
453            }
454        }
455
456        // -- bytes --
457        ("bytes", "len") => {
458            let b = expect_bytes(args.first())?;
459            Ok(Value::Int(b.len() as i64))
460        }
461        ("bytes", "eq") => {
462            let a = expect_bytes(args.first())?;
463            let b = expect_bytes(args.get(1))?;
464            Ok(Value::Bool(a == b))
465        }
466        ("bytes", "from_str") => {
467            let s = expect_str(args.first())?;
468            Ok(Value::Bytes(s.into_bytes()))
469        }
470        ("bytes", "to_str") => {
471            let b = expect_bytes(args.first())?;
472            match String::from_utf8(b.to_vec()) {
473                Ok(s) => Ok(ok_v(Value::Str(s))),
474                Err(e) => Ok(err_v(Value::Str(format!("{e}")))),
475            }
476        }
477        ("bytes", "slice") => {
478            let b = expect_bytes(args.first())?;
479            let lo = expect_int(args.get(1))? as usize;
480            let hi = expect_int(args.get(2))? as usize;
481            if lo > hi || hi > b.len() {
482                return Err(format!("bytes.slice: out of range [{lo}..{hi}] of {}", b.len()));
483            }
484            Ok(Value::Bytes(b[lo..hi].to_vec()))
485        }
486        ("bytes", "is_empty") => {
487            let b = expect_bytes(args.first())?;
488            Ok(Value::Bool(b.is_empty()))
489        }
490
491        // -- math --
492        // Matrices are stored as the F64Array fast-lane variant (a flat
493        // row-major Vec<f64> with shape). Lex code treats them as the
494        // type alias `Matrix = { rows :: Int, cols :: Int, data ::
495        // List[Float] }`; field access is unsupported, so all
496        // introspection happens through these helpers.
497        ("math", "exp")  => Ok(Value::Float(expect_float(args.first())?.exp())),
498        ("math", "log")  => Ok(Value::Float(expect_float(args.first())?.ln())),
499        ("math", "sqrt") => Ok(Value::Float(expect_float(args.first())?.sqrt())),
500        ("math", "abs")  => Ok(Value::Float(expect_float(args.first())?.abs())),
501        ("math", "zeros") => {
502            let r = expect_int(args.first())?;
503            let c = expect_int(args.get(1))?;
504            if r < 0 || c < 0 {
505                return Err(format!("math.zeros: negative dim {r}x{c}"));
506            }
507            let r = r as usize; let c = c as usize;
508            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data: vec![0.0; r * c] })
509        }
510        ("math", "ones") => {
511            let r = expect_int(args.first())?;
512            let c = expect_int(args.get(1))?;
513            if r < 0 || c < 0 {
514                return Err(format!("math.ones: negative dim {r}x{c}"));
515            }
516            let r = r as usize; let c = c as usize;
517            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data: vec![1.0; r * c] })
518        }
519        ("math", "from_lists") => {
520            let rows = expect_list(args.first())?;
521            let r = rows.len();
522            if r == 0 {
523                return Ok(Value::F64Array { rows: 0, cols: 0, data: Vec::new() });
524            }
525            let first_row = match &rows[0] {
526                Value::List(xs) => xs,
527                other => return Err(format!("math.from_lists: row 0 not List, got {other:?}")),
528            };
529            let c = first_row.len();
530            let mut data = Vec::with_capacity(r * c);
531            for (i, row) in rows.iter().enumerate() {
532                let row = match row {
533                    Value::List(xs) => xs,
534                    other => return Err(format!("math.from_lists: row {i} not List, got {other:?}")),
535                };
536                if row.len() != c {
537                    return Err(format!("math.from_lists: row {i} has {} cols, expected {c}", row.len()));
538                }
539                for (j, v) in row.iter().enumerate() {
540                    let f = match v {
541                        Value::Float(f) => *f,
542                        Value::Int(n) => *n as f64,
543                        other => return Err(format!("math.from_lists: ({i},{j}) not numeric, got {other:?}")),
544                    };
545                    data.push(f);
546                }
547            }
548            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data })
549        }
550        ("math", "from_flat") => {
551            let r = expect_int(args.first())?;
552            let c = expect_int(args.get(1))?;
553            let xs = expect_list(args.get(2))?;
554            if r < 0 || c < 0 {
555                return Err(format!("math.from_flat: negative dim {r}x{c}"));
556            }
557            let r = r as usize; let c = c as usize;
558            if xs.len() != r * c {
559                return Err(format!("math.from_flat: list len {} != {}*{}", xs.len(), r, c));
560            }
561            let mut data = Vec::with_capacity(r * c);
562            for v in xs {
563                data.push(match v {
564                    Value::Float(f) => *f,
565                    Value::Int(n)   => *n as f64,
566                    other => return Err(format!("math.from_flat: non-numeric element {other:?}")),
567                });
568            }
569            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data })
570        }
571        ("math", "rows") => {
572            let (r, _, _) = unpack_matrix(first_arg(args)?)?;
573            Ok(Value::Int(r as i64))
574        }
575        ("math", "cols") => {
576            let (_, c, _) = unpack_matrix(first_arg(args)?)?;
577            Ok(Value::Int(c as i64))
578        }
579        ("math", "get") => {
580            let (r, c, data) = unpack_matrix(first_arg(args)?)?;
581            let i = expect_int(args.get(1))? as usize;
582            let j = expect_int(args.get(2))? as usize;
583            if i >= r || j >= c {
584                return Err(format!("math.get: ({i},{j}) out of {r}x{c}"));
585            }
586            Ok(Value::Float(data[i * c + j]))
587        }
588        ("math", "to_flat") => {
589            let (_, _, data) = unpack_matrix(first_arg(args)?)?;
590            Ok(Value::List(data.into_iter().map(Value::Float).collect()))
591        }
592        ("math", "transpose") => {
593            let (r, c, data) = unpack_matrix(first_arg(args)?)?;
594            let mut out = vec![0.0; r * c];
595            for i in 0..r {
596                for j in 0..c {
597                    out[j * r + i] = data[i * c + j];
598                }
599            }
600            Ok(Value::F64Array { rows: c as u32, cols: r as u32, data: out })
601        }
602        ("math", "matmul") => {
603            let (m, k1, a) = unpack_matrix(first_arg(args)?)?;
604            let (k2, n, b) = unpack_matrix(args.get(1).ok_or("math.matmul: missing arg 1")?)?;
605            if k1 != k2 {
606                return Err(format!("math.matmul: dim mismatch {m}x{k1} · {k2}x{n}"));
607            }
608            // Plain triple loop. For the small matrices used in the ML
609            // demo (n<200, k<10) this is well under a millisecond and
610            // avoids pulling in matrixmultiply for the runtime crate.
611            let mut c = vec![0.0; m * n];
612            for i in 0..m {
613                for kk in 0..k1 {
614                    let aik = a[i * k1 + kk];
615                    for j in 0..n {
616                        c[i * n + j] += aik * b[kk * n + j];
617                    }
618                }
619            }
620            Ok(Value::F64Array { rows: m as u32, cols: n as u32, data: c })
621        }
622        ("math", "scale") => {
623            let s = expect_float(args.first())?;
624            let (r, c, mut data) = unpack_matrix(args.get(1).ok_or("math.scale: missing arg 1")?)?;
625            for x in &mut data { *x *= s; }
626            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data })
627        }
628        ("math", "add") | ("math", "sub") => {
629            let (ar, ac, a) = unpack_matrix(first_arg(args)?)?;
630            let (br, bc, b) = unpack_matrix(args.get(1).ok_or("math.add/sub: missing arg 1")?)?;
631            if ar != br || ac != bc {
632                return Err(format!("math.{op}: shape mismatch {ar}x{ac} vs {br}x{bc}"));
633            }
634            let neg = op == "sub";
635            let mut out = a;
636            for (i, x) in out.iter_mut().enumerate() {
637                if neg { *x -= b[i] } else { *x += b[i] }
638            }
639            Ok(Value::F64Array { rows: ar as u32, cols: ac as u32, data: out })
640        }
641        ("math", "sigmoid") => {
642            let (r, c, mut data) = unpack_matrix(first_arg(args)?)?;
643            for x in &mut data { *x = 1.0 / (1.0 + (-*x).exp()); }
644            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data })
645        }
646
647        // -- map --
648        ("map", "new") => Ok(Value::Map(BTreeMap::new())),
649        ("map", "size") => Ok(Value::Int(expect_map(args.first())?.len() as i64)),
650        ("map", "has") => {
651            let m = expect_map(args.first())?;
652            let k = MapKey::from_value(args.get(1).ok_or("map.has: missing key")?)?;
653            Ok(Value::Bool(m.contains_key(&k)))
654        }
655        ("map", "get") => {
656            let m = expect_map(args.first())?;
657            let k = MapKey::from_value(args.get(1).ok_or("map.get: missing key")?)?;
658            Ok(match m.get(&k) {
659                Some(v) => some(v.clone()),
660                None    => none(),
661            })
662        }
663        ("map", "set") => {
664            let mut m = expect_map(args.first())?.clone();
665            let k = MapKey::from_value(args.get(1).ok_or("map.set: missing key")?)?;
666            let v = args.get(2).ok_or("map.set: missing value")?.clone();
667            m.insert(k, v);
668            Ok(Value::Map(m))
669        }
670        ("map", "delete") => {
671            let mut m = expect_map(args.first())?.clone();
672            let k = MapKey::from_value(args.get(1).ok_or("map.delete: missing key")?)?;
673            m.remove(&k);
674            Ok(Value::Map(m))
675        }
676        ("map", "keys") => {
677            let m = expect_map(args.first())?;
678            Ok(Value::List(m.keys().cloned().map(MapKey::into_value).collect()))
679        }
680        ("map", "values") => {
681            let m = expect_map(args.first())?;
682            Ok(Value::List(m.values().cloned().collect()))
683        }
684        ("map", "entries") => {
685            let m = expect_map(args.first())?;
686            Ok(Value::List(m.iter()
687                .map(|(k, v)| Value::Tuple(vec![k.as_value(), v.clone()]))
688                .collect()))
689        }
690        ("map", "from_list") => {
691            let pairs = expect_list(args.first())?;
692            let mut m = BTreeMap::new();
693            for p in pairs {
694                let items = match p {
695                    Value::Tuple(items) if items.len() == 2 => items,
696                    other => return Err(format!(
697                        "map.from_list element must be a 2-tuple, got {other:?}")),
698                };
699                let k = MapKey::from_value(&items[0])?;
700                m.insert(k, items[1].clone());
701            }
702            Ok(Value::Map(m))
703        }
704
705        // -- set --
706        ("set", "new") => Ok(Value::Set(BTreeSet::new())),
707        ("set", "size") => Ok(Value::Int(expect_set(args.first())?.len() as i64)),
708        ("set", "has") => {
709            let s = expect_set(args.first())?;
710            let k = MapKey::from_value(args.get(1).ok_or("set.has: missing element")?)?;
711            Ok(Value::Bool(s.contains(&k)))
712        }
713        ("set", "add") => {
714            let mut s = expect_set(args.first())?.clone();
715            let k = MapKey::from_value(args.get(1).ok_or("set.add: missing element")?)?;
716            s.insert(k);
717            Ok(Value::Set(s))
718        }
719        ("set", "delete") => {
720            let mut s = expect_set(args.first())?.clone();
721            let k = MapKey::from_value(args.get(1).ok_or("set.delete: missing element")?)?;
722            s.remove(&k);
723            Ok(Value::Set(s))
724        }
725        ("set", "to_list") => {
726            let s = expect_set(args.first())?;
727            Ok(Value::List(s.iter().cloned().map(MapKey::into_value).collect()))
728        }
729        ("set", "from_list") => {
730            let xs = expect_list(args.first())?;
731            let mut s = BTreeSet::new();
732            for x in xs {
733                s.insert(MapKey::from_value(x)?);
734            }
735            Ok(Value::Set(s))
736        }
737        ("set", "union") => {
738            let a = expect_set(args.first())?;
739            let b = expect_set(args.get(1))?;
740            Ok(Value::Set(a.union(b).cloned().collect()))
741        }
742        ("set", "intersect") => {
743            let a = expect_set(args.first())?;
744            let b = expect_set(args.get(1))?;
745            Ok(Value::Set(a.intersection(b).cloned().collect()))
746        }
747        ("set", "diff") => {
748            let a = expect_set(args.first())?;
749            let b = expect_set(args.get(1))?;
750            Ok(Value::Set(a.difference(b).cloned().collect()))
751        }
752        ("set", "is_empty") => Ok(Value::Bool(expect_set(args.first())?.is_empty())),
753        ("set", "is_subset") => {
754            let a = expect_set(args.first())?;
755            let b = expect_set(args.get(1))?;
756            Ok(Value::Bool(a.is_subset(b)))
757        }
758
759        // -- map helpers --
760        ("map", "merge") => {
761            // b's entries override a's. We construct a new BTreeMap
762            // by extending a with b's pairs.
763            let a = expect_map(args.first())?.clone();
764            let b = expect_map(args.get(1))?;
765            let mut out = a;
766            for (k, v) in b {
767                out.insert(k.clone(), v.clone());
768            }
769            Ok(Value::Map(out))
770        }
771        ("map", "is_empty") => Ok(Value::Bool(expect_map(args.first())?.is_empty())),
772
773        // -- deque --
774        ("deque", "new") => Ok(Value::Deque(std::collections::VecDeque::new())),
775        ("deque", "size") => Ok(Value::Int(expect_deque(args.first())?.len() as i64)),
776        ("deque", "is_empty") => Ok(Value::Bool(expect_deque(args.first())?.is_empty())),
777        ("deque", "push_back") => {
778            let mut d = expect_deque(args.first())?.clone();
779            let x = args.get(1).ok_or("deque.push_back: missing value")?.clone();
780            d.push_back(x);
781            Ok(Value::Deque(d))
782        }
783        ("deque", "push_front") => {
784            let mut d = expect_deque(args.first())?.clone();
785            let x = args.get(1).ok_or("deque.push_front: missing value")?.clone();
786            d.push_front(x);
787            Ok(Value::Deque(d))
788        }
789        ("deque", "pop_back") => {
790            let mut d = expect_deque(args.first())?.clone();
791            match d.pop_back() {
792                Some(x) => Ok(Value::Variant {
793                    name: "Some".into(),
794                    args: vec![Value::Tuple(vec![x, Value::Deque(d)])],
795                }),
796                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
797            }
798        }
799        ("deque", "pop_front") => {
800            let mut d = expect_deque(args.first())?.clone();
801            match d.pop_front() {
802                Some(x) => Ok(Value::Variant {
803                    name: "Some".into(),
804                    args: vec![Value::Tuple(vec![x, Value::Deque(d)])],
805                }),
806                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
807            }
808        }
809        ("deque", "peek_back") => {
810            let d = expect_deque(args.first())?;
811            match d.back() {
812                Some(x) => Ok(Value::Variant {
813                    name: "Some".into(),
814                    args: vec![x.clone()],
815                }),
816                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
817            }
818        }
819        ("deque", "peek_front") => {
820            let d = expect_deque(args.first())?;
821            match d.front() {
822                Some(x) => Ok(Value::Variant {
823                    name: "Some".into(),
824                    args: vec![x.clone()],
825                }),
826                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
827            }
828        }
829        ("deque", "from_list") => {
830            let xs = expect_list(args.first())?;
831            Ok(Value::Deque(xs.iter().cloned().collect()))
832        }
833        ("deque", "to_list") => {
834            let d = expect_deque(args.first())?;
835            Ok(Value::List(d.iter().cloned().collect()))
836        }
837
838        // -- crypto (pure ops; crypto.random is effectful and routes
839        // through the handler under [random], see try_pure_builtin) --
840        ("crypto", "sha256") => {
841            use sha2::{Digest, Sha256};
842            let data = expect_bytes(args.first())?;
843            let mut h = Sha256::new();
844            h.update(data);
845            Ok(Value::Bytes(h.finalize().to_vec()))
846        }
847        ("crypto", "sha512") => {
848            use sha2::{Digest, Sha512};
849            let data = expect_bytes(args.first())?;
850            let mut h = Sha512::new();
851            h.update(data);
852            Ok(Value::Bytes(h.finalize().to_vec()))
853        }
854        ("crypto", "md5") => {
855            use md5::{Digest, Md5};
856            let data = expect_bytes(args.first())?;
857            let mut h = Md5::new();
858            h.update(data);
859            Ok(Value::Bytes(h.finalize().to_vec()))
860        }
861        ("crypto", "hmac_sha256") => {
862            use hmac::{Hmac, KeyInit, Mac};
863            type HmacSha256 = Hmac<sha2::Sha256>;
864            let key = expect_bytes(args.first())?;
865            let data = expect_bytes(args.get(1))?;
866            let mut mac = HmacSha256::new_from_slice(key)
867                .map_err(|e| format!("hmac_sha256 key: {e}"))?;
868            mac.update(data);
869            Ok(Value::Bytes(mac.finalize().into_bytes().to_vec()))
870        }
871        ("crypto", "hmac_sha512") => {
872            use hmac::{Hmac, KeyInit, Mac};
873            type HmacSha512 = Hmac<sha2::Sha512>;
874            let key = expect_bytes(args.first())?;
875            let data = expect_bytes(args.get(1))?;
876            let mut mac = HmacSha512::new_from_slice(key)
877                .map_err(|e| format!("hmac_sha512 key: {e}"))?;
878            mac.update(data);
879            Ok(Value::Bytes(mac.finalize().into_bytes().to_vec()))
880        }
881        ("crypto", "base64_encode") => {
882            use base64::{Engine, engine::general_purpose::STANDARD};
883            let data = expect_bytes(args.first())?;
884            Ok(Value::Str(STANDARD.encode(data)))
885        }
886        ("crypto", "base64_decode") => {
887            use base64::{Engine, engine::general_purpose::STANDARD};
888            let s = expect_str(args.first())?;
889            match STANDARD.decode(s) {
890                Ok(b)  => Ok(ok_v(Value::Bytes(b))),
891                Err(e) => Ok(err_v(Value::Str(format!("base64: {e}")))),
892            }
893        }
894        ("crypto", "hex_encode") => {
895            let data = expect_bytes(args.first())?;
896            Ok(Value::Str(hex::encode(data)))
897        }
898        ("crypto", "hex_decode") => {
899            let s = expect_str(args.first())?;
900            match hex::decode(s) {
901                Ok(b)  => Ok(ok_v(Value::Bytes(b))),
902                Err(e) => Ok(err_v(Value::Str(format!("hex: {e}")))),
903            }
904        }
905        ("crypto", "constant_time_eq") => {
906            use subtle::ConstantTimeEq;
907            let a = expect_bytes(args.first())?;
908            let b = expect_bytes(args.get(1))?;
909            // `subtle` returns Choice; comparison only meaningful when
910            // lengths match. For mismatched lengths return false in
911            // constant time (length itself isn't secret, but we want
912            // a single comparison shape).
913            let eq = if a.len() == b.len() {
914                a.ct_eq(b).into()
915            } else {
916                false
917            };
918            Ok(Value::Bool(eq))
919        }
920
921        // -- regex (the compiled `Regex` is stored as the pattern
922        // string; the runtime caches the actual `regex::Regex` so
923        // ops don't re-compile on every call) --
924        ("regex", "compile") => {
925            let pat = expect_str(args.first())?;
926            match get_or_compile_regex(&pat) {
927                Ok(_) => Ok(ok_v(Value::Str(pat))),
928                Err(e) => Ok(err_v(Value::Str(e))),
929            }
930        }
931        ("regex", "is_match") => {
932            let pat = expect_str(args.first())?;
933            let s = expect_str(args.get(1))?;
934            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.is_match: {e}"))?;
935            Ok(Value::Bool(re.is_match(&s)))
936        }
937        ("regex", "find") => {
938            let pat = expect_str(args.first())?;
939            let s = expect_str(args.get(1))?;
940            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.find: {e}"))?;
941            match re.captures(&s) {
942                Some(caps) => Ok(Value::Variant {
943                    name: "Some".into(),
944                    args: vec![match_value(&caps)],
945                }),
946                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
947            }
948        }
949        ("regex", "find_all") => {
950            let pat = expect_str(args.first())?;
951            let s = expect_str(args.get(1))?;
952            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.find_all: {e}"))?;
953            let items: Vec<Value> = re.captures_iter(&s).map(|caps| match_value(&caps)).collect();
954            Ok(Value::List(items))
955        }
956        ("regex", "replace") => {
957            let pat = expect_str(args.first())?;
958            let s = expect_str(args.get(1))?;
959            let rep = expect_str(args.get(2))?;
960            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.replace: {e}"))?;
961            Ok(Value::Str(re.replace(&s, rep.as_str()).into_owned()))
962        }
963        ("regex", "replace_all") => {
964            let pat = expect_str(args.first())?;
965            let s = expect_str(args.get(1))?;
966            let rep = expect_str(args.get(2))?;
967            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.replace_all: {e}"))?;
968            Ok(Value::Str(re.replace_all(&s, rep.as_str()).into_owned()))
969        }
970        // -- datetime (pure ops; datetime.now is effectful and routes
971        // through the handler under [time]) --
972        ("datetime", "parse_iso") => {
973            let s = expect_str(args.first())?;
974            match chrono::DateTime::parse_from_rfc3339(&s) {
975                Ok(dt) => Ok(ok_v(Value::Int(instant_from_chrono(dt)))),
976                Err(e) => Ok(err_v(Value::Str(format!("parse_iso: {e}")))),
977            }
978        }
979        ("datetime", "format_iso") => {
980            let n = expect_int(args.first())?;
981            Ok(Value::Str(format_iso(n)))
982        }
983        ("datetime", "parse") => {
984            let s = expect_str(args.first())?;
985            let fmt = expect_str(args.get(1))?;
986            match chrono::NaiveDateTime::parse_from_str(&s, &fmt) {
987                Ok(naive) => {
988                    use chrono::TimeZone;
989                    match chrono::Utc.from_local_datetime(&naive).single() {
990                        Some(dt) => Ok(ok_v(Value::Int(instant_from_chrono(dt)))),
991                        None => Ok(err_v(Value::Str("parse: ambiguous local time".into()))),
992                    }
993                }
994                Err(e) => Ok(err_v(Value::Str(format!("parse: {e}")))),
995            }
996        }
997        ("datetime", "format") => {
998            let n = expect_int(args.first())?;
999            let fmt = expect_str(args.get(1))?;
1000            let dt = chrono_from_instant(n);
1001            Ok(Value::Str(dt.format(&fmt).to_string()))
1002        }
1003        ("datetime", "to_components") => {
1004            let n = expect_int(args.first())?;
1005            let tz = match parse_tz_arg(args.get(1)) {
1006                Ok(t) => t,
1007                Err(e) => return Ok(err_v(Value::Str(e))),
1008            };
1009            match resolve_tz_to_components(n, &tz) {
1010                Ok(rec) => Ok(ok_v(rec)),
1011                Err(e) => Ok(err_v(Value::Str(e))),
1012            }
1013        }
1014        ("datetime", "from_components") => {
1015            let rec = match args.first() {
1016                Some(Value::Record(r)) => r.clone(),
1017                _ => return Err("from_components: expected DateTime record".into()),
1018            };
1019            match instant_from_components(&rec) {
1020                Ok(n) => Ok(ok_v(Value::Int(n))),
1021                Err(e) => Ok(err_v(Value::Str(e))),
1022            }
1023        }
1024        ("datetime", "add") => {
1025            let a = expect_int(args.first())?;
1026            let d = expect_int(args.get(1))?;
1027            Ok(Value::Int(a.saturating_add(d)))
1028        }
1029        ("datetime", "diff") => {
1030            let a = expect_int(args.first())?;
1031            let b = expect_int(args.get(1))?;
1032            Ok(Value::Int(a.saturating_sub(b)))
1033        }
1034        ("datetime", "duration_seconds") => {
1035            let s = expect_float(args.first())?;
1036            let nanos = (s * 1_000_000_000.0) as i64;
1037            Ok(Value::Int(nanos))
1038        }
1039        ("datetime", "duration_minutes") => {
1040            let m = expect_int(args.first())?;
1041            Ok(Value::Int(m.saturating_mul(60_000_000_000)))
1042        }
1043        ("datetime", "duration_days") => {
1044            let d = expect_int(args.first())?;
1045            Ok(Value::Int(d.saturating_mul(86_400_000_000_000)))
1046        }
1047
1048        ("regex", "split") => {
1049            let pat = expect_str(args.first())?;
1050            let s = expect_str(args.get(1))?;
1051            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.split: {e}"))?;
1052            let parts: Vec<Value> = re.split(&s).map(|p| Value::Str(p.to_string())).collect();
1053            Ok(Value::List(parts))
1054        }
1055
1056        // -- http (builders + decoders; wire ops live in the
1057        // effect handler under `[net]`) --
1058        ("http", "with_header") => {
1059            let req = expect_record_pure(args.first())?.clone();
1060            let k = expect_str(args.get(1))?;
1061            let v = expect_str(args.get(2))?;
1062            Ok(Value::Record(http_set_header(req, &k, &v)))
1063        }
1064        ("http", "with_auth") => {
1065            let req = expect_record_pure(args.first())?.clone();
1066            let scheme = expect_str(args.get(1))?;
1067            let token = expect_str(args.get(2))?;
1068            let value = format!("{scheme} {token}");
1069            Ok(Value::Record(http_set_header(req, "Authorization", &value)))
1070        }
1071        ("http", "with_query") => {
1072            let req = expect_record_pure(args.first())?.clone();
1073            let params = match args.get(1) {
1074                Some(Value::Map(m)) => m.clone(),
1075                Some(other) => return Err(format!(
1076                    "http.with_query: params must be Map[Str, Str], got {other:?}")),
1077                None => return Err("http.with_query: missing params argument".into()),
1078            };
1079            Ok(Value::Record(http_append_query(req, &params)))
1080        }
1081        ("http", "with_timeout_ms") => {
1082            let req = expect_record_pure(args.first())?.clone();
1083            let ms = expect_int(args.get(1))?;
1084            let mut out = req;
1085            out.insert("timeout_ms".into(), Value::Variant {
1086                name: "Some".into(),
1087                args: vec![Value::Int(ms)],
1088            });
1089            Ok(Value::Record(out))
1090        }
1091        ("http", "json_body") => {
1092            let resp = expect_record_pure(args.first())?;
1093            let body = match resp.get("body") {
1094                Some(Value::Bytes(b)) => b.clone(),
1095                _ => return Err("http.json_body: HttpResponse.body must be Bytes".into()),
1096            };
1097            let s = match std::str::from_utf8(&body) {
1098                Ok(s) => s,
1099                Err(e) => return Ok(http_decode_err_pure(format!("body not UTF-8: {e}"))),
1100            };
1101            match serde_json::from_str::<serde_json::Value>(s) {
1102                Ok(j) => Ok(ok_v(Value::from_json(&j))),
1103                Err(e) => Ok(http_decode_err_pure(format!("json parse: {e}"))),
1104            }
1105        }
1106        ("http", "text_body") => {
1107            let resp = expect_record_pure(args.first())?;
1108            let body = match resp.get("body") {
1109                Some(Value::Bytes(b)) => b.clone(),
1110                _ => return Err("http.text_body: HttpResponse.body must be Bytes".into()),
1111            };
1112            match String::from_utf8(body) {
1113                Ok(s) => Ok(ok_v(Value::Str(s))),
1114                Err(e) => Ok(http_decode_err_pure(format!("body not UTF-8: {e}"))),
1115            }
1116        }
1117
1118        _ => Err(format!("unknown pure builtin: {kind}.{op}")),
1119    }
1120}
1121
1122/// Process-wide cache of compiled regexes, keyed by the pattern
1123/// string. Compilation is the only cost we want to amortize; matching
1124/// the same `Regex` from multiple threads is safe (`regex::Regex` is
1125/// `Send + Sync`).
1126fn regex_cache() -> &'static Mutex<HashMap<String, regex::Regex>> {
1127    static CACHE: OnceLock<Mutex<HashMap<String, regex::Regex>>> = OnceLock::new();
1128    CACHE.get_or_init(|| Mutex::new(HashMap::new()))
1129}
1130
1131fn get_or_compile_regex(pattern: &str) -> Result<regex::Regex, String> {
1132    let cache = regex_cache();
1133    {
1134        let guard = cache.lock().unwrap();
1135        if let Some(re) = guard.get(pattern) {
1136            return Ok(re.clone());
1137        }
1138    }
1139    let re = regex::Regex::new(pattern).map_err(|e| format!("invalid regex: {e}"))?;
1140    let mut guard = cache.lock().unwrap();
1141    guard.insert(pattern.to_string(), re.clone());
1142    Ok(re)
1143}
1144
1145/// Build a `Match` record value: `{ text, start, end, groups }` where
1146/// `groups` is the captured groups in order (group 0 is the full match).
1147/// Missing optional groups become empty strings.
1148fn match_value(caps: &regex::Captures) -> Value {
1149    let m0 = caps.get(0).expect("regex match always has group 0");
1150    let mut rec = indexmap::IndexMap::new();
1151    rec.insert("text".into(), Value::Str(m0.as_str().to_string()));
1152    rec.insert("start".into(), Value::Int(m0.start() as i64));
1153    rec.insert("end".into(), Value::Int(m0.end() as i64));
1154    let groups: Vec<Value> = (1..caps.len())
1155        .map(|i| {
1156            Value::Str(
1157                caps.get(i)
1158                    .map(|m| m.as_str().to_string())
1159                    .unwrap_or_default(),
1160            )
1161        })
1162        .collect();
1163    rec.insert("groups".into(), Value::List(groups));
1164    Value::Record(rec)
1165}
1166
1167fn expect_map(v: Option<&Value>) -> Result<&BTreeMap<MapKey, Value>, String> {
1168    match v {
1169        Some(Value::Map(m)) => Ok(m),
1170        other => Err(format!("expected Map, got {other:?}")),
1171    }
1172}
1173
1174fn expect_set(v: Option<&Value>) -> Result<&BTreeSet<MapKey>, String> {
1175    match v {
1176        Some(Value::Set(s)) => Ok(s),
1177        other => Err(format!("expected Set, got {other:?}")),
1178    }
1179}
1180
1181/// Unpack any matrix-shaped Value into (rows, cols, flat row-major data).
1182/// Accepts the F64Array fast lane and the legacy `Record { rows, cols,
1183/// data: List[Float] }` shape for compatibility with hand-built matrices.
1184fn unpack_matrix(v: &Value) -> Result<(usize, usize, Vec<f64>), String> {
1185    if let Value::F64Array { rows, cols, data } = v {
1186        return Ok((*rows as usize, *cols as usize, data.clone()));
1187    }
1188    let rec = match v {
1189        Value::Record(r) => r,
1190        other => return Err(format!("expected matrix, got {other:?}")),
1191    };
1192    let rows = match rec.get("rows") {
1193        Some(Value::Int(n)) => *n as usize,
1194        _ => return Err("matrix: missing/invalid `rows`".into()),
1195    };
1196    let cols = match rec.get("cols") {
1197        Some(Value::Int(n)) => *n as usize,
1198        _ => return Err("matrix: missing/invalid `cols`".into()),
1199    };
1200    let data = match rec.get("data") {
1201        Some(Value::List(items)) => {
1202            let mut out = Vec::with_capacity(items.len());
1203            for it in items {
1204                out.push(match it {
1205                    Value::Float(f) => *f,
1206                    Value::Int(n) => *n as f64,
1207                    other => return Err(format!("matrix data: not numeric, got {other:?}")),
1208                });
1209            }
1210            out
1211        }
1212        _ => return Err("matrix: missing/invalid `data`".into()),
1213    };
1214    if data.len() != rows * cols {
1215        return Err(format!("matrix: data len {} != {rows}*{cols}", data.len()));
1216    }
1217    Ok((rows, cols, data))
1218}
1219
1220fn expect_bytes(v: Option<&Value>) -> Result<&Vec<u8>, String> {
1221    match v {
1222        Some(Value::Bytes(b)) => Ok(b),
1223        Some(other) => Err(format!("expected Bytes, got {other:?}")),
1224        None => Err("missing argument".into()),
1225    }
1226}
1227
1228fn first_arg(args: &[Value]) -> Result<&Value, String> {
1229    args.first().ok_or_else(|| "missing argument".into())
1230}
1231
1232fn tuple_index(v: &Value, i: usize) -> Result<Value, String> {
1233    match v {
1234        Value::Tuple(items) => items.get(i).cloned()
1235            .ok_or_else(|| format!("tuple index {i} out of range (len={})", items.len())),
1236        other => Err(format!("expected Tuple, got {other:?}")),
1237    }
1238}
1239
1240fn expect_str(v: Option<&Value>) -> Result<String, String> {
1241    match v {
1242        Some(Value::Str(s)) => Ok(s.clone()),
1243        Some(other) => Err(format!("expected Str, got {other:?}")),
1244        None => Err("missing argument".into()),
1245    }
1246}
1247
1248fn expect_int(v: Option<&Value>) -> Result<i64, String> {
1249    match v {
1250        Some(Value::Int(n)) => Ok(*n),
1251        Some(other) => Err(format!("expected Int, got {other:?}")),
1252        None => Err("missing argument".into()),
1253    }
1254}
1255
1256fn expect_float(v: Option<&Value>) -> Result<f64, String> {
1257    match v {
1258        Some(Value::Float(f)) => Ok(*f),
1259        Some(other) => Err(format!("expected Float, got {other:?}")),
1260        None => Err("missing argument".into()),
1261    }
1262}
1263
1264fn expect_list(v: Option<&Value>) -> Result<&Vec<Value>, String> {
1265    match v {
1266        Some(Value::List(xs)) => Ok(xs),
1267        Some(other) => Err(format!("expected List, got {other:?}")),
1268        None => Err("missing argument".into()),
1269    }
1270}
1271
1272fn expect_deque(v: Option<&Value>) -> Result<&std::collections::VecDeque<Value>, String> {
1273    match v {
1274        Some(Value::Deque(d)) => Ok(d),
1275        Some(other) => Err(format!("expected Deque, got {other:?}")),
1276        None => Err("missing argument".into()),
1277    }
1278}
1279
1280fn some(v: Value) -> Value { Value::Variant { name: "Some".into(), args: vec![v] } }
1281fn none() -> Value { Value::Variant { name: "None".into(), args: Vec::new() } }
1282fn ok_v(v: Value) -> Value { Value::Variant { name: "Ok".into(), args: vec![v] } }
1283fn err_v(v: Value) -> Value { Value::Variant { name: "Err".into(), args: vec![v] } }
1284
1285// -- helpers for `std.http` builders / decoders --
1286
1287fn expect_record_pure(v: Option<&Value>) -> Result<&indexmap::IndexMap<String, Value>, String> {
1288    match v {
1289        Some(Value::Record(r)) => Ok(r),
1290        Some(other) => Err(format!("expected Record, got {other:?}")),
1291        None => Err("missing Record argument".into()),
1292    }
1293}
1294
1295fn http_decode_err_pure(msg: String) -> Value {
1296    let inner = Value::Variant {
1297        name: "DecodeError".into(),
1298        args: vec![Value::Str(msg)],
1299    };
1300    err_v(inner)
1301}
1302
1303/// Apply or replace a header in an `HttpRequest` record's `headers`
1304/// field. Header names are normalized to lowercase to match HTTP/1.1
1305/// case-insensitivity; an existing entry under any casing is
1306/// overwritten by the new value.
1307fn http_set_header(
1308    mut req: indexmap::IndexMap<String, Value>,
1309    name: &str,
1310    value: &str,
1311) -> indexmap::IndexMap<String, Value> {
1312    use lex_bytecode::MapKey;
1313    let mut headers = match req.shift_remove("headers") {
1314        Some(Value::Map(m)) => m,
1315        _ => std::collections::BTreeMap::new(),
1316    };
1317    let key = MapKey::Str(name.to_lowercase());
1318    // Drop any case variant of the same header name first so casing
1319    // flips don't accumulate duplicates.
1320    let lowered = name.to_lowercase();
1321    headers.retain(|k, _| match k {
1322        MapKey::Str(s) => s.to_lowercase() != lowered,
1323        _ => true,
1324    });
1325    headers.insert(key, Value::Str(value.to_string()));
1326    req.insert("headers".into(), Value::Map(headers));
1327    req
1328}
1329
1330/// Append `?k=v&...` (URL-encoded) to the `url` field of an
1331/// `HttpRequest` record. Existing query string is preserved and
1332/// extended with `&`. Iteration order is the input map's natural
1333/// order (`BTreeMap` → sorted by key) so the produced URL is
1334/// deterministic.
1335fn http_append_query(
1336    mut req: indexmap::IndexMap<String, Value>,
1337    params: &std::collections::BTreeMap<lex_bytecode::MapKey, Value>,
1338) -> indexmap::IndexMap<String, Value> {
1339    use lex_bytecode::MapKey;
1340    let url = match req.get("url") {
1341        Some(Value::Str(s)) => s.clone(),
1342        _ => return req,
1343    };
1344    let mut pieces = Vec::new();
1345    for (k, v) in params {
1346        let kk = match k { MapKey::Str(s) => s.clone(), _ => continue };
1347        let vv = match v { Value::Str(s) => s.clone(), _ => continue };
1348        pieces.push(format!("{}={}", url_encode(&kk), url_encode(&vv)));
1349    }
1350    if pieces.is_empty() { return req; }
1351    let sep = if url.contains('?') { '&' } else { '?' };
1352    let new_url = format!("{url}{sep}{}", pieces.join("&"));
1353    req.insert("url".into(), Value::Str(new_url));
1354    req
1355}
1356
1357/// Minimal RFC-3986 percent-encode for `application/x-www-form-
1358/// urlencoded` query values. Pulling in `urlencoding` for one
1359/// callsite would drag a dep into the runtime; the inline version is
1360/// short and easy to audit.
1361fn url_encode(s: &str) -> String {
1362    let mut out = String::with_capacity(s.len());
1363    for b in s.bytes() {
1364        match b {
1365            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1366                out.push(b as char);
1367            }
1368            _ => out.push_str(&format!("%{:02X}", b)),
1369        }
1370    }
1371    out
1372}
1373
1374fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
1375
1376/// The `toml` crate's serde adapter wraps datetimes in a sentinel
1377/// object `{"$__toml_private_datetime": "<rfc3339>"}` so that the
1378/// `Datetime` type round-trips through `serde::Value`. For Lex's
1379/// purposes a plain RFC-3339 string is what we want — callers can
1380/// then pipe through `datetime.parse_iso` if they need an
1381/// `Instant`. Walk the tree and replace each wrapper with its
1382/// inner string, in-place.
1383fn unwrap_toml_datetime_markers(v: &mut serde_json::Value) {
1384    use serde_json::Value as J;
1385    match v {
1386        J::Object(map) => {
1387            // Detect single-key marker objects and replace them
1388            // with their inner string. We have to take care to
1389            // avoid borrow conflicts.
1390            if map.len() == 1 {
1391                if let Some(J::String(s)) = map.get("$__toml_private_datetime") {
1392                    let s = s.clone();
1393                    *v = J::String(s);
1394                    return;
1395                }
1396            }
1397            for (_, child) in map.iter_mut() {
1398                unwrap_toml_datetime_markers(child);
1399            }
1400        }
1401        J::Array(items) => {
1402            for item in items.iter_mut() {
1403                unwrap_toml_datetime_markers(item);
1404            }
1405        }
1406        _ => {}
1407    }
1408}
1409
1410fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
1411
1412/// Extract the `List[Str]` of required field names from the second
1413/// argument of `*.parse_strict`. The list is allowed to be empty
1414/// (the parse degenerates to plain `parse`); other shapes are a
1415/// caller bug rather than a parse error.
1416fn required_field_names(arg: Option<&Value>) -> Result<Vec<String>, String> {
1417    let list = expect_list(arg)?;
1418    let mut out = Vec::with_capacity(list.len());
1419    for v in list {
1420        match v {
1421            Value::Str(s) => out.push(s.clone()),
1422            other => return Err(format!(
1423                "parse_strict: required-fields list must contain Str, got {other:?}"
1424            )),
1425        }
1426    }
1427    Ok(out)
1428}
1429
1430/// Verify that `value`'s top level is an object containing every
1431/// name in `required`. Returns a stable, human-readable error
1432/// listing the missing field(s) so the agent's verifier can
1433/// surface it directly.
1434///
1435/// Tactical fix for #168 — gives users a way to make
1436/// `parse[T]` errors propagate as `Result::Err` instead of as
1437/// runtime `GetField` errors at access time. The full
1438/// type-driven fix (deriving `required` from `T` at type-check
1439/// time so plain `parse[T]` works) tracked in #168.
1440fn check_required_fields(
1441    value: &serde_json::Value,
1442    required: &[String],
1443) -> Result<(), String> {
1444    if required.is_empty() {
1445        return Ok(());
1446    }
1447    let obj = match value {
1448        serde_json::Value::Object(o) => o,
1449        _ => return Err(format!(
1450            "parse_strict: expected top-level object with fields {:?}, got {value}",
1451            required
1452        )),
1453    };
1454    let missing: Vec<&str> = required.iter()
1455        .filter(|n| !obj.contains_key(n.as_str()))
1456        .map(String::as_str)
1457        .collect();
1458    if missing.is_empty() {
1459        Ok(())
1460    } else {
1461        Err(format!("missing required field(s): {}", missing.join(", ")))
1462    }
1463}
1464
1465/// Parse a `.env`-style file into key→value pairs. Accepts:
1466///
1467/// * Blank lines and `# comment` lines (ignored).
1468/// * `KEY=VALUE` with no spaces around `=`. Optional surrounding
1469///   `"..."` or `'...'` quotes on the value. No escape sequences,
1470///   no shell expansion — by design; we want this to be a *data*
1471///   parser, not a shell snippet evaluator.
1472///
1473/// Errors carry the offending line number (1-indexed) so the
1474/// agent's verifier can point a human at the right place.
1475fn parse_dotenv(src: &str) -> Result<indexmap::IndexMap<String, String>, String> {
1476    let mut out = indexmap::IndexMap::new();
1477    for (idx, raw) in src.lines().enumerate() {
1478        let line = raw.trim();
1479        if line.is_empty() || line.starts_with('#') {
1480            continue;
1481        }
1482        // Optional `export KEY=VALUE` shell form — accepted for
1483        // compat with files that grew out of `set -a` workflows.
1484        let after_export = line.strip_prefix("export ").unwrap_or(line);
1485        let (k, v) = match after_export.split_once('=') {
1486            Some(kv) => kv,
1487            None => return Err(format!("dotenv.parse line {}: missing `=`", idx + 1)),
1488        };
1489        let key = k.trim();
1490        if key.is_empty() {
1491            return Err(format!("dotenv.parse line {}: empty key", idx + 1));
1492        }
1493        let v_trim = v.trim();
1494        let value = if let Some(q) = v_trim.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
1495            q.to_string()
1496        } else if let Some(q) = v_trim.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
1497            q.to_string()
1498        } else {
1499            v_trim.to_string()
1500        };
1501        out.insert(key.to_string(), value);
1502    }
1503    Ok(out)
1504}
1505
1506// -- datetime helpers (Instant ↔ chrono::DateTime<Utc>) --
1507
1508/// Convert a `chrono::DateTime` (any `TimeZone`) into a Lex `Instant`,
1509/// represented as nanoseconds since the UTC unix epoch. Saturates on
1510/// out-of-range timestamps so the runtime never panics.
1511fn instant_from_chrono<Tz: chrono::TimeZone>(dt: chrono::DateTime<Tz>) -> i64 {
1512    dt.timestamp_nanos_opt().unwrap_or(i64::MAX)
1513}
1514
1515fn chrono_from_instant(n: i64) -> chrono::DateTime<chrono::Utc> {
1516    let secs = n.div_euclid(1_000_000_000);
1517    let nanos = n.rem_euclid(1_000_000_000) as u32;
1518    use chrono::TimeZone;
1519    chrono::Utc
1520        .timestamp_opt(secs, nanos)
1521        .single()
1522        .unwrap_or_else(chrono::Utc::now)
1523}
1524
1525fn format_iso(n: i64) -> String {
1526    chrono_from_instant(n).to_rfc3339()
1527}
1528
1529/// Parsed form of the user-side `Tz` variant. Mirrors the type
1530/// registered in `TypeEnv::new_with_builtins`.
1531enum TzArg {
1532    Utc,
1533    Local,
1534    /// Fixed offset in minutes east of UTC.
1535    Offset(i32),
1536    /// IANA name like `"America/New_York"`.
1537    Iana(String),
1538}
1539
1540fn parse_tz_arg(v: Option<&Value>) -> Result<TzArg, String> {
1541    match v {
1542        Some(Value::Variant { name, args }) => match (name.as_str(), args.as_slice()) {
1543            ("Utc", []) => Ok(TzArg::Utc),
1544            ("Local", []) => Ok(TzArg::Local),
1545            ("Offset", [Value::Int(m)]) => {
1546                let m = i32::try_from(*m).map_err(|_| {
1547                    format!("Tz::Offset: minutes out of range: {m}")
1548                })?;
1549                Ok(TzArg::Offset(m))
1550            }
1551            ("Iana", [Value::Str(s)]) => Ok(TzArg::Iana(s.clone())),
1552            (other, _) => Err(format!(
1553                "expected Tz variant (Utc | Local | Offset(Int) | Iana(Str)), got `{other}` with {} arg(s)",
1554                args.len()
1555            )),
1556        },
1557        Some(other) => Err(format!("expected Tz variant, got {other:?}")),
1558        None => Err("missing Tz argument".into()),
1559    }
1560}
1561
1562fn resolve_tz_to_components(n: i64, tz: &TzArg) -> Result<Value, String> {
1563    use chrono::{TimeZone, Datelike, Timelike, Offset};
1564    let utc_dt = chrono_from_instant(n);
1565    let (y, m, d, hh, mm, ss, ns, off_min) = match tz {
1566        TzArg::Utc => {
1567            let d = utc_dt;
1568            (d.year(), d.month() as i32, d.day() as i32,
1569             d.hour() as i32, d.minute() as i32, d.second() as i32,
1570             d.nanosecond() as i32, 0)
1571        }
1572        TzArg::Local => {
1573            let d = utc_dt.with_timezone(&chrono::Local);
1574            let off = d.offset().fix().local_minus_utc() / 60;
1575            (d.year(), d.month() as i32, d.day() as i32,
1576             d.hour() as i32, d.minute() as i32, d.second() as i32,
1577             d.nanosecond() as i32, off)
1578        }
1579        TzArg::Offset(off_min) => {
1580            let off_secs = off_min.saturating_mul(60);
1581            let fixed = chrono::FixedOffset::east_opt(off_secs)
1582                .ok_or("to_components: offset out of range")?;
1583            let d = utc_dt.with_timezone(&fixed);
1584            (d.year(), d.month() as i32, d.day() as i32,
1585             d.hour() as i32, d.minute() as i32, d.second() as i32,
1586             d.nanosecond() as i32, *off_min)
1587        }
1588        TzArg::Iana(name) => {
1589            let tz: chrono_tz::Tz = name.parse()
1590                .map_err(|e| format!("to_components: unknown timezone `{name}`: {e}"))?;
1591            let d = utc_dt.with_timezone(&tz);
1592            let off = d.offset().fix().local_minus_utc() / 60;
1593            (d.year(), d.month() as i32, d.day() as i32,
1594             d.hour() as i32, d.minute() as i32, d.second() as i32,
1595             d.nanosecond() as i32, off)
1596        }
1597    };
1598    let mut rec = indexmap::IndexMap::new();
1599    rec.insert("year".into(),    Value::Int(y as i64));
1600    rec.insert("month".into(),   Value::Int(m as i64));
1601    rec.insert("day".into(),     Value::Int(d as i64));
1602    rec.insert("hour".into(),    Value::Int(hh as i64));
1603    rec.insert("minute".into(),  Value::Int(mm as i64));
1604    rec.insert("second".into(),  Value::Int(ss as i64));
1605    rec.insert("nano".into(),    Value::Int(ns as i64));
1606    rec.insert("tz_offset_minutes".into(), Value::Int(off_min as i64));
1607    let _ = chrono::Utc.timestamp_opt(0, 0); // touch TimeZone to suppress unused-import lint paths
1608    Ok(Value::Record(rec))
1609}
1610
1611
1612fn instant_from_components(rec: &indexmap::IndexMap<String, Value>) -> Result<i64, String> {
1613    use chrono::TimeZone;
1614    fn get_int(rec: &indexmap::IndexMap<String, Value>, k: &str) -> Result<i64, String> {
1615        match rec.get(k) {
1616            Some(Value::Int(n)) => Ok(*n),
1617            other => Err(format!("from_components: missing or non-int field `{k}`: {other:?}")),
1618        }
1619    }
1620    let y = get_int(rec, "year")? as i32;
1621    let m = get_int(rec, "month")? as u32;
1622    let d = get_int(rec, "day")? as u32;
1623    let hh = get_int(rec, "hour")? as u32;
1624    let mm = get_int(rec, "minute")? as u32;
1625    let ss = get_int(rec, "second")? as u32;
1626    let ns = get_int(rec, "nano")? as u32;
1627    let off_min = get_int(rec, "tz_offset_minutes")? as i32;
1628    let off = chrono::FixedOffset::east_opt(off_min * 60)
1629        .ok_or("from_components: offset out of range")?;
1630    let dt = off
1631        .with_ymd_and_hms(y, m, d, hh, mm, ss)
1632        .single()
1633        .ok_or("from_components: invalid or ambiguous date/time")?;
1634    let dt = dt + chrono::Duration::nanoseconds(ns as i64);
1635    Ok(instant_from_chrono(dt))
1636}