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