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