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            // serve_fn[Eff] :: (Int, (Request) -> [Eff] Response) -> [net, Eff] Unit
490            // Effect-polymorphic variant of serve that accepts a first-class closure
491            // instead of a handler name. open_var(0) captures the handler's effect row
492            // so callers that invoke e.g. [io] effects inside the closure propagate them
493            // to the serve_fn call site.
494            fields.insert("serve_fn".into(), Ty::function(
495                vec![
496                    Ty::int(),
497                    Ty::function(
498                        vec![Ty::Con("Request".into(), vec![])],
499                        EffectSet::open_var(0),
500                        Ty::Con("Response".into(), vec![]),
501                    ),
502                ],
503                EffectSet::open_var(0).union(&EffectSet::singleton("net")),
504                Ty::Unit,
505            ));
506            Some(Ty::Record(fields))
507        }
508        "chat" => {
509            let mut fields = IndexMap::new();
510            fields.insert("broadcast".into(), Ty::function(
511                vec![Ty::str(), Ty::str()],
512                EffectSet::singleton("chat"),
513                Ty::Unit,
514            ));
515            fields.insert("send".into(), Ty::function(
516                vec![Ty::int(), Ty::str()],
517                EffectSet::singleton("chat"),
518                Ty::bool(),
519            ));
520            Some(Ty::Record(fields))
521        }
522        "proc" => {
523            // Subprocess dispatch. Effect: [proc]. Returns a Result
524            // with a record on success carrying stdout / stderr /
525            // exit_code. The runtime allow-lists which binary
526            // basenames are spawnable — `cmd` is the program to
527            // run, `args` is the literal argv (no shell parsing).
528            //
529            // Read SECURITY.md before adding [proc] to a policy:
530            // it weakens the "we know what this fn does" claim.
531            let mut fields = IndexMap::new();
532            let mut result_rec = IndexMap::new();
533            result_rec.insert("stdout".into(), Ty::str());
534            result_rec.insert("stderr".into(), Ty::str());
535            result_rec.insert("exit_code".into(), Ty::int());
536            // spawn :: Str, List[Str] -> [proc] Result[{stdout, stderr, exit_code}, Str]
537            fields.insert("spawn".into(), Ty::function(
538                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
539                EffectSet::singleton("proc"),
540                Ty::Con("Result".into(), vec![
541                    Ty::Record(result_rec),
542                    Ty::str(),
543                ]),
544            ));
545            Some(Ty::Record(fields))
546        }
547        "json" => {
548            let mut fields = IndexMap::new();
549            // stringify :: T -> Str  (polymorphic on input)
550            fields.insert("stringify".into(), Ty::function(
551                vec![Ty::Var(0)], EffectSet::empty(), Ty::str(),
552            ));
553            // parse :: Str -> Result[T, Str]
554            fields.insert("parse".into(), Ty::function(
555                vec![Ty::str()], EffectSet::empty(),
556                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
557            ));
558            // parse_strict :: (Str, List[Str]) -> Result[T, Str]
559            // Tactical fix for #168 — caller passes the field names
560            // T requires; runtime returns Err if any are missing
561            // from the parsed object instead of letting field
562            // access panic later.
563            fields.insert("parse_strict".into(), Ty::function(
564                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
565                EffectSet::empty(),
566                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
567            ));
568            Some(Ty::Record(fields))
569        }
570        "result" => {
571            let mut fields = IndexMap::new();
572            // result.map :: Result[T, E], (T) -> [E2] U -> [E2] Result[U, E]
573            // Effect-polymorphic on the closure: result.map et al.
574            // propagate the closure's effects to the surrounding call.
575            fields.insert("map".into(), Ty::function(
576                vec![
577                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
578                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), Ty::Var(2)),
579                ],
580                EffectSet::open_var(3),
581                Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
582            ));
583            fields.insert("and_then".into(), Ty::function(
584                vec![
585                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
586                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(4),
587                        Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)])),
588                ],
589                EffectSet::open_var(4),
590                Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
591            ));
592            fields.insert("map_err".into(), Ty::function(
593                vec![
594                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
595                    Ty::function(vec![Ty::Var(1)], EffectSet::open_var(5), Ty::Var(2)),
596                ],
597                EffectSet::open_var(5),
598                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
599            ));
600            // result.or_else :: Result[T, E1], (E1) -> [E] Result[T, E2]
601            //                                    -> [E] Result[T, E2]
602            // Recovery combinator: closure runs only on Err and returns
603            // the next Result (which itself may swap the error type).
604            fields.insert("or_else".into(), Ty::function(
605                vec![
606                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
607                    Ty::function(vec![Ty::Var(1)], EffectSet::open_var(6),
608                        Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)])),
609                ],
610                EffectSet::open_var(6),
611                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
612            ));
613            Some(Ty::Record(fields))
614        }
615        "option" => {
616            let mut fields = IndexMap::new();
617            // option.map :: Option[T], (T) -> [E] U -> [E] Option[U]
618            fields.insert("map".into(), Ty::function(
619                vec![
620                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
621                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
622                ],
623                EffectSet::open_var(2),
624                Ty::Con("Option".into(), vec![Ty::Var(1)]),
625            ));
626            // option.and_then :: Option[T], (T) -> [E] Option[U] -> [E] Option[U]
627            // The compiler entry has been wired since the result/option
628            // variant_map work landed; this signature was missed,
629            // making the call fail to type-check until now.
630            fields.insert("and_then".into(), Ty::function(
631                vec![
632                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
633                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
634                        Ty::Con("Option".into(), vec![Ty::Var(1)])),
635                ],
636                EffectSet::open_var(3),
637                Ty::Con("Option".into(), vec![Ty::Var(1)]),
638            ));
639            fields.insert("unwrap_or".into(), Ty::function(
640                vec![Ty::Con("Option".into(), vec![Ty::Var(0)]), Ty::Var(0)],
641                EffectSet::empty(),
642                Ty::Var(0),
643            ));
644            // option.unwrap_or_else :: Option[T], () -> [E] T -> [E] T
645            // Lazy variant of unwrap_or: the default is computed by a closure
646            // only when the value is None (effect-polymorphic on the closure).
647            fields.insert("unwrap_or_else".into(), Ty::function(
648                vec![
649                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
650                    Ty::function(vec![], EffectSet::open_var(5), Ty::Var(0)),
651                ],
652                EffectSet::open_var(5),
653                Ty::Var(0),
654            ));
655            // option.or_else :: Option[T], () -> [E] Option[T] -> [E] Option[T]
656            // The closure takes no arguments because None has no payload to pass.
657            fields.insert("or_else".into(), Ty::function(
658                vec![
659                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
660                    Ty::function(vec![], EffectSet::open_var(4),
661                        Ty::Con("Option".into(), vec![Ty::Var(0)])),
662                ],
663                EffectSet::open_var(4),
664                Ty::Con("Option".into(), vec![Ty::Var(0)]),
665            ));
666            Some(Ty::Record(fields))
667        }
668        "tuple" => {
669            // Tuple accessors per §11.1. Polymorphic in the tuple's
670            // element types; we use the same row-variable shape used
671            // by list helpers. Tuples are heterogeneous, so each
672            // accessor is statically typed via independent type
673            // variables for each position.
674            let mut fields = IndexMap::new();
675            // fst :: (T0, T1) -> T0
676            fields.insert("fst".into(), Ty::function(
677                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
678                EffectSet::empty(),
679                Ty::Var(0),
680            ));
681            // snd :: (T0, T1) -> T1
682            fields.insert("snd".into(), Ty::function(
683                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
684                EffectSet::empty(),
685                Ty::Var(1),
686            ));
687            // third :: (T0, T1, T2) -> T2
688            fields.insert("third".into(), Ty::function(
689                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1), Ty::Var(2)])],
690                EffectSet::empty(),
691                Ty::Var(2),
692            ));
693            // len :: (T0, T1) -> Int  (covers any pair shape; Int back)
694            fields.insert("len".into(), Ty::function(
695                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
696                EffectSet::empty(),
697                Ty::int(),
698            ));
699            Some(Ty::Record(fields))
700        }
701        "map" => {
702            // Persistent map. Keys are `Str` or `Int` only — Lex's
703            // type system tracks them polymorphically as Var(0)
704            // ("K") and lets the runtime check the key shape; both
705            // cases fit into `MapKey`.
706            //
707            // Type variables: 0 = K, 1 = V.
708            let mt   = || Ty::Con("Map".into(), vec![Ty::Var(0), Ty::Var(1)]);
709            let pair = || Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]);
710            let mut fields = IndexMap::new();
711            // new :: () -> Map[K, V]
712            fields.insert("new".into(), Ty::function(
713                vec![], EffectSet::empty(), mt()));
714            // size :: Map[K, V] -> Int
715            fields.insert("size".into(), Ty::function(
716                vec![mt()], EffectSet::empty(), Ty::int()));
717            // has :: Map[K, V], K -> Bool
718            fields.insert("has".into(), Ty::function(
719                vec![mt(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
720            // get :: Map[K, V], K -> Option[V]
721            fields.insert("get".into(), Ty::function(
722                vec![mt(), Ty::Var(0)], EffectSet::empty(),
723                Ty::Con("Option".into(), vec![Ty::Var(1)])));
724            // set :: Map[K, V], K, V -> Map[K, V]
725            fields.insert("set".into(), Ty::function(
726                vec![mt(), Ty::Var(0), Ty::Var(1)],
727                EffectSet::empty(), mt()));
728            // delete :: Map[K, V], K -> Map[K, V]
729            fields.insert("delete".into(), Ty::function(
730                vec![mt(), Ty::Var(0)], EffectSet::empty(), mt()));
731            // keys :: Map[K, V] -> List[K]
732            fields.insert("keys".into(), Ty::function(
733                vec![mt()], EffectSet::empty(),
734                Ty::List(Box::new(Ty::Var(0)))));
735            // values :: Map[K, V] -> List[V]
736            fields.insert("values".into(), Ty::function(
737                vec![mt()], EffectSet::empty(),
738                Ty::List(Box::new(Ty::Var(1)))));
739            // entries :: Map[K, V] -> List[(K, V)]
740            fields.insert("entries".into(), Ty::function(
741                vec![mt()], EffectSet::empty(),
742                Ty::List(Box::new(pair()))));
743            // from_list :: List[(K, V)] -> Map[K, V]
744            fields.insert("from_list".into(), Ty::function(
745                vec![Ty::List(Box::new(pair()))],
746                EffectSet::empty(), mt()));
747            // merge :: Map[K, V], Map[K, V] -> Map[K, V]   (b overrides a)
748            fields.insert("merge".into(), Ty::function(
749                vec![mt(), mt()], EffectSet::empty(), mt()));
750            // is_empty :: Map[K, V] -> Bool
751            fields.insert("is_empty".into(), Ty::function(
752                vec![mt()], EffectSet::empty(), Ty::bool()));
753            // fold :: Map[K, V], A, (A, K, V) -> [E] A -> [E] A
754            // Iteration order matches `map.entries` (BTreeMap-sorted by
755            // key). Effect-polymorphic on the combiner like `list.fold`.
756            // Type variable 2 = A (accumulator), effect row 3.
757            fields.insert("fold".into(), Ty::function(
758                vec![
759                    mt(),
760                    Ty::Var(2),
761                    Ty::function(
762                        vec![Ty::Var(2), Ty::Var(0), Ty::Var(1)],
763                        EffectSet::open_var(3),
764                        Ty::Var(2),
765                    ),
766                ],
767                EffectSet::open_var(3),
768                Ty::Var(2),
769            ));
770            Some(Ty::Record(fields))
771        }
772        "set" => {
773            // Persistent set with the same key-type discipline as map.
774            // Type variable: 0 = T (the element type, also the key type).
775            let st   = || Ty::Con("Set".into(), vec![Ty::Var(0)]);
776            let mut fields = IndexMap::new();
777            // new :: () -> Set[T]
778            fields.insert("new".into(), Ty::function(
779                vec![], EffectSet::empty(), st()));
780            // size :: Set[T] -> Int
781            fields.insert("size".into(), Ty::function(
782                vec![st()], EffectSet::empty(), Ty::int()));
783            // has :: Set[T], T -> Bool
784            fields.insert("has".into(), Ty::function(
785                vec![st(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
786            // add :: Set[T], T -> Set[T]
787            fields.insert("add".into(), Ty::function(
788                vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
789            // delete :: Set[T], T -> Set[T]
790            fields.insert("delete".into(), Ty::function(
791                vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
792            // to_list :: Set[T] -> List[T]
793            fields.insert("to_list".into(), Ty::function(
794                vec![st()], EffectSet::empty(),
795                Ty::List(Box::new(Ty::Var(0)))));
796            // from_list :: List[T] -> Set[T]
797            fields.insert("from_list".into(), Ty::function(
798                vec![Ty::List(Box::new(Ty::Var(0)))],
799                EffectSet::empty(), st()));
800            // union :: Set[T], Set[T] -> Set[T]
801            fields.insert("union".into(), Ty::function(
802                vec![st(), st()], EffectSet::empty(), st()));
803            // intersect :: Set[T], Set[T] -> Set[T]
804            fields.insert("intersect".into(), Ty::function(
805                vec![st(), st()], EffectSet::empty(), st()));
806            // diff :: Set[T], Set[T] -> Set[T]
807            fields.insert("diff".into(), Ty::function(
808                vec![st(), st()], EffectSet::empty(), st()));
809            // is_empty :: Set[T] -> Bool
810            fields.insert("is_empty".into(), Ty::function(
811                vec![st()], EffectSet::empty(), Ty::bool()));
812            // is_subset :: Set[T], Set[T] -> Bool   (a is subset of b)
813            fields.insert("is_subset".into(), Ty::function(
814                vec![st(), st()], EffectSet::empty(), Ty::bool()));
815            Some(Ty::Record(fields))
816        }
817        "iter" => {
818            // Positional iterator (#364) + lazy variant via `iter.unfold`
819            // (#376). Internal value shapes are `__IterEager(list, idx)` or
820            // `__IterLazy(seed, step)`; all operations compile-inline and
821            // dispatch on the variant tag at runtime.
822            // Type var slots: 0 = T (element), 1 = U (mapped element) /
823            // A (fold acc), 2 = S (unfold seed).
824            let it = |n: u32| Ty::Con("Iter".into(), vec![Ty::Var(n)]);
825            let mut fields = IndexMap::new();
826            // from_list :: List[T] -> Iter[T]
827            fields.insert("from_list".into(), Ty::function(
828                vec![Ty::List(Box::new(Ty::Var(0)))],
829                EffectSet::empty(), it(0)));
830            // unfold[S, T] :: S, (S) -> Option[(T, S)] -> Iter[T] (#376)
831            // The step closure may carry any effect row; the iterator
832            // itself stays effect-free since the effects only fire when
833            // the step is invoked via `iter.next` / `iter.to_list`.
834            fields.insert("unfold".into(), Ty::function(
835                vec![
836                    Ty::Var(2), // seed S
837                    Ty::function(
838                        vec![Ty::Var(2)],
839                        EffectSet::open_var(3),
840                        Ty::Con("Option".into(), vec![
841                            Ty::Tuple(vec![Ty::Var(0), Ty::Var(2)])
842                        ]),
843                    ),
844                ],
845                EffectSet::empty(), it(0)));
846            // next :: Iter[T] -> Option[(T, Iter[T])]
847            fields.insert("next".into(), Ty::function(
848                vec![it(0)],
849                EffectSet::empty(),
850                Ty::Con("Option".into(), vec![
851                    Ty::Tuple(vec![Ty::Var(0), it(0)])
852                ])));
853            // is_empty :: Iter[T] -> Bool
854            fields.insert("is_empty".into(), Ty::function(
855                vec![it(0)], EffectSet::empty(), Ty::bool()));
856            // count :: Iter[T] -> Int   (remaining elements)
857            fields.insert("count".into(), Ty::function(
858                vec![it(0)], EffectSet::empty(), Ty::int()));
859            // take :: Iter[T], Int -> Iter[T]
860            fields.insert("take".into(), Ty::function(
861                vec![it(0), Ty::int()], EffectSet::empty(), it(0)));
862            // skip :: Iter[T], Int -> Iter[T]
863            fields.insert("skip".into(), Ty::function(
864                vec![it(0), Ty::int()], EffectSet::empty(), it(0)));
865            // to_list :: Iter[T] -> List[T]
866            fields.insert("to_list".into(), Ty::function(
867                vec![it(0)], EffectSet::empty(),
868                Ty::List(Box::new(Ty::Var(0)))));
869            // map :: [E] Iter[T], (T) -> [E] U -> [E] Iter[U]
870            fields.insert("map".into(), Ty::function(
871                vec![
872                    it(0),
873                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
874                ],
875                EffectSet::open_var(2), it(1)));
876            // filter :: [E] Iter[T], (T) -> [E] Bool -> [E] Iter[T]
877            fields.insert("filter".into(), Ty::function(
878                vec![
879                    it(0),
880                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(1), Ty::bool()),
881                ],
882                EffectSet::open_var(1), it(0)));
883            // fold :: [E] Iter[T], A, (A, T) -> [E] A -> [E] A
884            fields.insert("fold".into(), Ty::function(
885                vec![
886                    it(0),
887                    Ty::Var(1),
888                    Ty::function(vec![Ty::Var(1), Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
889                ],
890                EffectSet::open_var(2), Ty::Var(1)));
891            Some(Ty::Record(fields))
892        }
893        "flow" => {
894            // Orchestration primitives (spec §11.2). Each takes one or
895            // more closures and returns a closure with a derived shape.
896            let mut fields = IndexMap::new();
897            // sequential[T, U, V](f: (T) -> U, g: (U) -> V) -> (T) -> V
898            fields.insert("sequential".into(), Ty::function(
899                vec![
900                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
901                    Ty::function(vec![Ty::Var(1)], EffectSet::empty(), Ty::Var(2)),
902                ],
903                EffectSet::empty(),
904                Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(2)),
905            ));
906            // branch[T, U](cond: (T) -> Bool, t: (T) -> U, f: (T) -> U) -> (T) -> U
907            fields.insert("branch".into(), Ty::function(
908                vec![
909                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::bool()),
910                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
911                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
912                ],
913                EffectSet::empty(),
914                Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
915            ));
916            // retry[T, U, E, Eff](
917            //   f: (T) -> [Eff] Result[U, E], n: Int
918            // ) -> (T) -> [Eff] Result[U, E]
919            // open_var(3) is the effect row carried by `f`; the
920            // combinator itself is pure, so the outer EffectSet is
921            // empty. The returned closure propagates Eff unchanged.
922            let result_ty = Ty::Con("Result".into(), vec![Ty::Var(1), Ty::Var(2)]);
923            fields.insert("retry".into(), Ty::function(
924                vec![
925                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
926                    Ty::int(),
927                ],
928                EffectSet::empty(),
929                Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
930            ));
931            // retry_with_backoff[T, U, E, Eff](
932            //   f: (T) -> [Eff] Result[U, E], attempts: Int, base_ms: Int,
933            // ) -> (T) -> [Eff, time] Result[U, E]
934            // Same retry shape as `flow.retry` plus an exponential
935            // backoff between attempts. The result function carries
936            // `[time]` (from `time.sleep_ms`) unioned with the inner
937            // closure's effect row Eff, so e.g. a `[net]` closure
938            // produces a `[net, time]` result function. (#226)
939            fields.insert("retry_with_backoff".into(), Ty::function(
940                vec![
941                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
942                    Ty::int(),
943                    Ty::int(),
944                ],
945                EffectSet::empty(),
946                Ty::function(vec![Ty::Var(0)],
947                    EffectSet::open_var(3).union(&EffectSet::singleton("time")), result_ty),
948            ));
949            // parallel[A, B](fa: () -> A, fb: () -> B) -> () -> (A, B)
950            // Sequential implementation today; spec §11.2 reserves the
951            // option of a true-threaded scheduler. parallel_record is
952            // listed in the spec but not yet implemented — it needs row
953            // polymorphism over the input record's fields plus a
954            // record-iteration trampoline; tracked as follow-up.
955            fields.insert("parallel".into(), Ty::function(
956                vec![
957                    Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
958                    Ty::function(vec![], EffectSet::empty(), Ty::Var(1)),
959                ],
960                EffectSet::empty(),
961                Ty::function(vec![], EffectSet::empty(),
962                    Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])),
963            ));
964            // parallel_list[T](actions: List[() -> T]) -> List[T]
965            // Variadic counterpart to `parallel`. Runs each action and
966            // collects results in input order. Sequential under the
967            // hood (same caveat as `parallel`); spec §11.2 reserves
968            // true threading for a future scheduler. Unlike `parallel`,
969            // this returns the result list directly rather than a
970            // closure, since the input arity is dynamic.
971            fields.insert("parallel_list".into(), Ty::function(
972                vec![
973                    Ty::List(Box::new(
974                        Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
975                    )),
976                ],
977                EffectSet::empty(),
978                Ty::List(Box::new(Ty::Var(0))),
979            ));
980            Some(Ty::Record(fields))
981        }
982        "crypto" => {
983            let mut fields = IndexMap::new();
984            // Hashes: Bytes -> Bytes (digest as raw bytes).
985            // SHA-256 / SHA-512 are vetted. MD5 is retained only for
986            // interop with legacy systems — new code should not use it.
987            // BLAKE2b (#382) is included as a faster alternative to
988            // SHA-512 with the same security level.
989            for name in &["sha256", "sha512", "md5", "blake2b"] {
990                fields.insert((*name).into(), Ty::function(
991                    vec![Ty::bytes()],
992                    EffectSet::empty(),
993                    Ty::bytes(),
994                ));
995            }
996            // Hex-string convenience hashers (#382): hash a Str directly,
997            // return the digest as a lowercase hex Str. Equivalent to
998            // `crypto.hex_encode(crypto.shaN(bytes_from_str(s)))` but
999            // saves the two-step incantation for the common case.
1000            for name in &["sha256_str", "sha512_str"] {
1001                fields.insert((*name).into(), Ty::function(
1002                    vec![Ty::str()],
1003                    EffectSet::empty(),
1004                    Ty::str(),
1005                ));
1006            }
1007            // HMAC: (key :: Bytes, data :: Bytes) -> Bytes
1008            for name in &["hmac_sha256", "hmac_sha512"] {
1009                fields.insert((*name).into(), Ty::function(
1010                    vec![Ty::bytes(), Ty::bytes()],
1011                    EffectSet::empty(),
1012                    Ty::bytes(),
1013                ));
1014            }
1015            // base64 / hex
1016            fields.insert("base64_encode".into(), Ty::function(
1017                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1018            fields.insert("base64_decode".into(), Ty::function(
1019                vec![Ty::str()], EffectSet::empty(),
1020                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1021            // URL-safe base64 (#382): the alphabet swaps `+/` for `-_`
1022            // and omits padding. Required by JWT, signed-cookie, and
1023            // most token-bearing URL paths.
1024            fields.insert("base64url_encode".into(), Ty::function(
1025                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1026            fields.insert("base64url_decode".into(), Ty::function(
1027                vec![Ty::str()], EffectSet::empty(),
1028                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1029            fields.insert("hex_encode".into(), Ty::function(
1030                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1031            fields.insert("hex_decode".into(), Ty::function(
1032                vec![Ty::str()], EffectSet::empty(),
1033                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1034            // Constant-time equality (for HMAC verification etc.).
1035            // `eq` / `eq_str` (#382) are the recommended spelling;
1036            // `constant_time_eq` stays as a deprecated alias.
1037            fields.insert("constant_time_eq".into(), Ty::function(
1038                vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool()));
1039            fields.insert("eq".into(), Ty::function(
1040                vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool()));
1041            fields.insert("eq_str".into(), Ty::function(
1042                vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
1043            // Cryptographically-secure random bytes — OS RNG, not the
1044            // deterministic `rand.int_in` stub. The new `[random]`
1045            // effect is fine-grained on purpose so reviewers can find
1046            // every token-generating call via `lex audit --effect
1047            // random`.
1048            fields.insert("random".into(), Ty::function(
1049                vec![Ty::int()],
1050                EffectSet::singleton("random"),
1051                Ty::bytes(),
1052            ));
1053            // random_str_hex (#382): the most common token-mint pattern
1054            // — N random bytes rendered as 2N lowercase hex chars.
1055            // Suitable for session ids, request ids, OAuth `state`,
1056            // CSRF tokens; not suitable as a JWT signing key (use raw
1057            // `random` for that).
1058            fields.insert("random_str_hex".into(), Ty::function(
1059                vec![Ty::int()],
1060                EffectSet::singleton("random"),
1061                Ty::str(),
1062            ));
1063
1064            // AEAD: authenticated encryption with associated data
1065            // (#382 AEAD slice). Both algorithms use a 12-byte nonce
1066            // and a 16-byte authentication tag. `seal` returns the
1067            // structured `AeadResult { ciphertext, tag }`; `open`
1068            // returns `Result[Bytes, Str]` so authentication failures
1069            // surface as `Err`, not a panic.
1070            //
1071            // - **AES-GCM** (`aes_gcm_seal/open`): AES-128/192/256-GCM,
1072            //   key length determined by the supplied key bytes (16, 24,
1073            //   or 32). NIST-recommended; hardware-accelerated on most CPUs.
1074            // - **ChaCha20-Poly1305** (`chacha20_poly1305_seal/open`):
1075            //   Always a 32-byte key. Equivalent security to AES-GCM
1076            //   without needing AES-NI hardware; preferred on constrained
1077            //   targets.
1078            let aead_t = || Ty::Con("AeadResult".into(), vec![]);
1079            // Seal: returns Result[AeadResult, Str] rather than bare
1080            // AeadResult so input-validation errors (wrong key length,
1081            // wrong nonce length) surface as `Err` to the Lex caller
1082            // instead of panicking the VM. AES-GCM expects 16/24/32-byte
1083            // keys; ChaCha20-Poly1305 expects exactly 32. Both expect a
1084            // 12-byte nonce.
1085            for name in &["aes_gcm_seal", "chacha20_poly1305_seal"] {
1086                fields.insert((*name).into(), Ty::function(
1087                    // (key, nonce, aad, plaintext) -> Result[AeadResult, Str]
1088                    vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes()],
1089                    EffectSet::empty(),
1090                    Ty::Con("Result".into(), vec![aead_t(), Ty::str()]),
1091                ));
1092            }
1093            for name in &["aes_gcm_open", "chacha20_poly1305_open"] {
1094                fields.insert((*name).into(), Ty::function(
1095                    // (key, nonce, aad, ciphertext, tag) -> Result[Bytes, Str]
1096                    vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes()],
1097                    EffectSet::empty(),
1098                    Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1099                ));
1100            }
1101
1102            // KDFs: key-derivation functions (#382 KDF slice). All three
1103            // return `Result[Bytes, Str]` so caller-controlled inputs
1104            // (iteration count, output length, argon2id work factors)
1105            // that violate the underlying primitive's contract surface
1106            // as `Err` rather than panicking the VM. None require a new
1107            // effect — these are pure derivations.
1108            //
1109            // - **`pbkdf2_sha256(password, salt, iterations, len)`** —
1110            //   RFC 8018 PBKDF2 with HMAC-SHA256. Use ≥ 600_000 iterations
1111            //   for password storage (OWASP 2024). Older deployments
1112            //   pinning < 100_000 should rotate.
1113            // - **`hkdf_sha256(ikm, salt, info, len)`** — RFC 5869 extract+
1114            //   expand. Use for deriving multiple keys from a single
1115            //   high-entropy input (TLS, Noise, JWT-key rotation).
1116            //   Output length capped at 255 × 32 = 8160 bytes.
1117            // - **`argon2id(password, salt, t_cost, m_cost, len)`** —
1118            //   RFC 9106 Argon2id. Recommended for *new* password
1119            //   hashing. OWASP 2024 baseline: `t_cost=2, m_cost=19456`
1120            //   (19 MiB), or use `lex-crypto`'s vetted wrapper.
1121            fields.insert("pbkdf2_sha256".into(), Ty::function(
1122                // (password, salt, iterations, len) -> Result[Bytes, Str]
1123                vec![Ty::bytes(), Ty::bytes(), Ty::int(), Ty::int()],
1124                EffectSet::empty(),
1125                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1126            ));
1127            fields.insert("hkdf_sha256".into(), Ty::function(
1128                // (ikm, salt, info, len) -> Result[Bytes, Str]
1129                vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::int()],
1130                EffectSet::empty(),
1131                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1132            ));
1133            fields.insert("argon2id".into(), Ty::function(
1134                // (password, salt, t_cost, m_cost, len) -> Result[Bytes, Str]
1135                vec![Ty::bytes(), Ty::bytes(), Ty::int(), Ty::int(), Ty::int()],
1136                EffectSet::empty(),
1137                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1138            ));
1139
1140            Some(Ty::Record(fields))
1141        }
1142        "deque" => {
1143            // Persistent double-ended queue. Push/pop O(1) on both
1144            // ends; iteration order is front-to-back.
1145            // Type variable: 0 = T.
1146            let dt   = || Ty::Con("Deque".into(), vec![Ty::Var(0)]);
1147            let pair = || Ty::Tuple(vec![Ty::Var(0), dt()]);
1148            let mut fields = IndexMap::new();
1149            // new :: () -> Deque[T]
1150            fields.insert("new".into(), Ty::function(
1151                vec![], EffectSet::empty(), dt()));
1152            // size :: Deque[T] -> Int
1153            fields.insert("size".into(), Ty::function(
1154                vec![dt()], EffectSet::empty(), Ty::int()));
1155            // is_empty :: Deque[T] -> Bool
1156            fields.insert("is_empty".into(), Ty::function(
1157                vec![dt()], EffectSet::empty(), Ty::bool()));
1158            // push_back / push_front :: Deque[T], T -> Deque[T]
1159            for n in &["push_back", "push_front"] {
1160                fields.insert((*n).into(), Ty::function(
1161                    vec![dt(), Ty::Var(0)], EffectSet::empty(), dt()));
1162            }
1163            // pop_back / pop_front :: Deque[T] -> Option[(T, Deque[T])]
1164            for n in &["pop_back", "pop_front"] {
1165                fields.insert((*n).into(), Ty::function(
1166                    vec![dt()], EffectSet::empty(),
1167                    Ty::Con("Option".into(), vec![pair()])));
1168            }
1169            // peek_back / peek_front :: Deque[T] -> Option[T]
1170            for n in &["peek_back", "peek_front"] {
1171                fields.insert((*n).into(), Ty::function(
1172                    vec![dt()], EffectSet::empty(),
1173                    Ty::Con("Option".into(), vec![Ty::Var(0)])));
1174            }
1175            // from_list :: List[T] -> Deque[T]
1176            fields.insert("from_list".into(), Ty::function(
1177                vec![Ty::List(Box::new(Ty::Var(0)))],
1178                EffectSet::empty(), dt()));
1179            // to_list :: Deque[T] -> List[T]
1180            fields.insert("to_list".into(), Ty::function(
1181                vec![dt()], EffectSet::empty(),
1182                Ty::List(Box::new(Ty::Var(0)))));
1183            Some(Ty::Record(fields))
1184        }
1185        "log" => {
1186            // Structured logging behind a [log] effect. Emit ops route
1187            // through a runtime-configured sink (stderr by default;
1188            // can be redirected via set_sink). Configuration ops
1189            // mutate the global sink and so are gated [io].
1190            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1191            let mut fields = IndexMap::new();
1192            for level in &["debug", "info", "warn", "error"] {
1193                fields.insert((*level).into(), Ty::function(
1194                    vec![Ty::str()],
1195                    EffectSet::singleton("log"),
1196                    Ty::Unit,
1197                ));
1198            }
1199            // set_level :: Str -> [io] Result[Nil, Str]
1200            fields.insert("set_level".into(), Ty::function(
1201                vec![Ty::str()],
1202                EffectSet::singleton("io"),
1203                result_str(Ty::Unit)));
1204            // set_format :: Str -> [io] Result[Nil, Str]
1205            fields.insert("set_format".into(), Ty::function(
1206                vec![Ty::str()],
1207                EffectSet::singleton("io"),
1208                result_str(Ty::Unit)));
1209            // set_sink :: Str -> [io, fs_write] Result[Nil, Str]
1210            fields.insert("set_sink".into(), Ty::function(
1211                vec![Ty::str()],
1212                EffectSet {
1213                    concrete: [crate::types::EffectKind::bare("io"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
1214                    var: None,
1215                },
1216                result_str(Ty::Unit)));
1217            Some(Ty::Record(fields))
1218        }
1219        "datetime" => {
1220            // Instant and Duration are nominal opaque Ints under the
1221            // hood (nanoseconds-since-UTC-epoch and signed nanoseconds
1222            // respectively); the type checker tracks the distinction
1223            // even though both values look like Int at runtime.
1224            //
1225            // Tz is the variant
1226            //     Utc | Local | Offset(Int) | Iana(Str)
1227            // registered as a built-in nominal type in
1228            // `TypeEnv::new_with_builtins`. The pre-v1 stringly Tz
1229            // ("UTC"/"Local"/IANA-name/"+05:30") is no longer accepted
1230            // — passing a `Str` to `to_components` is now a type
1231            // error.
1232            let inst   = || Ty::Con("Instant".into(), vec![]);
1233            let dur    = || Ty::Con("Duration".into(), vec![]);
1234            let tz     = || Ty::Con("Tz".into(), vec![]);
1235            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1236            let dt_t = || {
1237                let mut fs = IndexMap::new();
1238                fs.insert("year".into(),    Ty::int());
1239                fs.insert("month".into(),   Ty::int());
1240                fs.insert("day".into(),     Ty::int());
1241                fs.insert("hour".into(),    Ty::int());
1242                fs.insert("minute".into(),  Ty::int());
1243                fs.insert("second".into(),  Ty::int());
1244                fs.insert("nano".into(),    Ty::int());
1245                fs.insert("tz_offset_minutes".into(), Ty::int());
1246                Ty::Record(fs)
1247            };
1248            let mut fields = IndexMap::new();
1249            fields.insert("now".into(), Ty::function(
1250                vec![], EffectSet::singleton("time"), inst()));
1251            fields.insert("parse_iso".into(), Ty::function(
1252                vec![Ty::str()], EffectSet::empty(), result_str(inst())));
1253            fields.insert("format_iso".into(), Ty::function(
1254                vec![inst()], EffectSet::empty(), Ty::str()));
1255            fields.insert("parse".into(), Ty::function(
1256                vec![Ty::str(), Ty::str()], EffectSet::empty(), result_str(inst())));
1257            fields.insert("format".into(), Ty::function(
1258                vec![inst(), Ty::str()], EffectSet::empty(), Ty::str()));
1259            fields.insert("to_components".into(), Ty::function(
1260                vec![inst(), tz()], EffectSet::empty(), result_str(dt_t())));
1261            fields.insert("from_components".into(), Ty::function(
1262                vec![dt_t()], EffectSet::empty(), result_str(inst())));
1263            fields.insert("add".into(), Ty::function(
1264                vec![inst(), dur()], EffectSet::empty(), inst()));
1265            fields.insert("diff".into(), Ty::function(
1266                vec![inst(), inst()], EffectSet::empty(), dur()));
1267            fields.insert("duration_seconds".into(), Ty::function(
1268                vec![Ty::float()], EffectSet::empty(), dur()));
1269            fields.insert("duration_minutes".into(), Ty::function(
1270                vec![Ty::int()], EffectSet::empty(), dur()));
1271            fields.insert("duration_days".into(), Ty::function(
1272                vec![Ty::int()], EffectSet::empty(), dur()));
1273            // #331: comparison ops on Instant.
1274            fields.insert("before".into(), Ty::function(
1275                vec![inst(), inst()], EffectSet::empty(), Ty::bool()));
1276            fields.insert("after".into(), Ty::function(
1277                vec![inst(), inst()], EffectSet::empty(), Ty::bool()));
1278            // compare :: Instant, Instant -> Int  (-1 / 0 / +1)
1279            fields.insert("compare".into(), Ty::function(
1280                vec![inst(), inst()], EffectSet::empty(), Ty::int()));
1281            Some(Ty::Record(fields))
1282        }
1283        // #331: duration module — scalar extraction from Duration values.
1284        "duration" => {
1285            let dur = || Ty::Con("Duration".into(), vec![]);
1286            let mut fields = IndexMap::new();
1287            // seconds :: Duration -> Int  (truncates toward zero)
1288            fields.insert("seconds".into(), Ty::function(
1289                vec![dur()], EffectSet::empty(), Ty::int()));
1290            Some(Ty::Record(fields))
1291        }
1292        "process" => {
1293            // Streaming subprocess. The opaque `ProcessHandle` type
1294            // is an Int handle into a process-wide registry holding
1295            // the `Child` plus its stdout/stderr `BufReader`s.
1296            let ph = || Ty::Con("ProcessHandle".into(), vec![]);
1297            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1298            let opts_t = || {
1299                let mut fs = IndexMap::new();
1300                fs.insert("cwd".into(),
1301                    Ty::Con("Option".into(), vec![Ty::str()]));
1302                fs.insert("env".into(),
1303                    Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
1304                fs.insert("stdin".into(),
1305                    Ty::Con("Option".into(), vec![Ty::bytes()]));
1306                Ty::Record(fs)
1307            };
1308            let exit_t = || {
1309                let mut fs = IndexMap::new();
1310                fs.insert("code".into(), Ty::int());
1311                fs.insert("signaled".into(), Ty::bool());
1312                Ty::Record(fs)
1313            };
1314            let output_t = || {
1315                let mut fs = IndexMap::new();
1316                fs.insert("stdout".into(), Ty::str());
1317                fs.insert("stderr".into(), Ty::str());
1318                fs.insert("exit_code".into(), Ty::int());
1319                Ty::Record(fs)
1320            };
1321            let mut fields = IndexMap::new();
1322            // spawn :: Str, List[Str], Opts -> [proc] Result[ProcessHandle, Str]
1323            fields.insert("spawn".into(), Ty::function(
1324                vec![Ty::str(), Ty::List(Box::new(Ty::str())), opts_t()],
1325                EffectSet::singleton("proc"),
1326                result_str(ph())));
1327            // read_stdout_line / read_stderr_line :: ProcessHandle -> [proc] Option[Str]
1328            for n in &["read_stdout_line", "read_stderr_line"] {
1329                fields.insert((*n).into(), Ty::function(
1330                    vec![ph()], EffectSet::singleton("proc"),
1331                    Ty::Con("Option".into(), vec![Ty::str()])));
1332            }
1333            // wait :: ProcessHandle -> [proc] ProcessExit
1334            fields.insert("wait".into(), Ty::function(
1335                vec![ph()], EffectSet::singleton("proc"), exit_t()));
1336            // kill :: ProcessHandle, Str -> [proc] Result[Nil, Str]
1337            fields.insert("kill".into(), Ty::function(
1338                vec![ph(), Ty::str()],
1339                EffectSet::singleton("proc"),
1340                result_str(Ty::Unit)));
1341            // run :: Str, List[Str] -> [proc] Result[ProcessOutput, Str]
1342            // Blocking convenience that captures stdout/stderr fully
1343            // and returns once the child exits. For programs that
1344            // need streaming, use spawn + read_*_line + wait.
1345            fields.insert("run".into(), Ty::function(
1346                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
1347                EffectSet::singleton("proc"),
1348                result_str(output_t())));
1349            Some(Ty::Record(fields))
1350        }
1351        "fs" => {
1352            // Filesystem walk + mutate. Walk-style ops (exists, walk,
1353            // glob, …) declare [fs_walk] — distinct from [fs_read]
1354            // (which is content reads via io.read), so reviewers can
1355            // separately track directory traversal vs file-content
1356            // exposure. Mutating ops (mkdir_p, remove, copy) declare
1357            // [fs_write]. Path scoping uses --allow-fs-read for walk
1358            // (a directory listing is an information disclosure on
1359            // the same path tree) and --allow-fs-write for mutations.
1360            let stat_t = || {
1361                let mut fs = IndexMap::new();
1362                fs.insert("size".into(), Ty::int());
1363                fs.insert("mtime".into(), Ty::int());
1364                fs.insert("is_dir".into(), Ty::bool());
1365                fs.insert("is_file".into(), Ty::bool());
1366                Ty::Record(fs)
1367            };
1368            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1369            let mut fields = IndexMap::new();
1370            // Walk-style queries [fs_walk]
1371            fields.insert("exists".into(), Ty::function(
1372                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
1373            fields.insert("is_file".into(), Ty::function(
1374                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
1375            fields.insert("is_dir".into(), Ty::function(
1376                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
1377            fields.insert("stat".into(), Ty::function(
1378                vec![Ty::str()], EffectSet::singleton("fs_walk"),
1379                result_str(stat_t())));
1380            fields.insert("list_dir".into(), Ty::function(
1381                vec![Ty::str()], EffectSet::singleton("fs_walk"),
1382                result_str(Ty::List(Box::new(Ty::str())))));
1383            fields.insert("walk".into(), Ty::function(
1384                vec![Ty::str()], EffectSet::singleton("fs_walk"),
1385                result_str(Ty::List(Box::new(Ty::str())))));
1386            fields.insert("glob".into(), Ty::function(
1387                vec![Ty::str()], EffectSet::singleton("fs_walk"),
1388                result_str(Ty::List(Box::new(Ty::str())))));
1389            // Mutations [fs_write]
1390            fields.insert("mkdir_p".into(), Ty::function(
1391                vec![Ty::str()], EffectSet::singleton("fs_write"),
1392                result_str(Ty::Unit)));
1393            fields.insert("remove".into(), Ty::function(
1394                vec![Ty::str()], EffectSet::singleton("fs_write"),
1395                result_str(Ty::Unit)));
1396            fields.insert("copy".into(), Ty::function(
1397                vec![Ty::str(), Ty::str()],
1398                EffectSet {
1399                    concrete: [crate::types::EffectKind::bare("fs_walk"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
1400                    var: None,
1401                },
1402                result_str(Ty::Unit)));
1403            Some(Ty::Record(fields))
1404        }
1405        "kv" => {
1406            // Embedded key-value store. The opaque `Kv` type is
1407            // backed by an Int handle into a process-wide registry.
1408            let kv_t = || Ty::Con("Kv".into(), vec![]);
1409            let mut fields = IndexMap::new();
1410            // open :: Str -> [kv, fs_write] Result[Kv, Str]
1411            fields.insert("open".into(), Ty::function(
1412                vec![Ty::str()],
1413                EffectSet {
1414                    concrete: [crate::types::EffectKind::bare("kv"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
1415                    var: None,
1416                },
1417                Ty::Con("Result".into(), vec![kv_t(), Ty::str()])));
1418            // close :: Kv -> [kv] Nil
1419            fields.insert("close".into(), Ty::function(
1420                vec![kv_t()],
1421                EffectSet::singleton("kv"),
1422                Ty::Unit));
1423            // get :: Kv, Str -> [kv] Option[Bytes]
1424            fields.insert("get".into(), Ty::function(
1425                vec![kv_t(), Ty::str()],
1426                EffectSet::singleton("kv"),
1427                Ty::Con("Option".into(), vec![Ty::bytes()])));
1428            // put :: Kv, Str, Bytes -> [kv] Result[Nil, Str]
1429            fields.insert("put".into(), Ty::function(
1430                vec![kv_t(), Ty::str(), Ty::bytes()],
1431                EffectSet::singleton("kv"),
1432                Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
1433            // delete :: Kv, Str -> [kv] Result[Nil, Str]
1434            fields.insert("delete".into(), Ty::function(
1435                vec![kv_t(), Ty::str()],
1436                EffectSet::singleton("kv"),
1437                Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
1438            // contains :: Kv, Str -> [kv] Bool
1439            fields.insert("contains".into(), Ty::function(
1440                vec![kv_t(), Ty::str()],
1441                EffectSet::singleton("kv"),
1442                Ty::bool()));
1443            // list_prefix :: Kv, Str -> [kv] List[Str]
1444            fields.insert("list_prefix".into(), Ty::function(
1445                vec![kv_t(), Ty::str()],
1446                EffectSet::singleton("kv"),
1447                Ty::List(Box::new(Ty::str()))));
1448            Some(Ty::Record(fields))
1449        }
1450        "sql" => {
1451            // Embedded SQL (SQLite via rusqlite). The opaque `Db` type is
1452            // backed by an Int handle into a process-wide registry (#362).
1453            //
1454            // Params use the typed `SqlParam` ADT (PStr|PInt|PFloat|PBool|PNull)
1455            // registered in env.rs, so callers don't have to stringify values.
1456            //
1457            // Transactions: sql.begin(db) → SqlTx; sql.commit/rollback(tx).
1458            // exec_tx / query_tx mirror exec / query but operate on a SqlTx.
1459            //
1460            // Row decoders: get_str / get_int / get_float / get_bool extract
1461            // typed columns from a row record by name.
1462            let db_t  = || Ty::Con("Db".into(), vec![]);
1463            let tx_t  = || Ty::Con("SqlTx".into(), vec![]);
1464            let sp_t  = || Ty::Con("SqlParam".into(), vec![]);
1465            let params_t = || Ty::List(Box::new(sp_t()));
1466            let mut fields = IndexMap::new();
1467
1468            // SqlError = { message, code, detail } — populated with
1469            // SQLSTATE (Postgres) or symbolic SQLite error name (#380).
1470            let se_t = || Ty::Con("SqlError".into(), vec![]);
1471
1472            // open :: Str -> [sql, fs_write] Result[Db, SqlError]
1473            fields.insert("open".into(), Ty::function(
1474                vec![Ty::str()],
1475                EffectSet {
1476                    concrete: [crate::types::EffectKind::bare("sql"),
1477                               crate::types::EffectKind::bare("fs_write")]
1478                        .into_iter().collect(),
1479                    var: None,
1480                },
1481                Ty::Con("Result".into(), vec![db_t(), se_t()])));
1482
1483            // close :: Db -> [sql] Unit
1484            fields.insert("close".into(), Ty::function(
1485                vec![db_t()],
1486                EffectSet::singleton("sql"),
1487                Ty::Unit));
1488
1489            // exec :: Db, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
1490            fields.insert("exec".into(), Ty::function(
1491                vec![db_t(), Ty::str(), params_t()],
1492                EffectSet::singleton("sql"),
1493                Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
1494
1495            // query[T] :: Db, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
1496            fields.insert("query".into(), Ty::function(
1497                vec![db_t(), Ty::str(), params_t()],
1498                EffectSet::singleton("sql"),
1499                Ty::Con("Result".into(), vec![
1500                    Ty::List(Box::new(Ty::Var(0))),
1501                    se_t(),
1502                ])));
1503
1504            // query_iter[T] :: Db, Str, List[SqlParam] -> [sql] Result[Iter[T], SqlError]
1505            // Streaming variant of `query` (#379). Rows are pulled from
1506            // the server one at a time via an mpsc-backed cursor —
1507            // memory stays bounded regardless of result-set size.
1508            // Other ops on the same `Db` handle block until the cursor
1509            // is drained (single connection per Db).
1510            fields.insert("query_iter".into(), Ty::function(
1511                vec![db_t(), Ty::str(), params_t()],
1512                EffectSet::singleton("sql"),
1513                Ty::Con("Result".into(), vec![
1514                    Ty::Con("Iter".into(), vec![Ty::Var(0)]),
1515                    se_t(),
1516                ])));
1517
1518            // begin :: Db -> [sql] Result[SqlTx, SqlError]
1519            fields.insert("begin".into(), Ty::function(
1520                vec![db_t()],
1521                EffectSet::singleton("sql"),
1522                Ty::Con("Result".into(), vec![tx_t(), se_t()])));
1523
1524            // commit :: SqlTx -> [sql] Result[Unit, SqlError]
1525            fields.insert("commit".into(), Ty::function(
1526                vec![tx_t()],
1527                EffectSet::singleton("sql"),
1528                Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
1529
1530            // rollback :: SqlTx -> [sql] Result[Unit, SqlError]
1531            fields.insert("rollback".into(), Ty::function(
1532                vec![tx_t()],
1533                EffectSet::singleton("sql"),
1534                Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
1535
1536            // exec_tx :: SqlTx, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
1537            fields.insert("exec_tx".into(), Ty::function(
1538                vec![tx_t(), Ty::str(), params_t()],
1539                EffectSet::singleton("sql"),
1540                Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
1541
1542            // query_tx[T] :: SqlTx, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
1543            fields.insert("query_tx".into(), Ty::function(
1544                vec![tx_t(), Ty::str(), params_t()],
1545                EffectSet::singleton("sql"),
1546                Ty::Con("Result".into(), vec![
1547                    Ty::List(Box::new(Ty::Var(0))),
1548                    se_t(),
1549                ])));
1550
1551            // Row decoders: get_X[T] :: T, Str -> Option[X]
1552            // T is polymorphic so these work on any row record shape.
1553            fields.insert("get_str".into(), Ty::function(
1554                vec![Ty::Var(0), Ty::str()],
1555                EffectSet::empty(),
1556                Ty::Con("Option".into(), vec![Ty::str()])));
1557            fields.insert("get_int".into(), Ty::function(
1558                vec![Ty::Var(0), Ty::str()],
1559                EffectSet::empty(),
1560                Ty::Con("Option".into(), vec![Ty::int()])));
1561            fields.insert("get_float".into(), Ty::function(
1562                vec![Ty::Var(0), Ty::str()],
1563                EffectSet::empty(),
1564                Ty::Con("Option".into(), vec![Ty::Con("Float".into(), vec![])])));
1565            fields.insert("get_bool".into(), Ty::function(
1566                vec![Ty::Var(0), Ty::str()],
1567                EffectSet::empty(),
1568                Ty::Con("Option".into(), vec![Ty::Con("Bool".into(), vec![])])));
1569
1570            Some(Ty::Record(fields))
1571        }
1572        "parser" => {
1573            // #217: structured parser combinators. Parser values are
1574            // tagged Records at runtime (`{ kind, ... }`), opaque at
1575            // the language level via `Ty::Con("Parser", [T])`.
1576            //
1577            // Surface:
1578            //   - primitives: char, string, digit, alpha, whitespace, eof
1579            //   - combinators: seq, alt, many, optional, map, and_then
1580            //   - run :: Parser[T], Str -> Result[T, ParseErr]
1581            //
1582            // `map` and `and_then` were deferred from #217's v1 because
1583            // their closure arguments carried call-site identity that
1584            // broke the canonical-parsers acceptance criterion. With
1585            // closure body-hash equality landed in #222, that concern
1586            // is gone, and #221 wires them in. The interpreter for
1587            // `parser.run` has been moved to `lex-bytecode::parser_runtime`
1588            // so it can invoke closures from `Map` / `AndThen` nodes.
1589            let pt = |t: Ty| Ty::Con("Parser".into(), vec![t]);
1590            let parse_err = || {
1591                let mut fs = IndexMap::new();
1592                fs.insert("pos".into(), Ty::int());
1593                fs.insert("message".into(), Ty::str());
1594                Ty::Record(fs)
1595            };
1596            let mut fields = IndexMap::new();
1597            // char :: Str -> Parser[Str] (single-char Str literal)
1598            fields.insert("char".into(), Ty::function(
1599                vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
1600            // string :: Str -> Parser[Str]
1601            fields.insert("string".into(), Ty::function(
1602                vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
1603            // digit :: () -> Parser[Str]
1604            fields.insert("digit".into(), Ty::function(
1605                vec![], EffectSet::empty(), pt(Ty::str())));
1606            // alpha :: () -> Parser[Str]
1607            fields.insert("alpha".into(), Ty::function(
1608                vec![], EffectSet::empty(), pt(Ty::str())));
1609            // whitespace :: () -> Parser[Str]
1610            fields.insert("whitespace".into(), Ty::function(
1611                vec![], EffectSet::empty(), pt(Ty::str())));
1612            // eof :: () -> Parser[Unit]
1613            fields.insert("eof".into(), Ty::function(
1614                vec![], EffectSet::empty(), pt(Ty::Unit)));
1615            // seq :: Parser[A], Parser[B] -> Parser[(A, B)]
1616            fields.insert("seq".into(), Ty::function(
1617                vec![pt(Ty::Var(0)), pt(Ty::Var(1))],
1618                EffectSet::empty(),
1619                pt(Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]))));
1620            // alt :: Parser[T], Parser[T] -> Parser[T]
1621            // PEG-style ordered choice: the second alternative is
1622            // tried only if the first fails.
1623            fields.insert("alt".into(), Ty::function(
1624                vec![pt(Ty::Var(0)), pt(Ty::Var(0))],
1625                EffectSet::empty(),
1626                pt(Ty::Var(0))));
1627            // many :: Parser[T] -> Parser[List[T]]
1628            // Zero-or-more. Stops as soon as the inner parser fails
1629            // OR doesn't advance the position (avoids infinite loop
1630            // on empty matches).
1631            fields.insert("many".into(), Ty::function(
1632                vec![pt(Ty::Var(0))],
1633                EffectSet::empty(),
1634                pt(Ty::List(Box::new(Ty::Var(0))))));
1635            // optional :: Parser[T] -> Parser[Option[T]]
1636            fields.insert("optional".into(), Ty::function(
1637                vec![pt(Ty::Var(0))],
1638                EffectSet::empty(),
1639                pt(Ty::Con("Option".into(), vec![Ty::Var(0)]))));
1640            // map :: Parser[T], (T) -> [E] U -> [E] Parser[U]
1641            // The closure runs at parse time when the Parser is run.
1642            // Effect-polymorphic on the closure: any effect the
1643            // closure declares propagates to the surrounding `run`.
1644            fields.insert("map".into(), Ty::function(
1645                vec![
1646                    pt(Ty::Var(0)),
1647                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1648                ],
1649                EffectSet::open_var(2),
1650                pt(Ty::Var(1))));
1651            // and_then :: Parser[T], (T) -> [E] Parser[U] -> [E] Parser[U]
1652            // Monadic bind: closure inspects the parsed value and
1653            // returns the next parser to run.
1654            fields.insert("and_then".into(), Ty::function(
1655                vec![
1656                    pt(Ty::Var(0)),
1657                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
1658                        pt(Ty::Var(1))),
1659                ],
1660                EffectSet::open_var(3),
1661                pt(Ty::Var(1))));
1662            // run :: Parser[T], Str -> Result[T, ParseErr]
1663            // ParseErr = { pos :: Int, message :: Str }
1664            fields.insert("run".into(), Ty::function(
1665                vec![pt(Ty::Var(0)), Ty::str()],
1666                EffectSet::empty(),
1667                Ty::Con("Result".into(), vec![Ty::Var(0), parse_err()])));
1668            Some(Ty::Record(fields))
1669        }
1670        "cli" => {
1671            // #224 Rubric port: argparse-equivalent for end-user
1672            // programs. Spec values are tagged `Json` records (opaque
1673            // to the language but inspectable). Construction via the
1674            // `flag` / `option` / `positional` / `spec` builders;
1675            // parse + introspection / help via the remaining ops.
1676            let json = || Ty::Con("Json".into(), vec![]);
1677            let opt_str = || Ty::Con("Option".into(), vec![Ty::str()]);
1678            let mut fields = IndexMap::new();
1679            // flag :: Str -> Option[Str] -> Str -> Json
1680            //   long_name -> short -> help -> CliArg
1681            fields.insert("flag".into(), Ty::function(
1682                vec![Ty::str(), opt_str(), Ty::str()],
1683                EffectSet::empty(),
1684                json()));
1685            // option :: Str -> Option[Str] -> Str -> Option[Str] -> Json
1686            //   long_name -> short -> help -> default -> CliArg
1687            fields.insert("option".into(), Ty::function(
1688                vec![Ty::str(), opt_str(), Ty::str(), opt_str()],
1689                EffectSet::empty(),
1690                json()));
1691            // positional :: Str -> Str -> Bool -> Json
1692            //   name -> help -> required -> CliArg
1693            fields.insert("positional".into(), Ty::function(
1694                vec![Ty::str(), Ty::str(), Ty::bool()],
1695                EffectSet::empty(),
1696                json()));
1697            // spec :: Str -> Str -> List[Json] -> List[Json] -> Json
1698            //   name -> help -> args -> subcommands -> CliSpec
1699            fields.insert("spec".into(), Ty::function(
1700                vec![Ty::str(), Ty::str(),
1701                     Ty::List(Box::new(json())),
1702                     Ty::List(Box::new(json()))],
1703                EffectSet::empty(),
1704                json()));
1705            // parse :: Json -> List[Str] -> Result[Json, Str]
1706            //   spec -> argv -> Result[CliParsed, error]
1707            fields.insert("parse".into(), Ty::function(
1708                vec![json(), Ty::List(Box::new(Ty::str()))],
1709                EffectSet::empty(),
1710                Ty::Con("Result".into(), vec![json(), Ty::str()])));
1711            // envelope :: Bool -> Str -> T -> Json
1712            //   ok -> command -> data -> ACLI-shaped envelope.
1713            // `data` is polymorphic so callers don't have to round-
1714            // trip through `json.parse` for trivial payloads.
1715            fields.insert("envelope".into(), Ty::function(
1716                vec![Ty::bool(), Ty::str(), Ty::Var(0)],
1717                EffectSet::empty(),
1718                json()));
1719            // describe :: Json -> Json — machine-readable spec dump
1720            fields.insert("describe".into(), Ty::function(
1721                vec![json()],
1722                EffectSet::empty(),
1723                json()));
1724            // help :: Json -> Str — human-readable help text
1725            fields.insert("help".into(), Ty::function(
1726                vec![json()],
1727                EffectSet::empty(),
1728                Ty::str()));
1729            Some(Ty::Record(fields))
1730        }
1731        "regex" => {
1732            // The compiled `Regex` is stored as a `Str` at runtime
1733            // (the pattern source) plus a process-wide cache of the
1734            // actual `regex::Regex`. So `Regex` is a nominal type at
1735            // the language level but its value is just the pattern.
1736            let regex_t = || Ty::Con("Regex".into(), vec![]);
1737            let match_t = || {
1738                let mut fs = IndexMap::new();
1739                fs.insert("text".into(), Ty::str());
1740                fs.insert("start".into(), Ty::int());
1741                fs.insert("end".into(), Ty::int());
1742                fs.insert("groups".into(), Ty::List(Box::new(Ty::str())));
1743                Ty::Record(fs)
1744            };
1745            let mut fields = IndexMap::new();
1746            // compile :: Str -> Result[Regex, Str]
1747            fields.insert("compile".into(), Ty::function(
1748                vec![Ty::str()], EffectSet::empty(),
1749                Ty::Con("Result".into(), vec![regex_t(), Ty::str()])));
1750            // is_match :: Regex, Str -> Bool
1751            fields.insert("is_match".into(), Ty::function(
1752                vec![regex_t(), Ty::str()], EffectSet::empty(), Ty::bool()));
1753            // is_match_str :: Str, Str -> Bool
1754            // Compiles the first argument as a pattern and matches against the second.
1755            // Returns false on invalid pattern instead of propagating an error.
1756            fields.insert("is_match_str".into(), Ty::function(
1757                vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
1758            // find :: Regex, Str -> Option[Match]
1759            fields.insert("find".into(), Ty::function(
1760                vec![regex_t(), Ty::str()], EffectSet::empty(),
1761                Ty::Con("Option".into(), vec![match_t()])));
1762            // find_all :: Regex, Str -> List[Match]
1763            fields.insert("find_all".into(), Ty::function(
1764                vec![regex_t(), Ty::str()], EffectSet::empty(),
1765                Ty::List(Box::new(match_t()))));
1766            // replace :: Regex, Str, Str -> Str
1767            fields.insert("replace".into(), Ty::function(
1768                vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
1769            // replace_all :: Regex, Str, Str -> Str
1770            fields.insert("replace_all".into(), Ty::function(
1771                vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
1772            // split :: Regex, Str -> List[Str]
1773            fields.insert("split".into(), Ty::function(
1774                vec![regex_t(), Ty::str()], EffectSet::empty(),
1775                Ty::List(Box::new(Ty::str()))));
1776            Some(Ty::Record(fields))
1777        }
1778        "http" => {
1779            // Rich HTTP client. `[net]` for the wire ops, pure for
1780            // the builders / decoders. `--allow-net-host` gates per
1781            // request. Multipart upload + streaming response bodies
1782            // are deferred to v1.5; the v1 surface covers the
1783            // common cases (auth, headers, query, timeouts, JSON /
1784            // text decoding).
1785            let req_t  = || Ty::Con("HttpRequest".into(), vec![]);
1786            let resp_t = || Ty::Con("HttpResponse".into(), vec![]);
1787            let err_t  = || Ty::Con("HttpError".into(), vec![]);
1788            let result_he = |t: Ty| Ty::Con("Result".into(), vec![t, err_t()]);
1789            let str_str_map = || Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]);
1790            let mut fields = IndexMap::new();
1791            // -- wire ops (effectful) --
1792            // send :: HttpRequest -> [net] Result[HttpResponse, HttpError]
1793            fields.insert("send".into(), Ty::function(
1794                vec![req_t()],
1795                EffectSet::singleton("net"),
1796                result_he(resp_t()),
1797            ));
1798            // get :: Str -> [net] Result[HttpResponse, HttpError]
1799            fields.insert("get".into(), Ty::function(
1800                vec![Ty::str()],
1801                EffectSet::singleton("net"),
1802                result_he(resp_t()),
1803            ));
1804            // post :: Str, Bytes, Str -> [net] Result[HttpResponse, HttpError]
1805            fields.insert("post".into(), Ty::function(
1806                vec![Ty::str(), Ty::bytes(), Ty::str()],
1807                EffectSet::singleton("net"),
1808                result_he(resp_t()),
1809            ));
1810            // -- pure builders (record transforms) --
1811            // with_header :: HttpRequest, Str, Str -> HttpRequest
1812            fields.insert("with_header".into(), Ty::function(
1813                vec![req_t(), Ty::str(), Ty::str()],
1814                EffectSet::empty(),
1815                req_t(),
1816            ));
1817            // with_auth :: HttpRequest, Str, Str -> HttpRequest
1818            // (Renders `<scheme> <token>` into the `Authorization`
1819            // header — `Bearer <jwt>`, `Basic <b64>`, etc.)
1820            fields.insert("with_auth".into(), Ty::function(
1821                vec![req_t(), Ty::str(), Ty::str()],
1822                EffectSet::empty(),
1823                req_t(),
1824            ));
1825            // with_query :: HttpRequest, Map[Str, Str] -> HttpRequest
1826            // (Appends a `?k=v&...` query string; values are URL-
1827            // encoded so `&` / `=` / spaces in values don't escape.)
1828            fields.insert("with_query".into(), Ty::function(
1829                vec![req_t(), str_str_map()],
1830                EffectSet::empty(),
1831                req_t(),
1832            ));
1833            // with_timeout_ms :: HttpRequest, Int -> HttpRequest
1834            fields.insert("with_timeout_ms".into(), Ty::function(
1835                vec![req_t(), Ty::int()],
1836                EffectSet::empty(),
1837                req_t(),
1838            ));
1839            // -- pure decoders --
1840            // json_body[T] :: HttpResponse -> Result[T, HttpError]
1841            // Polymorphic on the parsed shape, matching `json.parse`.
1842            fields.insert("json_body".into(), Ty::function(
1843                vec![resp_t()],
1844                EffectSet::empty(),
1845                result_he(Ty::Var(0)),
1846            ));
1847            // text_body :: HttpResponse -> Result[Str, HttpError]
1848            fields.insert("text_body".into(), Ty::function(
1849                vec![resp_t()],
1850                EffectSet::empty(),
1851                result_he(Ty::str()),
1852            ));
1853            Some(Ty::Record(fields))
1854        }
1855        "yaml" => {
1856            // YAML config parser. Same shape as `std.toml`: parse
1857            // is polymorphic, output Value layout matches std.json
1858            // (Str/Int/Float/Bool/List/Record). Anchors and tags
1859            // are flattened by serde_yaml's deserializer.
1860            let mut fields = IndexMap::new();
1861            fields.insert("parse".into(), Ty::function(
1862                vec![Ty::str()], EffectSet::empty(),
1863                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1864            ));
1865            // Tactical fix for #168 — caller-supplied required-field
1866            // list. See std.json's parse_strict for context.
1867            fields.insert("parse_strict".into(), Ty::function(
1868                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
1869                EffectSet::empty(),
1870                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1871            ));
1872            fields.insert("stringify".into(), Ty::function(
1873                vec![Ty::Var(0)], EffectSet::empty(),
1874                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1875            ));
1876            Some(Ty::Record(fields))
1877        }
1878        "dotenv" => {
1879            // .env-style files. parse :: Str -> Result[Map[Str,Str], Str].
1880            // Returns a map (not a polymorphic record) because
1881            // dotenv files don't carry shape — every value is a
1882            // string and keys aren't statically known.
1883            let mut fields = IndexMap::new();
1884            fields.insert("parse".into(), Ty::function(
1885                vec![Ty::str()], EffectSet::empty(),
1886                Ty::Con("Result".into(), vec![
1887                    Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
1888                    Ty::str(),
1889                ]),
1890            ));
1891            Some(Ty::Record(fields))
1892        }
1893        "csv" => {
1894            // CSV rows-as-lists. parse :: Str -> Result[List[List[Str]], Str].
1895            // Header awareness is left to the caller — row 0 is
1896            // whatever the file has. A `parse_with_headers` that
1897            // returns List[Map[Str,Str]] is a natural follow-up.
1898            let row_ty = Ty::List(Box::new(Ty::str()));
1899            let rows_ty = Ty::List(Box::new(row_ty.clone()));
1900            let mut fields = IndexMap::new();
1901            fields.insert("parse".into(), Ty::function(
1902                vec![Ty::str()], EffectSet::empty(),
1903                Ty::Con("Result".into(), vec![rows_ty.clone(), Ty::str()]),
1904            ));
1905            fields.insert("stringify".into(), Ty::function(
1906                vec![rows_ty], EffectSet::empty(),
1907                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1908            ));
1909            Some(Ty::Record(fields))
1910        }
1911        "test" => {
1912            // Tiny assertion library (#proposed-stdlib). Each helper
1913            // returns Result[Unit, Str] so a test is itself a fn
1914            // returning Result. Callers compose suites in user code
1915            // (a List of (name, () -> Result[Unit, Str]) pairs +
1916            // list.fold to accumulate verdicts). Property generators
1917            // and a Rust-side Suite type are deferred to v2.
1918            let mut fields = IndexMap::new();
1919            // assert_eq[a, b] :: T -> T -> Result[Unit, Str]
1920            // (T constrained equal by unification on the two args)
1921            let unit_result = || Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]);
1922            fields.insert("assert_eq".into(), Ty::function(
1923                vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
1924            ));
1925            fields.insert("assert_ne".into(), Ty::function(
1926                vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
1927            ));
1928            fields.insert("assert_true".into(), Ty::function(
1929                vec![Ty::bool()], EffectSet::empty(), unit_result(),
1930            ));
1931            fields.insert("assert_false".into(), Ty::function(
1932                vec![Ty::bool()], EffectSet::empty(), unit_result(),
1933            ));
1934            Some(Ty::Record(fields))
1935        }
1936        "toml" => {
1937            // TOML config parser. Mirrors `std.json`'s shape: parse
1938            // is polymorphic so callers annotate the expected
1939            // record / list / scalar shape and the type checker
1940            // unifies. The parsed TOML maps to the same Lex Value
1941            // shape as JSON does:
1942            //
1943            //   TOML String   → Value::Str
1944            //   TOML Integer  → Value::Int
1945            //   TOML Float    → Value::Float
1946            //   TOML Boolean  → Value::Bool
1947            //   TOML Array    → Value::List
1948            //   TOML Table    → Value::Record
1949            //   TOML Datetime → Value::Str (RFC 3339, lossless)
1950            //
1951            // The Datetime → Str fallback is the one info-losing
1952            // step; callers who want a real `Instant` can pipe the
1953            // string through `datetime.parse_iso`.
1954            let mut fields = IndexMap::new();
1955            // parse :: Str -> Result[T, Str]
1956            fields.insert("parse".into(), Ty::function(
1957                vec![Ty::str()], EffectSet::empty(),
1958                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1959            ));
1960            // parse_strict :: (Str, List[Str]) -> Result[T, Str]
1961            // Tactical fix for #168 — caller passes the field
1962            // names T requires; runtime returns Err if any are
1963            // missing from the parsed table instead of letting
1964            // field access panic later.
1965            fields.insert("parse_strict".into(), Ty::function(
1966                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
1967                EffectSet::empty(),
1968                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1969            ));
1970            // stringify :: T -> Result[Str, Str]
1971            // Returns Result (not Str) because not every Lex Value
1972            // has a TOML representation — top-level scalars,
1973            // closures, mixed-key maps etc. surface as Err rather
1974            // than panic.
1975            fields.insert("stringify".into(), Ty::function(
1976                vec![Ty::Var(0)], EffectSet::empty(),
1977                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1978            ));
1979            Some(Ty::Record(fields))
1980        }
1981        // `std.agent` (#184) — runtime primitives whose effects
1982        // separate (a) which LLM surface (`llm_local` vs
1983        // `llm_cloud`), (b) which peer protocol (`a2a`), and
1984        // (c) which tool boundary (`mcp`). The wire formats land
1985        // in downstream crates (`soft-agent`, `soft-a2a`) and
1986        // in #185 for MCP; what's typed here is the boundary
1987        // alone — agent code can be type-checked as
1988        // `[llm_local, a2a]` and will fail if it tries to reach
1989        // `[llm_cloud]` even before the wire layer is finished.
1990        "agent" => {
1991            let mut fields = IndexMap::new();
1992            // local_complete :: Str -> [llm_local] Result[Str, Str]
1993            fields.insert("local_complete".into(), Ty::function(
1994                vec![Ty::str()],
1995                EffectSet::singleton("llm_local"),
1996                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1997            ));
1998            // cloud_complete :: Str -> [llm_cloud] Result[Str, Str]
1999            fields.insert("cloud_complete".into(), Ty::function(
2000                vec![Ty::str()],
2001                EffectSet::singleton("llm_cloud"),
2002                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2003            ));
2004            // send_a2a :: (Str, Str) -> [a2a] Result[Str, Str]
2005            //              peer payload                   reply
2006            fields.insert("send_a2a".into(), Ty::function(
2007                vec![Ty::str(), Ty::str()],
2008                EffectSet::singleton("a2a"),
2009                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2010            ));
2011            // call_mcp :: (Str, Str, Str) -> [mcp] Result[Str, Str]
2012            //              server tool args_json         result_json
2013            fields.insert("call_mcp".into(), Ty::function(
2014                vec![Ty::str(), Ty::str(), Ty::str()],
2015                EffectSet::singleton("mcp"),
2016                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2017            ));
2018            // cloud_stream :: Str -> [llm_cloud] Result[Stream[Str], Str]
2019            // (#305 slice 3). Streaming counterpart to cloud_complete.
2020            // The result is `Result[Stream[Str], Str]` rather than a
2021            // bare Stream so transport errors surface synchronously
2022            // at handshake time; per-chunk errors collapse the
2023            // stream to early termination.
2024            fields.insert("cloud_stream".into(), Ty::function(
2025                vec![Ty::str()],
2026                EffectSet::singleton("llm_cloud"),
2027                Ty::Con("Result".into(), vec![
2028                    Ty::Con("Stream".into(), vec![Ty::str()]),
2029                    Ty::str(),
2030                ]),
2031            ));
2032            Some(Ty::Record(fields))
2033        }
2034        "stream" => {
2035            // #305 slice 3: opaque consumer-side operations on
2036            // `Stream[T]`. Producers live elsewhere (`agent.cloud_stream`
2037            // for now); future producers (`http.get_stream`, etc.)
2038            // will register the same Stream[T] surface.
2039            let mut fields = IndexMap::new();
2040            // next :: Stream[T] -> [stream] Option[T]
2041            // One pull. `None` signals end-of-stream (consumed by
2042            // the producer's lazy generator).
2043            fields.insert("next".into(), Ty::function(
2044                vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
2045                EffectSet::singleton("stream"),
2046                Ty::Con("Option".into(), vec![Ty::Var(0)]),
2047            ));
2048            // collect :: Stream[T] -> [stream] List[T]
2049            // Drain to a list. Eager; blocks until the producer
2050            // signals end-of-stream.
2051            fields.insert("collect".into(), Ty::function(
2052                vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
2053                EffectSet::singleton("stream"),
2054                Ty::List(Box::new(Ty::Var(0))),
2055            ));
2056            Some(Ty::Record(fields))
2057        }
2058        _ => None,
2059    }
2060}
2061
2062/// Resolve `import "std.foo" as alias` to a module name (e.g. "io").
2063pub fn module_for_import(reference: &str) -> Option<&'static str> {
2064    let suffix = reference.strip_prefix("std.")?;
2065    Some(match suffix {
2066        "io" => "io",
2067        "str" => "str",
2068        "int" => "int",
2069        "float" => "float",
2070        "list" => "list",
2071        "result" => "result",
2072        "option" => "option",
2073        "json" => "json",
2074        "flow" => "flow",
2075        "tuple" => "tuple",
2076        "time" => "time",
2077        "rand" => "rand",
2078        "random" => "random",
2079        "env" => "env",
2080        "bytes" => "bytes",
2081        "net" => "net",
2082        "chat" => "chat",
2083        "math" => "math",
2084        "map" => "map",
2085        "set" => "set",
2086        "iter" => "iter",
2087        "proc" => "proc",
2088        "crypto" => "crypto",
2089        "regex" => "regex",
2090        "parser" => "parser",
2091        "deque" => "deque",
2092        "kv" => "kv",
2093        "sql" => "sql",
2094        "fs" => "fs",
2095        "process" => "process",
2096        "datetime" => "datetime",
2097        "duration" => "duration",
2098        "log" => "log",
2099        "http" => "http",
2100        "toml" => "toml",
2101        "yaml" => "yaml",
2102        "dotenv" => "dotenv",
2103        "csv" => "csv",
2104        "test" => "test",
2105        "agent" => "agent",
2106        "cli" => "cli",
2107        "stream" => "stream",
2108        _ => return None,
2109    })
2110}