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