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 `true` if `(kind, op)` will be handled by the pure-builtin
10/// path (no side effects, no policy gate needed). Used by the effect
11/// handler to decide whether to consume `args` by value.
12pub fn is_pure_call(kind: &str, op: &str) -> bool {
13    if !is_pure_module(kind) { return false; }
14    !matches!(
15        (kind, op),
16        ("crypto", "random")
17        | ("crypto", "random_str_hex")
18        // p256_generate mints key material from the OS RNG → [random]
19        // effect, handled on the effect path (#651).
20        | ("crypto", "p256_generate")
21        // secp256k1_generate likewise mints from the OS RNG → [random] (#655).
22        | ("crypto", "secp256k1_generate")
23        | ("datetime", "now")
24        | ("http", "send")
25        | ("http", "get")
26        | ("http", "post")
27        | ("http", "stream_lines")
28        // arrow.read_csv reads from disk → effect-handler path (#426 I/O slice).
29        | ("arrow", "read_csv")
30        // arrow.{read,write}_parquet + arrow.write_csv — effect-gated I/O (#432).
31        | ("arrow", "read_parquet")
32        | ("arrow", "read_parquet_cols")
33        | ("arrow", "write_parquet")
34        | ("arrow", "write_csv")
35    )
36}
37
38/// Dispatch a pure-builtin call with owned args (no clone of arg values).
39/// Callers must first verify `is_pure_call(kind, op)` to ensure args
40/// ownership is only transferred for known-pure ops.
41///
42/// `list.cons` is handled here with move semantics so the tail `Vec<Value>`
43/// is extended without cloning each element (#405).
44pub fn call_pure_builtin(kind: &str, op: &str, args: Vec<Value>) -> Result<Value, String> {
45    if (kind, op) == ("list", "cons") {
46        let mut it = args.into_iter();
47        let head = it.next().unwrap_or(Value::Unit);
48        let mut tail = match it.next() {
49            Some(Value::List(v)) => v,
50            Some(other) => return Err(format!("list.cons: expected List, got {other:?}")),
51            None => std::collections::VecDeque::new(),
52        };
53        tail.push_front(head);
54        return Ok(Value::List(tail));
55    }
56    dispatch(kind, op, &args)
57}
58
59/// Returns Some(...) if `(kind, op)` names a known pure builtin.
60/// `None` means "not handled here; fall through to effect dispatch".
61///
62/// Prefer `is_pure_call` + `call_pure_builtin` in hot paths — this
63/// variant takes `&[Value]` and must clone args for operations like
64/// `list.cons`; kept for external callers that already hold a slice.
65pub fn try_pure_builtin(kind: &str, op: &str, args: &[Value]) -> Option<Result<Value, String>> {
66    if !is_pure_call(kind, op) { return None; }
67    Some(dispatch(kind, op, args))
68}
69
70/// `kind` is one of the known pure module aliases — used by the policy
71/// walk to skip pure builtins that programs reference via imports.
72pub fn is_pure_module(kind: &str) -> bool {
73    matches!(kind, "str" | "int" | "float" | "bool" | "list" | "iter"
74        | "option" | "result" | "tuple" | "json" | "bytes" | "flow" | "math"
75        | "map" | "set" | "crypto" | "regex" | "deque" | "datetime" | "duration" | "http"
76        | "toml" | "yaml" | "dotenv" | "csv" | "test" | "random" | "parser"
77        | "cli" | "arrow" | "df" | "decimal")
78}
79
80fn dispatch(kind: &str, op: &str, args: &[Value]) -> Result<Value, String> {
81    match (kind, op) {
82        // -- str --
83        ("str", "is_empty") => Ok(Value::Bool(expect_str(args.first())?.is_empty())),
84        ("str", "len") => Ok(Value::Int(expect_str(args.first())?.len() as i64)),
85        // O(1) single-char access. `str.slice(s, i, i+1)` resolves a codepoint
86        // index via `char_indices().nth(i)` — O(i) — so scanning a string
87        // char-by-char is O(n²). `char_at` indexes the UTF-8 bytes directly and
88        // returns the byte as a 1-char Str, letting ASCII-oriented scanners
89        // (e.g. the JSON parser, whose input is pre-sanitised to single bytes)
90        // run in O(n). Returns the char for ASCII bytes (< 128); out-of-range or
91        // a non-ASCII byte yields "" — total, never panics.
92        ("str", "char_at") => {
93            let s = expect_str(args.first())?;
94            let i = expect_int(args.get(1))?;
95            if i < 0 {
96                Ok(Value::Str("".into()))
97            } else {
98                match s.as_bytes().get(i as usize) {
99                    Some(&b) if b < 128 => {
100                        Ok(Value::Str((b as char).to_string().into()))
101                    }
102                    _ => Ok(Value::Str("".into())),
103                }
104            }
105        }
106        ("str", "concat") => {
107            let a = expect_str(args.first())?;
108            let b = expect_str(args.get(1))?;
109            Ok(Value::Str(format!("{a}{b}").into()))
110        }
111        ("str", "to_int") => {
112            let s = expect_str(args.first())?;
113            match s.parse::<i64>() {
114                Ok(n) => Ok(some(Value::Int(n))),
115                Err(_) => Ok(none()),
116            }
117        }
118        ("str", "split") => {
119            let s = expect_str(args.first())?;
120            let sep = expect_str(args.get(1))?;
121            let items: std::collections::VecDeque<Value> = if sep.is_empty() {
122                s.chars().map(|c| Value::Str(c.to_string().into())).collect()
123            } else {
124                s.split(sep.as_str()).map(|p| Value::Str(p.into())).collect()
125            };
126            Ok(Value::List(items))
127        }
128        ("str", "join") => {
129            let parts = expect_list(args.first())?;
130            let sep = expect_str(args.get(1))?;
131            let mut out = String::new();
132            for (i, p) in parts.iter().enumerate() {
133                if i > 0 { out.push_str(&sep); }
134                match p {
135                    Value::Str(s) => out.push_str(s),
136                    other => return Err(format!("str.join element must be Str, got {other:?}")),
137                }
138            }
139            Ok(Value::Str(out.into()))
140        }
141        ("str", "starts_with") => {
142            let s = expect_str(args.first())?;
143            let prefix = expect_str(args.get(1))?;
144            Ok(Value::Bool(s.starts_with(prefix.as_str())))
145        }
146        ("str", "ends_with") => {
147            let s = expect_str(args.first())?;
148            let suffix = expect_str(args.get(1))?;
149            Ok(Value::Bool(s.ends_with(suffix.as_str())))
150        }
151        ("str", "contains") => {
152            let s = expect_str(args.first())?;
153            let needle = expect_str(args.get(1))?;
154            Ok(Value::Bool(s.contains(needle.as_str())))
155        }
156        ("str", "cmp") => {
157            let a = expect_str(args.first())?;
158            let b = expect_str(args.get(1))?;
159            Ok(Value::Int(match a.as_str().cmp(b.as_str()) {
160                std::cmp::Ordering::Less => -1,
161                std::cmp::Ordering::Equal => 0,
162                std::cmp::Ordering::Greater => 1,
163            }))
164        }
165        ("str", "replace") => {
166            let s = expect_str(args.first())?;
167            let from = expect_str(args.get(1))?;
168            let to = expect_str(args.get(2))?;
169            Ok(Value::Str(s.replace(from.as_str(), to.as_str()).into()))
170        }
171        ("str", "trim") => Ok(Value::Str(expect_str(args.first())?.trim().into())),
172        ("str", "to_upper") => Ok(Value::Str(expect_str(args.first())?.to_uppercase().into())),
173        ("str", "to_lower") => Ok(Value::Str(expect_str(args.first())?.to_lowercase().into())),
174        ("str", "strip_prefix") => {
175            let s = expect_str(args.first())?;
176            let prefix = expect_str(args.get(1))?;
177            Ok(match s.strip_prefix(prefix.as_str()) {
178                Some(rest) => some(Value::Str(rest.into())),
179                None => none(),
180            })
181        }
182        ("str", "strip_suffix") => {
183            let s = expect_str(args.first())?;
184            let suffix = expect_str(args.get(1))?;
185            Ok(match s.strip_suffix(suffix.as_str()) {
186                Some(rest) => some(Value::Str(rest.into())),
187                None => none(),
188            })
189        }
190        ("str", "slice") => {
191            // Half-open codepoint-index slice. `lo` and `hi` are Unicode
192            // scalar value (codepoint) indices, not byte offsets. Out-of-range
193            // indices clamp to the codepoint count, mirroring Python's `s[lo:hi]`
194            // semantics. Reversed ranges error as a caller logic bug. (#620)
195            let s = expect_str(args.first())?;
196            let lo_i = expect_int(args.get(1))?;
197            let hi_i = expect_int(args.get(2))?;
198            let lo_cp = lo_i.max(0) as usize;
199            let hi_cp = hi_i.max(0) as usize;
200            if lo_cp > hi_cp {
201                return Err(format!(
202                    "str.slice: reversed range [{lo_cp}..{hi_cp}]"));
203            }
204            // Resolve codepoint indices to byte offsets in a single pass.
205            // Indices past the end clamp to s.len(), yielding an empty slice.
206            let lo_byte = s.char_indices().nth(lo_cp).map(|(b, _)| b).unwrap_or(s.len());
207            let hi_byte = s.char_indices().nth(hi_cp).map(|(b, _)| b).unwrap_or(s.len());
208            Ok(Value::Str(s[lo_byte..hi_byte].into()))
209        }
210
211        // -- int / float --
212        ("int", "to_str") => Ok(Value::Str(expect_int(args.first())?.to_string().into())),
213        ("int", "to_float") => Ok(Value::Float(expect_int(args.first())? as f64)),
214        // Int scalar helpers (#681) — integer counterparts to math.{abs,min,max}.
215        ("int", "abs") => Ok(Value::Int(expect_int(args.first())?.abs())),
216        ("int", "min") => Ok(Value::Int(expect_int(args.first())?.min(expect_int(args.get(1))?))),
217        ("int", "max") => Ok(Value::Int(expect_int(args.first())?.max(expect_int(args.get(1))?))),
218        ("float", "to_int") => Ok(Value::Int(expect_float(args.first())? as i64)),
219        ("float", "to_str") => Ok(Value::Str(expect_float(args.first())?.to_string().into())),
220        ("str", "to_float") => {
221            let s = expect_str(args.first())?;
222            match s.parse::<f64>() {
223                Ok(f) => Ok(some(Value::Float(f))),
224                Err(_) => Ok(none()),
225            }
226        }
227
228        // -- list --
229        ("list", "len") => Ok(Value::Int(expect_list(args.first())?.len() as i64)),
230        ("list", "is_empty") => Ok(Value::Bool(expect_list(args.first())?.is_empty())),
231        ("list", "head") => {
232            let xs = expect_list(args.first())?;
233            match xs.front() {
234                Some(v) => Ok(some(v.clone())),
235                None => Ok(none()),
236            }
237        }
238        ("list", "tail") => {
239            let xs = expect_list(args.first())?;
240            if xs.is_empty() { Ok(Value::List(std::collections::VecDeque::new())) }
241            else { Ok(Value::List(xs.iter().skip(1).cloned().collect::<std::collections::VecDeque<_>>())) }
242        }
243        ("list", "range") => {
244            let lo = expect_int(args.first())?;
245            let hi = expect_int(args.get(1))?;
246            Ok(Value::List((lo..hi).map(Value::Int).collect::<std::collections::VecDeque<_>>()))
247        }
248        ("list", "concat") => {
249            let mut out = expect_list(args.first())?.clone();
250            out.extend(expect_list(args.get(1))?.iter().cloned());
251            Ok(Value::List(out))
252        }
253        ("list", "reverse") => {
254            let out = expect_list(args.first())?.clone();
255            let rev: std::collections::VecDeque<Value> = out.into_iter().rev().collect();
256            Ok(Value::List(rev))
257        }
258        // #334: cons — prepend a single element to a list.
259        // (fast path via call_pure_builtin; this branch handles the
260        // borrow-based dispatch path which must clone)
261        ("list", "cons") => {
262            let head = args.first().cloned().unwrap_or(Value::Unit);
263            let mut out: std::collections::VecDeque<Value> =
264                expect_list(args.get(1))?.iter().cloned().collect();
265            out.push_front(head);
266            Ok(Value::List(out))
267        }
268        ("list", "enumerate") => {
269            let xs = expect_list(args.first())?;
270            let pairs = xs.iter().cloned().enumerate()
271                .map(|(i, v)| Value::Tuple(vec![Value::Int(i as i64), v]))
272                .collect::<std::collections::VecDeque<_>>();
273            Ok(Value::List(pairs))
274        }
275
276        // -- tuple --
277        // Per §11.1: fst, snd, third for 2- and 3-tuples. Index out of
278        // range is an error rather than a panic so calling `tuple.third`
279        // on a 2-tuple is a clean failure instead of a host crash.
280        ("tuple", "fst")   => tuple_index(first_arg(args)?, 0),
281        ("tuple", "snd")   => tuple_index(first_arg(args)?, 1),
282        ("tuple", "third") => tuple_index(first_arg(args)?, 2),
283        ("tuple", "len") => match first_arg(args)? {
284            Value::Tuple(items) => Ok(Value::Int(items.len() as i64)),
285            other => Err(format!("tuple.len: expected Tuple, got {other:?}")),
286        },
287
288        // -- option --
289        ("option", "unwrap_or") => {
290            let opt = first_arg(args)?;
291            let default = args.get(1).cloned().unwrap_or(Value::Unit);
292            match opt {
293                Value::Variant { name, args } if name == "Some" && !args.is_empty() => Ok(args[0].clone()),
294                Value::Variant { name, .. } if name == "None" => Ok(default),
295                other => Err(format!("option.unwrap_or expected Option, got {other:?}")),
296            }
297        }
298        // option.unwrap_or_else: lazy default via thunk — only called when None.
299        // Handled inline by the bytecode compiler; this arm is the interpreter
300        // fallback path (thunk is pre-applied as a Value::Unit default since the
301        // runtime cannot call closures itself — the compiler path is canonical).
302        ("option", "unwrap_or_else") => {
303            let opt = first_arg(args)?;
304            match opt {
305                Value::Variant { name, args } if name == "Some" && !args.is_empty() => Ok(args[0].clone()),
306                Value::Variant { name, .. } if name == "None" => {
307                    // The closure argument cannot be invoked from pure-builtin
308                    // context; callers that reach this path have already
309                    // evaluated the thunk and passed its result as args[1].
310                    Ok(args.get(1).cloned().unwrap_or(Value::Unit))
311                }
312                other => Err(format!("option.unwrap_or_else expected Option, got {other:?}")),
313            }
314        }
315        ("option", "is_some") => match first_arg(args)? {
316            Value::Variant { name, .. } => Ok(Value::Bool(name == "Some")),
317            other => Err(format!("option.is_some expected Option, got {other:?}")),
318        },
319        ("option", "is_none") => match first_arg(args)? {
320            Value::Variant { name, .. } => Ok(Value::Bool(name == "None")),
321            other => Err(format!("option.is_none expected Option, got {other:?}")),
322        },
323        // option.ok_or(opt, err): Some(x) -> Ok(x); None -> Err(err). (#679)
324        ("option", "ok_or") => {
325            let opt = first_arg(args)?;
326            let err_val = args.get(1).cloned().unwrap_or(Value::Unit);
327            match opt {
328                Value::Variant { name, args } if name == "Some" && !args.is_empty() =>
329                    Ok(ok_v(args[0].clone())),
330                Value::Variant { name, .. } if name == "None" => Ok(err_v(err_val)),
331                other => Err(format!("option.ok_or expected Option, got {other:?}")),
332            }
333        }
334
335        // -- result --
336        ("result", "is_ok") => match first_arg(args)? {
337            Value::Variant { name, .. } => Ok(Value::Bool(name == "Ok")),
338            other => Err(format!("result.is_ok expected Result, got {other:?}")),
339        },
340        ("result", "is_err") => match first_arg(args)? {
341            Value::Variant { name, .. } => Ok(Value::Bool(name == "Err")),
342            other => Err(format!("result.is_err expected Result, got {other:?}")),
343        },
344        ("result", "unwrap_or") => {
345            let res = first_arg(args)?;
346            let default = args.get(1).cloned().unwrap_or(Value::Unit);
347            match res {
348                Value::Variant { name, args } if name == "Ok" && !args.is_empty() => Ok(args[0].clone()),
349                Value::Variant { name, .. } if name == "Err" => Ok(default),
350                other => Err(format!("result.unwrap_or expected Result, got {other:?}")),
351            }
352        }
353        // result.unwrap_or_else: lazy fallback over the Err payload. The
354        // closure call is emitted inline by the bytecode compiler
355        // (`emit_result_unwrap_or_else`); this arm is the interpreter
356        // fallback, with the thunk result pre-evaluated into args[1]. (#679)
357        ("result", "unwrap_or_else") => {
358            let res = first_arg(args)?;
359            match res {
360                Value::Variant { name, args } if name == "Ok" && !args.is_empty() => Ok(args[0].clone()),
361                Value::Variant { name, .. } if name == "Err" =>
362                    Ok(args.get(1).cloned().unwrap_or(Value::Unit)),
363                other => Err(format!("result.unwrap_or_else expected Result, got {other:?}")),
364            }
365        }
366
367        // -- json --
368        ("json", "stringify") => {
369            let v = first_arg(args)?;
370            Ok(Value::Str(serde_json::to_string(&value_to_json(v)).unwrap_or_default().into()))
371        }
372        ("json", "parse") => {
373            let s = expect_str(args.first())?;
374            match serde_json::from_str::<serde_json::Value>(&s) {
375                Ok(v) => Ok(ok_v(json_to_value(&v))),
376                Err(e) => Ok(err_v(Value::Str(format!("{e}").into()))),
377            }
378        }
379        // Tactical fix for #168: validate required fields before
380        // returning Ok. #322: also validate field types via schema.
381        ("json", "parse_strict") => {
382            let s = expect_str(args.first())?;
383            let required = required_field_names(args.get(1))?;
384            let schema = extract_type_schema(args.get(2));
385            match serde_json::from_str::<serde_json::Value>(&s) {
386                Ok(v) => {
387                    if let Err(e) = check_required_fields(&v, &required) {
388                        return Ok(err_v(Value::Str(e.into())));
389                    }
390                    if let Err(e) = validate_field_types(&v, &schema) {
391                        return Ok(err_v(Value::Str(e.into())));
392                    }
393                    Ok(ok_v(apply_option_wrapping(json_to_value(&v), &v, &schema)))
394                }
395                Err(e) => Ok(err_v(Value::Str(format!("{e}").into()))),
396            }
397        }
398
399        // -- toml (config parser; routes through serde_json::Value
400        // so the parsed shape composes with the existing json
401        // tooling. Datetimes become RFC 3339 strings — the only
402        // info-losing step) --
403        ("toml", "parse") => {
404            let s = expect_str(args.first())?;
405            match toml::from_str::<serde_json::Value>(&s) {
406                Ok(mut v) => {
407                    unwrap_toml_datetime_markers(&mut v);
408                    Ok(ok_v(json_to_value(&v)))
409                }
410                Err(e) => Ok(err_v(Value::Str(format!("{e}").into()))),
411            }
412        }
413        // Compiler-emitted variant of parse_strict that carries the type
414        // schema injected by the type-checker rewrite pass (#322).
415        // Identical to parse_strict but the 3rd arg (schema) is always present.
416        ("json", "parse_strict_typed") => {
417            let s = expect_str(args.first())?;
418            let required = required_field_names(args.get(1))?;
419            let schema = extract_type_schema(args.get(2));
420            match serde_json::from_str::<serde_json::Value>(&s) {
421                Ok(v) => {
422                    if let Err(e) = check_required_fields(&v, &required) {
423                        return Ok(err_v(Value::Str(e.into())));
424                    }
425                    if let Err(e) = validate_field_types(&v, &schema) {
426                        return Ok(err_v(Value::Str(e.into())));
427                    }
428                    Ok(ok_v(apply_option_wrapping(json_to_value(&v), &v, &schema)))
429                }
430                Err(e) => Ok(err_v(Value::Str(format!("{e}").into()))),
431            }
432        }
433
434        // Tactical fix for #168: validate required fields before
435        // returning Ok. #322: also validate field types via schema.
436        ("toml", "parse_strict") => {
437            let s = expect_str(args.first())?;
438            let required = required_field_names(args.get(1))?;
439            let schema = extract_type_schema(args.get(2));
440            match toml::from_str::<serde_json::Value>(&s) {
441                Ok(mut v) => {
442                    unwrap_toml_datetime_markers(&mut v);
443                    if let Err(e) = check_required_fields(&v, &required) {
444                        return Ok(err_v(Value::Str(e.into())));
445                    }
446                    if let Err(e) = validate_field_types(&v, &schema) {
447                        return Ok(err_v(Value::Str(e.into())));
448                    }
449                    Ok(ok_v(apply_option_wrapping(json_to_value(&v), &v, &schema)))
450                }
451                Err(e) => Ok(err_v(Value::Str(format!("{e}").into()))),
452            }
453        }
454        ("toml", "parse_strict_typed") => {
455            let s = expect_str(args.first())?;
456            let required = required_field_names(args.get(1))?;
457            let schema = extract_type_schema(args.get(2));
458            match toml::from_str::<serde_json::Value>(&s) {
459                Ok(mut v) => {
460                    unwrap_toml_datetime_markers(&mut v);
461                    if let Err(e) = check_required_fields(&v, &required) {
462                        return Ok(err_v(Value::Str(e.into())));
463                    }
464                    if let Err(e) = validate_field_types(&v, &schema) {
465                        return Ok(err_v(Value::Str(e.into())));
466                    }
467                    Ok(ok_v(apply_option_wrapping(json_to_value(&v), &v, &schema)))
468                }
469                Err(e) => Ok(err_v(Value::Str(format!("{e}").into()))),
470            }
471        }
472        ("toml", "stringify") => {
473            let v = first_arg(args)?;
474            // serde_json::Value → toml::Value via its serde impls.
475            // TOML's grammar is stricter than JSON's (top-level
476            // must be a table; no `null`; no mixed-type arrays),
477            // so the conversion can fail — surface as Result::Err
478            // rather than panic.
479            let json = value_to_json(v);
480            match toml::to_string(&json) {
481                Ok(s)  => Ok(ok_v(Value::Str(s.into()))),
482                Err(e) => Ok(err_v(Value::Str(format!("toml.stringify: {e}").into()))),
483            }
484        }
485
486        // -- yaml -- mirrors std.toml. Wraps serde_yaml so values
487        // map to the same Lex shape as JSON. YAML's Tag/Anchor
488        // features are folded out by serde_yaml's deserialize-to-
489        // Value path; non-representable shapes (e.g. non-string
490        // map keys when stringifying) surface as Result::Err.
491        ("yaml", "parse") => {
492            let s = expect_str(args.first())?;
493            match serde_yaml::from_str::<serde_json::Value>(&s) {
494                Ok(v)  => Ok(ok_v(json_to_value(&v))),
495                Err(e) => Ok(err_v(Value::Str(format!("{e}").into()))),
496            }
497        }
498        // Tactical fix for #168 — same shape as toml.parse_strict.
499        // #322: also validate field types via schema.
500        ("yaml", "parse_strict") => {
501            let s = expect_str(args.first())?;
502            let required = required_field_names(args.get(1))?;
503            let schema = extract_type_schema(args.get(2));
504            match serde_yaml::from_str::<serde_json::Value>(&s) {
505                Ok(v) => {
506                    if let Err(e) = check_required_fields(&v, &required) {
507                        return Ok(err_v(Value::Str(e.into())));
508                    }
509                    if let Err(e) = validate_field_types(&v, &schema) {
510                        return Ok(err_v(Value::Str(e.into())));
511                    }
512                    Ok(ok_v(apply_option_wrapping(json_to_value(&v), &v, &schema)))
513                }
514                Err(e) => Ok(err_v(Value::Str(format!("{e}").into()))),
515            }
516        }
517        ("yaml", "parse_strict_typed") => {
518            let s = expect_str(args.first())?;
519            let required = required_field_names(args.get(1))?;
520            let schema = extract_type_schema(args.get(2));
521            match serde_yaml::from_str::<serde_json::Value>(&s) {
522                Ok(v) => {
523                    if let Err(e) = check_required_fields(&v, &required) {
524                        return Ok(err_v(Value::Str(e.into())));
525                    }
526                    if let Err(e) = validate_field_types(&v, &schema) {
527                        return Ok(err_v(Value::Str(e.into())));
528                    }
529                    Ok(ok_v(apply_option_wrapping(json_to_value(&v), &v, &schema)))
530                }
531                Err(e) => Ok(err_v(Value::Str(format!("{e}").into()))),
532            }
533        }
534        ("yaml", "stringify") => {
535            let v = first_arg(args)?;
536            let json = value_to_json(v);
537            match serde_yaml::to_string(&json) {
538                Ok(s)  => Ok(ok_v(Value::Str(s.into()))),
539                Err(e) => Ok(err_v(Value::Str(format!("yaml.stringify: {e}").into()))),
540            }
541        }
542
543        // -- dotenv -- KEY=VALUE pair files. Hand-rolled parser
544        // because the dotenvy crate's API is geared at loading
545        // into the process env, not parsing-to-data. The grammar
546        // we accept: blank lines, `# comment` lines, and
547        // `KEY=VALUE` (optional surrounding `"..."` or `'...'`,
548        // unescaped). Simple but covers the .env files in the
549        // wild that aren't trying to be shell.
550        ("dotenv", "parse") => {
551            use std::collections::BTreeMap;
552            use lex_bytecode::MapKey;
553            let s = expect_str(args.first())?;
554            match parse_dotenv(&s) {
555                Ok(map) => {
556                    let mut bt: BTreeMap<MapKey, Value> = BTreeMap::new();
557                    for (k, v) in map {
558                        bt.insert(MapKey::Str(k), Value::Str(v.into()));
559                    }
560                    Ok(ok_v(Value::Map(bt)))
561                }
562                Err(e) => Ok(err_v(Value::Str(e.into()))),
563            }
564        }
565
566        // -- csv -- rows-as-lists; first row is whatever the file
567        // has. The caller decides whether row 0 is a header. We
568        // could ship a `parse_with_headers` later that returns a
569        // List[Map[Str, Str]]; v1 keeps the surface tight.
570        ("csv", "parse") => {
571            let s = expect_str(args.first())?;
572            let mut rdr = csv::ReaderBuilder::new()
573                .has_headers(false)
574                .flexible(true)
575                .from_reader(s.as_bytes());
576            let mut rows: std::collections::VecDeque<Value> = std::collections::VecDeque::new();
577            for r in rdr.records() {
578                match r {
579                    Ok(rec) => {
580                        let row: std::collections::VecDeque<Value> = rec.iter()
581                            .map(|f| Value::Str(f.into()))
582                            .collect();
583                        rows.push_back(Value::List(row));
584                    }
585                    Err(e) => return Ok(err_v(Value::Str(format!("csv.parse: {e}").into()))),
586                }
587            }
588            Ok(ok_v(Value::List(rows)))
589        }
590        ("csv", "stringify") => {
591            // List[List[Str]] → CSV string. Mixed-type rows are
592            // not allowed (CSV is text-only); non-Str cells get
593            // stringified via to_json since that's already the
594            // convention for `json.stringify` etc.
595            let v = first_arg(args)?;
596            let rows = match v {
597                Value::List(rs) => rs,
598                _ => return Ok(err_v(Value::Str("csv.stringify expects List[List[Str]]".into()))),
599            };
600            let mut out = Vec::new();
601            {
602                let mut wtr = csv::WriterBuilder::new()
603                    .has_headers(false)
604                    .from_writer(&mut out);
605                for row in rows {
606                    let cells = match row {
607                        Value::List(cs) => cs,
608                        _ => return Ok(err_v(Value::Str("csv.stringify row must be List[Str]".into()))),
609                    };
610                    let strs: Vec<String> = cells.iter().map(|c| match c {
611                        Value::Str(s) => s.to_string(),
612                        other => serde_json::to_string(&other.to_json())
613                            .unwrap_or_else(|_| String::new()),
614                    }).collect();
615                    if let Err(e) = wtr.write_record(&strs) {
616                        return Ok(err_v(Value::Str(format!("csv.stringify: {e}").into())));
617                    }
618                }
619                if let Err(e) = wtr.flush() {
620                    return Ok(err_v(Value::Str(format!("csv.stringify flush: {e}").into())));
621                }
622            }
623            match String::from_utf8(out) {
624                Ok(s) => Ok(ok_v(Value::Str(s.into()))),
625                Err(e) => Ok(err_v(Value::Str(format!("csv.stringify utf8: {e}").into()))),
626            }
627        }
628
629        // -- test -- tiny assertion library. Each helper is pure
630        // and returns `Result[Unit, Str]` so tests are themselves
631        // functions returning a Result. A suite is a List the user
632        // iterates with `list.fold`; no Rust-side Suite/Runner
633        // types in v1, so the whole thing is 4 builtins + a few
634        // Lex-source helpers callers can copy into their tests/.
635        ("test", "assert_eq") => {
636            let a = first_arg(args)?;
637            let b = args.get(1).ok_or("test.assert_eq: missing second arg")?;
638            if a == b {
639                Ok(ok_v(Value::Unit))
640            } else {
641                Ok(err_v(Value::Str(format!("assert_eq: lhs {} != rhs {}",
642                    value_to_json(a), value_to_json(b)).into())))
643            }
644        }
645        ("test", "assert_ne") => {
646            let a = first_arg(args)?;
647            let b = args.get(1).ok_or("test.assert_ne: missing second arg")?;
648            if a != b {
649                Ok(ok_v(Value::Unit))
650            } else {
651                Ok(err_v(Value::Str(format!("assert_ne: both sides are {}",
652                    value_to_json(a)).into())))
653            }
654        }
655        ("test", "assert_true") => {
656            match first_arg(args)? {
657                Value::Bool(true) => Ok(ok_v(Value::Unit)),
658                Value::Bool(false) => Ok(err_v(Value::Str("assert_true: was false".into()))),
659                other => Err(format!("test.assert_true expects Bool, got {other:?}")),
660            }
661        }
662        ("test", "assert_false") => {
663            match first_arg(args)? {
664                Value::Bool(false) => Ok(ok_v(Value::Unit)),
665                Value::Bool(true)  => Ok(err_v(Value::Str("assert_false: was true".into()))),
666                other => Err(format!("test.assert_false expects Bool, got {other:?}")),
667            }
668        }
669
670        // -- bytes --
671        ("bytes", "len") => {
672            let b = expect_bytes(args.first())?;
673            Ok(Value::Int(b.len() as i64))
674        }
675        ("bytes", "eq") => {
676            let a = expect_bytes(args.first())?;
677            let b = expect_bytes(args.get(1))?;
678            Ok(Value::Bool(a == b))
679        }
680        ("bytes", "from_str") => {
681            let s = expect_str(args.first())?;
682            Ok(Value::Bytes(s.into_bytes()))
683        }
684        ("bytes", "to_str") => {
685            let b = expect_bytes(args.first())?;
686            match String::from_utf8(b.to_vec()) {
687                Ok(s) => Ok(ok_v(Value::Str(s.into()))),
688                Err(e) => Ok(err_v(Value::Str(format!("{e}").into()))),
689            }
690        }
691        ("bytes", "slice") => {
692            let b = expect_bytes(args.first())?;
693            let lo = expect_int(args.get(1))? as usize;
694            let hi = expect_int(args.get(2))? as usize;
695            if lo > hi || hi > b.len() {
696                return Err(format!("bytes.slice: out of range [{lo}..{hi}] of {}", b.len()));
697            }
698            Ok(Value::Bytes(b[lo..hi].to_vec()))
699        }
700        ("bytes", "is_empty") => {
701            let b = expect_bytes(args.first())?;
702            Ok(Value::Bool(b.is_empty()))
703        }
704        ("bytes", "concat") => {
705            let a = expect_bytes(args.first())?;
706            let b = expect_bytes(args.get(1))?;
707            let mut out = a.clone();
708            out.extend_from_slice(b);
709            Ok(Value::Bytes(out))
710        }
711        ("bytes", "concat_all") => {
712            let items = match args.first() {
713                Some(Value::List(xs)) => xs,
714                other => return Err(format!("bytes.concat_all expects a List[Bytes], got {other:?}")),
715            };
716            let mut out = Vec::new();
717            for item in items {
718                match item {
719                    Value::Bytes(b) => out.extend_from_slice(b),
720                    other => return Err(format!("bytes.concat_all: list element is not Bytes, got {other:?}")),
721                }
722            }
723            Ok(Value::Bytes(out))
724        }
725        ("bytes", "u8") => {
726            let n = expect_int(args.first())?;
727            Ok(Value::Bytes(vec![n as u8]))
728        }
729        ("bytes", "u16_le") => {
730            let n = expect_int(args.first())?;
731            Ok(Value::Bytes((n as u16).to_le_bytes().to_vec()))
732        }
733        ("bytes", "u32_le") => {
734            let n = expect_int(args.first())?;
735            Ok(Value::Bytes((n as u32).to_le_bytes().to_vec()))
736        }
737        ("bytes", "u64_le") => {
738            let n = expect_int(args.first())?;
739            Ok(Value::Bytes((n as u64).to_le_bytes().to_vec()))
740        }
741        ("bytes", "u8_at") => {
742            let b = expect_bytes(args.first())?;
743            let off = expect_int(args.get(1))? as usize;
744            match b.get(off) {
745                Some(&byte) => Ok(ok_v(Value::Int(byte as i64))),
746                None => Ok(err_v(Value::Str(format!("bytes.u8_at: offset {off} out of range of {} bytes", b.len()).into()))),
747            }
748        }
749        ("bytes", "u16_le_at") => {
750            let b = expect_bytes(args.first())?;
751            let off = expect_int(args.get(1))? as usize;
752            match b.get(off..off + 2) {
753                Some(slice) => {
754                    let arr: [u8; 2] = slice.try_into().unwrap();
755                    Ok(ok_v(Value::Int(u16::from_le_bytes(arr) as i64)))
756                }
757                None => Ok(err_v(Value::Str(format!("bytes.u16_le_at: offset {off} out of range of {} bytes", b.len()).into()))),
758            }
759        }
760        ("bytes", "u32_le_at") => {
761            let b = expect_bytes(args.first())?;
762            let off = expect_int(args.get(1))? as usize;
763            match b.get(off..off + 4) {
764                Some(slice) => {
765                    let arr: [u8; 4] = slice.try_into().unwrap();
766                    Ok(ok_v(Value::Int(u32::from_le_bytes(arr) as i64)))
767                }
768                None => Ok(err_v(Value::Str(format!("bytes.u32_le_at: offset {off} out of range of {} bytes", b.len()).into()))),
769            }
770        }
771        ("bytes", "u64_le_at") => {
772            let b = expect_bytes(args.first())?;
773            let off = expect_int(args.get(1))? as usize;
774            match b.get(off..off + 8) {
775                Some(slice) => {
776                    let arr: [u8; 8] = slice.try_into().unwrap();
777                    Ok(ok_v(Value::Int(u64::from_le_bytes(arr) as i64)))
778                }
779                None => Ok(err_v(Value::Str(format!("bytes.u64_le_at: offset {off} out of range of {} bytes", b.len()).into()))),
780            }
781        }
782
783        // -- math --
784        // Matrices are stored as the F64Array fast-lane variant (a flat
785        // row-major Vec<f64> with shape). Lex code treats them as the
786        // type alias `Matrix = { rows :: Int, cols :: Int, data ::
787        // List[Float] }`; field access is unsupported, so all
788        // introspection happens through these helpers.
789        ("math", "exp")   => Ok(Value::Float(expect_float(args.first())?.exp())),
790        ("math", "log")   => Ok(Value::Float(expect_float(args.first())?.ln())),
791        ("math", "log2")  => Ok(Value::Float(expect_float(args.first())?.log2())),
792        ("math", "log10") => Ok(Value::Float(expect_float(args.first())?.log10())),
793        ("math", "sqrt")  => Ok(Value::Float(expect_float(args.first())?.sqrt())),
794        ("math", "abs")   => Ok(Value::Float(expect_float(args.first())?.abs())),
795        ("math", "sin")   => Ok(Value::Float(expect_float(args.first())?.sin())),
796        ("math", "cos")   => Ok(Value::Float(expect_float(args.first())?.cos())),
797        ("math", "tan")   => Ok(Value::Float(expect_float(args.first())?.tan())),
798        ("math", "asin")  => Ok(Value::Float(expect_float(args.first())?.asin())),
799        ("math", "acos")  => Ok(Value::Float(expect_float(args.first())?.acos())),
800        ("math", "atan")  => Ok(Value::Float(expect_float(args.first())?.atan())),
801        ("math", "floor") => Ok(Value::Float(expect_float(args.first())?.floor())),
802        ("math", "ceil")  => Ok(Value::Float(expect_float(args.first())?.ceil())),
803        ("math", "round") => Ok(Value::Float(expect_float(args.first())?.round())),
804        ("math", "trunc") => Ok(Value::Float(expect_float(args.first())?.trunc())),
805        ("math", "pow") => {
806            let a = expect_float(args.first())?;
807            let b = expect_float(args.get(1))?;
808            Ok(Value::Float(a.powf(b)))
809        }
810        ("math", "atan2") => {
811            let y = expect_float(args.first())?;
812            let x = expect_float(args.get(1))?;
813            Ok(Value::Float(y.atan2(x)))
814        }
815        ("math", "min") => {
816            let a = expect_float(args.first())?;
817            let b = expect_float(args.get(1))?;
818            Ok(Value::Float(a.min(b)))
819        }
820        ("math", "max") => {
821            let a = expect_float(args.first())?;
822            let b = expect_float(args.get(1))?;
823            Ok(Value::Float(a.max(b)))
824        }
825        ("math", "zeros") => {
826            let r = expect_int(args.first())?;
827            let c = expect_int(args.get(1))?;
828            if r < 0 || c < 0 {
829                return Err(format!("math.zeros: negative dim {r}x{c}"));
830            }
831            let r = r as usize; let c = c as usize;
832            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data: vec![0.0; r * c] })
833        }
834        ("math", "ones") => {
835            let r = expect_int(args.first())?;
836            let c = expect_int(args.get(1))?;
837            if r < 0 || c < 0 {
838                return Err(format!("math.ones: negative dim {r}x{c}"));
839            }
840            let r = r as usize; let c = c as usize;
841            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data: vec![1.0; r * c] })
842        }
843        ("math", "from_lists") => {
844            let rows = expect_list(args.first())?;
845            let r = rows.len();
846            if r == 0 {
847                return Ok(Value::F64Array { rows: 0, cols: 0, data: Vec::new() });
848            }
849            let first_row = match &rows[0] {
850                Value::List(xs) => xs,
851                other => return Err(format!("math.from_lists: row 0 not List, got {other:?}")),
852            };
853            let c = first_row.len();
854            let mut data = Vec::with_capacity(r * c);
855            for (i, row) in rows.iter().enumerate() {
856                let row = match row {
857                    Value::List(xs) => xs,
858                    other => return Err(format!("math.from_lists: row {i} not List, got {other:?}")),
859                };
860                if row.len() != c {
861                    return Err(format!("math.from_lists: row {i} has {} cols, expected {c}", row.len()));
862                }
863                for (j, v) in row.iter().enumerate() {
864                    let f = match v {
865                        Value::Float(f) => *f,
866                        Value::Int(n) => *n as f64,
867                        other => return Err(format!("math.from_lists: ({i},{j}) not numeric, got {other:?}")),
868                    };
869                    data.push(f);
870                }
871            }
872            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data })
873        }
874        ("math", "from_flat") => {
875            let r = expect_int(args.first())?;
876            let c = expect_int(args.get(1))?;
877            let xs = expect_list(args.get(2))?;
878            if r < 0 || c < 0 {
879                return Err(format!("math.from_flat: negative dim {r}x{c}"));
880            }
881            let r = r as usize; let c = c as usize;
882            if xs.len() != r * c {
883                return Err(format!("math.from_flat: list len {} != {}*{}", xs.len(), r, c));
884            }
885            let mut data = Vec::with_capacity(r * c);
886            for v in xs {
887                data.push(match v {
888                    Value::Float(f) => *f,
889                    Value::Int(n)   => *n as f64,
890                    other => return Err(format!("math.from_flat: non-numeric element {other:?}")),
891                });
892            }
893            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data })
894        }
895        ("math", "rows") => {
896            let (r, _, _) = unpack_matrix(first_arg(args)?)?;
897            Ok(Value::Int(r as i64))
898        }
899        ("math", "cols") => {
900            let (_, c, _) = unpack_matrix(first_arg(args)?)?;
901            Ok(Value::Int(c as i64))
902        }
903        ("math", "get") => {
904            let (r, c, data) = unpack_matrix(first_arg(args)?)?;
905            let i = expect_int(args.get(1))? as usize;
906            let j = expect_int(args.get(2))? as usize;
907            if i >= r || j >= c {
908                return Err(format!("math.get: ({i},{j}) out of {r}x{c}"));
909            }
910            Ok(Value::Float(data[i * c + j]))
911        }
912        ("math", "to_flat") => {
913            let (_, _, data) = unpack_matrix(first_arg(args)?)?;
914            Ok(Value::List(data.into_iter().map(Value::Float).collect()))
915        }
916        ("math", "transpose") => {
917            let (r, c, data) = unpack_matrix(first_arg(args)?)?;
918            let mut out = vec![0.0; r * c];
919            for i in 0..r {
920                for j in 0..c {
921                    out[j * r + i] = data[i * c + j];
922                }
923            }
924            Ok(Value::F64Array { rows: c as u32, cols: r as u32, data: out })
925        }
926        ("math", "matmul") => {
927            let (m, k1, a) = unpack_matrix(first_arg(args)?)?;
928            let (k2, n, b) = unpack_matrix(args.get(1).ok_or("math.matmul: missing arg 1")?)?;
929            if k1 != k2 {
930                return Err(format!("math.matmul: dim mismatch {m}x{k1} · {k2}x{n}"));
931            }
932            // Plain triple loop. For the small matrices used in the ML
933            // demo (n<200, k<10) this is well under a millisecond and
934            // avoids pulling in matrixmultiply for the runtime crate.
935            let mut c = vec![0.0; m * n];
936            for i in 0..m {
937                for kk in 0..k1 {
938                    let aik = a[i * k1 + kk];
939                    for j in 0..n {
940                        c[i * n + j] += aik * b[kk * n + j];
941                    }
942                }
943            }
944            Ok(Value::F64Array { rows: m as u32, cols: n as u32, data: c })
945        }
946        ("math", "scale") => {
947            let s = expect_float(args.first())?;
948            let (r, c, mut data) = unpack_matrix(args.get(1).ok_or("math.scale: missing arg 1")?)?;
949            for x in &mut data { *x *= s; }
950            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data })
951        }
952        ("math", "add") | ("math", "sub") => {
953            let (ar, ac, a) = unpack_matrix(first_arg(args)?)?;
954            let (br, bc, b) = unpack_matrix(args.get(1).ok_or("math.add/sub: missing arg 1")?)?;
955            if ar != br || ac != bc {
956                return Err(format!("math.{op}: shape mismatch {ar}x{ac} vs {br}x{bc}"));
957            }
958            let neg = op == "sub";
959            let mut out = a;
960            for (i, x) in out.iter_mut().enumerate() {
961                if neg { *x -= b[i] } else { *x += b[i] }
962            }
963            Ok(Value::F64Array { rows: ar as u32, cols: ac as u32, data: out })
964        }
965        ("math", "sigmoid") => {
966            let (r, c, mut data) = unpack_matrix(first_arg(args)?)?;
967            for x in &mut data { *x = 1.0 / (1.0 + (-*x).exp()); }
968            Ok(Value::F64Array { rows: r as u32, cols: c as u32, data })
969        }
970
971        // -- map --
972        ("map", "new") => Ok(Value::Map(BTreeMap::new())),
973        ("map", "size") => Ok(Value::Int(expect_map(args.first())?.len() as i64)),
974        ("map", "has") => {
975            let m = expect_map(args.first())?;
976            let k = MapKey::from_value(args.get(1).ok_or("map.has: missing key")?)?;
977            Ok(Value::Bool(m.contains_key(&k)))
978        }
979        ("map", "get") => {
980            let m = expect_map(args.first())?;
981            let k = MapKey::from_value(args.get(1).ok_or("map.get: missing key")?)?;
982            Ok(match m.get(&k) {
983                Some(v) => some(v.clone()),
984                None    => none(),
985            })
986        }
987        ("map", "set") => {
988            let mut m = expect_map(args.first())?.clone();
989            let k = MapKey::from_value(args.get(1).ok_or("map.set: missing key")?)?;
990            let v = args.get(2).ok_or("map.set: missing value")?.clone();
991            m.insert(k, v);
992            Ok(Value::Map(m))
993        }
994        ("map", "delete") => {
995            let mut m = expect_map(args.first())?.clone();
996            let k = MapKey::from_value(args.get(1).ok_or("map.delete: missing key")?)?;
997            m.remove(&k);
998            Ok(Value::Map(m))
999        }
1000        ("map", "keys") => {
1001            let m = expect_map(args.first())?;
1002            Ok(Value::List(m.keys().cloned().map(MapKey::into_value).collect()))
1003        }
1004        ("map", "values") => {
1005            let m = expect_map(args.first())?;
1006            Ok(Value::List(m.values().cloned().collect()))
1007        }
1008        ("map", "entries") => {
1009            let m = expect_map(args.first())?;
1010            Ok(Value::List(m.iter()
1011                .map(|(k, v)| Value::Tuple(vec![k.as_value(), v.clone()]))
1012                .collect()))
1013        }
1014        ("map", "from_list") => {
1015            let pairs = expect_list(args.first())?;
1016            let mut m = BTreeMap::new();
1017            for p in pairs {
1018                let items = match p {
1019                    Value::Tuple(items) if items.len() == 2 => items,
1020                    other => return Err(format!(
1021                        "map.from_list element must be a 2-tuple, got {other:?}")),
1022                };
1023                let k = MapKey::from_value(&items[0])?;
1024                m.insert(k, items[1].clone());
1025            }
1026            Ok(Value::Map(m))
1027        }
1028
1029        // -- set --
1030        ("set", "new") => Ok(Value::Set(BTreeSet::new())),
1031        ("set", "size") => Ok(Value::Int(expect_set(args.first())?.len() as i64)),
1032        ("set", "has") => {
1033            let s = expect_set(args.first())?;
1034            let k = MapKey::from_value(args.get(1).ok_or("set.has: missing element")?)?;
1035            Ok(Value::Bool(s.contains(&k)))
1036        }
1037        ("set", "add") => {
1038            let mut s = expect_set(args.first())?.clone();
1039            let k = MapKey::from_value(args.get(1).ok_or("set.add: missing element")?)?;
1040            s.insert(k);
1041            Ok(Value::Set(s))
1042        }
1043        ("set", "delete") => {
1044            let mut s = expect_set(args.first())?.clone();
1045            let k = MapKey::from_value(args.get(1).ok_or("set.delete: missing element")?)?;
1046            s.remove(&k);
1047            Ok(Value::Set(s))
1048        }
1049        ("set", "to_list") => {
1050            let s = expect_set(args.first())?;
1051            Ok(Value::List(s.iter().cloned().map(MapKey::into_value).collect()))
1052        }
1053        ("set", "from_list") => {
1054            let xs = expect_list(args.first())?;
1055            let mut s = BTreeSet::new();
1056            for x in xs {
1057                s.insert(MapKey::from_value(x)?);
1058            }
1059            Ok(Value::Set(s))
1060        }
1061        ("set", "union") => {
1062            let a = expect_set(args.first())?;
1063            let b = expect_set(args.get(1))?;
1064            Ok(Value::Set(a.union(b).cloned().collect()))
1065        }
1066        ("set", "intersect") => {
1067            let a = expect_set(args.first())?;
1068            let b = expect_set(args.get(1))?;
1069            Ok(Value::Set(a.intersection(b).cloned().collect()))
1070        }
1071        ("set", "diff") => {
1072            let a = expect_set(args.first())?;
1073            let b = expect_set(args.get(1))?;
1074            Ok(Value::Set(a.difference(b).cloned().collect()))
1075        }
1076        ("set", "is_empty") => Ok(Value::Bool(expect_set(args.first())?.is_empty())),
1077        ("set", "is_subset") => {
1078            let a = expect_set(args.first())?;
1079            let b = expect_set(args.get(1))?;
1080            Ok(Value::Bool(a.is_subset(b)))
1081        }
1082
1083        // -- map helpers --
1084        ("map", "merge") => {
1085            // b's entries override a's. We construct a new BTreeMap
1086            // by extending a with b's pairs.
1087            let a = expect_map(args.first())?.clone();
1088            let b = expect_map(args.get(1))?;
1089            let mut out = a;
1090            for (k, v) in b {
1091                out.insert(k.clone(), v.clone());
1092            }
1093            Ok(Value::Map(out))
1094        }
1095        ("map", "is_empty") => Ok(Value::Bool(expect_map(args.first())?.is_empty())),
1096
1097        // -- deque --
1098        ("deque", "new") => Ok(Value::Deque(std::collections::VecDeque::new())),
1099        ("deque", "size") => Ok(Value::Int(expect_deque(args.first())?.len() as i64)),
1100        ("deque", "is_empty") => Ok(Value::Bool(expect_deque(args.first())?.is_empty())),
1101        ("deque", "push_back") => {
1102            let mut d = expect_deque(args.first())?.clone();
1103            let x = args.get(1).ok_or("deque.push_back: missing value")?.clone();
1104            d.push_back(x);
1105            Ok(Value::Deque(d))
1106        }
1107        ("deque", "push_front") => {
1108            let mut d = expect_deque(args.first())?.clone();
1109            let x = args.get(1).ok_or("deque.push_front: missing value")?.clone();
1110            d.push_front(x);
1111            Ok(Value::Deque(d))
1112        }
1113        ("deque", "pop_back") => {
1114            let mut d = expect_deque(args.first())?.clone();
1115            match d.pop_back() {
1116                Some(x) => Ok(Value::Variant {
1117                    name: "Some".into(),
1118                    args: vec![Value::Tuple(vec![x, Value::Deque(d)])],
1119                }),
1120                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
1121            }
1122        }
1123        ("deque", "pop_front") => {
1124            let mut d = expect_deque(args.first())?.clone();
1125            match d.pop_front() {
1126                Some(x) => Ok(Value::Variant {
1127                    name: "Some".into(),
1128                    args: vec![Value::Tuple(vec![x, Value::Deque(d)])],
1129                }),
1130                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
1131            }
1132        }
1133        ("deque", "peek_back") => {
1134            let d = expect_deque(args.first())?;
1135            match d.back() {
1136                Some(x) => Ok(Value::Variant {
1137                    name: "Some".into(),
1138                    args: vec![x.clone()],
1139                }),
1140                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
1141            }
1142        }
1143        ("deque", "peek_front") => {
1144            let d = expect_deque(args.first())?;
1145            match d.front() {
1146                Some(x) => Ok(Value::Variant {
1147                    name: "Some".into(),
1148                    args: vec![x.clone()],
1149                }),
1150                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
1151            }
1152        }
1153        ("deque", "from_list") => {
1154            let xs = expect_list(args.first())?;
1155            Ok(Value::Deque(xs.iter().cloned().collect()))
1156        }
1157        ("deque", "to_list") => {
1158            let d = expect_deque(args.first())?;
1159            Ok(Value::List(d.iter().cloned().collect()))
1160        }
1161
1162        // -- crypto (pure ops; crypto.random is effectful and routes
1163        // through the handler under [random], see try_pure_builtin) --
1164        ("crypto", "sha256") => {
1165            use sha2::{Digest, Sha256};
1166            let data = expect_bytes(args.first())?;
1167            let mut h = Sha256::new();
1168            h.update(data);
1169            Ok(Value::Bytes(h.finalize().to_vec()))
1170        }
1171        ("crypto", "sha512") => {
1172            use sha2::{Digest, Sha512};
1173            let data = expect_bytes(args.first())?;
1174            let mut h = Sha512::new();
1175            h.update(data);
1176            Ok(Value::Bytes(h.finalize().to_vec()))
1177        }
1178        ("crypto", "md5") => {
1179            use md5::{Digest, Md5};
1180            let data = expect_bytes(args.first())?;
1181            let mut h = Md5::new();
1182            h.update(data);
1183            Ok(Value::Bytes(h.finalize().to_vec()))
1184        }
1185        // BLAKE2b (#382) — 64-byte digest, faster than SHA-512 on most
1186        // CPUs with the same security level. Backed by the `blake2`
1187        // crate; uses `Blake2b512` (the standard 512-bit variant).
1188        ("crypto", "blake2b") => {
1189            use blake2::{Blake2b512, Digest};
1190            let data = expect_bytes(args.first())?;
1191            let mut h = Blake2b512::new();
1192            h.update(data);
1193            Ok(Value::Bytes(h.finalize().to_vec()))
1194        }
1195        // Keccak-256 (#655) — Ethereum's hash. This is the original
1196        // Keccak padding (0x01), NOT NIST SHA3-256 (0x06); they produce
1197        // different digests for the same input. Used for EIP-712 struct
1198        // hashing, the final signing digest, and address derivation.
1199        ("crypto", "keccak256") => {
1200            use sha3::{Digest, Keccak256};
1201            let data = expect_bytes(args.first())?;
1202            let mut h = Keccak256::new();
1203            h.update(data);
1204            Ok(Value::Bytes(h.finalize().to_vec()))
1205        }
1206        // Hex-string convenience hashers (#382). Equivalent to
1207        // `hex_encode(shaN(bytes_of_str(s)))` for the common case
1208        // where the caller has a Str and wants a hex Str digest.
1209        ("crypto", "sha256_str") => {
1210            use sha2::{Digest, Sha256};
1211            let s = expect_str(args.first())?;
1212            let mut h = Sha256::new();
1213            h.update(s.as_bytes());
1214            Ok(Value::Str(hex::encode(h.finalize()).into()))
1215        }
1216        ("crypto", "sha512_str") => {
1217            use sha2::{Digest, Sha512};
1218            let s = expect_str(args.first())?;
1219            let mut h = Sha512::new();
1220            h.update(s.as_bytes());
1221            Ok(Value::Str(hex::encode(h.finalize()).into()))
1222        }
1223        ("crypto", "hmac_sha256") => {
1224            use hmac::{Hmac, KeyInit, Mac};
1225            type HmacSha256 = Hmac<sha2::Sha256>;
1226            let key = expect_bytes(args.first())?;
1227            let data = expect_bytes(args.get(1))?;
1228            let mut mac = HmacSha256::new_from_slice(key)
1229                .map_err(|e| format!("hmac_sha256 key: {e}"))?;
1230            mac.update(data);
1231            Ok(Value::Bytes(mac.finalize().into_bytes().to_vec()))
1232        }
1233        ("crypto", "hmac_sha512") => {
1234            use hmac::{Hmac, KeyInit, Mac};
1235            type HmacSha512 = Hmac<sha2::Sha512>;
1236            let key = expect_bytes(args.first())?;
1237            let data = expect_bytes(args.get(1))?;
1238            let mut mac = HmacSha512::new_from_slice(key)
1239                .map_err(|e| format!("hmac_sha512 key: {e}"))?;
1240            mac.update(data);
1241            Ok(Value::Bytes(mac.finalize().into_bytes().to_vec()))
1242        }
1243        // ed25519 asymmetric signatures (#643). A secret key is its 32-byte
1244        // seed — generate one with the effectful `crypto.random(32)`. These three
1245        // ops are pure (deterministic given their inputs).
1246        ("crypto", "ed25519_public_key") => {
1247            use ed25519_dalek::SigningKey;
1248            let secret = expect_bytes(args.first())?;
1249            let seed: [u8; 32] = match secret.as_slice().try_into() {
1250                Ok(s)  => s,
1251                Err(_) => return Ok(err_v(Value::Str("ed25519_public_key: secret must be 32 bytes".into()))),
1252            };
1253            let sk = SigningKey::from_bytes(&seed);
1254            Ok(ok_v(Value::Bytes(sk.verifying_key().to_bytes().to_vec())))
1255        }
1256        ("crypto", "ed25519_sign") => {
1257            use ed25519_dalek::{Signer, SigningKey};
1258            let secret = expect_bytes(args.first())?;
1259            let message = expect_bytes(args.get(1))?;
1260            let seed: [u8; 32] = match secret.as_slice().try_into() {
1261                Ok(s)  => s,
1262                Err(_) => return Ok(err_v(Value::Str("ed25519_sign: secret must be 32 bytes".into()))),
1263            };
1264            let sk = SigningKey::from_bytes(&seed);
1265            Ok(ok_v(Value::Bytes(sk.sign(message).to_bytes().to_vec())))
1266        }
1267        ("crypto", "ed25519_verify") => {
1268            use ed25519_dalek::{Signature, Verifier, VerifyingKey};
1269            let public = expect_bytes(args.first())?;
1270            let message = expect_bytes(args.get(1))?;
1271            let sig_bytes = expect_bytes(args.get(2))?;
1272            let pk_arr: [u8; 32] = match public.as_slice().try_into() {
1273                Ok(p)  => p,
1274                Err(_) => return Ok(Value::Bool(false)),
1275            };
1276            let sig_arr: [u8; 64] = match sig_bytes.as_slice().try_into() {
1277                Ok(s)  => s,
1278                Err(_) => return Ok(Value::Bool(false)),
1279            };
1280            let vk = match VerifyingKey::from_bytes(&pk_arr) {
1281                Ok(v)  => v,
1282                Err(_) => return Ok(Value::Bool(false)),
1283            };
1284            let sig = Signature::from_bytes(&sig_arr);
1285            Ok(Value::Bool(vk.verify(message, &sig).is_ok()))
1286        }
1287        ("crypto", "ed25519_is_valid_point") => {
1288            use ed25519_dalek::VerifyingKey;
1289            let candidate = expect_bytes(args.first())?;
1290            let arr: [u8; 32] = match candidate.as_slice().try_into() {
1291                Ok(a) => a,
1292                Err(_) => return Ok(Value::Bool(false)),
1293            };
1294            Ok(Value::Bool(VerifyingKey::from_bytes(&arr).is_ok()))
1295        }
1296        // P-256 ECDSA / ES256 (#651). Backs the JWT/SD-JWT signing
1297        // primitives `lex-jose` needs for AP2 mandates. Key minting
1298        // (`p256_generate`) is effectful (`[random]`) and lives in the
1299        // handler; these three ops are deterministic given their inputs.
1300        //
1301        // - Secret key: 32-byte scalar (`SigningKey::to_bytes`).
1302        // - Public key: 33-byte SEC1 *compressed* point.
1303        // - Signature: ASN.1 DER-encoded (standard for ES256/JOSE
1304        //   producers that emit DER; JWK/raw-r||s conversion is a
1305        //   `lex-jose` concern).
1306        // Signing hashes `msg` with SHA-256 internally (ES256).
1307        ("crypto", "p256_public_key") => {
1308            use p256::ecdsa::SigningKey;
1309            let secret = expect_bytes(args.first())?;
1310            let sk = match SigningKey::from_slice(secret) {
1311                Ok(k)  => k,
1312                Err(_) => return Ok(err_v(Value::Str(
1313                    "p256_public_key: secret must be a 32-byte P-256 scalar".into()))),
1314            };
1315            let point = sk.verifying_key().to_encoded_point(true);
1316            Ok(ok_v(Value::Bytes(point.as_bytes().to_vec())))
1317        }
1318        ("crypto", "p256_sign") => {
1319            use p256::ecdsa::{signature::Signer, Signature, SigningKey};
1320            let secret = expect_bytes(args.first())?;
1321            let message = expect_bytes(args.get(1))?;
1322            let sk = match SigningKey::from_slice(secret) {
1323                Ok(k)  => k,
1324                Err(_) => return Ok(err_v(Value::Str(
1325                    "p256_sign: secret must be a 32-byte P-256 scalar".into()))),
1326            };
1327            let sig: Signature = sk.sign(message);
1328            Ok(ok_v(Value::Bytes(sig.to_der().as_bytes().to_vec())))
1329        }
1330        ("crypto", "p256_verify") => {
1331            use p256::ecdsa::{signature::Verifier, Signature, VerifyingKey};
1332            let public = expect_bytes(args.first())?;
1333            let message = expect_bytes(args.get(1))?;
1334            let sig_bytes = expect_bytes(args.get(2))?;
1335            let vk = match VerifyingKey::from_sec1_bytes(public) {
1336                Ok(v)  => v,
1337                Err(_) => return Ok(Value::Bool(false)),
1338            };
1339            let sig = match Signature::from_der(sig_bytes) {
1340                Ok(s)  => s,
1341                Err(_) => return Ok(Value::Bool(false)),
1342            };
1343            Ok(Value::Bool(vk.verify(message, &sig).is_ok()))
1344        }
1345        // secp256k1 ECDSA + recovery (#655) — the EVM curve, for EIP-712
1346        // typed-data signing (EIP-3009 / x402 `exact`). Key minting
1347        // (`secp256k1_generate`) is effectful (`[random]`) and lives in
1348        // the handler; these ops are deterministic given their inputs.
1349        //
1350        // Unlike `p256_*`, sign/verify take a PRE-HASHED 32-byte digest
1351        // (EIP-712 already hashed) and do not hash again.
1352        // - Secret key: 32-byte scalar.
1353        // - Public key: 65-byte uncompressed SEC1 point (0x04‖X‖Y).
1354        // - Signature: 65 bytes `r‖s‖v`, v ∈ {27,28}, low-S (EIP-2).
1355        ("crypto", "secp256k1_public_key") => {
1356            use k256::ecdsa::SigningKey;
1357            let secret = expect_bytes(args.first())?;
1358            let sk = match SigningKey::from_slice(secret) {
1359                Ok(k)  => k,
1360                Err(_) => return Ok(err_v(Value::Str(
1361                    "secp256k1_public_key: secret must be a 32-byte secp256k1 scalar".into()))),
1362            };
1363            // Uncompressed SEC1 so callers can derive an Ethereum address
1364            // as keccak256(point[1..])[12..] without decompressing.
1365            let point = sk.verifying_key().to_encoded_point(false);
1366            Ok(ok_v(Value::Bytes(point.as_bytes().to_vec())))
1367        }
1368        ("crypto", "secp256k1_sign_digest") => {
1369            use k256::ecdsa::SigningKey;
1370            let secret = expect_bytes(args.first())?;
1371            let digest = expect_bytes(args.get(1))?;
1372            if digest.len() != 32 {
1373                return Ok(err_v(Value::Str(
1374                    "secp256k1_sign_digest: digest must be exactly 32 bytes".into())));
1375            }
1376            let sk = match SigningKey::from_slice(secret) {
1377                Ok(k)  => k,
1378                Err(_) => return Ok(err_v(Value::Str(
1379                    "secp256k1_sign_digest: secret must be a 32-byte secp256k1 scalar".into()))),
1380            };
1381            // RustCrypto normalizes to low-S (EIP-2) and returns the
1382            // recovery id. Ethereum's `v` is 27 + recid.
1383            match sk.sign_prehash_recoverable(digest) {
1384                Ok((sig, recid)) => {
1385                    let mut out = sig.to_bytes().to_vec(); // 64 bytes: r‖s
1386                    out.push(27u8 + recid.to_byte());
1387                    Ok(ok_v(Value::Bytes(out)))
1388                }
1389                Err(e) => Ok(err_v(Value::Str(
1390                    format!("secp256k1_sign_digest: {e}").into()))),
1391            }
1392        }
1393        ("crypto", "secp256k1_recover") => {
1394            use k256::ecdsa::{RecoveryId, Signature, VerifyingKey};
1395            let digest = expect_bytes(args.first())?;
1396            let sig_bytes = expect_bytes(args.get(1))?;
1397            if digest.len() != 32 {
1398                return Ok(err_v(Value::Str(
1399                    "secp256k1_recover: digest must be exactly 32 bytes".into())));
1400            }
1401            if sig_bytes.len() != 65 {
1402                return Ok(err_v(Value::Str(
1403                    "secp256k1_recover: signature must be 65 bytes (r‖s‖v)".into())));
1404            }
1405            let sig = match Signature::from_slice(&sig_bytes[..64]) {
1406                Ok(s)  => s,
1407                Err(_) => return Ok(err_v(Value::Str(
1408                    "secp256k1_recover: malformed r‖s".into()))),
1409            };
1410            // Accept both Ethereum {27,28} and raw {0,1} encodings of v.
1411            let v = sig_bytes[64];
1412            let recid_byte = if v >= 27 { v - 27 } else { v };
1413            let recid = match RecoveryId::from_byte(recid_byte) {
1414                Some(r) => r,
1415                None    => return Ok(err_v(Value::Str(
1416                    "secp256k1_recover: invalid recovery id".into()))),
1417            };
1418            match VerifyingKey::recover_from_prehash(digest, &sig, recid) {
1419                Ok(vk) => Ok(ok_v(Value::Bytes(
1420                    vk.to_encoded_point(false).as_bytes().to_vec()))),
1421                Err(e) => Ok(err_v(Value::Str(
1422                    format!("secp256k1_recover: {e}").into()))),
1423            }
1424        }
1425        ("crypto", "secp256k1_verify") => {
1426            use k256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey};
1427            let public = expect_bytes(args.first())?;
1428            let digest = expect_bytes(args.get(1))?;
1429            let sig_bytes = expect_bytes(args.get(2))?;
1430            if digest.len() != 32 {
1431                return Ok(Value::Bool(false));
1432            }
1433            let vk = match VerifyingKey::from_sec1_bytes(public) {
1434                Ok(v)  => v,
1435                Err(_) => return Ok(Value::Bool(false)),
1436            };
1437            // Accept a 65-byte recoverable sig (drop v) or a bare 64-byte r‖s.
1438            let rs = if sig_bytes.len() == 65 { &sig_bytes[..64] } else { sig_bytes.as_slice() };
1439            let sig = match Signature::from_slice(rs) {
1440                Ok(s)  => s,
1441                Err(_) => return Ok(Value::Bool(false)),
1442            };
1443            Ok(Value::Bool(vk.verify_prehash(digest, &sig).is_ok()))
1444        }
1445        ("crypto", "base64_encode") => {
1446            use base64::{Engine, engine::general_purpose::STANDARD};
1447            let data = expect_bytes(args.first())?;
1448            Ok(Value::Str(STANDARD.encode(data).into()))
1449        }
1450        ("crypto", "base64_decode") => {
1451            use base64::{Engine, engine::general_purpose::STANDARD};
1452            let s = expect_str(args.first())?;
1453            match STANDARD.decode(s) {
1454                Ok(b)  => Ok(ok_v(Value::Bytes(b))),
1455                Err(e) => Ok(err_v(Value::Str(format!("base64: {e}").into()))),
1456            }
1457        }
1458        // URL-safe base64 (#382). Alphabet `-_` instead of `+/`,
1459        // padding stripped. Use for JWT segments, signed cookies, any
1460        // token that travels in a URL or path component.
1461        ("crypto", "base64url_encode") => {
1462            use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
1463            let data = expect_bytes(args.first())?;
1464            Ok(Value::Str(URL_SAFE_NO_PAD.encode(data).into()))
1465        }
1466        ("crypto", "base64url_decode") => {
1467            use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
1468            let s = expect_str(args.first())?;
1469            match URL_SAFE_NO_PAD.decode(s) {
1470                Ok(b)  => Ok(ok_v(Value::Bytes(b))),
1471                Err(e) => Ok(err_v(Value::Str(format!("base64url: {e}").into()))),
1472            }
1473        }
1474        ("crypto", "hex_encode") => {
1475            let data = expect_bytes(args.first())?;
1476            Ok(Value::Str(hex::encode(data).into()))
1477        }
1478        ("crypto", "hex_decode") => {
1479            let s = expect_str(args.first())?;
1480            match hex::decode(s) {
1481                Ok(b)  => Ok(ok_v(Value::Bytes(b))),
1482                Err(e) => Ok(err_v(Value::Str(format!("hex: {e}").into()))),
1483            }
1484        }
1485        // base58 (#658). Bitcoin/Solana alphabet, no Base58Check checksum —
1486        // the encoding Solana uses for pubkeys, signatures, and the x402
1487        // `exact` payload. Pure, like base64/hex.
1488        ("crypto", "base58_encode") => {
1489            let data = expect_bytes(args.first())?;
1490            Ok(Value::Str(bs58::encode(data).into_string().into()))
1491        }
1492        ("crypto", "base58_decode") => {
1493            let s = expect_str(args.first())?;
1494            match bs58::decode(s).into_vec() {
1495                Ok(b)  => Ok(ok_v(Value::Bytes(b))),
1496                Err(e) => Ok(err_v(Value::Str(format!("base58: {e}").into()))),
1497            }
1498        }
1499        ("crypto", "constant_time_eq") | ("crypto", "eq") => {
1500            use subtle::ConstantTimeEq;
1501            let a = expect_bytes(args.first())?;
1502            let b = expect_bytes(args.get(1))?;
1503            // `subtle` returns Choice; comparison only meaningful when
1504            // lengths match. For mismatched lengths return false in
1505            // constant time (length itself isn't secret, but we want
1506            // a single comparison shape).
1507            //
1508            // `eq` (#382) is the recommended spelling — same semantics,
1509            // shorter name. `constant_time_eq` stays as an alias for
1510            // existing callers.
1511            let eq = if a.len() == b.len() {
1512                a.ct_eq(b).into()
1513            } else {
1514                false
1515            };
1516            Ok(Value::Bool(eq))
1517        }
1518        // Constant-time string equality (#382). Compares the bytes of
1519        // both strings; semantics identical to `eq` after `.as_bytes()`.
1520        ("crypto", "eq_str") => {
1521            use subtle::ConstantTimeEq;
1522            let a = expect_str(args.first())?;
1523            let b = expect_str(args.get(1))?;
1524            let eq = if a.len() == b.len() {
1525                a.as_bytes().ct_eq(b.as_bytes()).into()
1526            } else {
1527                false
1528            };
1529            Ok(Value::Bool(eq))
1530        }
1531
1532        // -- AEAD (#382 AEAD slice). Pure: same key + nonce + aad +
1533        // plaintext always produce the same ciphertext + tag. The
1534        // `[random]` effect lives one level up at the caller, where the
1535        // nonce is generated; AEAD ops themselves are deterministic and
1536        // therefore pure.
1537        //
1538        // AES-GCM key length is 128 / 192 / 256 bits; we pick the
1539        // variant from the key size at runtime so callers don't have
1540        // to choose between three near-identical wrappers.
1541        ("crypto", "aes_gcm_seal") => Ok(aes_gcm_seal_impl(args)),
1542        ("crypto", "aes_gcm_open") => Ok(aes_gcm_open_impl(args)),
1543        ("crypto", "chacha20_poly1305_seal") => Ok(chacha20_seal_impl(args)),
1544        ("crypto", "chacha20_poly1305_open") => Ok(chacha20_open_impl(args)),
1545        ("crypto", "pbkdf2_sha256") => Ok(pbkdf2_sha256_impl(args)),
1546        ("crypto", "hkdf_sha256")   => Ok(hkdf_sha256_impl(args)),
1547        ("crypto", "argon2id")      => Ok(argon2id_impl(args)),
1548
1549        // -- random (#219): pure, seeded RNG. Backed by SplitMix64;
1550        // state is the u64 mixer state stored as a single i64 in
1551        // `Rng = { state :: Int }`. Threading the Rng through the
1552        // call site is the user's responsibility — there is no
1553        // global RNG and therefore no `[random]` effect tag for
1554        // pure-seeded usage. --
1555        ("random", "seed") => {
1556            let s = args.first().ok_or("random.seed: missing arg")?.as_int();
1557            // Hash the user-supplied seed once before installing it.
1558            // SplitMix64 is fine when seeded with any u64, but
1559            // hashing first protects against pathological seeds
1560            // (e.g., 0) that would make the very first draw zero.
1561            let mixed = splitmix64(s as u64).0;
1562            Ok(rng_value(mixed))
1563        }
1564        ("random", "int") => {
1565            let state = rng_decode(args.first())?;
1566            let lo = args.get(1).ok_or("random.int: missing lo")?.as_int();
1567            let hi = args.get(2).ok_or("random.int: missing hi")?.as_int();
1568            if hi < lo {
1569                return Err(format!(
1570                    "random.int: hi ({hi}) must be >= lo ({lo})"));
1571            }
1572            let span = (hi as i128) - (lo as i128) + 1;
1573            let (raw, next_state) = splitmix64(state);
1574            // Reduce uniformly to [lo, hi]. The bias from a plain
1575            // modulo is at most `(u64::MAX % span) / u64::MAX`,
1576            // which for any practical span is invisible. Crypto
1577            // applications should use `crypto.random` instead.
1578            let drawn = lo as i128 + (raw as u128 % span as u128) as i128;
1579            Ok(Value::Tuple(vec![
1580                Value::Int(drawn as i64),
1581                rng_value(next_state),
1582            ]))
1583        }
1584        ("random", "float") => {
1585            let state = rng_decode(args.first())?;
1586            let (raw, next_state) = splitmix64(state);
1587            // Take the top 53 bits and divide by 2^53 to land in
1588            // [0.0, 1.0); this is the standard f64 uniform draw.
1589            let f = ((raw >> 11) as f64) / ((1u64 << 53) as f64);
1590            Ok(Value::Tuple(vec![Value::Float(f), rng_value(next_state)]))
1591        }
1592        ("random", "choose") => {
1593            let state = rng_decode(args.first())?;
1594            let xs = match args.get(1) {
1595                Some(Value::List(xs)) => xs,
1596                _ => return Err("random.choose: expected List".into()),
1597            };
1598            if xs.is_empty() {
1599                return Ok(Value::Variant {
1600                    name: "None".into(), args: vec![],
1601                });
1602            }
1603            let (raw, next_state) = splitmix64(state);
1604            let idx = (raw as usize) % xs.len();
1605            let pick = xs[idx].clone();
1606            Ok(Value::Variant {
1607                name: "Some".into(),
1608                args: vec![Value::Tuple(vec![pick, rng_value(next_state)])],
1609            })
1610        }
1611
1612        // -- parser (#217): parser combinators. Parser values are
1613        // tagged Records — `{ kind: "Char", ch: "x" }` etc. — so
1614        // canonical equality follows from the canonical Record
1615        // encoding. The interpreter is `parser_run_impl`. --
1616        ("parser", "char") => {
1617            let s = expect_str(args.first())?;
1618            if s.chars().count() != 1 {
1619                return Err(format!(
1620                    "parser.char: expected 1-character string, got {s:?}"));
1621            }
1622            Ok(parser_node("Char", &[("ch", Value::Str(s.into()))]))
1623        }
1624        ("parser", "string") => {
1625            let s = expect_str(args.first())?;
1626            Ok(parser_node("String", &[("s", Value::Str(s.into()))]))
1627        }
1628        ("parser", "digit") => Ok(parser_node("Digit", &[])),
1629        ("parser", "alpha") => Ok(parser_node("Alpha", &[])),
1630        ("parser", "whitespace") => Ok(parser_node("Whitespace", &[])),
1631        ("parser", "eof") => Ok(parser_node("Eof", &[])),
1632        ("parser", "seq") => {
1633            let a = args.first().cloned()
1634                .ok_or_else(|| "parser.seq: missing first parser".to_string())?;
1635            let b = args.get(1).cloned()
1636                .ok_or_else(|| "parser.seq: missing second parser".to_string())?;
1637            Ok(parser_node("Seq", &[("a", a), ("b", b)]))
1638        }
1639        ("parser", "alt") => {
1640            let a = args.first().cloned()
1641                .ok_or_else(|| "parser.alt: missing first parser".to_string())?;
1642            let b = args.get(1).cloned()
1643                .ok_or_else(|| "parser.alt: missing second parser".to_string())?;
1644            Ok(parser_node("Alt", &[("a", a), ("b", b)]))
1645        }
1646        ("parser", "many") => {
1647            let p = args.first().cloned()
1648                .ok_or_else(|| "parser.many: missing inner parser".to_string())?;
1649            Ok(parser_node("Many", &[("p", p)]))
1650        }
1651        ("parser", "optional") => {
1652            let p = args.first().cloned()
1653                .ok_or_else(|| "parser.optional: missing inner parser".to_string())?;
1654            Ok(parser_node("Optional", &[("p", p)]))
1655        }
1656        // `parser.map` and `parser.and_then` (#221): closure-bearing
1657        // combinators. Constructors only — actual closure invocation
1658        // happens at parser.run time via the Vm-level interpreter.
1659        ("parser", "map") => {
1660            let p = args.first().cloned()
1661                .ok_or_else(|| "parser.map: missing parser".to_string())?;
1662            let f = args.get(1).cloned()
1663                .ok_or_else(|| "parser.map: missing closure".to_string())?;
1664            Ok(parser_node("Map", &[("p", p), ("f", f)]))
1665        }
1666        ("parser", "and_then") => {
1667            let p = args.first().cloned()
1668                .ok_or_else(|| "parser.and_then: missing parser".to_string())?;
1669            let f = args.get(1).cloned()
1670                .ok_or_else(|| "parser.and_then: missing closure".to_string())?;
1671            Ok(parser_node("AndThen", &[("p", p), ("f", f)]))
1672        }
1673        // `parser.run` is handled at the Vm level (lex-bytecode's
1674        // `Op::EffectCall` intercept) — it needs reentrant Vm access
1675        // to invoke the closures inside `Map` / `AndThen` nodes. The
1676        // pure-builtin path doesn't have that, so we deliberately do
1677        // *not* have a `("parser", "run")` arm here.
1678
1679        // -- regex (the compiled `Regex` is stored as the pattern
1680        // string; the runtime caches the actual `regex::Regex` so
1681        // ops don't re-compile on every call) --
1682        ("regex", "compile") => {
1683            let pat = expect_str(args.first())?;
1684            match get_or_compile_regex(&pat) {
1685                Ok(_) => Ok(ok_v(Value::Str(pat.into()))),
1686                Err(e) => Ok(err_v(Value::Str(e.into()))),
1687            }
1688        }
1689        ("regex", "is_match") => {
1690            let pat = expect_str(args.first())?;
1691            let s = expect_str(args.get(1))?;
1692            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.is_match: {e}"))?;
1693            Ok(Value::Bool(re.is_match(&s)))
1694        }
1695        // is_match_str :: Str, Str -> Bool
1696        // Compiles the first argument as a pattern on the fly (uses the shared
1697        // cache) and matches against the second.  Returns false on invalid
1698        // pattern rather than propagating an error, keeping the pure signature.
1699        ("regex", "is_match_str") => {
1700            let pat = expect_str(args.first())?;
1701            let s = expect_str(args.get(1))?;
1702            match get_or_compile_regex(&pat) {
1703                Ok(re) => Ok(Value::Bool(re.is_match(&s))),
1704                Err(_) => Ok(Value::Bool(false)),
1705            }
1706        }
1707        ("regex", "find") => {
1708            let pat = expect_str(args.first())?;
1709            let s = expect_str(args.get(1))?;
1710            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.find: {e}"))?;
1711            match re.captures(&s) {
1712                Some(caps) => Ok(Value::Variant {
1713                    name: "Some".into(),
1714                    args: vec![match_value(&caps)],
1715                }),
1716                None => Ok(Value::Variant { name: "None".into(), args: vec![] }),
1717            }
1718        }
1719        ("regex", "find_all") => {
1720            let pat = expect_str(args.first())?;
1721            let s = expect_str(args.get(1))?;
1722            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.find_all: {e}"))?;
1723            let items: std::collections::VecDeque<Value> = re.captures_iter(&s).map(|caps| match_value(&caps)).collect();
1724            Ok(Value::List(items))
1725        }
1726        ("regex", "replace") => {
1727            let pat = expect_str(args.first())?;
1728            let s = expect_str(args.get(1))?;
1729            let rep = expect_str(args.get(2))?;
1730            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.replace: {e}"))?;
1731            Ok(Value::Str(re.replace(&s, rep.as_str()).into_owned().into()))
1732        }
1733        ("regex", "replace_all") => {
1734            let pat = expect_str(args.first())?;
1735            let s = expect_str(args.get(1))?;
1736            let rep = expect_str(args.get(2))?;
1737            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.replace_all: {e}"))?;
1738            Ok(Value::Str(re.replace_all(&s, rep.as_str()).into_owned().into()))
1739        }
1740        // -- datetime (pure ops; datetime.now is effectful and routes
1741        // through the handler under [time]) --
1742        ("datetime", "parse_iso") => {
1743            let s = expect_str(args.first())?;
1744            match chrono::DateTime::parse_from_rfc3339(&s) {
1745                Ok(dt) => Ok(ok_v(Value::Int(instant_from_chrono(dt)))),
1746                Err(e) => Ok(err_v(Value::Str(format!("parse_iso: {e}").into()))),
1747            }
1748        }
1749        ("datetime", "format_iso") => {
1750            let n = expect_int(args.first())?;
1751            Ok(Value::Str(format_iso(n).into()))
1752        }
1753        ("datetime", "parse") => {
1754            let s = expect_str(args.first())?;
1755            let fmt = expect_str(args.get(1))?;
1756            match chrono::NaiveDateTime::parse_from_str(&s, &fmt) {
1757                Ok(naive) => {
1758                    use chrono::TimeZone;
1759                    match chrono::Utc.from_local_datetime(&naive).single() {
1760                        Some(dt) => Ok(ok_v(Value::Int(instant_from_chrono(dt)))),
1761                        None => Ok(err_v(Value::Str("parse: ambiguous local time".into()))),
1762                    }
1763                }
1764                Err(e) => Ok(err_v(Value::Str(format!("parse: {e}").into()))),
1765            }
1766        }
1767        ("datetime", "format") => {
1768            let n = expect_int(args.first())?;
1769            let fmt = expect_str(args.get(1))?;
1770            let dt = chrono_from_instant(n);
1771            Ok(Value::Str(dt.format(&fmt).to_string().into()))
1772        }
1773        ("datetime", "to_components") => {
1774            let n = expect_int(args.first())?;
1775            let tz = match parse_tz_arg(args.get(1)) {
1776                Ok(t) => t,
1777                Err(e) => return Ok(err_v(Value::Str(e.into()))),
1778            };
1779            match resolve_tz_to_components(n, &tz) {
1780                Ok(rec) => Ok(ok_v(rec)),
1781                Err(e) => Ok(err_v(Value::Str(e.into()))),
1782            }
1783        }
1784        ("datetime", "from_components") => {
1785            let rec = match args.first() {
1786                Some(Value::Record { fields: r, .. }) => r.clone(),
1787                _ => return Err("from_components: expected DateTime record".into()),
1788            };
1789            match instant_from_components(&rec) {
1790                Ok(n) => Ok(ok_v(Value::Int(n))),
1791                Err(e) => Ok(err_v(Value::Str(e.into()))),
1792            }
1793        }
1794        ("datetime", "add") => {
1795            let a = expect_int(args.first())?;
1796            let d = expect_int(args.get(1))?;
1797            Ok(Value::Int(a.saturating_add(d)))
1798        }
1799        ("datetime", "diff") => {
1800            let a = expect_int(args.first())?;
1801            let b = expect_int(args.get(1))?;
1802            Ok(Value::Int(a.saturating_sub(b)))
1803        }
1804        ("datetime", "duration_seconds") => {
1805            let s = expect_float(args.first())?;
1806            let nanos = (s * 1_000_000_000.0) as i64;
1807            Ok(Value::Int(nanos))
1808        }
1809        ("datetime", "duration_minutes") => {
1810            let m = expect_int(args.first())?;
1811            Ok(Value::Int(m.saturating_mul(60_000_000_000)))
1812        }
1813        ("datetime", "duration_days") => {
1814            let d = expect_int(args.first())?;
1815            Ok(Value::Int(d.saturating_mul(86_400_000_000_000)))
1816        }
1817        // #331: Instant comparison ops.
1818        ("datetime", "before") => {
1819            let a = expect_int(args.first())?;
1820            let b = expect_int(args.get(1))?;
1821            Ok(Value::Bool(a < b))
1822        }
1823        ("datetime", "after") => {
1824            let a = expect_int(args.first())?;
1825            let b = expect_int(args.get(1))?;
1826            Ok(Value::Bool(a > b))
1827        }
1828        ("datetime", "compare") => {
1829            let a = expect_int(args.first())?;
1830            let b = expect_int(args.get(1))?;
1831            Ok(Value::Int(a.cmp(&b) as i64))
1832        }
1833        // #331: Duration scalar extraction (nanoseconds under the hood).
1834        // #681 rounds out the unit set; each truncates toward zero.
1835        ("duration", "millis")  => Ok(Value::Int(expect_int(args.first())? / 1_000_000)),
1836        ("duration", "seconds") => Ok(Value::Int(expect_int(args.first())? / 1_000_000_000)),
1837        ("duration", "minutes") => Ok(Value::Int(expect_int(args.first())? / 60_000_000_000)),
1838        ("duration", "hours")   => Ok(Value::Int(expect_int(args.first())? / 3_600_000_000_000)),
1839        ("duration", "days")    => Ok(Value::Int(expect_int(args.first())? / 86_400_000_000_000)),
1840
1841        ("regex", "split") => {
1842            let pat = expect_str(args.first())?;
1843            let s = expect_str(args.get(1))?;
1844            let re = get_or_compile_regex(&pat).map_err(|e| format!("regex.split: {e}"))?;
1845            let parts: std::collections::VecDeque<Value> = re.split(&s).map(|p| Value::Str(p.into())).collect();
1846            Ok(Value::List(parts))
1847        }
1848
1849        // -- http (builders + decoders; wire ops live in the
1850        // effect handler under `[net]`) --
1851        ("http", "with_header") => {
1852            let req = expect_record_pure(args.first())?.clone();
1853            let k = expect_str(args.get(1))?;
1854            let v = expect_str(args.get(2))?;
1855            Ok(Value::record_interned(http_set_header(req, &k, &v)))
1856        }
1857        ("http", "with_auth") => {
1858            let req = expect_record_pure(args.first())?.clone();
1859            let scheme = expect_str(args.get(1))?;
1860            let token = expect_str(args.get(2))?;
1861            let value = format!("{scheme} {token}");
1862            Ok(Value::record_interned(http_set_header(req, "Authorization", &value)))
1863        }
1864        ("http", "with_query") => {
1865            let req = expect_record_pure(args.first())?.clone();
1866            let params = match args.get(1) {
1867                Some(Value::Map(m)) => m.clone(),
1868                Some(other) => return Err(format!(
1869                    "http.with_query: params must be Map[Str, Str], got {other:?}")),
1870                None => return Err("http.with_query: missing params argument".into()),
1871            };
1872            Ok(Value::record_interned(http_append_query(req, &params)))
1873        }
1874        ("http", "with_timeout_ms") => {
1875            let req = expect_record_pure(args.first())?.clone();
1876            let ms = expect_int(args.get(1))?;
1877            let mut out = req;
1878            out.insert("timeout_ms".into(), Value::Variant {
1879                name: "Some".into(),
1880                args: vec![Value::Int(ms)],
1881            });
1882            Ok(Value::record_interned(out))
1883        }
1884        ("http", "json_body") => {
1885            let resp = expect_record_pure(args.first())?;
1886            let body = match resp.get("body") {
1887                Some(Value::Bytes(b)) => b.clone(),
1888                _ => return Err("http.json_body: HttpResponse.body must be Bytes".into()),
1889            };
1890            let s = match std::str::from_utf8(&body) {
1891                Ok(s) => s,
1892                Err(e) => return Ok(http_decode_err_pure(format!("body not UTF-8: {e}"))),
1893            };
1894            match serde_json::from_str::<serde_json::Value>(s) {
1895                Ok(j) => Ok(ok_v(Value::from_json(&j))),
1896                Err(e) => Ok(http_decode_err_pure(format!("json parse: {e}"))),
1897            }
1898        }
1899        // Compiler-emitted typed variant of http.json_body (#684): the
1900        // type-checker rewrite injects the required-field list and the type
1901        // schema derived from T (when T is a record), so a missing or
1902        // wrong-typed field surfaces as a DecodeError instead of a later
1903        // field-access panic — the same guarantee json.parse_strict gives,
1904        // now on the most common API-decode path. Errors are HttpError
1905        // (via http_decode_err_pure), matching json_body's error type.
1906        ("http", "json_body_typed") => {
1907            let resp = expect_record_pure(args.first())?;
1908            let required = required_field_names(args.get(1))?;
1909            let schema = extract_type_schema(args.get(2));
1910            let body = match resp.get("body") {
1911                Some(Value::Bytes(b)) => b.clone(),
1912                _ => return Err("http.json_body: HttpResponse.body must be Bytes".into()),
1913            };
1914            let s = match std::str::from_utf8(&body) {
1915                Ok(s) => s,
1916                Err(e) => return Ok(http_decode_err_pure(format!("body not UTF-8: {e}"))),
1917            };
1918            match serde_json::from_str::<serde_json::Value>(s) {
1919                Ok(j) => {
1920                    if let Err(e) = check_required_fields(&j, &required) {
1921                        return Ok(http_decode_err_pure(e));
1922                    }
1923                    if let Err(e) = validate_field_types(&j, &schema) {
1924                        return Ok(http_decode_err_pure(e));
1925                    }
1926                    Ok(ok_v(apply_option_wrapping(json_to_value(&j), &j, &schema)))
1927                }
1928                Err(e) => Ok(http_decode_err_pure(format!("json parse: {e}"))),
1929            }
1930        }
1931        ("http", "text_body") => {
1932            let resp = expect_record_pure(args.first())?;
1933            let body = match resp.get("body") {
1934                Some(Value::Bytes(b)) => b.clone(),
1935                _ => return Err("http.text_body: HttpResponse.body must be Bytes".into()),
1936            };
1937            match String::from_utf8(body) {
1938                Ok(s) => Ok(ok_v(Value::Str(s.into()))),
1939                Err(e) => Ok(http_decode_err_pure(format!("body not UTF-8: {e}"))),
1940            }
1941        }
1942
1943        // -- std.cli (Rubric port): argparse-equivalent for end-user
1944        // programs. Specs are tagged Json values; the parser walks
1945        // argv against the spec and returns a CliParsed Json record.
1946        ("cli", "flag") => {
1947            let name = expect_str(args.first())?;
1948            let short = opt_str(args.get(1));
1949            let help = expect_str(args.get(2))?;
1950            Ok(value_from_json(crate::cli::flag_spec(&name, short.as_deref(), &help)))
1951        }
1952        ("cli", "option") => {
1953            let name = expect_str(args.first())?;
1954            let short = opt_str(args.get(1));
1955            let help = expect_str(args.get(2))?;
1956            let default = opt_str(args.get(3));
1957            Ok(value_from_json(crate::cli::option_spec(&name, short.as_deref(), &help, default.as_deref())))
1958        }
1959        ("cli", "positional") => {
1960            let name = expect_str(args.first())?;
1961            let help = expect_str(args.get(1))?;
1962            let required = expect_bool(args.get(2))?;
1963            Ok(value_from_json(crate::cli::positional_spec(&name, &help, required)))
1964        }
1965        ("cli", "spec") => {
1966            let name = expect_str(args.first())?;
1967            let help = expect_str(args.get(1))?;
1968            let arg_specs: Vec<serde_json::Value> = expect_list(args.get(2))?
1969                .iter().map(value_to_json).collect();
1970            let subs: Vec<serde_json::Value> = expect_list(args.get(3))?
1971                .iter().map(value_to_json).collect();
1972            Ok(value_from_json(crate::cli::build_spec(&name, &help, arg_specs, subs)))
1973        }
1974        ("cli", "parse") => {
1975            let spec = value_to_json(args.first().unwrap_or(&Value::Unit));
1976            let argv: Vec<String> = expect_list(args.get(1))?
1977                .iter().map(|v| match v {
1978                    Value::Str(s) => Ok(s.to_string()),
1979                    other => Err(format!("cli.parse: argv must be List[Str], got {other:?}")),
1980                }).collect::<Result<_, _>>()?;
1981            match crate::cli::parse(&spec, &argv) {
1982                Ok(parsed) => Ok(ok_v(value_from_json(parsed))),
1983                Err(msg) => Ok(err_v(Value::Str(msg.into()))),
1984            }
1985        }
1986        ("cli", "envelope") => {
1987            let ok = expect_bool(args.first())?;
1988            let cmd = expect_str(args.get(1))?;
1989            let data = value_to_json(args.get(2).unwrap_or(&Value::Unit));
1990            Ok(value_from_json(crate::cli::envelope(ok, &cmd, data)))
1991        }
1992        ("cli", "describe") => {
1993            let spec = value_to_json(args.first().unwrap_or(&Value::Unit));
1994            Ok(value_from_json(crate::cli::describe(&spec)))
1995        }
1996        ("cli", "help") => {
1997            let spec = value_to_json(args.first().unwrap_or(&Value::Unit));
1998            Ok(Value::Str(crate::cli::help_text(&spec).into()))
1999        }
2000
2001        // -- arrow -- delegated to a dedicated module (#426)
2002        ("arrow", op) => match crate::arrow::dispatch(op, args) {
2003            Some(r) => r,
2004            None => Err(format!("unknown pure builtin: arrow.{op}")),
2005        },
2006        // -- df -- Polars-backed query ops (#427), gated behind the
2007        // `df` feature so embedders that don't need dataframes avoid
2008        // the polars dep tree.
2009        #[cfg(feature = "df")]
2010        ("df", op) => match crate::df::dispatch(op, args) {
2011            Some(r) => r,
2012            None => Err(format!("unknown pure builtin: df.{op}")),
2013        },
2014        #[cfg(not(feature = "df"))]
2015        ("df", op) => Err(format!(
2016            "df.{op}: this build was compiled without the `df` feature; \
2017             Polars-backed dataframe query ops are unavailable"
2018        )),
2019
2020        // -- std.decimal (#574): exact scaled-integer decimal arithmetic.
2021        // Decimal values are `{ coefficient :: Int, exponent :: Int }` records
2022        // representing `coefficient × 10^exponent`. All arithmetic is exact
2023        // (no IEEE 754 rounding); precision loss happens only at `round_to`,
2024        // which requires an explicit rounding mode string.
2025
2026        ("decimal", "decimal") => {
2027            let coef = expect_int(args.first())?;
2028            let exp  = expect_int(args.get(1))?;
2029            Ok(make_decimal(coef, exp))
2030        }
2031        ("decimal", "zero") => Ok(make_decimal(0, 0)),
2032        ("decimal", "one")  => Ok(make_decimal(1, 0)),
2033        ("decimal", "from_int") => {
2034            Ok(make_decimal(expect_int(args.first())?, 0))
2035        }
2036        ("decimal", "pow10") => {
2037            Ok(Value::Int(decimal_pow10(expect_int(args.first())?)?))
2038        }
2039        ("decimal", "add") => {
2040            let (ca, ea) = expect_decimal(args.first())?;
2041            let (cb, eb) = expect_decimal(args.get(1))?;
2042            let (a2, b2, e) = decimal_align(ca, ea, cb, eb)?;
2043            Ok(make_decimal(
2044                a2.checked_add(b2).ok_or("decimal.add: overflow")?, e))
2045        }
2046        ("decimal", "sub") => {
2047            let (ca, ea) = expect_decimal(args.first())?;
2048            let (cb, eb) = expect_decimal(args.get(1))?;
2049            let (a2, b2, e) = decimal_align(ca, ea, cb, eb)?;
2050            Ok(make_decimal(
2051                a2.checked_sub(b2).ok_or("decimal.sub: overflow")?, e))
2052        }
2053        ("decimal", "mul") => {
2054            let (ca, ea) = expect_decimal(args.first())?;
2055            let (cb, eb) = expect_decimal(args.get(1))?;
2056            Ok(make_decimal(
2057                ca.checked_mul(cb).ok_or("decimal.mul: overflow")?,
2058                ea.checked_add(eb).ok_or("decimal.mul: exponent overflow")?,
2059            ))
2060        }
2061        ("decimal", "compare") => {
2062            let (ca, ea) = expect_decimal(args.first())?;
2063            let (cb, eb) = expect_decimal(args.get(1))?;
2064            let (a2, b2, _) = decimal_align(ca, ea, cb, eb)?;
2065            Ok(Value::Int(if a2 < b2 { -1 } else if a2 > b2 { 1 } else { 0 }))
2066        }
2067        ("decimal", "is_zero")     => {
2068            let (c, _) = expect_decimal(args.first())?;
2069            Ok(Value::Bool(c == 0))
2070        }
2071        ("decimal", "is_positive") => {
2072            let (c, _) = expect_decimal(args.first())?;
2073            Ok(Value::Bool(c > 0))
2074        }
2075        ("decimal", "is_negative") => {
2076            let (c, _) = expect_decimal(args.first())?;
2077            Ok(Value::Bool(c < 0))
2078        }
2079        ("decimal", "negate") => {
2080            let (c, e) = expect_decimal(args.first())?;
2081            Ok(make_decimal(-c, e))
2082        }
2083        ("decimal", "abs") => {
2084            let (c, e) = expect_decimal(args.first())?;
2085            Ok(make_decimal(c.abs(), e))
2086        }
2087        ("decimal", "normalize") => {
2088            let (mut c, mut e) = expect_decimal(args.first())?;
2089            if c == 0 { return Ok(make_decimal(0, 0)); }
2090            while c % 10 == 0 { c /= 10; e += 1; }
2091            Ok(make_decimal(c, e))
2092        }
2093        ("decimal", "round_to") => {
2094            let (c, e)   = expect_decimal(args.first())?;
2095            let target_e = expect_int(args.get(1))?;
2096            let mode     = expect_str(args.get(2))?;
2097            Ok(make_decimal(decimal_round(c, e, target_e, &mode)?, target_e))
2098        }
2099        ("decimal", "to_str") => {
2100            let (c, e) = expect_decimal(args.first())?;
2101            Ok(Value::Str(decimal_to_str(c, e)?.into()))
2102        }
2103
2104        _ => Err(format!("unknown pure builtin: {kind}.{op}")),
2105    }
2106}
2107
2108// -- std.decimal helpers (#574) ------------------------------------------
2109
2110/// Extract `(coefficient, exponent)` from a `Decimal` record value.
2111fn expect_decimal(v: Option<&Value>) -> Result<(i64, i64), String> {
2112    match v {
2113        Some(Value::Record { fields, .. }) => {
2114            let coef = match fields.get("coefficient") {
2115                Some(Value::Int(n)) => *n,
2116                _ => return Err("decimal: missing or invalid 'coefficient' field".into()),
2117            };
2118            let exp = match fields.get("exponent") {
2119                Some(Value::Int(n)) => *n,
2120                _ => return Err("decimal: missing or invalid 'exponent' field".into()),
2121            };
2122            Ok((coef, exp))
2123        }
2124        Some(other) => Err(format!("decimal: expected {{ coefficient, exponent }} record, got {other:?}")),
2125        None => Err("decimal: missing argument".into()),
2126    }
2127}
2128
2129/// Build a `Decimal` `Value::Record`.
2130fn make_decimal(coefficient: i64, exponent: i64) -> Value {
2131    let mut fields = indexmap::IndexMap::new();
2132    fields.insert("coefficient".into(), Value::Int(coefficient));
2133    fields.insert("exponent".into(), Value::Int(exponent));
2134    Value::record_interned(fields)
2135}
2136
2137/// 10^n for n in [0, 18]. Returns an error outside that range.
2138fn decimal_pow10(n: i64) -> Result<i64, String> {
2139    if n < 0  { return Err(format!("decimal.pow10: negative exponent {n}")); }
2140    if n > 18 { return Err(format!("decimal.pow10: exponent {n} exceeds max (18)")); }
2141    Ok(10i64.pow(n as u32))
2142}
2143
2144/// Bring two Decimals to the same exponent.
2145/// Returns `(coef_a_aligned, coef_b_aligned, common_exponent)`.
2146fn decimal_align(ca: i64, ea: i64, cb: i64, eb: i64) -> Result<(i64, i64, i64), String> {
2147    if ea == eb { return Ok((ca, cb, ea)); }
2148    if ea > eb {
2149        let scale = decimal_pow10(ea - eb)?;
2150        let ca2 = ca.checked_mul(scale)
2151            .ok_or_else(|| format!("decimal: overflow aligning (shift {})", ea - eb))?;
2152        Ok((ca2, cb, eb))
2153    } else {
2154        let scale = decimal_pow10(eb - ea)?;
2155        let cb2 = cb.checked_mul(scale)
2156            .ok_or_else(|| format!("decimal: overflow aligning (shift {})", eb - ea))?;
2157        Ok((ca, cb2, ea))
2158    }
2159}
2160
2161/// Compute the rounded coefficient when scaling `c × 10^e` to `target_e`.
2162/// `target_e > e` (we're reducing precision): divides by `10^(target_e - e)`
2163/// and applies `mode`. When `target_e <= e` (gaining precision) multiplies
2164/// exactly — no rounding needed.
2165fn decimal_round(c: i64, e: i64, target_e: i64, mode: &str) -> Result<i64, String> {
2166    if e >= target_e {
2167        // Gaining precision (or staying equal) — exact, no rounding.
2168        let shift = e - target_e;
2169        let scale = decimal_pow10(shift)?;
2170        return c.checked_mul(scale)
2171            .ok_or_else(|| format!("decimal.round_to: overflow scaling (shift {shift})"));
2172    }
2173    // Losing precision — divide and round.
2174    let shift   = target_e - e;
2175    let divisor = decimal_pow10(shift)?;
2176    let q = c / divisor;
2177    let r = c % divisor;  // same sign as c (Rust truncation toward zero)
2178
2179    if r == 0 { return Ok(q); }
2180
2181    let abs_r   = r.abs();
2182    let positive = r > 0; // sign of the original value when q is near zero
2183
2184    let rounded = match mode {
2185        "Down"     => q,
2186        "Up"       => if positive { q + 1 } else { q - 1 },
2187        "Floor"    => if positive { q }     else { q - 1 },
2188        "Ceiling"  => if positive { q + 1 } else { q },
2189        "HalfUp"   => {
2190            if abs_r * 2 >= divisor {
2191                if positive { q + 1 } else { q - 1 }
2192            } else { q }
2193        }
2194        "HalfDown" => {
2195            if abs_r * 2 > divisor {
2196                if positive { q + 1 } else { q - 1 }
2197            } else { q }
2198        }
2199        "HalfEven" => {
2200            if abs_r * 2 > divisor {
2201                if positive { q + 1 } else { q - 1 }
2202            } else if abs_r * 2 == divisor {
2203                // Round to nearest even (banker's rounding)
2204                if q % 2 == 0 { q } else { if positive { q + 1 } else { q - 1 } }
2205            } else { q }
2206        }
2207        other => return Err(format!(
2208            "decimal.round_to: unknown rounding mode {other:?}; \
2209             valid modes: HalfUp HalfDown HalfEven Down Up Ceiling Floor")),
2210    };
2211    Ok(rounded)
2212}
2213
2214/// Format a Decimal as a decimal-notation string.
2215/// e.g. `(12345, -2)` → `"123.45"`, `(7, 2)` → `"700"`, `(-63, -2)` → `"-0.63"`.
2216fn decimal_to_str(c: i64, e: i64) -> Result<String, String> {
2217    if e == 0 { return Ok(c.to_string()); }
2218    if e > 0 {
2219        let scale = decimal_pow10(e)?;
2220        let val   = c.checked_mul(scale)
2221            .ok_or("decimal.to_str: overflow")?;
2222        return Ok(val.to_string());
2223    }
2224    // e < 0: render fractional digits
2225    let scale          = decimal_pow10(-e)?;
2226    let sign           = if c < 0 { "-" } else { "" };
2227    let abs_c          = c.abs();
2228    let int_part       = abs_c / scale;
2229    let frac_part      = abs_c % scale;
2230    let decimal_places = (-e) as usize;
2231    Ok(format!("{sign}{int_part}.{frac_part:0>decimal_places$}"))
2232}
2233
2234/// Extract `Option[Str]` arg as `Option<String>`. None and missing
2235/// arg both map to `None`. Used by the `cli` builders so callers can
2236/// pass `option.none()` or `Some("v")` interchangeably.
2237fn opt_str(arg: Option<&Value>) -> Option<String> {
2238    match arg {
2239        Some(Value::Variant { name, args }) if name == "Some" => {
2240            args.first().and_then(|v| match v {
2241                Value::Str(s) => Some(s.to_string()),
2242                _ => None,
2243            })
2244        }
2245        _ => None,
2246    }
2247}
2248
2249fn value_from_json(v: serde_json::Value) -> Value { Value::from_json(&v) }
2250
2251/// Process-wide cache of compiled regexes, keyed by the pattern
2252/// string. Compilation is the only cost we want to amortize; matching
2253/// the same `Regex` from multiple threads is safe (`regex::Regex` is
2254/// `Send + Sync`).
2255fn regex_cache() -> &'static Mutex<HashMap<String, regex::Regex>> {
2256    static CACHE: OnceLock<Mutex<HashMap<String, regex::Regex>>> = OnceLock::new();
2257    CACHE.get_or_init(|| Mutex::new(HashMap::new()))
2258}
2259
2260fn get_or_compile_regex(pattern: &str) -> Result<regex::Regex, String> {
2261    let cache = regex_cache();
2262    {
2263        let guard = cache.lock().unwrap();
2264        if let Some(re) = guard.get(pattern) {
2265            return Ok(re.clone());
2266        }
2267    }
2268    let re = regex::Regex::new(pattern).map_err(|e| format!("invalid regex: {e}"))?;
2269    let mut guard = cache.lock().unwrap();
2270    guard.insert(pattern.to_string(), re.clone());
2271    Ok(re)
2272}
2273
2274/// Build a `Match` record value: `{ text, start, end, groups }` where
2275/// `groups` is the captured groups in order (group 0 is the full match).
2276/// Missing optional groups become empty strings.
2277fn match_value(caps: &regex::Captures) -> Value {
2278    let m0 = caps.get(0).expect("regex match always has group 0");
2279    let mut rec = indexmap::IndexMap::new();
2280    rec.insert("text".into(), Value::Str(m0.as_str().into()));
2281    rec.insert("start".into(), Value::Int(m0.start() as i64));
2282    rec.insert("end".into(), Value::Int(m0.end() as i64));
2283    let groups: std::collections::VecDeque<Value> = (1..caps.len())
2284        .map(|i| {
2285            Value::Str(
2286                caps.get(i)
2287                    .map(|m| m.as_str())
2288                    .unwrap_or_default()
2289                    .into(),
2290            )
2291        })
2292        .collect();
2293    rec.insert("groups".into(), Value::List(groups));
2294    Value::record_dynamic(rec)
2295}
2296
2297fn expect_map(v: Option<&Value>) -> Result<&BTreeMap<MapKey, Value>, String> {
2298    match v {
2299        Some(Value::Map(m)) => Ok(m),
2300        other => Err(format!("expected Map, got {other:?}")),
2301    }
2302}
2303
2304fn expect_set(v: Option<&Value>) -> Result<&BTreeSet<MapKey>, String> {
2305    match v {
2306        Some(Value::Set(s)) => Ok(s),
2307        other => Err(format!("expected Set, got {other:?}")),
2308    }
2309}
2310
2311/// Unpack any matrix-shaped Value into (rows, cols, flat row-major data).
2312/// Accepts the F64Array fast lane and the legacy `Record { rows, cols,
2313/// data: List[Float] }` shape for compatibility with hand-built matrices.
2314fn unpack_matrix(v: &Value) -> Result<(usize, usize, Vec<f64>), String> {
2315    if let Value::F64Array { rows, cols, data } = v {
2316        return Ok((*rows as usize, *cols as usize, data.clone()));
2317    }
2318    let rec = match v {
2319        Value::Record { fields: r, .. } => r,
2320        other => return Err(format!("expected matrix, got {other:?}")),
2321    };
2322    let rows = match rec.get("rows") {
2323        Some(Value::Int(n)) => *n as usize,
2324        _ => return Err("matrix: missing/invalid `rows`".into()),
2325    };
2326    let cols = match rec.get("cols") {
2327        Some(Value::Int(n)) => *n as usize,
2328        _ => return Err("matrix: missing/invalid `cols`".into()),
2329    };
2330    let data = match rec.get("data") {
2331        Some(Value::List(items)) => {
2332            let mut out = Vec::with_capacity(items.len());
2333            for it in items {
2334                out.push(match it {
2335                    Value::Float(f) => *f,
2336                    Value::Int(n) => *n as f64,
2337                    other => return Err(format!("matrix data: not numeric, got {other:?}")),
2338                });
2339            }
2340            out
2341        }
2342        _ => return Err("matrix: missing/invalid `data`".into()),
2343    };
2344    if data.len() != rows * cols {
2345        return Err(format!("matrix: data len {} != {rows}*{cols}", data.len()));
2346    }
2347    Ok((rows, cols, data))
2348}
2349
2350fn expect_bytes(v: Option<&Value>) -> Result<&Vec<u8>, String> {
2351    match v {
2352        Some(Value::Bytes(b)) => Ok(b),
2353        Some(other) => Err(format!("expected Bytes, got {other:?}")),
2354        None => Err("missing argument".into()),
2355    }
2356}
2357
2358fn first_arg(args: &[Value]) -> Result<&Value, String> {
2359    args.first().ok_or_else(|| "missing argument".into())
2360}
2361
2362fn tuple_index(v: &Value, i: usize) -> Result<Value, String> {
2363    match v {
2364        Value::Tuple(items) => items.get(i).cloned()
2365            .ok_or_else(|| format!("tuple index {i} out of range (len={})", items.len())),
2366        other => Err(format!("expected Tuple, got {other:?}")),
2367    }
2368}
2369
2370fn expect_str(v: Option<&Value>) -> Result<String, String> {
2371    match v {
2372        Some(Value::Str(s)) => Ok(s.to_string()),
2373        Some(other) => Err(format!("expected Str, got {other:?}")),
2374        None => Err("missing argument".into()),
2375    }
2376}
2377
2378fn expect_int(v: Option<&Value>) -> Result<i64, String> {
2379    match v {
2380        Some(Value::Int(n)) => Ok(*n),
2381        Some(other) => Err(format!("expected Int, got {other:?}")),
2382        None => Err("missing argument".into()),
2383    }
2384}
2385
2386fn expect_float(v: Option<&Value>) -> Result<f64, String> {
2387    match v {
2388        Some(Value::Float(f)) => Ok(*f),
2389        Some(other) => Err(format!("expected Float, got {other:?}")),
2390        None => Err("missing argument".into()),
2391    }
2392}
2393
2394fn expect_list(v: Option<&Value>) -> Result<&std::collections::VecDeque<Value>, String> {
2395    match v {
2396        Some(Value::List(xs)) => Ok(xs),
2397        Some(other) => Err(format!("expected List, got {other:?}")),
2398        None => Err("missing argument".into()),
2399    }
2400}
2401
2402fn expect_bool(v: Option<&Value>) -> Result<bool, String> {
2403    match v {
2404        Some(Value::Bool(b)) => Ok(*b),
2405        Some(other) => Err(format!("expected Bool, got {other:?}")),
2406        None => Err("missing argument".into()),
2407    }
2408}
2409
2410fn expect_deque(v: Option<&Value>) -> Result<&std::collections::VecDeque<Value>, String> {
2411    match v {
2412        Some(Value::Deque(d)) => Ok(d),
2413        Some(other) => Err(format!("expected Deque, got {other:?}")),
2414        None => Err("missing argument".into()),
2415    }
2416}
2417
2418fn some(v: Value) -> Value { Value::Variant { name: "Some".into(), args: vec![v] } }
2419fn none() -> Value { Value::Variant { name: "None".into(), args: Vec::new() } }
2420fn ok_v(v: Value) -> Value { Value::Variant { name: "Ok".into(), args: vec![v] } }
2421fn err_v(v: Value) -> Value { Value::Variant { name: "Err".into(), args: vec![v] } }
2422
2423// -- std.parser helpers (#217) ----------------------------------------
2424
2425/// Construct a tagged parser-AST node. The runtime representation is
2426/// `{ kind: "Char" | "Seq" | ..., ...children }`; the type system
2427/// treats these as opaque `Parser[T]` so user code can't poke at the
2428/// fields. Encoding is canonical because `IndexMap` insertion order
2429/// is stable and we always insert `kind` first.
2430fn parser_node(kind: &str, fields: &[(&str, Value)]) -> Value {
2431    let mut r = indexmap::IndexMap::new();
2432    r.insert("kind".into(), Value::Str(kind.into()));
2433    for (k, v) in fields {
2434        r.insert((*k).into(), v.clone());
2435    }
2436    Value::record_dynamic(r)
2437}
2438
2439// `parser.run` interpretation lives in `lex-bytecode::parser_runtime`
2440// (#221) — it needs reentrant Vm access to invoke closures inside
2441// `Map` / `AndThen` nodes, which the pure-builtin path doesn't have.
2442
2443// -- std.random helpers (#219) ----------------------------------------
2444
2445/// SplitMix64 — single-`u64` state PRNG that is byte-identical
2446/// across platforms (no float math, no platform-dependent reductions).
2447/// Returns `(drawn, next_state)`. Constants are the canonical
2448/// SplitMix64 mixer from the original 2014 paper.
2449fn splitmix64(state: u64) -> (u64, u64) {
2450    let next = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
2451    let mut z = next;
2452    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2453    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2454    let z = z ^ (z >> 31);
2455    (z, next)
2456}
2457
2458/// Encode a SplitMix64 state as the user-facing `Rng` value.
2459/// `Rng = { state :: Int }`; the type-checker treats `Rng` as
2460/// opaque so users can't poke at the field.
2461fn rng_value(state: u64) -> Value {
2462    let mut fields = indexmap::IndexMap::new();
2463    fields.insert("state".into(), Value::Int(state as i64));
2464    Value::record_dynamic(fields)
2465}
2466
2467/// Pull the SplitMix64 state out of a `Value::Record { state }`.
2468fn rng_decode(v: Option<&Value>) -> Result<u64, String> {
2469    let rec = match v {
2470        Some(Value::Record { fields: r, .. }) => r,
2471        Some(other) => return Err(format!("expected Rng, got {other:?}")),
2472        None => return Err("missing Rng arg".into()),
2473    };
2474    match rec.get("state") {
2475        Some(Value::Int(n)) => Ok(*n as u64),
2476        _ => Err("malformed Rng: missing `state :: Int`".into()),
2477    }
2478}
2479
2480// -- helpers for `std.http` builders / decoders --
2481
2482fn expect_record_pure(v: Option<&Value>) -> Result<&indexmap::IndexMap<smol_str::SmolStr, Value>, String> {
2483    match v {
2484        Some(Value::Record { fields: r, .. }) => Ok(r),
2485        Some(other) => Err(format!("expected Record, got {other:?}")),
2486        None => Err("missing Record argument".into()),
2487    }
2488}
2489
2490fn http_decode_err_pure(msg: String) -> Value {
2491    let inner = Value::Variant {
2492        name: "DecodeError".into(),
2493        args: vec![Value::Str(msg.into())],
2494    };
2495    err_v(inner)
2496}
2497
2498/// Apply or replace a header in an `HttpRequest` record's `headers`
2499/// field. Header names are normalized to lowercase to match HTTP/1.1
2500/// case-insensitivity; an existing entry under any casing is
2501/// overwritten by the new value.
2502fn http_set_header(
2503    mut req: indexmap::IndexMap<smol_str::SmolStr, Value>,
2504    name: &str,
2505    value: &str,
2506) -> indexmap::IndexMap<smol_str::SmolStr, Value> {
2507    use lex_bytecode::MapKey;
2508    let mut headers = match req.shift_remove("headers") {
2509        Some(Value::Map(m)) => m,
2510        _ => std::collections::BTreeMap::new(),
2511    };
2512    let key = MapKey::Str(name.to_lowercase());
2513    // Drop any case variant of the same header name first so casing
2514    // flips don't accumulate duplicates.
2515    let lowered = name.to_lowercase();
2516    headers.retain(|k, _| match k {
2517        MapKey::Str(s) => s.to_lowercase() != lowered,
2518        _ => true,
2519    });
2520    headers.insert(key, Value::Str(value.into()));
2521    req.insert("headers".into(), Value::Map(headers));
2522    req
2523}
2524
2525/// Append `?k=v&...` (URL-encoded) to the `url` field of an
2526/// `HttpRequest` record. Existing query string is preserved and
2527/// extended with `&`. Iteration order is the input map's natural
2528/// order (`BTreeMap` → sorted by key) so the produced URL is
2529/// deterministic.
2530fn http_append_query(
2531    mut req: indexmap::IndexMap<smol_str::SmolStr, Value>,
2532    params: &std::collections::BTreeMap<lex_bytecode::MapKey, Value>,
2533) -> indexmap::IndexMap<smol_str::SmolStr, Value> {
2534    use lex_bytecode::MapKey;
2535    let url = match req.get("url") {
2536        Some(Value::Str(s)) => s.clone(),
2537        _ => return req,
2538    };
2539    let mut pieces = Vec::new();
2540    for (k, v) in params {
2541        let kk = match k { MapKey::Str(s) => s.to_string(), _ => continue };
2542        let vv = match v { Value::Str(s) => s.to_string(), _ => continue };
2543        pieces.push(format!("{}={}", url_encode(&kk), url_encode(&vv)));
2544    }
2545    if pieces.is_empty() { return req; }
2546    let sep = if url.contains('?') { '&' } else { '?' };
2547    let new_url = format!("{url}{sep}{}", pieces.join("&"));
2548    req.insert("url".into(), Value::Str(new_url.into()));
2549    req
2550}
2551
2552/// Minimal RFC-3986 percent-encode for `application/x-www-form-
2553/// urlencoded` query values. Pulling in `urlencoding` for one
2554/// callsite would drag a dep into the runtime; the inline version is
2555/// short and easy to audit.
2556fn url_encode(s: &str) -> String {
2557    let mut out = String::with_capacity(s.len());
2558    for b in s.bytes() {
2559        match b {
2560            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
2561                out.push(b as char);
2562            }
2563            _ => out.push_str(&format!("%{:02X}", b)),
2564        }
2565    }
2566    out
2567}
2568
2569fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
2570
2571/// The `toml` crate's serde adapter wraps datetimes in a sentinel
2572/// object `{"$__toml_private_datetime": "<rfc3339>"}` so that the
2573/// `Datetime` type round-trips through `serde::Value`. For Lex's
2574/// purposes a plain RFC-3339 string is what we want — callers can
2575/// then pipe through `datetime.parse_iso` if they need an
2576/// `Instant`. Walk the tree and replace each wrapper with its
2577/// inner string, in-place.
2578fn unwrap_toml_datetime_markers(v: &mut serde_json::Value) {
2579    use serde_json::Value as J;
2580    match v {
2581        J::Object(map) => {
2582            // Detect single-key marker objects and replace them
2583            // with their inner string. We have to take care to
2584            // avoid borrow conflicts.
2585            if map.len() == 1 {
2586                if let Some(J::String(s)) = map.get("$__toml_private_datetime") {
2587                    let s = s.clone();
2588                    *v = J::String(s);
2589                    return;
2590                }
2591            }
2592            for (_, child) in map.iter_mut() {
2593                unwrap_toml_datetime_markers(child);
2594            }
2595        }
2596        J::Array(items) => {
2597            for item in items.iter_mut() {
2598                unwrap_toml_datetime_markers(item);
2599            }
2600        }
2601        _ => {}
2602    }
2603}
2604
2605fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
2606
2607/// Extract the `List[Str]` of required field names from the second
2608/// argument of `*.parse_strict`. The list is allowed to be empty
2609/// (the parse degenerates to plain `parse`); other shapes are a
2610/// caller bug rather than a parse error.
2611fn required_field_names(arg: Option<&Value>) -> Result<Vec<String>, String> {
2612    let list = expect_list(arg)?;
2613    let mut out = Vec::with_capacity(list.len());
2614    for v in list {
2615        match v {
2616            Value::Str(s) => out.push(s.to_string()),
2617            other => return Err(format!(
2618                "parse_strict: required-fields list must contain Str, got {other:?}"
2619            )),
2620        }
2621    }
2622    Ok(out)
2623}
2624
2625/// Verify that `value` is an object containing every entry in
2626/// `required`. A required entry may be a plain field name (must
2627/// exist at the top level) or a dotted path (`"project.license"`)
2628/// which descends through nested objects. Returns a stable,
2629/// human-readable error listing every missing path so the agent's
2630/// verifier can surface it directly.
2631///
2632/// Tactical fix for #168 — gives users a way to make `parse[T]`
2633/// errors propagate as `Result::Err` instead of as runtime
2634/// `GetField` errors at access time. The full type-driven fix
2635/// (deriving `required` from `T` at type-check time so plain
2636/// `parse[T]` works, including auto-wrapping `Option[F]` fields
2637/// as not-required) is the cleaner endgame; see #168.
2638///
2639/// Path semantics:
2640/// * `"name"` → top-level `name` must be present (any value).
2641/// * `"a.b.c"` → walk `a`, then `b`, then check `c` exists. Each
2642///   intermediate value must itself be an object.
2643/// * `\\.` is the literal-dot escape (e.g. `"weird\\.key"` for a
2644///   field that genuinely contains a dot in its name).
2645fn check_required_fields(
2646    value: &serde_json::Value,
2647    required: &[String],
2648) -> Result<(), String> {
2649    if required.is_empty() {
2650        return Ok(());
2651    }
2652    if !matches!(value, serde_json::Value::Object(_)) {
2653        return Err(format!(
2654            "parse_strict: expected top-level object with fields {:?}, got {value}",
2655            required
2656        ));
2657    }
2658    let mut missing: Vec<String> = Vec::new();
2659    for path in required {
2660        if !path_exists(value, path) {
2661            missing.push(path.clone());
2662        }
2663    }
2664    if missing.is_empty() {
2665        Ok(())
2666    } else {
2667        Err(format!("missing required field(s): {}", missing.join(", ")))
2668    }
2669}
2670
2671/// Walk `value` along the dotted `path` and report whether the
2672/// terminal segment exists. Intermediate non-object stops surface
2673/// as "missing" — a path can't traverse through a string, list, or
2674/// scalar.
2675fn path_exists(value: &serde_json::Value, path: &str) -> bool {
2676    let mut cursor = value;
2677    let segments = split_dotted_path(path);
2678    for seg in &segments {
2679        match cursor {
2680            serde_json::Value::Object(o) => match o.get(seg.as_str()) {
2681                Some(next) => cursor = next,
2682                None => return false,
2683            },
2684            _ => return false,
2685        }
2686    }
2687    true
2688}
2689
2690/// Split `"a.b.c"` into `["a", "b", "c"]`, with `\.` recognised
2691/// as a literal-dot escape so legitimate dotted field names
2692/// (e.g. `"package\.json"`) don't accidentally start a descent.
2693fn split_dotted_path(path: &str) -> Vec<String> {
2694    let mut out: Vec<String> = Vec::new();
2695    let mut cur = String::new();
2696    let mut iter = path.chars().peekable();
2697    while let Some(c) = iter.next() {
2698        if c == '\\' {
2699            // Backslash at end is preserved; only `\.` is special.
2700            if let Some(&'.') = iter.peek() {
2701                cur.push('.');
2702                iter.next();
2703                continue;
2704            }
2705            cur.push(c);
2706        } else if c == '.' {
2707            out.push(std::mem::take(&mut cur));
2708        } else {
2709            cur.push(c);
2710        }
2711    }
2712    out.push(cur);
2713    out
2714}
2715
2716/// Extract the `List[(Str, Str)]` type schema from the third argument
2717/// of `*.parse_strict` (#322). If the argument is absent or malformed,
2718/// returns an empty vec — callers treat that as "skip type validation".
2719fn extract_type_schema(v: Option<&Value>) -> Vec<(String, String)> {
2720    match v {
2721        Some(Value::List(pairs)) => pairs.iter().filter_map(|p| {
2722            if let Value::Tuple(items) = p {
2723                if items.len() == 2 {
2724                    if let (Value::Str(name), Value::Str(tag)) = (&items[0], &items[1]) {
2725                        return Some((name.to_string(), tag.to_string()));
2726                    }
2727                }
2728            }
2729            None
2730        }).collect(),
2731        _ => vec![],
2732    }
2733}
2734
2735/// Validate each field in `json` against its declared type tag from
2736/// the schema. Returns `Err` for the first field whose JSON value
2737/// doesn't match its tag. Fields not present in the JSON object are
2738/// silently skipped (presence is enforced separately by
2739/// `check_required_fields`).
2740fn validate_field_types(
2741    json: &serde_json::Value,
2742    schema: &[(String, String)],
2743) -> Result<(), String> {
2744    if schema.is_empty() {
2745        return Ok(());
2746    }
2747    let obj = match json.as_object() {
2748        Some(o) => o,
2749        None => return Ok(()), // not an object — let other validation handle it
2750    };
2751    for (field, tag) in schema {
2752        if let Some(val) = obj.get(field) {
2753            if let Err(e) = check_json_type(val, tag) {
2754                return Err(format!("field `{field}`: {e}"));
2755            }
2756        }
2757    }
2758    Ok(())
2759}
2760
2761/// Post-process a Record produced by `json_to_value` to correctly wrap
2762/// `Option[X]` fields. `json_to_value` is schema-blind: it converts JSON null
2763/// to `Value::Unit` and never wraps non-null values in `some(...)`. This pass
2764/// fixes that for every field declared as `Option[X]` in the type schema.
2765fn apply_option_wrapping(v: Value, json: &serde_json::Value, schema: &[(String, String)]) -> Value {
2766    if schema.is_empty() {
2767        return v;
2768    }
2769    let fields = match v {
2770        Value::Record { fields, .. } => *fields,
2771        other => return other,
2772    };
2773    let json_obj = match json.as_object() {
2774        Some(o) => o,
2775        None => return Value::record_interned(fields),
2776    };
2777    let mut new_fields = fields;
2778    for (field_name, tag) in schema {
2779        if tag.starts_with("Option[") && tag.ends_with(']') {
2780            let json_val = json_obj.get(field_name.as_str());
2781            let wrapped = match json_val {
2782                None | Some(serde_json::Value::Null) => none(),
2783                Some(_) => {
2784                    let inner = new_fields
2785                        .get(field_name.as_str())
2786                        .cloned()
2787                        .unwrap_or(Value::Unit);
2788                    some(inner)
2789                }
2790            };
2791            new_fields.insert(smol_str::SmolStr::from(field_name.as_str()), wrapped);
2792        }
2793    }
2794    Value::record_interned(new_fields)
2795}
2796
2797/// Recursively check that `val` conforms to the compact type `tag`.
2798fn check_json_type(val: &serde_json::Value, tag: &str) -> Result<(), String> {
2799    use serde_json::Value as J;
2800    match (tag, val) {
2801        ("Int", J::Number(n)) if n.is_i64() || n.is_u64() => Ok(()),
2802        ("Int", other) => Err(format!("expected Int, got {}", json_type_name(other))),
2803        ("Float", J::Number(_)) => Ok(()),
2804        ("Float", other) => Err(format!("expected Float, got {}", json_type_name(other))),
2805        ("Bool", J::Bool(_)) => Ok(()),
2806        ("Bool", other) => Err(format!("expected Bool, got {}", json_type_name(other))),
2807        ("Str", J::String(_)) => Ok(()),
2808        ("Str", other) => Err(format!("expected Str, got {}", json_type_name(other))),
2809        // Option[X]: null maps to None — any null is acceptable
2810        (tag, J::Null) if tag.starts_with("Option[") => Ok(()),
2811        (tag, val) if tag.starts_with("Option[") && tag.ends_with(']') => {
2812            let inner = &tag[7..tag.len() - 1]; // strip "Option[" and "]"
2813            check_json_type(val, inner)
2814        }
2815        // List[X]: validate each element
2816        (tag, J::Array(items)) if tag.starts_with("List[") && tag.ends_with(']') => {
2817            let inner = &tag[5..tag.len() - 1]; // strip "List[" and "]"
2818            for (i, item) in items.iter().enumerate() {
2819                if let Err(e) = check_json_type(item, inner) {
2820                    return Err(format!("[{i}]: {e}"));
2821                }
2822            }
2823            Ok(())
2824        }
2825        ("Record", _) => Ok(()), // opaque nested record — skip deep check
2826        ("Any", _) => Ok(()),    // unknown type — skip
2827        _ => Ok(()),             // unrecognized tag — skip
2828    }
2829}
2830
2831fn json_type_name(v: &serde_json::Value) -> &'static str {
2832    match v {
2833        serde_json::Value::Null => "null",
2834        serde_json::Value::Bool(_) => "Bool",
2835        serde_json::Value::Number(_) => "Number",
2836        serde_json::Value::String(_) => "Str",
2837        serde_json::Value::Array(_) => "Array",
2838        serde_json::Value::Object(_) => "Object",
2839    }
2840}
2841
2842/// Parse a `.env`-style file into key→value pairs. Accepts:
2843///
2844/// * Blank lines and `# comment` lines (ignored).
2845/// * `KEY=VALUE` with no spaces around `=`. Optional surrounding
2846///   `"..."` or `'...'` quotes on the value. No escape sequences,
2847///   no shell expansion — by design; we want this to be a *data*
2848///   parser, not a shell snippet evaluator.
2849///
2850/// Errors carry the offending line number (1-indexed) so the
2851/// agent's verifier can point a human at the right place.
2852fn parse_dotenv(src: &str) -> Result<indexmap::IndexMap<String, String>, String> {
2853    let mut out = indexmap::IndexMap::new();
2854    for (idx, raw) in src.lines().enumerate() {
2855        let line = raw.trim();
2856        if line.is_empty() || line.starts_with('#') {
2857            continue;
2858        }
2859        // Optional `export KEY=VALUE` shell form — accepted for
2860        // compat with files that grew out of `set -a` workflows.
2861        let after_export = line.strip_prefix("export ").unwrap_or(line);
2862        let (k, v) = match after_export.split_once('=') {
2863            Some(kv) => kv,
2864            None => return Err(format!("dotenv.parse line {}: missing `=`", idx + 1)),
2865        };
2866        let key = k.trim();
2867        if key.is_empty() {
2868            return Err(format!("dotenv.parse line {}: empty key", idx + 1));
2869        }
2870        let v_trim = v.trim();
2871        let value = if let Some(q) = v_trim.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
2872            q.to_string()
2873        } else if let Some(q) = v_trim.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
2874            q.to_string()
2875        } else {
2876            v_trim.to_string()
2877        };
2878        out.insert(key.to_string(), value);
2879    }
2880    Ok(out)
2881}
2882
2883// -- datetime helpers (Instant ↔ chrono::DateTime<Utc>) --
2884
2885/// Convert a `chrono::DateTime` (any `TimeZone`) into a Lex `Instant`,
2886/// represented as nanoseconds since the UTC unix epoch. Saturates on
2887/// out-of-range timestamps so the runtime never panics.
2888fn instant_from_chrono<Tz: chrono::TimeZone>(dt: chrono::DateTime<Tz>) -> i64 {
2889    dt.timestamp_nanos_opt().unwrap_or(i64::MAX)
2890}
2891
2892fn chrono_from_instant(n: i64) -> chrono::DateTime<chrono::Utc> {
2893    let secs = n.div_euclid(1_000_000_000);
2894    let nanos = n.rem_euclid(1_000_000_000) as u32;
2895    use chrono::TimeZone;
2896    chrono::Utc
2897        .timestamp_opt(secs, nanos)
2898        .single()
2899        .unwrap_or_else(chrono::Utc::now)
2900}
2901
2902fn format_iso(n: i64) -> String {
2903    chrono_from_instant(n).to_rfc3339()
2904}
2905
2906/// Parsed form of the user-side `Tz` variant. Mirrors the type
2907/// registered in `TypeEnv::new_with_builtins`.
2908enum TzArg {
2909    Utc,
2910    Local,
2911    /// Fixed offset in minutes east of UTC.
2912    Offset(i32),
2913    /// IANA name like `"America/New_York"`.
2914    Iana(String),
2915}
2916
2917fn parse_tz_arg(v: Option<&Value>) -> Result<TzArg, String> {
2918    match v {
2919        Some(Value::Variant { name, args }) => match (name.as_str(), args.as_slice()) {
2920            ("Utc", []) => Ok(TzArg::Utc),
2921            ("Local", []) => Ok(TzArg::Local),
2922            ("Offset", [Value::Int(m)]) => {
2923                let m = i32::try_from(*m).map_err(|_| {
2924                    format!("Tz::Offset: minutes out of range: {m}")
2925                })?;
2926                Ok(TzArg::Offset(m))
2927            }
2928            ("Iana", [Value::Str(s)]) => Ok(TzArg::Iana(s.to_string())),
2929            (other, _) => Err(format!(
2930                "expected Tz variant (Utc | Local | Offset(Int) | Iana(Str)), got `{other}` with {} arg(s)",
2931                args.len()
2932            )),
2933        },
2934        Some(other) => Err(format!("expected Tz variant, got {other:?}")),
2935        None => Err("missing Tz argument".into()),
2936    }
2937}
2938
2939fn resolve_tz_to_components(n: i64, tz: &TzArg) -> Result<Value, String> {
2940    use chrono::{TimeZone, Datelike, Timelike, Offset};
2941    let utc_dt = chrono_from_instant(n);
2942    let (y, m, d, hh, mm, ss, ns, off_min) = match tz {
2943        TzArg::Utc => {
2944            let d = utc_dt;
2945            (d.year(), d.month() as i32, d.day() as i32,
2946             d.hour() as i32, d.minute() as i32, d.second() as i32,
2947             d.nanosecond() as i32, 0)
2948        }
2949        TzArg::Local => {
2950            let d = utc_dt.with_timezone(&chrono::Local);
2951            let off = d.offset().fix().local_minus_utc() / 60;
2952            (d.year(), d.month() as i32, d.day() as i32,
2953             d.hour() as i32, d.minute() as i32, d.second() as i32,
2954             d.nanosecond() as i32, off)
2955        }
2956        TzArg::Offset(off_min) => {
2957            let off_secs = off_min.saturating_mul(60);
2958            let fixed = chrono::FixedOffset::east_opt(off_secs)
2959                .ok_or("to_components: offset out of range")?;
2960            let d = utc_dt.with_timezone(&fixed);
2961            (d.year(), d.month() as i32, d.day() as i32,
2962             d.hour() as i32, d.minute() as i32, d.second() as i32,
2963             d.nanosecond() as i32, *off_min)
2964        }
2965        TzArg::Iana(name) => {
2966            let tz: chrono_tz::Tz = name.parse()
2967                .map_err(|e| format!("to_components: unknown timezone `{name}`: {e}"))?;
2968            let d = utc_dt.with_timezone(&tz);
2969            let off = d.offset().fix().local_minus_utc() / 60;
2970            (d.year(), d.month() as i32, d.day() as i32,
2971             d.hour() as i32, d.minute() as i32, d.second() as i32,
2972             d.nanosecond() as i32, off)
2973        }
2974    };
2975    let mut rec = indexmap::IndexMap::new();
2976    rec.insert("year".into(),    Value::Int(y as i64));
2977    rec.insert("month".into(),   Value::Int(m as i64));
2978    rec.insert("day".into(),     Value::Int(d as i64));
2979    rec.insert("hour".into(),    Value::Int(hh as i64));
2980    rec.insert("minute".into(),  Value::Int(mm as i64));
2981    rec.insert("second".into(),  Value::Int(ss as i64));
2982    rec.insert("nano".into(),    Value::Int(ns as i64));
2983    rec.insert("tz_offset_minutes".into(), Value::Int(off_min as i64));
2984    let _ = chrono::Utc.timestamp_opt(0, 0); // touch TimeZone to suppress unused-import lint paths
2985    Ok(Value::record_dynamic(rec))
2986}
2987
2988
2989fn instant_from_components(rec: &indexmap::IndexMap<smol_str::SmolStr, Value>) -> Result<i64, String> {
2990    use chrono::TimeZone;
2991    fn get_int(rec: &indexmap::IndexMap<smol_str::SmolStr, Value>, k: &str) -> Result<i64, String> {
2992        match rec.get(k) {
2993            Some(Value::Int(n)) => Ok(*n),
2994            other => Err(format!("from_components: missing or non-int field `{k}`: {other:?}")),
2995        }
2996    }
2997    let y = get_int(rec, "year")? as i32;
2998    let m = get_int(rec, "month")? as u32;
2999    let d = get_int(rec, "day")? as u32;
3000    let hh = get_int(rec, "hour")? as u32;
3001    let mm = get_int(rec, "minute")? as u32;
3002    let ss = get_int(rec, "second")? as u32;
3003    let ns = get_int(rec, "nano")? as u32;
3004    let off_min = get_int(rec, "tz_offset_minutes")? as i32;
3005    let off = chrono::FixedOffset::east_opt(off_min * 60)
3006        .ok_or("from_components: offset out of range")?;
3007    let dt = off
3008        .with_ymd_and_hms(y, m, d, hh, mm, ss)
3009        .single()
3010        .ok_or("from_components: invalid or ambiguous date/time")?;
3011    let dt = dt + chrono::Duration::nanoseconds(ns as i64);
3012    Ok(instant_from_chrono(dt))
3013}
3014
3015// ── AEAD helpers (#382 AEAD slice) ────────────────────────────────────
3016//
3017// Each `*_seal_impl` returns a `Result[AeadResult, Str]` Lex Variant:
3018// `Ok(AeadResult { ciphertext, tag })` on success, `Err(msg)` on input
3019// validation failure (wrong key/nonce length). Each `*_open_impl`
3020// returns `Result[Bytes, Str]` — authentication failure (bad tag /
3021// modified ciphertext) surfaces as `Err`, not a panic.
3022//
3023// Pure ops: every output is a deterministic function of the inputs;
3024// no syscalls, no clock reads, no entropy. Live in the pure-builtin
3025// dispatch table so callers don't need an effect grant beyond
3026// whatever they used to obtain key + nonce in the first place.
3027
3028/// `(key, nonce, aad, plaintext)` references unpacked from a 4-arg
3029/// AEAD seal call. Aliased so the `type_complexity` clippy lint stays
3030/// quiet on the tuple of four borrows.
3031type Aead4<'a> = (&'a Vec<u8>, &'a Vec<u8>, &'a Vec<u8>, &'a Vec<u8>);
3032
3033/// `(key, nonce, aad, ciphertext, tag)` references unpacked from a
3034/// 5-arg AEAD open call.
3035type Aead5<'a> = (&'a Vec<u8>, &'a Vec<u8>, &'a Vec<u8>, &'a Vec<u8>, &'a Vec<u8>);
3036
3037fn unpack4_bytes<'a>(
3038    args: &'a [Value],
3039    op: &str,
3040) -> Result<Aead4<'a>, String> {
3041    let pick = |i: usize, name: &str| -> Result<&'a Vec<u8>, String> {
3042        match args.get(i) {
3043            Some(Value::Bytes(b)) => Ok(b),
3044            Some(other) => Err(format!("{op}: {name} must be Bytes, got {other:?}")),
3045            None => Err(format!("{op}: missing {name} argument")),
3046        }
3047    };
3048    Ok((pick(0, "key")?, pick(1, "nonce")?, pick(2, "aad")?, pick(3, "plaintext")?))
3049}
3050
3051fn unpack5_bytes<'a>(
3052    args: &'a [Value],
3053    op: &str,
3054) -> Result<Aead5<'a>, String> {
3055    let pick = |i: usize, name: &str| -> Result<&'a Vec<u8>, String> {
3056        match args.get(i) {
3057            Some(Value::Bytes(b)) => Ok(b),
3058            Some(other) => Err(format!("{op}: {name} must be Bytes, got {other:?}")),
3059            None => Err(format!("{op}: missing {name} argument")),
3060        }
3061    };
3062    Ok((
3063        pick(0, "key")?,
3064        pick(1, "nonce")?,
3065        pick(2, "aad")?,
3066        pick(3, "ciphertext")?,
3067        pick(4, "tag")?,
3068    ))
3069}
3070
3071fn aead_result(ciphertext: Vec<u8>, tag: Vec<u8>) -> Value {
3072    let mut rec = indexmap::IndexMap::new();
3073    rec.insert("ciphertext".into(), Value::Bytes(ciphertext));
3074    rec.insert("tag".into(), Value::Bytes(tag));
3075    Value::record_dynamic(rec)
3076}
3077
3078fn aead_err(msg: impl Into<String>) -> Value {
3079    let s: String = msg.into();
3080    err_v(Value::Str(s.into()))
3081}
3082
3083fn aes_gcm_seal_impl(args: &[Value]) -> Value {
3084    use aes_gcm::aead::{Aead, KeyInit, Payload};
3085    use aes_gcm::{Aes128Gcm, Aes256Gcm, Nonce};
3086    let (key, nonce, aad, plaintext) = match unpack4_bytes(args, "aes_gcm_seal") {
3087        Ok(t) => t,
3088        Err(e) => return aead_err(e),
3089    };
3090    if nonce.len() != 12 {
3091        return aead_err(format!(
3092            "aes_gcm_seal: nonce must be exactly 12 bytes, got {}", nonce.len()
3093        ));
3094    }
3095    let n = Nonce::from_slice(nonce);
3096    let payload = Payload { msg: plaintext, aad };
3097    // Encrypts and appends the 16-byte tag. We split the tag back out so
3098    // the caller sees the structured AeadResult shape.
3099    let combined = match key.len() {
3100        16 => {
3101            let cipher = Aes128Gcm::new_from_slice(key)
3102                .map_err(|e| e.to_string());
3103            match cipher {
3104                Ok(c) => c.encrypt(n, payload).map_err(|e| format!("aes_gcm_seal: {e}")),
3105                Err(e) => Err(format!("aes_gcm_seal: {e}")),
3106            }
3107        }
3108        32 => {
3109            let cipher = Aes256Gcm::new_from_slice(key)
3110                .map_err(|e| e.to_string());
3111            match cipher {
3112                Ok(c) => c.encrypt(n, payload).map_err(|e| format!("aes_gcm_seal: {e}")),
3113                Err(e) => Err(format!("aes_gcm_seal: {e}")),
3114            }
3115        }
3116        // AES-192 is rarely used; the aes-gcm crate doesn't expose
3117        // Aes192Gcm in its default API. Reject other sizes explicitly.
3118        other => return aead_err(format!(
3119            "aes_gcm_seal: key must be 16 or 32 bytes, got {other}"
3120        )),
3121    };
3122    match combined {
3123        Ok(mut buf) => {
3124            // tag is the last 16 bytes.
3125            let tag_start = buf.len() - 16;
3126            let tag = buf.split_off(tag_start);
3127            ok_v(aead_result(buf, tag))
3128        }
3129        Err(e) => aead_err(e),
3130    }
3131}
3132
3133fn aes_gcm_open_impl(args: &[Value]) -> Value {
3134    use aes_gcm::aead::{Aead, KeyInit, Payload};
3135    use aes_gcm::{Aes128Gcm, Aes256Gcm, Nonce};
3136    let (key, nonce, aad, ciphertext, tag) = match unpack5_bytes(args, "aes_gcm_open") {
3137        Ok(t) => t,
3138        Err(e) => return err_v(Value::Str(e.into())),
3139    };
3140    if nonce.len() != 12 {
3141        return err_v(Value::Str(format!(
3142            "aes_gcm_open: nonce must be exactly 12 bytes, got {}", nonce.len()
3143        ).into()));
3144    }
3145    if tag.len() != 16 {
3146        return err_v(Value::Str(format!(
3147            "aes_gcm_open: tag must be exactly 16 bytes, got {}", tag.len()
3148        ).into()));
3149    }
3150    // Rebuild the "ciphertext || tag" buffer the aes-gcm crate expects.
3151    let mut combined = Vec::with_capacity(ciphertext.len() + tag.len());
3152    combined.extend_from_slice(ciphertext);
3153    combined.extend_from_slice(tag);
3154    let n = Nonce::from_slice(nonce);
3155    let payload = Payload { msg: &combined, aad };
3156    let plaintext = match key.len() {
3157        16 => Aes128Gcm::new_from_slice(key)
3158            .map_err(|e| format!("aes_gcm_open: {e}"))
3159            .and_then(|c| c.decrypt(n, payload).map_err(|e| format!("aes_gcm_open: {e}"))),
3160        32 => Aes256Gcm::new_from_slice(key)
3161            .map_err(|e| format!("aes_gcm_open: {e}"))
3162            .and_then(|c| c.decrypt(n, payload).map_err(|e| format!("aes_gcm_open: {e}"))),
3163        other => return err_v(Value::Str(format!(
3164            "aes_gcm_open: key must be 16 or 32 bytes, got {other}"
3165        ).into())),
3166    };
3167    match plaintext {
3168        Ok(p) => ok_v(Value::Bytes(p)),
3169        Err(e) => err_v(Value::Str(e.into())),
3170    }
3171}
3172
3173fn chacha20_seal_impl(args: &[Value]) -> Value {
3174    use chacha20poly1305::aead::{Aead, KeyInit, Payload};
3175    use chacha20poly1305::{ChaCha20Poly1305, Nonce};
3176    let (key, nonce, aad, plaintext) = match unpack4_bytes(args, "chacha20_poly1305_seal") {
3177        Ok(t) => t,
3178        Err(e) => return aead_err(e),
3179    };
3180    if key.len() != 32 {
3181        return aead_err(format!(
3182            "chacha20_poly1305_seal: key must be exactly 32 bytes, got {}", key.len()
3183        ));
3184    }
3185    if nonce.len() != 12 {
3186        return aead_err(format!(
3187            "chacha20_poly1305_seal: nonce must be exactly 12 bytes, got {}", nonce.len()
3188        ));
3189    }
3190    let cipher = ChaCha20Poly1305::new_from_slice(key)
3191        .map_err(|e| format!("chacha20_poly1305_seal: {e}"));
3192    let n = Nonce::from_slice(nonce);
3193    let payload = Payload { msg: plaintext, aad };
3194    let combined = match cipher {
3195        Ok(c) => c.encrypt(n, payload).map_err(|e| format!("chacha20_poly1305_seal: {e}")),
3196        Err(e) => Err(e),
3197    };
3198    match combined {
3199        Ok(mut buf) => {
3200            let tag_start = buf.len() - 16;
3201            let tag = buf.split_off(tag_start);
3202            ok_v(aead_result(buf, tag))
3203        }
3204        Err(e) => aead_err(e),
3205    }
3206}
3207
3208fn chacha20_open_impl(args: &[Value]) -> Value {
3209    use chacha20poly1305::aead::{Aead, KeyInit, Payload};
3210    use chacha20poly1305::{ChaCha20Poly1305, Nonce};
3211    let (key, nonce, aad, ciphertext, tag) = match unpack5_bytes(args, "chacha20_poly1305_open") {
3212        Ok(t) => t,
3213        Err(e) => return err_v(Value::Str(e.into())),
3214    };
3215    if key.len() != 32 {
3216        return err_v(Value::Str(format!(
3217            "chacha20_poly1305_open: key must be exactly 32 bytes, got {}", key.len()
3218        ).into()));
3219    }
3220    if nonce.len() != 12 {
3221        return err_v(Value::Str(format!(
3222            "chacha20_poly1305_open: nonce must be exactly 12 bytes, got {}", nonce.len()
3223        ).into()));
3224    }
3225    if tag.len() != 16 {
3226        return err_v(Value::Str(format!(
3227            "chacha20_poly1305_open: tag must be exactly 16 bytes, got {}", tag.len()
3228        ).into()));
3229    }
3230    let mut combined = Vec::with_capacity(ciphertext.len() + tag.len());
3231    combined.extend_from_slice(ciphertext);
3232    combined.extend_from_slice(tag);
3233    let cipher = ChaCha20Poly1305::new_from_slice(key)
3234        .map_err(|e| format!("chacha20_poly1305_open: {e}"));
3235    let n = Nonce::from_slice(nonce);
3236    let payload = Payload { msg: &combined, aad };
3237    match cipher.and_then(|c| c.decrypt(n, payload).map_err(|e| format!("chacha20_poly1305_open: {e}"))) {
3238        Ok(p) => ok_v(Value::Bytes(p)),
3239        Err(e) => err_v(Value::Str(e.into())),
3240    }
3241}
3242
3243// ── KDFs (#382 KDF slice) ──────────────────────────────────────────────────
3244//
3245// All three primitives return Result[Bytes, Str] so caller-controlled
3246// inputs (iteration count, output length, argon2id work factors) that
3247// violate the underlying crate's contract surface as Err, never as a
3248// VM panic.
3249
3250/// `(password :: Bytes, salt :: Bytes, iterations :: Int, len :: Int)`
3251/// references unpacked from a 4-arg KDF call.
3252type Kdf4<'a> = (&'a Vec<u8>, &'a Vec<u8>, i64, i64);
3253
3254/// `(ikm :: Bytes, salt :: Bytes, info :: Bytes, len :: Int)`
3255/// references unpacked from a 4-arg HKDF call.
3256type Hkdf4<'a> = (&'a Vec<u8>, &'a Vec<u8>, &'a Vec<u8>, i64);
3257
3258/// `(password :: Bytes, salt :: Bytes, t_cost :: Int, m_cost :: Int, len :: Int)`
3259/// for argon2id.
3260type Argon5<'a> = (&'a Vec<u8>, &'a Vec<u8>, i64, i64, i64);
3261
3262fn pick_bytes<'a>(args: &'a [Value], i: usize, op: &str, name: &str)
3263    -> Result<&'a Vec<u8>, String>
3264{
3265    match args.get(i) {
3266        Some(Value::Bytes(b)) => Ok(b),
3267        Some(other) => Err(format!("{op}: {name} must be Bytes, got {other:?}")),
3268        None => Err(format!("{op}: missing {name} argument")),
3269    }
3270}
3271
3272fn pick_int(args: &[Value], i: usize, op: &str, name: &str) -> Result<i64, String> {
3273    match args.get(i) {
3274        Some(Value::Int(n)) => Ok(*n),
3275        Some(other) => Err(format!("{op}: {name} must be Int, got {other:?}")),
3276        None => Err(format!("{op}: missing {name} argument")),
3277    }
3278}
3279
3280fn unpack_kdf4<'a>(args: &'a [Value], op: &str) -> Result<Kdf4<'a>, String> {
3281    Ok((
3282        pick_bytes(args, 0, op, "password")?,
3283        pick_bytes(args, 1, op, "salt")?,
3284        pick_int(args, 2, op, "iterations")?,
3285        pick_int(args, 3, op, "len")?,
3286    ))
3287}
3288
3289fn unpack_hkdf4<'a>(args: &'a [Value], op: &str) -> Result<Hkdf4<'a>, String> {
3290    Ok((
3291        pick_bytes(args, 0, op, "ikm")?,
3292        pick_bytes(args, 1, op, "salt")?,
3293        pick_bytes(args, 2, op, "info")?,
3294        pick_int(args, 3, op, "len")?,
3295    ))
3296}
3297
3298fn unpack_argon5<'a>(args: &'a [Value], op: &str) -> Result<Argon5<'a>, String> {
3299    Ok((
3300        pick_bytes(args, 0, op, "password")?,
3301        pick_bytes(args, 1, op, "salt")?,
3302        pick_int(args, 2, op, "t_cost")?,
3303        pick_int(args, 3, op, "m_cost")?,
3304        pick_int(args, 4, op, "len")?,
3305    ))
3306}
3307
3308/// Output-length sanity check shared by all three KDFs. A negative or
3309/// absurdly large `len` is a programmer error, not a runtime concern;
3310/// we cap at 1 MiB to keep accidental `i64::MAX` calls from OOMing the
3311/// process.
3312const KDF_MAX_LEN: usize = 1024 * 1024;
3313
3314fn check_len(op: &str, len: i64) -> Result<usize, String> {
3315    if len <= 0 {
3316        return Err(format!("{op}: len must be > 0, got {len}"));
3317    }
3318    if (len as u64) > KDF_MAX_LEN as u64 {
3319        return Err(format!(
3320            "{op}: len must be <= {KDF_MAX_LEN}, got {len}"
3321        ));
3322    }
3323    Ok(len as usize)
3324}
3325
3326fn pbkdf2_sha256_impl(args: &[Value]) -> Value {
3327    use hmac::Hmac;
3328    use sha2::Sha256;
3329    let op = "pbkdf2_sha256";
3330    let (password, salt, iterations, len) = match unpack_kdf4(args, op) {
3331        Ok(t) => t,
3332        Err(e) => return err_v(Value::Str(e.into())),
3333    };
3334    if iterations <= 0 {
3335        return err_v(Value::Str(format!(
3336            "{op}: iterations must be > 0, got {iterations}"
3337        ).into()));
3338    }
3339    let out_len = match check_len(op, len) {
3340        Ok(n) => n,
3341        Err(e) => return err_v(Value::Str(e.into())),
3342    };
3343    let rounds = match u32::try_from(iterations) {
3344        Ok(r) => r,
3345        Err(_) => {
3346            return err_v(Value::Str(format!(
3347                "{op}: iterations must fit in u32, got {iterations}"
3348            ).into()))
3349        }
3350    };
3351    let mut out = vec![0u8; out_len];
3352    if let Err(e) = pbkdf2::pbkdf2::<Hmac<Sha256>>(password, salt, rounds, &mut out) {
3353        return err_v(Value::Str(format!("{op}: {e}").into()));
3354    }
3355    ok_v(Value::Bytes(out))
3356}
3357
3358fn hkdf_sha256_impl(args: &[Value]) -> Value {
3359    use hkdf::Hkdf;
3360    use sha2::Sha256;
3361    let op = "hkdf_sha256";
3362    let (ikm, salt, info, len) = match unpack_hkdf4(args, op) {
3363        Ok(t) => t,
3364        Err(e) => return err_v(Value::Str(e.into())),
3365    };
3366    let out_len = match check_len(op, len) {
3367        Ok(n) => n,
3368        Err(e) => return err_v(Value::Str(e.into())),
3369    };
3370    // RFC 5869 caps output at 255 * HashLen; the `expand` call below
3371    // returns InvalidLength when exceeded — surface that as Err.
3372    let salt_opt: Option<&[u8]> = if salt.is_empty() { None } else { Some(salt) };
3373    let hk = Hkdf::<Sha256>::new(salt_opt, ikm);
3374    let mut out = vec![0u8; out_len];
3375    match hk.expand(info, &mut out) {
3376        Ok(()) => ok_v(Value::Bytes(out)),
3377        Err(e) => err_v(Value::Str(format!("{op}: {e}").into())),
3378    }
3379}
3380
3381fn argon2id_impl(args: &[Value]) -> Value {
3382    use argon2::{Algorithm, Argon2, Params, Version};
3383    let op = "argon2id";
3384    let (password, salt, t_cost, m_cost, len) = match unpack_argon5(args, op) {
3385        Ok(t) => t,
3386        Err(e) => return err_v(Value::Str(e.into())),
3387    };
3388    let out_len = match check_len(op, len) {
3389        Ok(n) => n,
3390        Err(e) => return err_v(Value::Str(e.into())),
3391    };
3392    let t = match u32::try_from(t_cost) {
3393        Ok(n) if n >= 1 => n,
3394        _ => return err_v(Value::Str(format!(
3395            "{op}: t_cost must be a u32 >= 1, got {t_cost}"
3396        ).into())),
3397    };
3398    let m = match u32::try_from(m_cost) {
3399        Ok(n) if n >= Params::MIN_M_COST => n,
3400        _ => return err_v(Value::Str(format!(
3401            "{op}: m_cost must be a u32 >= {}, got {m_cost}",
3402            Params::MIN_M_COST
3403        ).into())),
3404    };
3405    // p=1 is the default and what every interop spec assumes (PHC
3406    // string, libsodium's argon2id_str). We don't expose parallelism
3407    // as a knob for now to keep callers from picking a value that
3408    // makes hashes uncomparable across machines.
3409    let params = match Params::new(m, t, 1, Some(out_len)) {
3410        Ok(p) => p,
3411        Err(e) => return err_v(Value::Str(format!("{op}: {e}").into())),
3412    };
3413    let hasher = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
3414    let mut out = vec![0u8; out_len];
3415    if let Err(e) = hasher.hash_password_into(password, salt, &mut out) {
3416        return err_v(Value::Str(format!("{op}: {e}").into()));
3417    }
3418    ok_v(Value::Bytes(out))
3419}
3420
3421#[cfg(test)]
3422mod bytes_builtin_tests {
3423    use super::*;
3424
3425    fn call(op: &str, args: Vec<Value>) -> Result<Value, String> {
3426        dispatch("bytes", op, &args)
3427    }
3428
3429    #[test]
3430    fn concat_appends_in_order() {
3431        let a = Value::Bytes(b"AB".to_vec());
3432        let b = Value::Bytes(b"CD".to_vec());
3433        let got = call("concat", vec![a, b]).unwrap();
3434        assert_eq!(got, Value::Bytes(b"ABCD".to_vec()));
3435    }
3436
3437    #[test]
3438    fn concat_all_joins_a_list_in_order() {
3439        let items = Value::List(vec![
3440            Value::Bytes(b"Hi".to_vec()),
3441            Value::Bytes(b", ".to_vec()),
3442            Value::Bytes(b"there".to_vec()),
3443        ].into());
3444        let got = call("concat_all", vec![items]).unwrap();
3445        assert_eq!(got, Value::Bytes(b"Hi, there".to_vec()));
3446    }
3447
3448    #[test]
3449    fn concat_all_rejects_non_bytes_elements() {
3450        let items = Value::List(vec![Value::Int(1)].into());
3451        assert!(call("concat_all", vec![items]).is_err());
3452    }
3453
3454    #[test]
3455    fn u8_encodes_and_truncates_to_one_byte() {
3456        let got = call("u8", vec![Value::Int(65)]).unwrap();
3457        assert_eq!(got, Value::Bytes(vec![65]));
3458        // Encoding is a wrapping truncation, matching Rust's `as u8` — the
3459        // caller is responsible for keeping values in range; this isn't a
3460        // validating encoder.
3461        let wrapped = call("u8", vec![Value::Int(256 + 65)]).unwrap();
3462        assert_eq!(wrapped, Value::Bytes(vec![65]));
3463    }
3464
3465    #[test]
3466    fn u16_le_round_trips() {
3467        let encoded = call("u16_le", vec![Value::Int(258)]).unwrap();
3468        assert_eq!(encoded, Value::Bytes(vec![2, 1]));
3469        let decoded = call("u16_le_at", vec![encoded, Value::Int(0)]).unwrap();
3470        assert_eq!(decoded, ok_v(Value::Int(258)));
3471    }
3472
3473    #[test]
3474    fn u32_le_round_trips() {
3475        let encoded = call("u32_le", vec![Value::Int(70000)]).unwrap();
3476        let decoded = call("u32_le_at", vec![encoded, Value::Int(0)]).unwrap();
3477        assert_eq!(decoded, ok_v(Value::Int(70000)));
3478    }
3479
3480    #[test]
3481    fn u64_le_round_trips_beyond_u32_range() {
3482        let n = 9_007_199_254_740_993_i64; // > u32::MAX, within i64
3483        let encoded = call("u64_le", vec![Value::Int(n)]).unwrap();
3484        let decoded = call("u64_le_at", vec![encoded, Value::Int(0)]).unwrap();
3485        assert_eq!(decoded, ok_v(Value::Int(n)));
3486    }
3487
3488    #[test]
3489    fn decoders_report_out_of_range_offsets_as_err_not_panic() {
3490        let one_byte = Value::Bytes(vec![5]);
3491        let got = call("u16_le_at", vec![one_byte, Value::Int(0)]).unwrap();
3492        match got {
3493            Value::Variant { name, .. } => assert_eq!(name, "Err"),
3494            other => panic!("expected an Err variant, got {other:?}"),
3495        }
3496    }
3497
3498    #[test]
3499    fn u16_le_at_reads_at_a_nonzero_offset() {
3500        // [pad byte, then the u16] -- confirms offset is honored, not
3501        // just decoding from the start of the buffer.
3502        let buf = Value::Bytes(vec![0xFF, 2, 1]);
3503        let decoded = call("u16_le_at", vec![buf, Value::Int(1)]).unwrap();
3504        assert_eq!(decoded, ok_v(Value::Int(258)));
3505    }
3506}
3507
3508#[cfg(test)]
3509mod ed25519_curve_tests {
3510    use super::*;
3511
3512    fn call(op: &str, args: Vec<Value>) -> Result<Value, String> {
3513        dispatch("crypto", op, &args)
3514    }
3515
3516    #[test]
3517    fn a_real_public_key_is_a_valid_point() {
3518        use ed25519_dalek::SigningKey;
3519        let seed = [7u8; 32];
3520        let sk = SigningKey::from_bytes(&seed);
3521        let pk = sk.verifying_key().to_bytes().to_vec();
3522        let got = call("ed25519_is_valid_point", vec![Value::Bytes(pk)]).unwrap();
3523        assert_eq!(got, Value::Bool(true));
3524    }
3525
3526    #[test]
3527    fn wrong_length_is_not_a_valid_point() {
3528        let got = call("ed25519_is_valid_point", vec![Value::Bytes(vec![1, 2, 3])]).unwrap();
3529        assert_eq!(got, Value::Bool(false));
3530    }
3531
3532    #[test]
3533    fn a_non_curve_point_is_rejected() {
3534        // 31 bytes of 0x01 followed by a 0x00 sign/high byte does not
3535        // decompress to a point on Edwards25519 -- exactly the kind of
3536        // candidate a PDA bump search must reject to be sure the address
3537        // has no known private key.
3538        let mut candidate = [1u8; 32];
3539        candidate[31] = 0;
3540        let got = call("ed25519_is_valid_point", vec![Value::Bytes(candidate.to_vec())]).unwrap();
3541        assert_eq!(got, Value::Bool(false));
3542    }
3543}