Skip to main content

lex_types/
builtins.rs

1//! Built-in module signatures used by §3.13 examples and beyond.
2//!
3//! These are stub signatures that let the type-checker verify code that
4//! imports `std.io`, `std.str`, `std.list`, etc. They will be backed by
5//! real stages once the stdlib lands (M11).
6
7use crate::env::TypeEnv;
8use crate::types::*;
9use indexmap::IndexMap;
10
11/// Build the value-level scope of a module: a record of named functions.
12pub fn module_scope(name: &str, _env: &TypeEnv) -> Option<Ty> {
13    match name {
14        "io" => {
15            let mut fields = IndexMap::new();
16            // io.print(line :: Str) -> [io] Nil
17            fields.insert("print".into(), Ty::function(
18                vec![Ty::str()],
19                EffectSet::singleton("io"),
20                Ty::Unit,
21            ));
22            // io.read(path :: Str) -> [io] Result[Str, Str]
23            fields.insert("read".into(), Ty::function(
24                vec![Ty::str()],
25                EffectSet::singleton("io"),
26                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
27            ));
28            // io.write(path :: Str, contents :: Str) -> [io] Result[Unit, Str]
29            fields.insert("write".into(), Ty::function(
30                vec![Ty::str(), Ty::str()],
31                EffectSet::singleton("io"),
32                Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]),
33            ));
34            Some(Ty::Record(fields))
35        }
36        "str" => {
37            let mut fields = IndexMap::new();
38            fields.insert("is_empty".into(), Ty::function(vec![Ty::str()], EffectSet::empty(), Ty::bool()));
39            fields.insert("to_int".into(), Ty::function(vec![Ty::str()], EffectSet::empty(),
40                Ty::Con("Option".into(), vec![Ty::int()])));
41            fields.insert("to_float".into(), Ty::function(vec![Ty::str()], EffectSet::empty(),
42                Ty::Con("Option".into(), vec![Ty::float()])));
43            fields.insert("concat".into(), Ty::function(vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
44            fields.insert("len".into(), Ty::function(vec![Ty::str()], EffectSet::empty(), Ty::int()));
45            fields.insert("split".into(), Ty::function(
46                vec![Ty::str(), Ty::str()],
47                EffectSet::empty(),
48                Ty::List(Box::new(Ty::str())),
49            ));
50            fields.insert("join".into(), Ty::function(
51                vec![Ty::List(Box::new(Ty::str())), Ty::str()],
52                EffectSet::empty(),
53                Ty::str(),
54            ));
55            // -- predicates --
56            for name in &["starts_with", "ends_with", "contains"] {
57                fields.insert((*name).into(), Ty::function(
58                    vec![Ty::str(), Ty::str()],
59                    EffectSet::empty(),
60                    Ty::bool(),
61                ));
62            }
63            // -- transformers --
64            fields.insert("replace".into(), Ty::function(
65                vec![Ty::str(), Ty::str(), Ty::str()],
66                EffectSet::empty(),
67                Ty::str(),
68            ));
69            for name in &["trim", "to_upper", "to_lower"] {
70                fields.insert((*name).into(), Ty::function(
71                    vec![Ty::str()], EffectSet::empty(), Ty::str(),
72                ));
73            }
74            for name in &["strip_prefix", "strip_suffix"] {
75                fields.insert((*name).into(), Ty::function(
76                    vec![Ty::str(), Ty::str()],
77                    EffectSet::empty(),
78                    Ty::Con("Option".into(), vec![Ty::str()]),
79                ));
80            }
81            // slice :: (Str, Int, Int) -> Str  — byte-range half-open
82            fields.insert("slice".into(), Ty::function(
83                vec![Ty::str(), Ty::int(), Ty::int()],
84                EffectSet::empty(),
85                Ty::str(),
86            ));
87            Some(Ty::Record(fields))
88        }
89        "int" => {
90            let mut fields = IndexMap::new();
91            fields.insert("to_str".into(), Ty::function(vec![Ty::int()], EffectSet::empty(), Ty::str()));
92            fields.insert("to_float".into(), Ty::function(vec![Ty::int()], EffectSet::empty(), Ty::float()));
93            Some(Ty::Record(fields))
94        }
95        "math" => {
96            let mut fields = IndexMap::new();
97            // Matrix is registered as a built-in type alias in
98            // TypeEnv::new_with_builtins; refer to it nominally so call
99            // sites unify against the user's `:: Matrix` annotations.
100            let mat = || Ty::Con("Matrix".into(), Vec::new());
101            // Scalar floats — single-arg `Float -> Float`.
102            for name in &[
103                "exp", "log", "log2", "log10", "sqrt", "abs",
104                "sin", "cos", "tan", "asin", "acos", "atan",
105                "floor", "ceil", "round", "trunc",
106            ] {
107                fields.insert((*name).into(), Ty::function(
108                    vec![Ty::float()], EffectSet::empty(), Ty::float(),
109                ));
110            }
111            // Two-arg `Float, Float -> Float`.
112            for name in &["pow", "atan2", "min", "max"] {
113                fields.insert((*name).into(), Ty::function(
114                    vec![Ty::float(), Ty::float()], EffectSet::empty(), Ty::float(),
115                ));
116            }
117            // Constructors.
118            fields.insert("zeros".into(), Ty::function(
119                vec![Ty::int(), Ty::int()], EffectSet::empty(), mat(),
120            ));
121            fields.insert("ones".into(), Ty::function(
122                vec![Ty::int(), Ty::int()], EffectSet::empty(), mat(),
123            ));
124            fields.insert("from_lists".into(), Ty::function(
125                vec![Ty::List(Box::new(Ty::List(Box::new(Ty::float()))))],
126                EffectSet::empty(),
127                mat(),
128            ));
129            fields.insert("from_flat".into(), Ty::function(
130                vec![Ty::int(), Ty::int(), Ty::List(Box::new(Ty::float()))],
131                EffectSet::empty(),
132                mat(),
133            ));
134            // Accessors.
135            fields.insert("rows".into(), Ty::function(vec![mat()], EffectSet::empty(), Ty::int()));
136            fields.insert("cols".into(), Ty::function(vec![mat()], EffectSet::empty(), Ty::int()));
137            fields.insert("get".into(), Ty::function(
138                vec![mat(), Ty::int(), Ty::int()], EffectSet::empty(), Ty::float(),
139            ));
140            fields.insert("to_flat".into(), Ty::function(
141                vec![mat()], EffectSet::empty(),
142                Ty::List(Box::new(Ty::float())),
143            ));
144            // Linalg ops.
145            fields.insert("transpose".into(), Ty::function(
146                vec![mat()], EffectSet::empty(), mat(),
147            ));
148            fields.insert("matmul".into(), Ty::function(
149                vec![mat(), mat()], EffectSet::empty(), mat(),
150            ));
151            fields.insert("scale".into(), Ty::function(
152                vec![Ty::float(), mat()], EffectSet::empty(), mat(),
153            ));
154            for name in &["add", "sub"] {
155                fields.insert((*name).into(), Ty::function(
156                    vec![mat(), mat()], EffectSet::empty(), mat(),
157                ));
158            }
159            fields.insert("sigmoid".into(), Ty::function(
160                vec![mat()], EffectSet::empty(), mat(),
161            ));
162            Some(Ty::Record(fields))
163        }
164        "float" => {
165            let mut fields = IndexMap::new();
166            fields.insert("to_int".into(), Ty::function(vec![Ty::float()], EffectSet::empty(), Ty::int()));
167            fields.insert("to_str".into(), Ty::function(vec![Ty::float()], EffectSet::empty(), Ty::str()));
168            Some(Ty::Record(fields))
169        }
170        "list" => {
171            // list polymorphic functions need fresh vars at use sites; we
172            // encode them with placeholder Var ids that get instantiated.
173            let mut fields = IndexMap::new();
174            // Effect polymorphism: each HOF carries an effect-row
175            // variable so an effectful closure (e.g. one that calls
176            // net.get inside list.map's lambda) propagates its
177            // effects to the result type. Spec §7.3.
178            //
179            // map :: [E] List[a], (a) -> [E] b -> [E] List[b]
180            fields.insert("map".into(), Ty::function(
181                vec![
182                    Ty::List(Box::new(Ty::Var(0))),
183                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
184                ],
185                EffectSet::open_var(2),
186                Ty::List(Box::new(Ty::Var(1))),
187            ));
188            // #305 slice 1: parallel map. Same signature shape as
189            // `map`; the runtime spawns OS threads (capped by
190            // LEX_PAR_MAX_CONCURRENCY) to apply the closure
191            // concurrently. Effect row stays open so a closure with
192            // declared effects still type-checks against
193            // par_map — though slice 1's runtime currently refuses
194            // effectful closures at execution (queued as slice 2).
195            fields.insert("par_map".into(), Ty::function(
196                vec![
197                    Ty::List(Box::new(Ty::Var(0))),
198                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(7), Ty::Var(1)),
199                ],
200                EffectSet::open_var(7),
201                Ty::List(Box::new(Ty::Var(1))),
202            ));
203            // #338: sort_by :: List[T], (T) -> [E] K -> [E] List[T]
204            // Stable sort by the key the closure derives from each
205            // element. K is intended to be one of Int / Float / Str
206            // (the runtime comparator falls back to equality for
207            // other shapes, preserving original order via the
208            // stable sort) but the type system doesn't enforce that
209            // — keep the signature minimal so callers can pass any
210            // K and trust the comparator.
211            fields.insert("sort_by".into(), Ty::function(
212                vec![
213                    Ty::List(Box::new(Ty::Var(0))),
214                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(8), Ty::Var(1)),
215                ],
216                EffectSet::open_var(8),
217                Ty::List(Box::new(Ty::Var(0))),
218            ));
219            fields.insert("filter".into(), Ty::function(
220                vec![
221                    Ty::List(Box::new(Ty::Var(0))),
222                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), Ty::bool()),
223                ],
224                EffectSet::open_var(3),
225                Ty::List(Box::new(Ty::Var(0))),
226            ));
227            fields.insert("fold".into(), Ty::function(
228                vec![
229                    Ty::List(Box::new(Ty::Var(0))),
230                    Ty::Var(1),
231                    Ty::function(vec![Ty::Var(1), Ty::Var(0)], EffectSet::open_var(4), Ty::Var(1)),
232                ],
233                EffectSet::open_var(4),
234                Ty::Var(1),
235            ));
236            fields.insert("len".into(), Ty::function(
237                vec![Ty::List(Box::new(Ty::Var(0)))],
238                EffectSet::empty(),
239                Ty::int(),
240            ));
241            fields.insert("is_empty".into(), Ty::function(
242                vec![Ty::List(Box::new(Ty::Var(0)))],
243                EffectSet::empty(),
244                Ty::bool(),
245            ));
246            fields.insert("range".into(), Ty::function(
247                vec![Ty::int(), Ty::int()],
248                EffectSet::empty(),
249                Ty::List(Box::new(Ty::int())),
250            ));
251            fields.insert("head".into(), Ty::function(
252                vec![Ty::List(Box::new(Ty::Var(0)))],
253                EffectSet::empty(),
254                Ty::Con("Option".into(), vec![Ty::Var(0)]),
255            ));
256            fields.insert("tail".into(), Ty::function(
257                vec![Ty::List(Box::new(Ty::Var(0)))],
258                EffectSet::empty(),
259                Ty::List(Box::new(Ty::Var(0))),
260            ));
261            fields.insert("concat".into(), Ty::function(
262                vec![Ty::List(Box::new(Ty::Var(0))), Ty::List(Box::new(Ty::Var(0)))],
263                EffectSet::empty(),
264                Ty::List(Box::new(Ty::Var(0))),
265            ));
266            // reverse :: List[T] -> List[T]
267            fields.insert("reverse".into(), Ty::function(
268                vec![Ty::List(Box::new(Ty::Var(0)))],
269                EffectSet::empty(),
270                Ty::List(Box::new(Ty::Var(0))),
271            ));
272            // #334: cons :: T, List[T] -> List[T]  — O(1)-amortised prepend.
273            fields.insert("cons".into(), Ty::function(
274                vec![Ty::Var(0), Ty::List(Box::new(Ty::Var(0)))],
275                EffectSet::empty(),
276                Ty::List(Box::new(Ty::Var(0))),
277            ));
278            // enumerate :: List[T] -> List[(Int, T)]
279            // Pairs each element with its zero-based index.
280            fields.insert("enumerate".into(), Ty::function(
281                vec![Ty::List(Box::new(Ty::Var(0)))],
282                EffectSet::empty(),
283                Ty::List(Box::new(Ty::Tuple(vec![Ty::int(), Ty::Var(0)]))),
284            ));
285            Some(Ty::Record(fields))
286        }
287        "bytes" => {
288            let mut fields = IndexMap::new();
289            fields.insert("len".into(), Ty::function(
290                vec![Ty::bytes()], EffectSet::empty(), Ty::int(),
291            ));
292            fields.insert("is_empty".into(), Ty::function(
293                vec![Ty::bytes()], EffectSet::empty(), Ty::bool(),
294            ));
295            fields.insert("eq".into(), Ty::function(
296                vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool(),
297            ));
298            fields.insert("from_str".into(), Ty::function(
299                vec![Ty::str()], EffectSet::empty(), Ty::bytes(),
300            ));
301            fields.insert("to_str".into(), Ty::function(
302                vec![Ty::bytes()], EffectSet::empty(),
303                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
304            ));
305            fields.insert("slice".into(), Ty::function(
306                vec![Ty::bytes(), Ty::int(), Ty::int()],
307                EffectSet::empty(), Ty::bytes(),
308            ));
309            Some(Ty::Record(fields))
310        }
311        "time" => {
312            // time.now() -> [time] Int — unix timestamp seconds.
313            // Reading the clock is an effect for two reasons: it's
314            // non-deterministic (replay needs the captured value) and
315            // it's a side-channel surface (see "Capability ≠
316            // correctness" on the landing page).
317            let mut fields = IndexMap::new();
318            fields.insert("now".into(), Ty::function(
319                vec![],
320                EffectSet::singleton("time"),
321                Ty::int(),
322            ));
323            // now_ms :: () -> [time] Int — unix milliseconds (#378).
324            // Resolution beyond what `time.now` (seconds) offers, for
325            // request-latency measurement / rate-limiter windows.
326            // Honors `LEX_TEST_NOW` for deterministic tests.
327            fields.insert("now_ms".into(), Ty::function(
328                vec![],
329                EffectSet::singleton("time"),
330                Ty::int(),
331            ));
332            // now_str :: () -> [time] Str — wall-clock instant rendered
333            // as an ISO-8601 / RFC 3339 string in UTC (#378). Suitable
334            // for auto-managed `created_at` / `updated_at` timestamps
335            // and structured log lines. Honors `LEX_TEST_NOW`.
336            fields.insert("now_str".into(), Ty::function(
337                vec![],
338                EffectSet::singleton("time"),
339                Ty::str(),
340            ));
341            // mono_ns :: () -> [time] Int — monotonic-clock nanoseconds
342            // since process start (#378). Use for *duration*
343            // measurement (`end - start`); the value carries no wall-
344            // clock meaning and the clock can never go backwards
345            // (unlike `time.now_ms` under NTP jitter). Not affected by
346            // `LEX_TEST_NOW` — pinning a monotonic clock would defeat
347            // its purpose; tests that need a fake monotonic clock
348            // should inject one through `EffectHandler`.
349            fields.insert("mono_ns".into(), Ty::function(
350                vec![],
351                EffectSet::singleton("time"),
352                Ty::int(),
353            ));
354            // sleep_ms :: Int -> [time] Unit (#226).
355            // Used internally by flow.retry_with_backoff for
356            // exponential-backoff delays; also available to user
357            // code under `--allow-effects time`.
358            fields.insert("sleep_ms".into(), Ty::function(
359                vec![Ty::int()],
360                EffectSet::singleton("time"),
361                Ty::Unit,
362            ));
363            Some(Ty::Record(fields))
364        }
365        "rand" => {
366            // rand.int_in(lo, hi) -> [rand] Int — currently a deterministic
367            // stub (midpoint) per spec §13; replaced when randomness lands.
368            let mut fields = IndexMap::new();
369            fields.insert("int_in".into(), Ty::function(
370                vec![Ty::int(), Ty::int()],
371                EffectSet::singleton("rand"),
372                Ty::int(),
373            ));
374            Some(Ty::Record(fields))
375        }
376        "random" => {
377            // #219: pure, seeded RNG. The caller threads the `Rng`
378            // value through computations explicitly — there is no
379            // global state and no effect tag, because the seed is
380            // visible in the program's value flow and replay is
381            // therefore deterministic by construction.
382            //
383            // Backed at runtime by SplitMix64 (deterministic across
384            // platforms, single-u64 state). The proposal mentioned
385            // `rand_chacha` for cryptographic-strength bias, but the
386            // acceptance criterion is just "byte-identical sequence
387            // across platforms," and SplitMix64 satisfies that with
388            // a state shape that fits in `Value::Int` cleanly.
389            let rng_t = || Ty::Con("Rng".into(), vec![]);
390            let mut fields = IndexMap::new();
391            // seed :: Int -> Rng
392            fields.insert("seed".into(), Ty::function(
393                vec![Ty::int()], EffectSet::empty(), rng_t()));
394            // int :: Rng, Int, Int -> (Int, Rng)
395            // Uniform in [lo, hi] inclusive at both ends. Returns
396            // the drawn value and the advanced Rng.
397            fields.insert("int".into(), Ty::function(
398                vec![rng_t(), Ty::int(), Ty::int()],
399                EffectSet::empty(),
400                Ty::Tuple(vec![Ty::int(), rng_t()])));
401            // float :: Rng -> (Float, Rng)
402            // Uniform in [0.0, 1.0).
403            fields.insert("float".into(), Ty::function(
404                vec![rng_t()], EffectSet::empty(),
405                Ty::Tuple(vec![Ty::float(), rng_t()])));
406            // choose :: Rng, List[T] -> Option[(T, Rng)]
407            // Returns None if the list is empty.
408            fields.insert("choose".into(), Ty::function(
409                vec![rng_t(), Ty::List(Box::new(Ty::Var(0)))],
410                EffectSet::empty(),
411                Ty::Con("Option".into(), vec![
412                    Ty::Tuple(vec![Ty::Var(0), rng_t()]),
413                ]),
414            ));
415            Some(Ty::Record(fields))
416        }
417        "env" => {
418            // #216: env.get(name) -> [env] Option[Str].
419            // Per-var scoping (`[env(NAME)]`) lands with the
420            // per-capability effect parameterization work (#207); the
421            // flat `[env]` is the v1 surface.
422            let mut fields = IndexMap::new();
423            fields.insert("get".into(), Ty::function(
424                vec![Ty::str()],
425                EffectSet::singleton("env"),
426                Ty::Con("Option".into(), vec![Ty::str()]),
427            ));
428            Some(Ty::Record(fields))
429        }
430        "net" => {
431            let mut fields = IndexMap::new();
432            // get :: Str -> [net] Result[Str, Str]
433            fields.insert("get".into(), Ty::function(
434                vec![Ty::str()],
435                EffectSet::singleton("net"),
436                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
437            ));
438            fields.insert("post".into(), Ty::function(
439                vec![Ty::str(), Ty::str()],
440                EffectSet::singleton("net"),
441                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
442            ));
443            // serve :: (Int, Str) -> [net] Unit  (blocks; never returns
444            // under normal use). Handler's signature isn't carried in
445            // the type system here — looked up by name at runtime.
446            fields.insert("serve".into(), Ty::function(
447                vec![Ty::int(), Ty::str()],
448                EffectSet::singleton("net"),
449                Ty::Unit,
450            ));
451            // serve_tls :: (Int, Str, Str, Str) -> [net] Unit
452            //              port  cert  key   handler
453            // cert and key are filesystem paths to PEM-encoded files.
454            fields.insert("serve_tls".into(), Ty::function(
455                vec![Ty::int(), Ty::str(), Ty::str(), Ty::str()],
456                EffectSet::singleton("net"),
457                Ty::Unit,
458            ));
459            // serve_ws :: (Int, Str) -> [net] Unit
460            //             port  on_message_handler_name
461            // The handler is looked up by name at runtime.
462            fields.insert("serve_ws".into(), Ty::function(
463                vec![Ty::int(), Ty::str()],
464                EffectSet::singleton("net"),
465                Ty::Unit,
466            ));
467            // serve_ws_fn[Eff] :: (Int, Str, (WsConn, WsMessage) -> [Eff] WsAction)
468            //                      -> [net, Eff] Unit
469            // Effect-polymorphic WebSocket server that accepts a handler closure.
470            // The second argument is the subprotocol string for the
471            // Sec-WebSocket-Protocol handshake header ("" for none).
472            // open_var(0) propagates the handler's effect row to the call site.
473            fields.insert("serve_ws_fn".into(), Ty::function(
474                vec![
475                    Ty::int(),
476                    Ty::str(), // subprotocol
477                    Ty::function(
478                        vec![
479                            Ty::Con("WsConn".into(), vec![]),
480                            Ty::Con("WsMessage".into(), vec![]),
481                        ],
482                        EffectSet::open_var(0),
483                        Ty::Con("WsAction".into(), vec![]),
484                    ),
485                ],
486                EffectSet::open_var(0).union(&EffectSet::singleton("net")),
487                Ty::Unit,
488            ));
489            // dial_ws[Eff] :: (Str, Str, () -> [Eff] WsAction,
490            //                  (WsMessage) -> [Eff] WsAction)
491            //                  -> [net, Eff] Result[Unit, Str]
492            //
493            // WebSocket *client* — the inverse of serve_ws_fn (#390).
494            // Connects to `url` (ws:// or wss://) with the given
495            // subprotocol header, calls `on_open` once after the
496            // handshake completes, then loops invoking `on_message`
497            // for every inbound frame. Each callback returns a
498            // `WsAction` that gets applied to the socket — same enum
499            // as the server side, same semantics for `WsSend` /
500            // `WsSendBinary` / `WsNoOp`. open_var(0) propagates the
501            // handler effects so callers that touch [io], [time],
502            // [random] etc. inside their handlers see those propagate
503            // out of the dial_ws call.
504            //
505            // Returns `Result[Unit, Str]` rather than the bare `Unit`
506            // that serve_ws_fn returns: a dial can fail on connect
507            // (DNS, refused, bad TLS) or mid-stream (read error,
508            // unexpected close) and the caller usually wants to know.
509            fields.insert("dial_ws".into(), Ty::function(
510                vec![
511                    Ty::str(), // url (ws:// or wss://)
512                    Ty::str(), // subprotocol (Sec-WebSocket-Protocol)
513                    Ty::function(
514                        vec![],
515                        EffectSet::open_var(0),
516                        Ty::Con("WsAction".into(), vec![]),
517                    ),
518                    Ty::function(
519                        vec![Ty::Con("WsMessage".into(), vec![])],
520                        EffectSet::open_var(0),
521                        Ty::Con("WsAction".into(), vec![]),
522                    ),
523                ],
524                EffectSet::open_var(0).union(&EffectSet::singleton("net")),
525                Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]),
526            ));
527            // serve_fn[Eff] :: (Int, (Request) -> [Eff] Response) -> [net, Eff] Unit
528            // Effect-polymorphic variant of serve that accepts a first-class closure
529            // instead of a handler name. open_var(0) captures the handler's effect row
530            // so callers that invoke e.g. [io] effects inside the closure propagate them
531            // to the serve_fn call site.
532            fields.insert("serve_fn".into(), Ty::function(
533                vec![
534                    Ty::int(),
535                    Ty::function(
536                        vec![Ty::Con("Request".into(), vec![])],
537                        EffectSet::open_var(0),
538                        Ty::Con("Response".into(), vec![]),
539                    ),
540                ],
541                EffectSet::open_var(0).union(&EffectSet::singleton("net")),
542                Ty::Unit,
543            ));
544            Some(Ty::Record(fields))
545        }
546        "chat" => {
547            let mut fields = IndexMap::new();
548            fields.insert("broadcast".into(), Ty::function(
549                vec![Ty::str(), Ty::str()],
550                EffectSet::singleton("chat"),
551                Ty::Unit,
552            ));
553            fields.insert("send".into(), Ty::function(
554                vec![Ty::int(), Ty::str()],
555                EffectSet::singleton("chat"),
556                Ty::bool(),
557            ));
558            Some(Ty::Record(fields))
559        }
560        "proc" => {
561            // Subprocess dispatch. Effect: [proc]. Returns a Result
562            // with a record on success carrying stdout / stderr /
563            // exit_code. The runtime allow-lists which binary
564            // basenames are spawnable — `cmd` is the program to
565            // run, `args` is the literal argv (no shell parsing).
566            //
567            // Read SECURITY.md before adding [proc] to a policy:
568            // it weakens the "we know what this fn does" claim.
569            let mut fields = IndexMap::new();
570            let mut result_rec = IndexMap::new();
571            result_rec.insert("stdout".into(), Ty::str());
572            result_rec.insert("stderr".into(), Ty::str());
573            result_rec.insert("exit_code".into(), Ty::int());
574            // spawn :: Str, List[Str] -> [proc] Result[{stdout, stderr, exit_code}, Str]
575            fields.insert("spawn".into(), Ty::function(
576                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
577                EffectSet::singleton("proc"),
578                Ty::Con("Result".into(), vec![
579                    Ty::Record(result_rec),
580                    Ty::str(),
581                ]),
582            ));
583            Some(Ty::Record(fields))
584        }
585        "json" => {
586            let mut fields = IndexMap::new();
587            // stringify :: T -> Str  (polymorphic on input)
588            fields.insert("stringify".into(), Ty::function(
589                vec![Ty::Var(0)], EffectSet::empty(), Ty::str(),
590            ));
591            // parse :: Str -> Result[T, Str]
592            fields.insert("parse".into(), Ty::function(
593                vec![Ty::str()], EffectSet::empty(),
594                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
595            ));
596            // parse_strict :: (Str, List[Str]) -> Result[T, Str]
597            // Tactical fix for #168 — caller passes the field names
598            // T requires; runtime returns Err if any are missing
599            // from the parsed object instead of letting field
600            // access panic later.
601            fields.insert("parse_strict".into(), Ty::function(
602                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
603                EffectSet::empty(),
604                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
605            ));
606            Some(Ty::Record(fields))
607        }
608        "result" => {
609            let mut fields = IndexMap::new();
610            // result.map :: Result[T, E], (T) -> [E2] U -> [E2] Result[U, E]
611            // Effect-polymorphic on the closure: result.map et al.
612            // propagate the closure's effects to the surrounding call.
613            fields.insert("map".into(), Ty::function(
614                vec![
615                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
616                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), Ty::Var(2)),
617                ],
618                EffectSet::open_var(3),
619                Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
620            ));
621            fields.insert("and_then".into(), Ty::function(
622                vec![
623                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
624                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(4),
625                        Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)])),
626                ],
627                EffectSet::open_var(4),
628                Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
629            ));
630            fields.insert("map_err".into(), Ty::function(
631                vec![
632                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
633                    Ty::function(vec![Ty::Var(1)], EffectSet::open_var(5), Ty::Var(2)),
634                ],
635                EffectSet::open_var(5),
636                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
637            ));
638            // result.or_else :: Result[T, E1], (E1) -> [E] Result[T, E2]
639            //                                    -> [E] Result[T, E2]
640            // Recovery combinator: closure runs only on Err and returns
641            // the next Result (which itself may swap the error type).
642            fields.insert("or_else".into(), Ty::function(
643                vec![
644                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
645                    Ty::function(vec![Ty::Var(1)], EffectSet::open_var(6),
646                        Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)])),
647                ],
648                EffectSet::open_var(6),
649                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
650            ));
651            Some(Ty::Record(fields))
652        }
653        "option" => {
654            let mut fields = IndexMap::new();
655            // option.map :: Option[T], (T) -> [E] U -> [E] Option[U]
656            fields.insert("map".into(), Ty::function(
657                vec![
658                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
659                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
660                ],
661                EffectSet::open_var(2),
662                Ty::Con("Option".into(), vec![Ty::Var(1)]),
663            ));
664            // option.and_then :: Option[T], (T) -> [E] Option[U] -> [E] Option[U]
665            // The compiler entry has been wired since the result/option
666            // variant_map work landed; this signature was missed,
667            // making the call fail to type-check until now.
668            fields.insert("and_then".into(), Ty::function(
669                vec![
670                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
671                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
672                        Ty::Con("Option".into(), vec![Ty::Var(1)])),
673                ],
674                EffectSet::open_var(3),
675                Ty::Con("Option".into(), vec![Ty::Var(1)]),
676            ));
677            fields.insert("unwrap_or".into(), Ty::function(
678                vec![Ty::Con("Option".into(), vec![Ty::Var(0)]), Ty::Var(0)],
679                EffectSet::empty(),
680                Ty::Var(0),
681            ));
682            // option.unwrap_or_else :: Option[T], () -> [E] T -> [E] T
683            // Lazy variant of unwrap_or: the default is computed by a closure
684            // only when the value is None (effect-polymorphic on the closure).
685            fields.insert("unwrap_or_else".into(), Ty::function(
686                vec![
687                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
688                    Ty::function(vec![], EffectSet::open_var(5), Ty::Var(0)),
689                ],
690                EffectSet::open_var(5),
691                Ty::Var(0),
692            ));
693            // option.or_else :: Option[T], () -> [E] Option[T] -> [E] Option[T]
694            // The closure takes no arguments because None has no payload to pass.
695            fields.insert("or_else".into(), Ty::function(
696                vec![
697                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
698                    Ty::function(vec![], EffectSet::open_var(4),
699                        Ty::Con("Option".into(), vec![Ty::Var(0)])),
700                ],
701                EffectSet::open_var(4),
702                Ty::Con("Option".into(), vec![Ty::Var(0)]),
703            ));
704            Some(Ty::Record(fields))
705        }
706        "tuple" => {
707            // Tuple accessors per §11.1. Polymorphic in the tuple's
708            // element types; we use the same row-variable shape used
709            // by list helpers. Tuples are heterogeneous, so each
710            // accessor is statically typed via independent type
711            // variables for each position.
712            let mut fields = IndexMap::new();
713            // fst :: (T0, T1) -> T0
714            fields.insert("fst".into(), Ty::function(
715                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
716                EffectSet::empty(),
717                Ty::Var(0),
718            ));
719            // snd :: (T0, T1) -> T1
720            fields.insert("snd".into(), Ty::function(
721                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
722                EffectSet::empty(),
723                Ty::Var(1),
724            ));
725            // third :: (T0, T1, T2) -> T2
726            fields.insert("third".into(), Ty::function(
727                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1), Ty::Var(2)])],
728                EffectSet::empty(),
729                Ty::Var(2),
730            ));
731            // len :: (T0, T1) -> Int  (covers any pair shape; Int back)
732            fields.insert("len".into(), Ty::function(
733                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
734                EffectSet::empty(),
735                Ty::int(),
736            ));
737            Some(Ty::Record(fields))
738        }
739        "map" => {
740            // Persistent map. Keys are `Str` or `Int` only — Lex's
741            // type system tracks them polymorphically as Var(0)
742            // ("K") and lets the runtime check the key shape; both
743            // cases fit into `MapKey`.
744            //
745            // Type variables: 0 = K, 1 = V.
746            let mt   = || Ty::Con("Map".into(), vec![Ty::Var(0), Ty::Var(1)]);
747            let pair = || Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]);
748            let mut fields = IndexMap::new();
749            // new :: () -> Map[K, V]
750            fields.insert("new".into(), Ty::function(
751                vec![], EffectSet::empty(), mt()));
752            // size :: Map[K, V] -> Int
753            fields.insert("size".into(), Ty::function(
754                vec![mt()], EffectSet::empty(), Ty::int()));
755            // has :: Map[K, V], K -> Bool
756            fields.insert("has".into(), Ty::function(
757                vec![mt(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
758            // get :: Map[K, V], K -> Option[V]
759            fields.insert("get".into(), Ty::function(
760                vec![mt(), Ty::Var(0)], EffectSet::empty(),
761                Ty::Con("Option".into(), vec![Ty::Var(1)])));
762            // set :: Map[K, V], K, V -> Map[K, V]
763            fields.insert("set".into(), Ty::function(
764                vec![mt(), Ty::Var(0), Ty::Var(1)],
765                EffectSet::empty(), mt()));
766            // delete :: Map[K, V], K -> Map[K, V]
767            fields.insert("delete".into(), Ty::function(
768                vec![mt(), Ty::Var(0)], EffectSet::empty(), mt()));
769            // keys :: Map[K, V] -> List[K]
770            fields.insert("keys".into(), Ty::function(
771                vec![mt()], EffectSet::empty(),
772                Ty::List(Box::new(Ty::Var(0)))));
773            // values :: Map[K, V] -> List[V]
774            fields.insert("values".into(), Ty::function(
775                vec![mt()], EffectSet::empty(),
776                Ty::List(Box::new(Ty::Var(1)))));
777            // entries :: Map[K, V] -> List[(K, V)]
778            fields.insert("entries".into(), Ty::function(
779                vec![mt()], EffectSet::empty(),
780                Ty::List(Box::new(pair()))));
781            // from_list :: List[(K, V)] -> Map[K, V]
782            fields.insert("from_list".into(), Ty::function(
783                vec![Ty::List(Box::new(pair()))],
784                EffectSet::empty(), mt()));
785            // merge :: Map[K, V], Map[K, V] -> Map[K, V]   (b overrides a)
786            fields.insert("merge".into(), Ty::function(
787                vec![mt(), mt()], EffectSet::empty(), mt()));
788            // is_empty :: Map[K, V] -> Bool
789            fields.insert("is_empty".into(), Ty::function(
790                vec![mt()], EffectSet::empty(), Ty::bool()));
791            // fold :: Map[K, V], A, (A, K, V) -> [E] A -> [E] A
792            // Iteration order matches `map.entries` (BTreeMap-sorted by
793            // key). Effect-polymorphic on the combiner like `list.fold`.
794            // Type variable 2 = A (accumulator), effect row 3.
795            fields.insert("fold".into(), Ty::function(
796                vec![
797                    mt(),
798                    Ty::Var(2),
799                    Ty::function(
800                        vec![Ty::Var(2), Ty::Var(0), Ty::Var(1)],
801                        EffectSet::open_var(3),
802                        Ty::Var(2),
803                    ),
804                ],
805                EffectSet::open_var(3),
806                Ty::Var(2),
807            ));
808            Some(Ty::Record(fields))
809        }
810        "set" => {
811            // Persistent set with the same key-type discipline as map.
812            // Type variable: 0 = T (the element type, also the key type).
813            let st   = || Ty::Con("Set".into(), vec![Ty::Var(0)]);
814            let mut fields = IndexMap::new();
815            // new :: () -> Set[T]
816            fields.insert("new".into(), Ty::function(
817                vec![], EffectSet::empty(), st()));
818            // size :: Set[T] -> Int
819            fields.insert("size".into(), Ty::function(
820                vec![st()], EffectSet::empty(), Ty::int()));
821            // has :: Set[T], T -> Bool
822            fields.insert("has".into(), Ty::function(
823                vec![st(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
824            // add :: Set[T], T -> Set[T]
825            fields.insert("add".into(), Ty::function(
826                vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
827            // delete :: Set[T], T -> Set[T]
828            fields.insert("delete".into(), Ty::function(
829                vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
830            // to_list :: Set[T] -> List[T]
831            fields.insert("to_list".into(), Ty::function(
832                vec![st()], EffectSet::empty(),
833                Ty::List(Box::new(Ty::Var(0)))));
834            // from_list :: List[T] -> Set[T]
835            fields.insert("from_list".into(), Ty::function(
836                vec![Ty::List(Box::new(Ty::Var(0)))],
837                EffectSet::empty(), st()));
838            // union :: Set[T], Set[T] -> Set[T]
839            fields.insert("union".into(), Ty::function(
840                vec![st(), st()], EffectSet::empty(), st()));
841            // intersect :: Set[T], Set[T] -> Set[T]
842            fields.insert("intersect".into(), Ty::function(
843                vec![st(), st()], EffectSet::empty(), st()));
844            // diff :: Set[T], Set[T] -> Set[T]
845            fields.insert("diff".into(), Ty::function(
846                vec![st(), st()], EffectSet::empty(), st()));
847            // is_empty :: Set[T] -> Bool
848            fields.insert("is_empty".into(), Ty::function(
849                vec![st()], EffectSet::empty(), Ty::bool()));
850            // is_subset :: Set[T], Set[T] -> Bool   (a is subset of b)
851            fields.insert("is_subset".into(), Ty::function(
852                vec![st(), st()], EffectSet::empty(), Ty::bool()));
853            Some(Ty::Record(fields))
854        }
855        "iter" => {
856            // Positional iterator (#364) + lazy variant via `iter.unfold`
857            // (#376). Internal value shapes are `__IterEager(list, idx)` or
858            // `__IterLazy(seed, step)`; all operations compile-inline and
859            // dispatch on the variant tag at runtime.
860            // Type var slots: 0 = T (element), 1 = U (mapped element) /
861            // A (fold acc), 2 = S (unfold seed).
862            let it = |n: u32| Ty::Con("Iter".into(), vec![Ty::Var(n)]);
863            let mut fields = IndexMap::new();
864            // from_list :: List[T] -> Iter[T]
865            fields.insert("from_list".into(), Ty::function(
866                vec![Ty::List(Box::new(Ty::Var(0)))],
867                EffectSet::empty(), it(0)));
868            // unfold[S, T] :: S, (S) -> Option[(T, S)] -> Iter[T] (#376)
869            // The step closure may carry any effect row; the iterator
870            // itself stays effect-free since the effects only fire when
871            // the step is invoked via `iter.next` / `iter.to_list`.
872            fields.insert("unfold".into(), Ty::function(
873                vec![
874                    Ty::Var(2), // seed S
875                    Ty::function(
876                        vec![Ty::Var(2)],
877                        EffectSet::open_var(3),
878                        Ty::Con("Option".into(), vec![
879                            Ty::Tuple(vec![Ty::Var(0), Ty::Var(2)])
880                        ]),
881                    ),
882                ],
883                EffectSet::empty(), it(0)));
884            // next :: Iter[T] -> Option[(T, Iter[T])]
885            fields.insert("next".into(), Ty::function(
886                vec![it(0)],
887                EffectSet::empty(),
888                Ty::Con("Option".into(), vec![
889                    Ty::Tuple(vec![Ty::Var(0), it(0)])
890                ])));
891            // is_empty :: Iter[T] -> Bool
892            fields.insert("is_empty".into(), Ty::function(
893                vec![it(0)], EffectSet::empty(), Ty::bool()));
894            // count :: Iter[T] -> Int   (remaining elements)
895            fields.insert("count".into(), Ty::function(
896                vec![it(0)], EffectSet::empty(), Ty::int()));
897            // take :: Iter[T], Int -> Iter[T]
898            fields.insert("take".into(), Ty::function(
899                vec![it(0), Ty::int()], EffectSet::empty(), it(0)));
900            // skip :: Iter[T], Int -> Iter[T]
901            fields.insert("skip".into(), Ty::function(
902                vec![it(0), Ty::int()], EffectSet::empty(), it(0)));
903            // to_list :: Iter[T] -> List[T]
904            fields.insert("to_list".into(), Ty::function(
905                vec![it(0)], EffectSet::empty(),
906                Ty::List(Box::new(Ty::Var(0)))));
907            // map :: [E] Iter[T], (T) -> [E] U -> [E] Iter[U]
908            fields.insert("map".into(), Ty::function(
909                vec![
910                    it(0),
911                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
912                ],
913                EffectSet::open_var(2), it(1)));
914            // filter :: [E] Iter[T], (T) -> [E] Bool -> [E] Iter[T]
915            fields.insert("filter".into(), Ty::function(
916                vec![
917                    it(0),
918                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(1), Ty::bool()),
919                ],
920                EffectSet::open_var(1), it(0)));
921            // fold :: [E] Iter[T], A, (A, T) -> [E] A -> [E] A
922            fields.insert("fold".into(), Ty::function(
923                vec![
924                    it(0),
925                    Ty::Var(1),
926                    Ty::function(vec![Ty::Var(1), Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
927                ],
928                EffectSet::open_var(2), Ty::Var(1)));
929            Some(Ty::Record(fields))
930        }
931        "flow" => {
932            // Orchestration primitives (spec §11.2). Each takes one or
933            // more closures and returns a closure with a derived shape.
934            let mut fields = IndexMap::new();
935            // sequential[T, U, V](f: (T) -> U, g: (U) -> V) -> (T) -> V
936            fields.insert("sequential".into(), Ty::function(
937                vec![
938                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
939                    Ty::function(vec![Ty::Var(1)], EffectSet::empty(), Ty::Var(2)),
940                ],
941                EffectSet::empty(),
942                Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(2)),
943            ));
944            // branch[T, U](cond: (T) -> Bool, t: (T) -> U, f: (T) -> U) -> (T) -> U
945            fields.insert("branch".into(), Ty::function(
946                vec![
947                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::bool()),
948                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
949                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
950                ],
951                EffectSet::empty(),
952                Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
953            ));
954            // retry[T, U, E, Eff](
955            //   f: (T) -> [Eff] Result[U, E], n: Int
956            // ) -> (T) -> [Eff] Result[U, E]
957            // open_var(3) is the effect row carried by `f`; the
958            // combinator itself is pure, so the outer EffectSet is
959            // empty. The returned closure propagates Eff unchanged.
960            let result_ty = Ty::Con("Result".into(), vec![Ty::Var(1), Ty::Var(2)]);
961            fields.insert("retry".into(), Ty::function(
962                vec![
963                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
964                    Ty::int(),
965                ],
966                EffectSet::empty(),
967                Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
968            ));
969            // retry_with_backoff[T, U, E, Eff](
970            //   f: (T) -> [Eff] Result[U, E], attempts: Int, base_ms: Int,
971            // ) -> (T) -> [Eff, time] Result[U, E]
972            // Same retry shape as `flow.retry` plus an exponential
973            // backoff between attempts. The result function carries
974            // `[time]` (from `time.sleep_ms`) unioned with the inner
975            // closure's effect row Eff, so e.g. a `[net]` closure
976            // produces a `[net, time]` result function. (#226)
977            fields.insert("retry_with_backoff".into(), Ty::function(
978                vec![
979                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
980                    Ty::int(),
981                    Ty::int(),
982                ],
983                EffectSet::empty(),
984                Ty::function(vec![Ty::Var(0)],
985                    EffectSet::open_var(3).union(&EffectSet::singleton("time")), result_ty),
986            ));
987            // parallel[A, B](fa: () -> A, fb: () -> B) -> () -> (A, B)
988            // Sequential implementation today; spec §11.2 reserves the
989            // option of a true-threaded scheduler. parallel_record is
990            // listed in the spec but not yet implemented — it needs row
991            // polymorphism over the input record's fields plus a
992            // record-iteration trampoline; tracked as follow-up.
993            fields.insert("parallel".into(), Ty::function(
994                vec![
995                    Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
996                    Ty::function(vec![], EffectSet::empty(), Ty::Var(1)),
997                ],
998                EffectSet::empty(),
999                Ty::function(vec![], EffectSet::empty(),
1000                    Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])),
1001            ));
1002            // parallel_list[T](actions: List[() -> T]) -> List[T]
1003            // Variadic counterpart to `parallel`. Runs each action and
1004            // collects results in input order. Sequential under the
1005            // hood (same caveat as `parallel`); spec §11.2 reserves
1006            // true threading for a future scheduler. Unlike `parallel`,
1007            // this returns the result list directly rather than a
1008            // closure, since the input arity is dynamic.
1009            fields.insert("parallel_list".into(), Ty::function(
1010                vec![
1011                    Ty::List(Box::new(
1012                        Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
1013                    )),
1014                ],
1015                EffectSet::empty(),
1016                Ty::List(Box::new(Ty::Var(0))),
1017            ));
1018            Some(Ty::Record(fields))
1019        }
1020        "crypto" => {
1021            let mut fields = IndexMap::new();
1022            // Hashes: Bytes -> Bytes (digest as raw bytes).
1023            // SHA-256 / SHA-512 are vetted. MD5 is retained only for
1024            // interop with legacy systems — new code should not use it.
1025            // BLAKE2b (#382) is included as a faster alternative to
1026            // SHA-512 with the same security level.
1027            for name in &["sha256", "sha512", "md5", "blake2b"] {
1028                fields.insert((*name).into(), Ty::function(
1029                    vec![Ty::bytes()],
1030                    EffectSet::empty(),
1031                    Ty::bytes(),
1032                ));
1033            }
1034            // Hex-string convenience hashers (#382): hash a Str directly,
1035            // return the digest as a lowercase hex Str. Equivalent to
1036            // `crypto.hex_encode(crypto.shaN(bytes_from_str(s)))` but
1037            // saves the two-step incantation for the common case.
1038            for name in &["sha256_str", "sha512_str"] {
1039                fields.insert((*name).into(), Ty::function(
1040                    vec![Ty::str()],
1041                    EffectSet::empty(),
1042                    Ty::str(),
1043                ));
1044            }
1045            // HMAC: (key :: Bytes, data :: Bytes) -> Bytes
1046            for name in &["hmac_sha256", "hmac_sha512"] {
1047                fields.insert((*name).into(), Ty::function(
1048                    vec![Ty::bytes(), Ty::bytes()],
1049                    EffectSet::empty(),
1050                    Ty::bytes(),
1051                ));
1052            }
1053            // base64 / hex
1054            fields.insert("base64_encode".into(), Ty::function(
1055                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1056            fields.insert("base64_decode".into(), Ty::function(
1057                vec![Ty::str()], EffectSet::empty(),
1058                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1059            // URL-safe base64 (#382): the alphabet swaps `+/` for `-_`
1060            // and omits padding. Required by JWT, signed-cookie, and
1061            // most token-bearing URL paths.
1062            fields.insert("base64url_encode".into(), Ty::function(
1063                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1064            fields.insert("base64url_decode".into(), Ty::function(
1065                vec![Ty::str()], EffectSet::empty(),
1066                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1067            fields.insert("hex_encode".into(), Ty::function(
1068                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1069            fields.insert("hex_decode".into(), Ty::function(
1070                vec![Ty::str()], EffectSet::empty(),
1071                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1072            // Constant-time equality (for HMAC verification etc.).
1073            // `eq` / `eq_str` (#382) are the recommended spelling;
1074            // `constant_time_eq` stays as a deprecated alias.
1075            fields.insert("constant_time_eq".into(), Ty::function(
1076                vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool()));
1077            fields.insert("eq".into(), Ty::function(
1078                vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool()));
1079            fields.insert("eq_str".into(), Ty::function(
1080                vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
1081            // Cryptographically-secure random bytes — OS RNG, not the
1082            // deterministic `rand.int_in` stub. The new `[random]`
1083            // effect is fine-grained on purpose so reviewers can find
1084            // every token-generating call via `lex audit --effect
1085            // random`.
1086            fields.insert("random".into(), Ty::function(
1087                vec![Ty::int()],
1088                EffectSet::singleton("random"),
1089                Ty::bytes(),
1090            ));
1091            // random_str_hex (#382): the most common token-mint pattern
1092            // — N random bytes rendered as 2N lowercase hex chars.
1093            // Suitable for session ids, request ids, OAuth `state`,
1094            // CSRF tokens; not suitable as a JWT signing key (use raw
1095            // `random` for that).
1096            fields.insert("random_str_hex".into(), Ty::function(
1097                vec![Ty::int()],
1098                EffectSet::singleton("random"),
1099                Ty::str(),
1100            ));
1101
1102            // AEAD: authenticated encryption with associated data
1103            // (#382 AEAD slice). Both algorithms use a 12-byte nonce
1104            // and a 16-byte authentication tag. `seal` returns the
1105            // structured `AeadResult { ciphertext, tag }`; `open`
1106            // returns `Result[Bytes, Str]` so authentication failures
1107            // surface as `Err`, not a panic.
1108            //
1109            // - **AES-GCM** (`aes_gcm_seal/open`): AES-128/192/256-GCM,
1110            //   key length determined by the supplied key bytes (16, 24,
1111            //   or 32). NIST-recommended; hardware-accelerated on most CPUs.
1112            // - **ChaCha20-Poly1305** (`chacha20_poly1305_seal/open`):
1113            //   Always a 32-byte key. Equivalent security to AES-GCM
1114            //   without needing AES-NI hardware; preferred on constrained
1115            //   targets.
1116            let aead_t = || Ty::Con("AeadResult".into(), vec![]);
1117            // Seal: returns Result[AeadResult, Str] rather than bare
1118            // AeadResult so input-validation errors (wrong key length,
1119            // wrong nonce length) surface as `Err` to the Lex caller
1120            // instead of panicking the VM. AES-GCM expects 16/24/32-byte
1121            // keys; ChaCha20-Poly1305 expects exactly 32. Both expect a
1122            // 12-byte nonce.
1123            for name in &["aes_gcm_seal", "chacha20_poly1305_seal"] {
1124                fields.insert((*name).into(), Ty::function(
1125                    // (key, nonce, aad, plaintext) -> Result[AeadResult, Str]
1126                    vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes()],
1127                    EffectSet::empty(),
1128                    Ty::Con("Result".into(), vec![aead_t(), Ty::str()]),
1129                ));
1130            }
1131            for name in &["aes_gcm_open", "chacha20_poly1305_open"] {
1132                fields.insert((*name).into(), Ty::function(
1133                    // (key, nonce, aad, ciphertext, tag) -> Result[Bytes, Str]
1134                    vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes()],
1135                    EffectSet::empty(),
1136                    Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1137                ));
1138            }
1139
1140            // KDFs: key-derivation functions (#382 KDF slice). All three
1141            // return `Result[Bytes, Str]` so caller-controlled inputs
1142            // (iteration count, output length, argon2id work factors)
1143            // that violate the underlying primitive's contract surface
1144            // as `Err` rather than panicking the VM. None require a new
1145            // effect — these are pure derivations.
1146            //
1147            // - **`pbkdf2_sha256(password, salt, iterations, len)`** —
1148            //   RFC 8018 PBKDF2 with HMAC-SHA256. Use ≥ 600_000 iterations
1149            //   for password storage (OWASP 2024). Older deployments
1150            //   pinning < 100_000 should rotate.
1151            // - **`hkdf_sha256(ikm, salt, info, len)`** — RFC 5869 extract+
1152            //   expand. Use for deriving multiple keys from a single
1153            //   high-entropy input (TLS, Noise, JWT-key rotation).
1154            //   Output length capped at 255 × 32 = 8160 bytes.
1155            // - **`argon2id(password, salt, t_cost, m_cost, len)`** —
1156            //   RFC 9106 Argon2id. Recommended for *new* password
1157            //   hashing. OWASP 2024 baseline: `t_cost=2, m_cost=19456`
1158            //   (19 MiB), or use `lex-crypto`'s vetted wrapper.
1159            fields.insert("pbkdf2_sha256".into(), Ty::function(
1160                // (password, salt, iterations, len) -> Result[Bytes, Str]
1161                vec![Ty::bytes(), Ty::bytes(), Ty::int(), Ty::int()],
1162                EffectSet::empty(),
1163                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1164            ));
1165            fields.insert("hkdf_sha256".into(), Ty::function(
1166                // (ikm, salt, info, len) -> Result[Bytes, Str]
1167                vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::int()],
1168                EffectSet::empty(),
1169                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1170            ));
1171            fields.insert("argon2id".into(), Ty::function(
1172                // (password, salt, t_cost, m_cost, len) -> Result[Bytes, Str]
1173                vec![Ty::bytes(), Ty::bytes(), Ty::int(), Ty::int(), Ty::int()],
1174                EffectSet::empty(),
1175                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1176            ));
1177
1178            Some(Ty::Record(fields))
1179        }
1180        "deque" => {
1181            // Persistent double-ended queue. Push/pop O(1) on both
1182            // ends; iteration order is front-to-back.
1183            // Type variable: 0 = T.
1184            let dt   = || Ty::Con("Deque".into(), vec![Ty::Var(0)]);
1185            let pair = || Ty::Tuple(vec![Ty::Var(0), dt()]);
1186            let mut fields = IndexMap::new();
1187            // new :: () -> Deque[T]
1188            fields.insert("new".into(), Ty::function(
1189                vec![], EffectSet::empty(), dt()));
1190            // size :: Deque[T] -> Int
1191            fields.insert("size".into(), Ty::function(
1192                vec![dt()], EffectSet::empty(), Ty::int()));
1193            // is_empty :: Deque[T] -> Bool
1194            fields.insert("is_empty".into(), Ty::function(
1195                vec![dt()], EffectSet::empty(), Ty::bool()));
1196            // push_back / push_front :: Deque[T], T -> Deque[T]
1197            for n in &["push_back", "push_front"] {
1198                fields.insert((*n).into(), Ty::function(
1199                    vec![dt(), Ty::Var(0)], EffectSet::empty(), dt()));
1200            }
1201            // pop_back / pop_front :: Deque[T] -> Option[(T, Deque[T])]
1202            for n in &["pop_back", "pop_front"] {
1203                fields.insert((*n).into(), Ty::function(
1204                    vec![dt()], EffectSet::empty(),
1205                    Ty::Con("Option".into(), vec![pair()])));
1206            }
1207            // peek_back / peek_front :: Deque[T] -> Option[T]
1208            for n in &["peek_back", "peek_front"] {
1209                fields.insert((*n).into(), Ty::function(
1210                    vec![dt()], EffectSet::empty(),
1211                    Ty::Con("Option".into(), vec![Ty::Var(0)])));
1212            }
1213            // from_list :: List[T] -> Deque[T]
1214            fields.insert("from_list".into(), Ty::function(
1215                vec![Ty::List(Box::new(Ty::Var(0)))],
1216                EffectSet::empty(), dt()));
1217            // to_list :: Deque[T] -> List[T]
1218            fields.insert("to_list".into(), Ty::function(
1219                vec![dt()], EffectSet::empty(),
1220                Ty::List(Box::new(Ty::Var(0)))));
1221            Some(Ty::Record(fields))
1222        }
1223        "log" => {
1224            // Structured logging behind a [log] effect. Emit ops route
1225            // through a runtime-configured sink (stderr by default;
1226            // can be redirected via set_sink). Configuration ops
1227            // mutate the global sink and so are gated [io].
1228            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1229            let mut fields = IndexMap::new();
1230            for level in &["debug", "info", "warn", "error"] {
1231                fields.insert((*level).into(), Ty::function(
1232                    vec![Ty::str()],
1233                    EffectSet::singleton("log"),
1234                    Ty::Unit,
1235                ));
1236            }
1237            // set_level :: Str -> [io] Result[Nil, Str]
1238            fields.insert("set_level".into(), Ty::function(
1239                vec![Ty::str()],
1240                EffectSet::singleton("io"),
1241                result_str(Ty::Unit)));
1242            // set_format :: Str -> [io] Result[Nil, Str]
1243            fields.insert("set_format".into(), Ty::function(
1244                vec![Ty::str()],
1245                EffectSet::singleton("io"),
1246                result_str(Ty::Unit)));
1247            // set_sink :: Str -> [io, fs_write] Result[Nil, Str]
1248            fields.insert("set_sink".into(), Ty::function(
1249                vec![Ty::str()],
1250                EffectSet {
1251                    concrete: [crate::types::EffectKind::bare("io"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
1252                    var: None,
1253                },
1254                result_str(Ty::Unit)));
1255            Some(Ty::Record(fields))
1256        }
1257        "datetime" => {
1258            // Instant and Duration are nominal opaque Ints under the
1259            // hood (nanoseconds-since-UTC-epoch and signed nanoseconds
1260            // respectively); the type checker tracks the distinction
1261            // even though both values look like Int at runtime.
1262            //
1263            // Tz is the variant
1264            //     Utc | Local | Offset(Int) | Iana(Str)
1265            // registered as a built-in nominal type in
1266            // `TypeEnv::new_with_builtins`. The pre-v1 stringly Tz
1267            // ("UTC"/"Local"/IANA-name/"+05:30") is no longer accepted
1268            // — passing a `Str` to `to_components` is now a type
1269            // error.
1270            let inst   = || Ty::Con("Instant".into(), vec![]);
1271            let dur    = || Ty::Con("Duration".into(), vec![]);
1272            let tz     = || Ty::Con("Tz".into(), vec![]);
1273            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1274            let dt_t = || {
1275                let mut fs = IndexMap::new();
1276                fs.insert("year".into(),    Ty::int());
1277                fs.insert("month".into(),   Ty::int());
1278                fs.insert("day".into(),     Ty::int());
1279                fs.insert("hour".into(),    Ty::int());
1280                fs.insert("minute".into(),  Ty::int());
1281                fs.insert("second".into(),  Ty::int());
1282                fs.insert("nano".into(),    Ty::int());
1283                fs.insert("tz_offset_minutes".into(), Ty::int());
1284                Ty::Record(fs)
1285            };
1286            let mut fields = IndexMap::new();
1287            fields.insert("now".into(), Ty::function(
1288                vec![], EffectSet::singleton("time"), inst()));
1289            fields.insert("parse_iso".into(), Ty::function(
1290                vec![Ty::str()], EffectSet::empty(), result_str(inst())));
1291            fields.insert("format_iso".into(), Ty::function(
1292                vec![inst()], EffectSet::empty(), Ty::str()));
1293            fields.insert("parse".into(), Ty::function(
1294                vec![Ty::str(), Ty::str()], EffectSet::empty(), result_str(inst())));
1295            fields.insert("format".into(), Ty::function(
1296                vec![inst(), Ty::str()], EffectSet::empty(), Ty::str()));
1297            fields.insert("to_components".into(), Ty::function(
1298                vec![inst(), tz()], EffectSet::empty(), result_str(dt_t())));
1299            fields.insert("from_components".into(), Ty::function(
1300                vec![dt_t()], EffectSet::empty(), result_str(inst())));
1301            fields.insert("add".into(), Ty::function(
1302                vec![inst(), dur()], EffectSet::empty(), inst()));
1303            fields.insert("diff".into(), Ty::function(
1304                vec![inst(), inst()], EffectSet::empty(), dur()));
1305            fields.insert("duration_seconds".into(), Ty::function(
1306                vec![Ty::float()], EffectSet::empty(), dur()));
1307            fields.insert("duration_minutes".into(), Ty::function(
1308                vec![Ty::int()], EffectSet::empty(), dur()));
1309            fields.insert("duration_days".into(), Ty::function(
1310                vec![Ty::int()], EffectSet::empty(), dur()));
1311            // #331: comparison ops on Instant.
1312            fields.insert("before".into(), Ty::function(
1313                vec![inst(), inst()], EffectSet::empty(), Ty::bool()));
1314            fields.insert("after".into(), Ty::function(
1315                vec![inst(), inst()], EffectSet::empty(), Ty::bool()));
1316            // compare :: Instant, Instant -> Int  (-1 / 0 / +1)
1317            fields.insert("compare".into(), Ty::function(
1318                vec![inst(), inst()], EffectSet::empty(), Ty::int()));
1319            Some(Ty::Record(fields))
1320        }
1321        // #331: duration module — scalar extraction from Duration values.
1322        "duration" => {
1323            let dur = || Ty::Con("Duration".into(), vec![]);
1324            let mut fields = IndexMap::new();
1325            // seconds :: Duration -> Int  (truncates toward zero)
1326            fields.insert("seconds".into(), Ty::function(
1327                vec![dur()], EffectSet::empty(), Ty::int()));
1328            Some(Ty::Record(fields))
1329        }
1330        "process" => {
1331            // Streaming subprocess. The opaque `ProcessHandle` type
1332            // is an Int handle into a process-wide registry holding
1333            // the `Child` plus its stdout/stderr `BufReader`s.
1334            let ph = || Ty::Con("ProcessHandle".into(), vec![]);
1335            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1336            let opts_t = || {
1337                let mut fs = IndexMap::new();
1338                fs.insert("cwd".into(),
1339                    Ty::Con("Option".into(), vec![Ty::str()]));
1340                fs.insert("env".into(),
1341                    Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
1342                fs.insert("stdin".into(),
1343                    Ty::Con("Option".into(), vec![Ty::bytes()]));
1344                Ty::Record(fs)
1345            };
1346            let exit_t = || {
1347                let mut fs = IndexMap::new();
1348                fs.insert("code".into(), Ty::int());
1349                fs.insert("signaled".into(), Ty::bool());
1350                Ty::Record(fs)
1351            };
1352            let output_t = || {
1353                let mut fs = IndexMap::new();
1354                fs.insert("stdout".into(), Ty::str());
1355                fs.insert("stderr".into(), Ty::str());
1356                fs.insert("exit_code".into(), Ty::int());
1357                Ty::Record(fs)
1358            };
1359            let mut fields = IndexMap::new();
1360            // spawn :: Str, List[Str], Opts -> [proc] Result[ProcessHandle, Str]
1361            fields.insert("spawn".into(), Ty::function(
1362                vec![Ty::str(), Ty::List(Box::new(Ty::str())), opts_t()],
1363                EffectSet::singleton("proc"),
1364                result_str(ph())));
1365            // read_stdout_line / read_stderr_line :: ProcessHandle -> [proc] Option[Str]
1366            for n in &["read_stdout_line", "read_stderr_line"] {
1367                fields.insert((*n).into(), Ty::function(
1368                    vec![ph()], EffectSet::singleton("proc"),
1369                    Ty::Con("Option".into(), vec![Ty::str()])));
1370            }
1371            // wait :: ProcessHandle -> [proc] ProcessExit
1372            fields.insert("wait".into(), Ty::function(
1373                vec![ph()], EffectSet::singleton("proc"), exit_t()));
1374            // kill :: ProcessHandle, Str -> [proc] Result[Nil, Str]
1375            fields.insert("kill".into(), Ty::function(
1376                vec![ph(), Ty::str()],
1377                EffectSet::singleton("proc"),
1378                result_str(Ty::Unit)));
1379            // run :: Str, List[Str] -> [proc] Result[ProcessOutput, Str]
1380            // Blocking convenience that captures stdout/stderr fully
1381            // and returns once the child exits. For programs that
1382            // need streaming, use spawn + read_*_line + wait.
1383            fields.insert("run".into(), Ty::function(
1384                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
1385                EffectSet::singleton("proc"),
1386                result_str(output_t())));
1387            Some(Ty::Record(fields))
1388        }
1389        "fs" => {
1390            // Filesystem walk + mutate. Walk-style ops (exists, walk,
1391            // glob, …) declare [fs_walk] — distinct from [fs_read]
1392            // (which is content reads via io.read), so reviewers can
1393            // separately track directory traversal vs file-content
1394            // exposure. Mutating ops (mkdir_p, remove, copy) declare
1395            // [fs_write]. Path scoping uses --allow-fs-read for walk
1396            // (a directory listing is an information disclosure on
1397            // the same path tree) and --allow-fs-write for mutations.
1398            let stat_t = || {
1399                let mut fs = IndexMap::new();
1400                fs.insert("size".into(), Ty::int());
1401                fs.insert("mtime".into(), Ty::int());
1402                fs.insert("is_dir".into(), Ty::bool());
1403                fs.insert("is_file".into(), Ty::bool());
1404                Ty::Record(fs)
1405            };
1406            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1407            let mut fields = IndexMap::new();
1408            // Walk-style queries [fs_walk]
1409            fields.insert("exists".into(), Ty::function(
1410                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
1411            fields.insert("is_file".into(), Ty::function(
1412                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
1413            fields.insert("is_dir".into(), Ty::function(
1414                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
1415            fields.insert("stat".into(), Ty::function(
1416                vec![Ty::str()], EffectSet::singleton("fs_walk"),
1417                result_str(stat_t())));
1418            fields.insert("list_dir".into(), Ty::function(
1419                vec![Ty::str()], EffectSet::singleton("fs_walk"),
1420                result_str(Ty::List(Box::new(Ty::str())))));
1421            fields.insert("walk".into(), Ty::function(
1422                vec![Ty::str()], EffectSet::singleton("fs_walk"),
1423                result_str(Ty::List(Box::new(Ty::str())))));
1424            fields.insert("glob".into(), Ty::function(
1425                vec![Ty::str()], EffectSet::singleton("fs_walk"),
1426                result_str(Ty::List(Box::new(Ty::str())))));
1427            // Mutations [fs_write]
1428            fields.insert("mkdir_p".into(), Ty::function(
1429                vec![Ty::str()], EffectSet::singleton("fs_write"),
1430                result_str(Ty::Unit)));
1431            fields.insert("remove".into(), Ty::function(
1432                vec![Ty::str()], EffectSet::singleton("fs_write"),
1433                result_str(Ty::Unit)));
1434            fields.insert("copy".into(), Ty::function(
1435                vec![Ty::str(), Ty::str()],
1436                EffectSet {
1437                    concrete: [crate::types::EffectKind::bare("fs_walk"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
1438                    var: None,
1439                },
1440                result_str(Ty::Unit)));
1441            Some(Ty::Record(fields))
1442        }
1443        "kv" => {
1444            // Embedded key-value store. The opaque `Kv` type is
1445            // backed by an Int handle into a process-wide registry.
1446            let kv_t = || Ty::Con("Kv".into(), vec![]);
1447            let mut fields = IndexMap::new();
1448            // open :: Str -> [kv, fs_write] Result[Kv, Str]
1449            fields.insert("open".into(), Ty::function(
1450                vec![Ty::str()],
1451                EffectSet {
1452                    concrete: [crate::types::EffectKind::bare("kv"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
1453                    var: None,
1454                },
1455                Ty::Con("Result".into(), vec![kv_t(), Ty::str()])));
1456            // close :: Kv -> [kv] Nil
1457            fields.insert("close".into(), Ty::function(
1458                vec![kv_t()],
1459                EffectSet::singleton("kv"),
1460                Ty::Unit));
1461            // get :: Kv, Str -> [kv] Option[Bytes]
1462            fields.insert("get".into(), Ty::function(
1463                vec![kv_t(), Ty::str()],
1464                EffectSet::singleton("kv"),
1465                Ty::Con("Option".into(), vec![Ty::bytes()])));
1466            // put :: Kv, Str, Bytes -> [kv] Result[Nil, Str]
1467            fields.insert("put".into(), Ty::function(
1468                vec![kv_t(), Ty::str(), Ty::bytes()],
1469                EffectSet::singleton("kv"),
1470                Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
1471            // delete :: Kv, Str -> [kv] Result[Nil, Str]
1472            fields.insert("delete".into(), Ty::function(
1473                vec![kv_t(), Ty::str()],
1474                EffectSet::singleton("kv"),
1475                Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
1476            // contains :: Kv, Str -> [kv] Bool
1477            fields.insert("contains".into(), Ty::function(
1478                vec![kv_t(), Ty::str()],
1479                EffectSet::singleton("kv"),
1480                Ty::bool()));
1481            // list_prefix :: Kv, Str -> [kv] List[Str]
1482            fields.insert("list_prefix".into(), Ty::function(
1483                vec![kv_t(), Ty::str()],
1484                EffectSet::singleton("kv"),
1485                Ty::List(Box::new(Ty::str()))));
1486            Some(Ty::Record(fields))
1487        }
1488        "sql" => {
1489            // Embedded SQL (SQLite via rusqlite). The opaque `Db` type is
1490            // backed by an Int handle into a process-wide registry (#362).
1491            //
1492            // Params use the typed `SqlParam` ADT (PStr|PInt|PFloat|PBool|PNull)
1493            // registered in env.rs, so callers don't have to stringify values.
1494            //
1495            // Transactions: sql.begin(db) → SqlTx; sql.commit/rollback(tx).
1496            // exec_tx / query_tx mirror exec / query but operate on a SqlTx.
1497            //
1498            // Row decoders: get_str / get_int / get_float / get_bool extract
1499            // typed columns from a row record by name.
1500            let db_t  = || Ty::Con("Db".into(), vec![]);
1501            let tx_t  = || Ty::Con("SqlTx".into(), vec![]);
1502            let sp_t  = || Ty::Con("SqlParam".into(), vec![]);
1503            let params_t = || Ty::List(Box::new(sp_t()));
1504            let mut fields = IndexMap::new();
1505
1506            // SqlError = { message, code, detail } — populated with
1507            // SQLSTATE (Postgres) or symbolic SQLite error name (#380).
1508            let se_t = || Ty::Con("SqlError".into(), vec![]);
1509
1510            // open :: Str -> [sql, fs_write] Result[Db, SqlError]
1511            fields.insert("open".into(), Ty::function(
1512                vec![Ty::str()],
1513                EffectSet {
1514                    concrete: [crate::types::EffectKind::bare("sql"),
1515                               crate::types::EffectKind::bare("fs_write")]
1516                        .into_iter().collect(),
1517                    var: None,
1518                },
1519                Ty::Con("Result".into(), vec![db_t(), se_t()])));
1520
1521            // close :: Db -> [sql] Unit
1522            fields.insert("close".into(), Ty::function(
1523                vec![db_t()],
1524                EffectSet::singleton("sql"),
1525                Ty::Unit));
1526
1527            // exec :: Db, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
1528            fields.insert("exec".into(), Ty::function(
1529                vec![db_t(), Ty::str(), params_t()],
1530                EffectSet::singleton("sql"),
1531                Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
1532
1533            // query[T] :: Db, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
1534            fields.insert("query".into(), Ty::function(
1535                vec![db_t(), Ty::str(), params_t()],
1536                EffectSet::singleton("sql"),
1537                Ty::Con("Result".into(), vec![
1538                    Ty::List(Box::new(Ty::Var(0))),
1539                    se_t(),
1540                ])));
1541
1542            // query_iter[T] :: Db, Str, List[SqlParam] -> [sql] Result[Iter[T], SqlError]
1543            // Streaming variant of `query` (#379). Rows are pulled from
1544            // the server one at a time via an mpsc-backed cursor —
1545            // memory stays bounded regardless of result-set size.
1546            // Other ops on the same `Db` handle block until the cursor
1547            // is drained (single connection per Db).
1548            fields.insert("query_iter".into(), Ty::function(
1549                vec![db_t(), Ty::str(), params_t()],
1550                EffectSet::singleton("sql"),
1551                Ty::Con("Result".into(), vec![
1552                    Ty::Con("Iter".into(), vec![Ty::Var(0)]),
1553                    se_t(),
1554                ])));
1555
1556            // begin :: Db -> [sql] Result[SqlTx, SqlError]
1557            fields.insert("begin".into(), Ty::function(
1558                vec![db_t()],
1559                EffectSet::singleton("sql"),
1560                Ty::Con("Result".into(), vec![tx_t(), se_t()])));
1561
1562            // commit :: SqlTx -> [sql] Result[Unit, SqlError]
1563            fields.insert("commit".into(), Ty::function(
1564                vec![tx_t()],
1565                EffectSet::singleton("sql"),
1566                Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
1567
1568            // rollback :: SqlTx -> [sql] Result[Unit, SqlError]
1569            fields.insert("rollback".into(), Ty::function(
1570                vec![tx_t()],
1571                EffectSet::singleton("sql"),
1572                Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
1573
1574            // exec_tx :: SqlTx, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
1575            fields.insert("exec_tx".into(), Ty::function(
1576                vec![tx_t(), Ty::str(), params_t()],
1577                EffectSet::singleton("sql"),
1578                Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
1579
1580            // query_tx[T] :: SqlTx, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
1581            fields.insert("query_tx".into(), Ty::function(
1582                vec![tx_t(), Ty::str(), params_t()],
1583                EffectSet::singleton("sql"),
1584                Ty::Con("Result".into(), vec![
1585                    Ty::List(Box::new(Ty::Var(0))),
1586                    se_t(),
1587                ])));
1588
1589            // Row decoders: get_X[T] :: T, Str -> Option[X]
1590            // T is polymorphic so these work on any row record shape.
1591            fields.insert("get_str".into(), Ty::function(
1592                vec![Ty::Var(0), Ty::str()],
1593                EffectSet::empty(),
1594                Ty::Con("Option".into(), vec![Ty::str()])));
1595            fields.insert("get_int".into(), Ty::function(
1596                vec![Ty::Var(0), Ty::str()],
1597                EffectSet::empty(),
1598                Ty::Con("Option".into(), vec![Ty::int()])));
1599            fields.insert("get_float".into(), Ty::function(
1600                vec![Ty::Var(0), Ty::str()],
1601                EffectSet::empty(),
1602                Ty::Con("Option".into(), vec![Ty::Con("Float".into(), vec![])])));
1603            fields.insert("get_bool".into(), Ty::function(
1604                vec![Ty::Var(0), Ty::str()],
1605                EffectSet::empty(),
1606                Ty::Con("Option".into(), vec![Ty::Con("Bool".into(), vec![])])));
1607
1608            Some(Ty::Record(fields))
1609        }
1610        "parser" => {
1611            // #217: structured parser combinators. Parser values are
1612            // tagged Records at runtime (`{ kind, ... }`), opaque at
1613            // the language level via `Ty::Con("Parser", [T])`.
1614            //
1615            // Surface:
1616            //   - primitives: char, string, digit, alpha, whitespace, eof
1617            //   - combinators: seq, alt, many, optional, map, and_then
1618            //   - run :: Parser[T], Str -> Result[T, ParseErr]
1619            //
1620            // `map` and `and_then` were deferred from #217's v1 because
1621            // their closure arguments carried call-site identity that
1622            // broke the canonical-parsers acceptance criterion. With
1623            // closure body-hash equality landed in #222, that concern
1624            // is gone, and #221 wires them in. The interpreter for
1625            // `parser.run` has been moved to `lex-bytecode::parser_runtime`
1626            // so it can invoke closures from `Map` / `AndThen` nodes.
1627            let pt = |t: Ty| Ty::Con("Parser".into(), vec![t]);
1628            let parse_err = || {
1629                let mut fs = IndexMap::new();
1630                fs.insert("pos".into(), Ty::int());
1631                fs.insert("message".into(), Ty::str());
1632                Ty::Record(fs)
1633            };
1634            let mut fields = IndexMap::new();
1635            // char :: Str -> Parser[Str] (single-char Str literal)
1636            fields.insert("char".into(), Ty::function(
1637                vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
1638            // string :: Str -> Parser[Str]
1639            fields.insert("string".into(), Ty::function(
1640                vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
1641            // digit :: () -> Parser[Str]
1642            fields.insert("digit".into(), Ty::function(
1643                vec![], EffectSet::empty(), pt(Ty::str())));
1644            // alpha :: () -> Parser[Str]
1645            fields.insert("alpha".into(), Ty::function(
1646                vec![], EffectSet::empty(), pt(Ty::str())));
1647            // whitespace :: () -> Parser[Str]
1648            fields.insert("whitespace".into(), Ty::function(
1649                vec![], EffectSet::empty(), pt(Ty::str())));
1650            // eof :: () -> Parser[Unit]
1651            fields.insert("eof".into(), Ty::function(
1652                vec![], EffectSet::empty(), pt(Ty::Unit)));
1653            // seq :: Parser[A], Parser[B] -> Parser[(A, B)]
1654            fields.insert("seq".into(), Ty::function(
1655                vec![pt(Ty::Var(0)), pt(Ty::Var(1))],
1656                EffectSet::empty(),
1657                pt(Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]))));
1658            // alt :: Parser[T], Parser[T] -> Parser[T]
1659            // PEG-style ordered choice: the second alternative is
1660            // tried only if the first fails.
1661            fields.insert("alt".into(), Ty::function(
1662                vec![pt(Ty::Var(0)), pt(Ty::Var(0))],
1663                EffectSet::empty(),
1664                pt(Ty::Var(0))));
1665            // many :: Parser[T] -> Parser[List[T]]
1666            // Zero-or-more. Stops as soon as the inner parser fails
1667            // OR doesn't advance the position (avoids infinite loop
1668            // on empty matches).
1669            fields.insert("many".into(), Ty::function(
1670                vec![pt(Ty::Var(0))],
1671                EffectSet::empty(),
1672                pt(Ty::List(Box::new(Ty::Var(0))))));
1673            // optional :: Parser[T] -> Parser[Option[T]]
1674            fields.insert("optional".into(), Ty::function(
1675                vec![pt(Ty::Var(0))],
1676                EffectSet::empty(),
1677                pt(Ty::Con("Option".into(), vec![Ty::Var(0)]))));
1678            // map :: Parser[T], (T) -> [E] U -> [E] Parser[U]
1679            // The closure runs at parse time when the Parser is run.
1680            // Effect-polymorphic on the closure: any effect the
1681            // closure declares propagates to the surrounding `run`.
1682            fields.insert("map".into(), Ty::function(
1683                vec![
1684                    pt(Ty::Var(0)),
1685                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1686                ],
1687                EffectSet::open_var(2),
1688                pt(Ty::Var(1))));
1689            // and_then :: Parser[T], (T) -> [E] Parser[U] -> [E] Parser[U]
1690            // Monadic bind: closure inspects the parsed value and
1691            // returns the next parser to run.
1692            fields.insert("and_then".into(), Ty::function(
1693                vec![
1694                    pt(Ty::Var(0)),
1695                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
1696                        pt(Ty::Var(1))),
1697                ],
1698                EffectSet::open_var(3),
1699                pt(Ty::Var(1))));
1700            // run :: Parser[T], Str -> Result[T, ParseErr]
1701            // ParseErr = { pos :: Int, message :: Str }
1702            fields.insert("run".into(), Ty::function(
1703                vec![pt(Ty::Var(0)), Ty::str()],
1704                EffectSet::empty(),
1705                Ty::Con("Result".into(), vec![Ty::Var(0), parse_err()])));
1706            Some(Ty::Record(fields))
1707        }
1708        "cli" => {
1709            // #224 Rubric port: argparse-equivalent for end-user
1710            // programs. Spec values are tagged `Json` records (opaque
1711            // to the language but inspectable). Construction via the
1712            // `flag` / `option` / `positional` / `spec` builders;
1713            // parse + introspection / help via the remaining ops.
1714            let json = || Ty::Con("Json".into(), vec![]);
1715            let opt_str = || Ty::Con("Option".into(), vec![Ty::str()]);
1716            let mut fields = IndexMap::new();
1717            // flag :: Str -> Option[Str] -> Str -> Json
1718            //   long_name -> short -> help -> CliArg
1719            fields.insert("flag".into(), Ty::function(
1720                vec![Ty::str(), opt_str(), Ty::str()],
1721                EffectSet::empty(),
1722                json()));
1723            // option :: Str -> Option[Str] -> Str -> Option[Str] -> Json
1724            //   long_name -> short -> help -> default -> CliArg
1725            fields.insert("option".into(), Ty::function(
1726                vec![Ty::str(), opt_str(), Ty::str(), opt_str()],
1727                EffectSet::empty(),
1728                json()));
1729            // positional :: Str -> Str -> Bool -> Json
1730            //   name -> help -> required -> CliArg
1731            fields.insert("positional".into(), Ty::function(
1732                vec![Ty::str(), Ty::str(), Ty::bool()],
1733                EffectSet::empty(),
1734                json()));
1735            // spec :: Str -> Str -> List[Json] -> List[Json] -> Json
1736            //   name -> help -> args -> subcommands -> CliSpec
1737            fields.insert("spec".into(), Ty::function(
1738                vec![Ty::str(), Ty::str(),
1739                     Ty::List(Box::new(json())),
1740                     Ty::List(Box::new(json()))],
1741                EffectSet::empty(),
1742                json()));
1743            // parse :: Json -> List[Str] -> Result[Json, Str]
1744            //   spec -> argv -> Result[CliParsed, error]
1745            fields.insert("parse".into(), Ty::function(
1746                vec![json(), Ty::List(Box::new(Ty::str()))],
1747                EffectSet::empty(),
1748                Ty::Con("Result".into(), vec![json(), Ty::str()])));
1749            // envelope :: Bool -> Str -> T -> Json
1750            //   ok -> command -> data -> ACLI-shaped envelope.
1751            // `data` is polymorphic so callers don't have to round-
1752            // trip through `json.parse` for trivial payloads.
1753            fields.insert("envelope".into(), Ty::function(
1754                vec![Ty::bool(), Ty::str(), Ty::Var(0)],
1755                EffectSet::empty(),
1756                json()));
1757            // describe :: Json -> Json — machine-readable spec dump
1758            fields.insert("describe".into(), Ty::function(
1759                vec![json()],
1760                EffectSet::empty(),
1761                json()));
1762            // help :: Json -> Str — human-readable help text
1763            fields.insert("help".into(), Ty::function(
1764                vec![json()],
1765                EffectSet::empty(),
1766                Ty::str()));
1767            Some(Ty::Record(fields))
1768        }
1769        "regex" => {
1770            // The compiled `Regex` is stored as a `Str` at runtime
1771            // (the pattern source) plus a process-wide cache of the
1772            // actual `regex::Regex`. So `Regex` is a nominal type at
1773            // the language level but its value is just the pattern.
1774            let regex_t = || Ty::Con("Regex".into(), vec![]);
1775            let match_t = || {
1776                let mut fs = IndexMap::new();
1777                fs.insert("text".into(), Ty::str());
1778                fs.insert("start".into(), Ty::int());
1779                fs.insert("end".into(), Ty::int());
1780                fs.insert("groups".into(), Ty::List(Box::new(Ty::str())));
1781                Ty::Record(fs)
1782            };
1783            let mut fields = IndexMap::new();
1784            // compile :: Str -> Result[Regex, Str]
1785            fields.insert("compile".into(), Ty::function(
1786                vec![Ty::str()], EffectSet::empty(),
1787                Ty::Con("Result".into(), vec![regex_t(), Ty::str()])));
1788            // is_match :: Regex, Str -> Bool
1789            fields.insert("is_match".into(), Ty::function(
1790                vec![regex_t(), Ty::str()], EffectSet::empty(), Ty::bool()));
1791            // is_match_str :: Str, Str -> Bool
1792            // Compiles the first argument as a pattern and matches against the second.
1793            // Returns false on invalid pattern instead of propagating an error.
1794            fields.insert("is_match_str".into(), Ty::function(
1795                vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
1796            // find :: Regex, Str -> Option[Match]
1797            fields.insert("find".into(), Ty::function(
1798                vec![regex_t(), Ty::str()], EffectSet::empty(),
1799                Ty::Con("Option".into(), vec![match_t()])));
1800            // find_all :: Regex, Str -> List[Match]
1801            fields.insert("find_all".into(), Ty::function(
1802                vec![regex_t(), Ty::str()], EffectSet::empty(),
1803                Ty::List(Box::new(match_t()))));
1804            // replace :: Regex, Str, Str -> Str
1805            fields.insert("replace".into(), Ty::function(
1806                vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
1807            // replace_all :: Regex, Str, Str -> Str
1808            fields.insert("replace_all".into(), Ty::function(
1809                vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
1810            // split :: Regex, Str -> List[Str]
1811            fields.insert("split".into(), Ty::function(
1812                vec![regex_t(), Ty::str()], EffectSet::empty(),
1813                Ty::List(Box::new(Ty::str()))));
1814            Some(Ty::Record(fields))
1815        }
1816        "http" => {
1817            // Rich HTTP client. `[net]` for the wire ops, pure for
1818            // the builders / decoders. `--allow-net-host` gates per
1819            // request. Multipart upload + streaming response bodies
1820            // are deferred to v1.5; the v1 surface covers the
1821            // common cases (auth, headers, query, timeouts, JSON /
1822            // text decoding).
1823            let req_t  = || Ty::Con("HttpRequest".into(), vec![]);
1824            let resp_t = || Ty::Con("HttpResponse".into(), vec![]);
1825            let err_t  = || Ty::Con("HttpError".into(), vec![]);
1826            let result_he = |t: Ty| Ty::Con("Result".into(), vec![t, err_t()]);
1827            let str_str_map = || Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]);
1828            let mut fields = IndexMap::new();
1829            // -- wire ops (effectful) --
1830            // send :: HttpRequest -> [net] Result[HttpResponse, HttpError]
1831            fields.insert("send".into(), Ty::function(
1832                vec![req_t()],
1833                EffectSet::singleton("net"),
1834                result_he(resp_t()),
1835            ));
1836            // get :: Str -> [net] Result[HttpResponse, HttpError]
1837            fields.insert("get".into(), Ty::function(
1838                vec![Ty::str()],
1839                EffectSet::singleton("net"),
1840                result_he(resp_t()),
1841            ));
1842            // post :: Str, Bytes, Str -> [net] Result[HttpResponse, HttpError]
1843            fields.insert("post".into(), Ty::function(
1844                vec![Ty::str(), Ty::bytes(), Ty::str()],
1845                EffectSet::singleton("net"),
1846                result_he(resp_t()),
1847            ));
1848            // -- pure builders (record transforms) --
1849            // with_header :: HttpRequest, Str, Str -> HttpRequest
1850            fields.insert("with_header".into(), Ty::function(
1851                vec![req_t(), Ty::str(), Ty::str()],
1852                EffectSet::empty(),
1853                req_t(),
1854            ));
1855            // with_auth :: HttpRequest, Str, Str -> HttpRequest
1856            // (Renders `<scheme> <token>` into the `Authorization`
1857            // header — `Bearer <jwt>`, `Basic <b64>`, etc.)
1858            fields.insert("with_auth".into(), Ty::function(
1859                vec![req_t(), Ty::str(), Ty::str()],
1860                EffectSet::empty(),
1861                req_t(),
1862            ));
1863            // with_query :: HttpRequest, Map[Str, Str] -> HttpRequest
1864            // (Appends a `?k=v&...` query string; values are URL-
1865            // encoded so `&` / `=` / spaces in values don't escape.)
1866            fields.insert("with_query".into(), Ty::function(
1867                vec![req_t(), str_str_map()],
1868                EffectSet::empty(),
1869                req_t(),
1870            ));
1871            // with_timeout_ms :: HttpRequest, Int -> HttpRequest
1872            fields.insert("with_timeout_ms".into(), Ty::function(
1873                vec![req_t(), Ty::int()],
1874                EffectSet::empty(),
1875                req_t(),
1876            ));
1877            // -- pure decoders --
1878            // json_body[T] :: HttpResponse -> Result[T, HttpError]
1879            // Polymorphic on the parsed shape, matching `json.parse`.
1880            fields.insert("json_body".into(), Ty::function(
1881                vec![resp_t()],
1882                EffectSet::empty(),
1883                result_he(Ty::Var(0)),
1884            ));
1885            // text_body :: HttpResponse -> Result[Str, HttpError]
1886            fields.insert("text_body".into(), Ty::function(
1887                vec![resp_t()],
1888                EffectSet::empty(),
1889                result_he(Ty::str()),
1890            ));
1891            Some(Ty::Record(fields))
1892        }
1893        "yaml" => {
1894            // YAML config parser. Same shape as `std.toml`: parse
1895            // is polymorphic, output Value layout matches std.json
1896            // (Str/Int/Float/Bool/List/Record). Anchors and tags
1897            // are flattened by serde_yaml's deserializer.
1898            let mut fields = IndexMap::new();
1899            fields.insert("parse".into(), Ty::function(
1900                vec![Ty::str()], EffectSet::empty(),
1901                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1902            ));
1903            // Tactical fix for #168 — caller-supplied required-field
1904            // list. See std.json's parse_strict for context.
1905            fields.insert("parse_strict".into(), Ty::function(
1906                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
1907                EffectSet::empty(),
1908                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1909            ));
1910            fields.insert("stringify".into(), Ty::function(
1911                vec![Ty::Var(0)], EffectSet::empty(),
1912                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1913            ));
1914            Some(Ty::Record(fields))
1915        }
1916        "dotenv" => {
1917            // .env-style files. parse :: Str -> Result[Map[Str,Str], Str].
1918            // Returns a map (not a polymorphic record) because
1919            // dotenv files don't carry shape — every value is a
1920            // string and keys aren't statically known.
1921            let mut fields = IndexMap::new();
1922            fields.insert("parse".into(), Ty::function(
1923                vec![Ty::str()], EffectSet::empty(),
1924                Ty::Con("Result".into(), vec![
1925                    Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
1926                    Ty::str(),
1927                ]),
1928            ));
1929            Some(Ty::Record(fields))
1930        }
1931        "csv" => {
1932            // CSV rows-as-lists. parse :: Str -> Result[List[List[Str]], Str].
1933            // Header awareness is left to the caller — row 0 is
1934            // whatever the file has. A `parse_with_headers` that
1935            // returns List[Map[Str,Str]] is a natural follow-up.
1936            let row_ty = Ty::List(Box::new(Ty::str()));
1937            let rows_ty = Ty::List(Box::new(row_ty.clone()));
1938            let mut fields = IndexMap::new();
1939            fields.insert("parse".into(), Ty::function(
1940                vec![Ty::str()], EffectSet::empty(),
1941                Ty::Con("Result".into(), vec![rows_ty.clone(), Ty::str()]),
1942            ));
1943            fields.insert("stringify".into(), Ty::function(
1944                vec![rows_ty], EffectSet::empty(),
1945                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1946            ));
1947            Some(Ty::Record(fields))
1948        }
1949        "test" => {
1950            // Tiny assertion library (#proposed-stdlib). Each helper
1951            // returns Result[Unit, Str] so a test is itself a fn
1952            // returning Result. Callers compose suites in user code
1953            // (a List of (name, () -> Result[Unit, Str]) pairs +
1954            // list.fold to accumulate verdicts). Property generators
1955            // and a Rust-side Suite type are deferred to v2.
1956            let mut fields = IndexMap::new();
1957            // assert_eq[a, b] :: T -> T -> Result[Unit, Str]
1958            // (T constrained equal by unification on the two args)
1959            let unit_result = || Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]);
1960            fields.insert("assert_eq".into(), Ty::function(
1961                vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
1962            ));
1963            fields.insert("assert_ne".into(), Ty::function(
1964                vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
1965            ));
1966            fields.insert("assert_true".into(), Ty::function(
1967                vec![Ty::bool()], EffectSet::empty(), unit_result(),
1968            ));
1969            fields.insert("assert_false".into(), Ty::function(
1970                vec![Ty::bool()], EffectSet::empty(), unit_result(),
1971            ));
1972            Some(Ty::Record(fields))
1973        }
1974        "toml" => {
1975            // TOML config parser. Mirrors `std.json`'s shape: parse
1976            // is polymorphic so callers annotate the expected
1977            // record / list / scalar shape and the type checker
1978            // unifies. The parsed TOML maps to the same Lex Value
1979            // shape as JSON does:
1980            //
1981            //   TOML String   → Value::Str
1982            //   TOML Integer  → Value::Int
1983            //   TOML Float    → Value::Float
1984            //   TOML Boolean  → Value::Bool
1985            //   TOML Array    → Value::List
1986            //   TOML Table    → Value::Record
1987            //   TOML Datetime → Value::Str (RFC 3339, lossless)
1988            //
1989            // The Datetime → Str fallback is the one info-losing
1990            // step; callers who want a real `Instant` can pipe the
1991            // string through `datetime.parse_iso`.
1992            let mut fields = IndexMap::new();
1993            // parse :: Str -> Result[T, Str]
1994            fields.insert("parse".into(), Ty::function(
1995                vec![Ty::str()], EffectSet::empty(),
1996                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1997            ));
1998            // parse_strict :: (Str, List[Str]) -> Result[T, Str]
1999            // Tactical fix for #168 — caller passes the field
2000            // names T requires; runtime returns Err if any are
2001            // missing from the parsed table instead of letting
2002            // field access panic later.
2003            fields.insert("parse_strict".into(), Ty::function(
2004                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
2005                EffectSet::empty(),
2006                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
2007            ));
2008            // stringify :: T -> Result[Str, Str]
2009            // Returns Result (not Str) because not every Lex Value
2010            // has a TOML representation — top-level scalars,
2011            // closures, mixed-key maps etc. surface as Err rather
2012            // than panic.
2013            fields.insert("stringify".into(), Ty::function(
2014                vec![Ty::Var(0)], EffectSet::empty(),
2015                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2016            ));
2017            Some(Ty::Record(fields))
2018        }
2019        // `std.agent` (#184) — runtime primitives whose effects
2020        // separate (a) which LLM surface (`llm_local` vs
2021        // `llm_cloud`), (b) which peer protocol (`a2a`), and
2022        // (c) which tool boundary (`mcp`). The wire formats land
2023        // in downstream crates (`soft-agent`, `soft-a2a`) and
2024        // in #185 for MCP; what's typed here is the boundary
2025        // alone — agent code can be type-checked as
2026        // `[llm_local, a2a]` and will fail if it tries to reach
2027        // `[llm_cloud]` even before the wire layer is finished.
2028        "agent" => {
2029            let mut fields = IndexMap::new();
2030            // local_complete :: Str -> [llm_local] Result[Str, Str]
2031            fields.insert("local_complete".into(), Ty::function(
2032                vec![Ty::str()],
2033                EffectSet::singleton("llm_local"),
2034                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2035            ));
2036            // cloud_complete :: Str -> [llm_cloud] Result[Str, Str]
2037            fields.insert("cloud_complete".into(), Ty::function(
2038                vec![Ty::str()],
2039                EffectSet::singleton("llm_cloud"),
2040                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2041            ));
2042            // send_a2a :: (Str, Str) -> [a2a] Result[Str, Str]
2043            //              peer payload                   reply
2044            fields.insert("send_a2a".into(), Ty::function(
2045                vec![Ty::str(), Ty::str()],
2046                EffectSet::singleton("a2a"),
2047                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2048            ));
2049            // call_mcp :: (Str, Str, Str) -> [mcp] Result[Str, Str]
2050            //              server tool args_json         result_json
2051            fields.insert("call_mcp".into(), Ty::function(
2052                vec![Ty::str(), Ty::str(), Ty::str()],
2053                EffectSet::singleton("mcp"),
2054                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2055            ));
2056            // cloud_stream :: Str -> [llm_cloud] Result[Stream[Str], Str]
2057            // (#305 slice 3). Streaming counterpart to cloud_complete.
2058            // The result is `Result[Stream[Str], Str]` rather than a
2059            // bare Stream so transport errors surface synchronously
2060            // at handshake time; per-chunk errors collapse the
2061            // stream to early termination.
2062            fields.insert("cloud_stream".into(), Ty::function(
2063                vec![Ty::str()],
2064                EffectSet::singleton("llm_cloud"),
2065                Ty::Con("Result".into(), vec![
2066                    Ty::Con("Stream".into(), vec![Ty::str()]),
2067                    Ty::str(),
2068                ]),
2069            ));
2070            Some(Ty::Record(fields))
2071        }
2072        "stream" => {
2073            // #305 slice 3: opaque consumer-side operations on
2074            // `Stream[T]`. Producers live elsewhere (`agent.cloud_stream`
2075            // for now); future producers (`http.get_stream`, etc.)
2076            // will register the same Stream[T] surface.
2077            let mut fields = IndexMap::new();
2078            // next :: Stream[T] -> [stream] Option[T]
2079            // One pull. `None` signals end-of-stream (consumed by
2080            // the producer's lazy generator).
2081            fields.insert("next".into(), Ty::function(
2082                vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
2083                EffectSet::singleton("stream"),
2084                Ty::Con("Option".into(), vec![Ty::Var(0)]),
2085            ));
2086            // collect :: Stream[T] -> [stream] List[T]
2087            // Drain to a list. Eager; blocks until the producer
2088            // signals end-of-stream.
2089            fields.insert("collect".into(), Ty::function(
2090                vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
2091                EffectSet::singleton("stream"),
2092                Ty::List(Box::new(Ty::Var(0))),
2093            ));
2094            Some(Ty::Record(fields))
2095        }
2096        _ => None,
2097    }
2098}
2099
2100/// Resolve `import "std.foo" as alias` to a module name (e.g. "io").
2101pub fn module_for_import(reference: &str) -> Option<&'static str> {
2102    let suffix = reference.strip_prefix("std.")?;
2103    Some(match suffix {
2104        "io" => "io",
2105        "str" => "str",
2106        "int" => "int",
2107        "float" => "float",
2108        "list" => "list",
2109        "result" => "result",
2110        "option" => "option",
2111        "json" => "json",
2112        "flow" => "flow",
2113        "tuple" => "tuple",
2114        "time" => "time",
2115        "rand" => "rand",
2116        "random" => "random",
2117        "env" => "env",
2118        "bytes" => "bytes",
2119        "net" => "net",
2120        "chat" => "chat",
2121        "math" => "math",
2122        "map" => "map",
2123        "set" => "set",
2124        "iter" => "iter",
2125        "proc" => "proc",
2126        "crypto" => "crypto",
2127        "regex" => "regex",
2128        "parser" => "parser",
2129        "deque" => "deque",
2130        "kv" => "kv",
2131        "sql" => "sql",
2132        "fs" => "fs",
2133        "process" => "process",
2134        "datetime" => "datetime",
2135        "duration" => "duration",
2136        "log" => "log",
2137        "http" => "http",
2138        "toml" => "toml",
2139        "yaml" => "yaml",
2140        "dotenv" => "dotenv",
2141        "csv" => "csv",
2142        "test" => "test",
2143        "agent" => "agent",
2144        "cli" => "cli",
2145        "stream" => "stream",
2146        _ => return None,
2147    })
2148}