Skip to main content

lex_types/
builtins.rs

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