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