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