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