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