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.
102            for name in &["exp", "log", "sqrt", "abs"] {
103                fields.insert((*name).into(), Ty::function(
104                    vec![Ty::float()], EffectSet::empty(), Ty::float(),
105                ));
106            }
107            // Constructors.
108            fields.insert("zeros".into(), Ty::function(
109                vec![Ty::int(), Ty::int()], EffectSet::empty(), mat(),
110            ));
111            fields.insert("ones".into(), Ty::function(
112                vec![Ty::int(), Ty::int()], EffectSet::empty(), mat(),
113            ));
114            fields.insert("from_lists".into(), Ty::function(
115                vec![Ty::List(Box::new(Ty::List(Box::new(Ty::float()))))],
116                EffectSet::empty(),
117                mat(),
118            ));
119            fields.insert("from_flat".into(), Ty::function(
120                vec![Ty::int(), Ty::int(), Ty::List(Box::new(Ty::float()))],
121                EffectSet::empty(),
122                mat(),
123            ));
124            // Accessors.
125            fields.insert("rows".into(), Ty::function(vec![mat()], EffectSet::empty(), Ty::int()));
126            fields.insert("cols".into(), Ty::function(vec![mat()], EffectSet::empty(), Ty::int()));
127            fields.insert("get".into(), Ty::function(
128                vec![mat(), Ty::int(), Ty::int()], EffectSet::empty(), Ty::float(),
129            ));
130            fields.insert("to_flat".into(), Ty::function(
131                vec![mat()], EffectSet::empty(),
132                Ty::List(Box::new(Ty::float())),
133            ));
134            // Linalg ops.
135            fields.insert("transpose".into(), Ty::function(
136                vec![mat()], EffectSet::empty(), mat(),
137            ));
138            fields.insert("matmul".into(), Ty::function(
139                vec![mat(), mat()], EffectSet::empty(), mat(),
140            ));
141            fields.insert("scale".into(), Ty::function(
142                vec![Ty::float(), mat()], EffectSet::empty(), mat(),
143            ));
144            for name in &["add", "sub"] {
145                fields.insert((*name).into(), Ty::function(
146                    vec![mat(), mat()], EffectSet::empty(), mat(),
147                ));
148            }
149            fields.insert("sigmoid".into(), Ty::function(
150                vec![mat()], EffectSet::empty(), mat(),
151            ));
152            Some(Ty::Record(fields))
153        }
154        "float" => {
155            let mut fields = IndexMap::new();
156            fields.insert("to_int".into(), Ty::function(vec![Ty::float()], EffectSet::empty(), Ty::int()));
157            fields.insert("to_str".into(), Ty::function(vec![Ty::float()], EffectSet::empty(), Ty::str()));
158            Some(Ty::Record(fields))
159        }
160        "list" => {
161            // list polymorphic functions need fresh vars at use sites; we
162            // encode them with placeholder Var ids that get instantiated.
163            let mut fields = IndexMap::new();
164            // Effect polymorphism: each HOF carries an effect-row
165            // variable so an effectful closure (e.g. one that calls
166            // net.get inside list.map's lambda) propagates its
167            // effects to the result type. Spec §7.3.
168            //
169            // map :: [E] List[a], (a) -> [E] b -> [E] List[b]
170            fields.insert("map".into(), Ty::function(
171                vec![
172                    Ty::List(Box::new(Ty::Var(0))),
173                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
174                ],
175                EffectSet::open_var(2),
176                Ty::List(Box::new(Ty::Var(1))),
177            ));
178            fields.insert("filter".into(), Ty::function(
179                vec![
180                    Ty::List(Box::new(Ty::Var(0))),
181                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), Ty::bool()),
182                ],
183                EffectSet::open_var(3),
184                Ty::List(Box::new(Ty::Var(0))),
185            ));
186            fields.insert("fold".into(), Ty::function(
187                vec![
188                    Ty::List(Box::new(Ty::Var(0))),
189                    Ty::Var(1),
190                    Ty::function(vec![Ty::Var(1), Ty::Var(0)], EffectSet::open_var(4), Ty::Var(1)),
191                ],
192                EffectSet::open_var(4),
193                Ty::Var(1),
194            ));
195            fields.insert("len".into(), Ty::function(
196                vec![Ty::List(Box::new(Ty::Var(0)))],
197                EffectSet::empty(),
198                Ty::int(),
199            ));
200            fields.insert("is_empty".into(), Ty::function(
201                vec![Ty::List(Box::new(Ty::Var(0)))],
202                EffectSet::empty(),
203                Ty::bool(),
204            ));
205            fields.insert("range".into(), Ty::function(
206                vec![Ty::int(), Ty::int()],
207                EffectSet::empty(),
208                Ty::List(Box::new(Ty::int())),
209            ));
210            fields.insert("head".into(), Ty::function(
211                vec![Ty::List(Box::new(Ty::Var(0)))],
212                EffectSet::empty(),
213                Ty::Con("Option".into(), vec![Ty::Var(0)]),
214            ));
215            fields.insert("tail".into(), Ty::function(
216                vec![Ty::List(Box::new(Ty::Var(0)))],
217                EffectSet::empty(),
218                Ty::List(Box::new(Ty::Var(0))),
219            ));
220            fields.insert("concat".into(), Ty::function(
221                vec![Ty::List(Box::new(Ty::Var(0))), Ty::List(Box::new(Ty::Var(0)))],
222                EffectSet::empty(),
223                Ty::List(Box::new(Ty::Var(0))),
224            ));
225            Some(Ty::Record(fields))
226        }
227        "bytes" => {
228            let mut fields = IndexMap::new();
229            fields.insert("len".into(), Ty::function(
230                vec![Ty::bytes()], EffectSet::empty(), Ty::int(),
231            ));
232            fields.insert("is_empty".into(), Ty::function(
233                vec![Ty::bytes()], EffectSet::empty(), Ty::bool(),
234            ));
235            fields.insert("eq".into(), Ty::function(
236                vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool(),
237            ));
238            fields.insert("from_str".into(), Ty::function(
239                vec![Ty::str()], EffectSet::empty(), Ty::bytes(),
240            ));
241            fields.insert("to_str".into(), Ty::function(
242                vec![Ty::bytes()], EffectSet::empty(),
243                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
244            ));
245            fields.insert("slice".into(), Ty::function(
246                vec![Ty::bytes(), Ty::int(), Ty::int()],
247                EffectSet::empty(), Ty::bytes(),
248            ));
249            Some(Ty::Record(fields))
250        }
251        "time" => {
252            // time.now() -> [time] Int — unix timestamp seconds.
253            // Reading the clock is an effect for two reasons: it's
254            // non-deterministic (replay needs the captured value) and
255            // it's a side-channel surface (see "Capability ≠
256            // correctness" on the landing page).
257            let mut fields = IndexMap::new();
258            fields.insert("now".into(), Ty::function(
259                vec![],
260                EffectSet::singleton("time"),
261                Ty::int(),
262            ));
263            Some(Ty::Record(fields))
264        }
265        "rand" => {
266            // rand.int_in(lo, hi) -> [rand] Int — currently a deterministic
267            // stub (midpoint) per spec §13; replaced when randomness lands.
268            let mut fields = IndexMap::new();
269            fields.insert("int_in".into(), Ty::function(
270                vec![Ty::int(), Ty::int()],
271                EffectSet::singleton("rand"),
272                Ty::int(),
273            ));
274            Some(Ty::Record(fields))
275        }
276        "net" => {
277            let mut fields = IndexMap::new();
278            // get :: Str -> [net] Result[Str, Str]
279            fields.insert("get".into(), Ty::function(
280                vec![Ty::str()],
281                EffectSet::singleton("net"),
282                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
283            ));
284            fields.insert("post".into(), Ty::function(
285                vec![Ty::str(), Ty::str()],
286                EffectSet::singleton("net"),
287                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
288            ));
289            // serve :: (Int, Str) -> [net] Unit  (blocks; never returns
290            // under normal use). Handler's signature isn't carried in
291            // the type system here — looked up by name at runtime.
292            fields.insert("serve".into(), Ty::function(
293                vec![Ty::int(), Ty::str()],
294                EffectSet::singleton("net"),
295                Ty::Unit,
296            ));
297            // serve_tls :: (Int, Str, Str, Str) -> [net] Unit
298            //              port  cert  key   handler
299            // cert and key are filesystem paths to PEM-encoded files.
300            fields.insert("serve_tls".into(), Ty::function(
301                vec![Ty::int(), Ty::str(), Ty::str(), Ty::str()],
302                EffectSet::singleton("net"),
303                Ty::Unit,
304            ));
305            // serve_ws :: (Int, Str) -> [net] Unit
306            //             port  on_message_handler_name
307            // The handler is looked up by name at runtime.
308            fields.insert("serve_ws".into(), Ty::function(
309                vec![Ty::int(), Ty::str()],
310                EffectSet::singleton("net"),
311                Ty::Unit,
312            ));
313            Some(Ty::Record(fields))
314        }
315        "chat" => {
316            let mut fields = IndexMap::new();
317            fields.insert("broadcast".into(), Ty::function(
318                vec![Ty::str(), Ty::str()],
319                EffectSet::singleton("chat"),
320                Ty::Unit,
321            ));
322            fields.insert("send".into(), Ty::function(
323                vec![Ty::int(), Ty::str()],
324                EffectSet::singleton("chat"),
325                Ty::bool(),
326            ));
327            Some(Ty::Record(fields))
328        }
329        "proc" => {
330            // Subprocess dispatch. Effect: [proc]. Returns a Result
331            // with a record on success carrying stdout / stderr /
332            // exit_code. The runtime allow-lists which binary
333            // basenames are spawnable — `cmd` is the program to
334            // run, `args` is the literal argv (no shell parsing).
335            //
336            // Read SECURITY.md before adding [proc] to a policy:
337            // it weakens the "we know what this fn does" claim.
338            let mut fields = IndexMap::new();
339            let mut result_rec = IndexMap::new();
340            result_rec.insert("stdout".into(), Ty::str());
341            result_rec.insert("stderr".into(), Ty::str());
342            result_rec.insert("exit_code".into(), Ty::int());
343            // spawn :: Str, List[Str] -> [proc] Result[{stdout, stderr, exit_code}, Str]
344            fields.insert("spawn".into(), Ty::function(
345                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
346                EffectSet::singleton("proc"),
347                Ty::Con("Result".into(), vec![
348                    Ty::Record(result_rec),
349                    Ty::str(),
350                ]),
351            ));
352            Some(Ty::Record(fields))
353        }
354        "json" => {
355            let mut fields = IndexMap::new();
356            // stringify :: T -> Str  (polymorphic on input)
357            fields.insert("stringify".into(), Ty::function(
358                vec![Ty::Var(0)], EffectSet::empty(), Ty::str(),
359            ));
360            // parse :: Str -> Result[T, Str]
361            fields.insert("parse".into(), Ty::function(
362                vec![Ty::str()], EffectSet::empty(),
363                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
364            ));
365            // parse_strict :: (Str, List[Str]) -> Result[T, Str]
366            // Tactical fix for #168 — caller passes the field names
367            // T requires; runtime returns Err if any are missing
368            // from the parsed object instead of letting field
369            // access panic later.
370            fields.insert("parse_strict".into(), Ty::function(
371                vec![Ty::str(), Ty::List(Box::new(Ty::str()))], EffectSet::empty(),
372                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
373            ));
374            Some(Ty::Record(fields))
375        }
376        "result" => {
377            let mut fields = IndexMap::new();
378            // result.map :: Result[T, E], (T) -> [E2] U -> [E2] Result[U, E]
379            // Effect-polymorphic on the closure: result.map et al.
380            // propagate the closure's effects to the surrounding call.
381            fields.insert("map".into(), Ty::function(
382                vec![
383                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
384                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), Ty::Var(2)),
385                ],
386                EffectSet::open_var(3),
387                Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
388            ));
389            fields.insert("and_then".into(), Ty::function(
390                vec![
391                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
392                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(4),
393                        Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)])),
394                ],
395                EffectSet::open_var(4),
396                Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
397            ));
398            fields.insert("map_err".into(), Ty::function(
399                vec![
400                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
401                    Ty::function(vec![Ty::Var(1)], EffectSet::open_var(5), Ty::Var(2)),
402                ],
403                EffectSet::open_var(5),
404                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
405            ));
406            Some(Ty::Record(fields))
407        }
408        "option" => {
409            let mut fields = IndexMap::new();
410            // option.map :: Option[T], (T) -> [E] U -> [E] Option[U]
411            fields.insert("map".into(), Ty::function(
412                vec![
413                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
414                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
415                ],
416                EffectSet::open_var(2),
417                Ty::Con("Option".into(), vec![Ty::Var(1)]),
418            ));
419            fields.insert("unwrap_or".into(), Ty::function(
420                vec![Ty::Con("Option".into(), vec![Ty::Var(0)]), Ty::Var(0)],
421                EffectSet::empty(),
422                Ty::Var(0),
423            ));
424            Some(Ty::Record(fields))
425        }
426        "tuple" => {
427            // Tuple accessors per §11.1. Polymorphic in the tuple's
428            // element types; we use the same row-variable shape used
429            // by list helpers. Tuples are heterogeneous, so each
430            // accessor is statically typed via independent type
431            // variables for each position.
432            let mut fields = IndexMap::new();
433            // fst :: (T0, T1) -> T0
434            fields.insert("fst".into(), Ty::function(
435                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
436                EffectSet::empty(),
437                Ty::Var(0),
438            ));
439            // snd :: (T0, T1) -> T1
440            fields.insert("snd".into(), Ty::function(
441                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
442                EffectSet::empty(),
443                Ty::Var(1),
444            ));
445            // third :: (T0, T1, T2) -> T2
446            fields.insert("third".into(), Ty::function(
447                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1), Ty::Var(2)])],
448                EffectSet::empty(),
449                Ty::Var(2),
450            ));
451            // len :: (T0, T1) -> Int  (covers any pair shape; Int back)
452            fields.insert("len".into(), Ty::function(
453                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
454                EffectSet::empty(),
455                Ty::int(),
456            ));
457            Some(Ty::Record(fields))
458        }
459        "map" => {
460            // Persistent map. Keys are `Str` or `Int` only — Lex's
461            // type system tracks them polymorphically as Var(0)
462            // ("K") and lets the runtime check the key shape; both
463            // cases fit into `MapKey`.
464            //
465            // Type variables: 0 = K, 1 = V.
466            let mt   = || Ty::Con("Map".into(), vec![Ty::Var(0), Ty::Var(1)]);
467            let pair = || Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]);
468            let mut fields = IndexMap::new();
469            // new :: () -> Map[K, V]
470            fields.insert("new".into(), Ty::function(
471                vec![], EffectSet::empty(), mt()));
472            // size :: Map[K, V] -> Int
473            fields.insert("size".into(), Ty::function(
474                vec![mt()], EffectSet::empty(), Ty::int()));
475            // has :: Map[K, V], K -> Bool
476            fields.insert("has".into(), Ty::function(
477                vec![mt(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
478            // get :: Map[K, V], K -> Option[V]
479            fields.insert("get".into(), Ty::function(
480                vec![mt(), Ty::Var(0)], EffectSet::empty(),
481                Ty::Con("Option".into(), vec![Ty::Var(1)])));
482            // set :: Map[K, V], K, V -> Map[K, V]
483            fields.insert("set".into(), Ty::function(
484                vec![mt(), Ty::Var(0), Ty::Var(1)],
485                EffectSet::empty(), mt()));
486            // delete :: Map[K, V], K -> Map[K, V]
487            fields.insert("delete".into(), Ty::function(
488                vec![mt(), Ty::Var(0)], EffectSet::empty(), mt()));
489            // keys :: Map[K, V] -> List[K]
490            fields.insert("keys".into(), Ty::function(
491                vec![mt()], EffectSet::empty(),
492                Ty::List(Box::new(Ty::Var(0)))));
493            // values :: Map[K, V] -> List[V]
494            fields.insert("values".into(), Ty::function(
495                vec![mt()], EffectSet::empty(),
496                Ty::List(Box::new(Ty::Var(1)))));
497            // entries :: Map[K, V] -> List[(K, V)]
498            fields.insert("entries".into(), Ty::function(
499                vec![mt()], EffectSet::empty(),
500                Ty::List(Box::new(pair()))));
501            // from_list :: List[(K, V)] -> Map[K, V]
502            fields.insert("from_list".into(), Ty::function(
503                vec![Ty::List(Box::new(pair()))],
504                EffectSet::empty(), mt()));
505            // merge :: Map[K, V], Map[K, V] -> Map[K, V]   (b overrides a)
506            fields.insert("merge".into(), Ty::function(
507                vec![mt(), mt()], EffectSet::empty(), mt()));
508            // is_empty :: Map[K, V] -> Bool
509            fields.insert("is_empty".into(), Ty::function(
510                vec![mt()], EffectSet::empty(), Ty::bool()));
511            // fold :: Map[K, V], A, (A, K, V) -> [E] A -> [E] A
512            // Iteration order matches `map.entries` (BTreeMap-sorted by
513            // key). Effect-polymorphic on the combiner like `list.fold`.
514            // Type variable 2 = A (accumulator), effect row 3.
515            fields.insert("fold".into(), Ty::function(
516                vec![
517                    mt(),
518                    Ty::Var(2),
519                    Ty::function(
520                        vec![Ty::Var(2), Ty::Var(0), Ty::Var(1)],
521                        EffectSet::open_var(3),
522                        Ty::Var(2),
523                    ),
524                ],
525                EffectSet::open_var(3),
526                Ty::Var(2),
527            ));
528            Some(Ty::Record(fields))
529        }
530        "set" => {
531            // Persistent set with the same key-type discipline as map.
532            // Type variable: 0 = T (the element type, also the key type).
533            let st   = || Ty::Con("Set".into(), vec![Ty::Var(0)]);
534            let mut fields = IndexMap::new();
535            // new :: () -> Set[T]
536            fields.insert("new".into(), Ty::function(
537                vec![], EffectSet::empty(), st()));
538            // size :: Set[T] -> Int
539            fields.insert("size".into(), Ty::function(
540                vec![st()], EffectSet::empty(), Ty::int()));
541            // has :: Set[T], T -> Bool
542            fields.insert("has".into(), Ty::function(
543                vec![st(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
544            // add :: Set[T], T -> Set[T]
545            fields.insert("add".into(), Ty::function(
546                vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
547            // delete :: Set[T], T -> Set[T]
548            fields.insert("delete".into(), Ty::function(
549                vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
550            // to_list :: Set[T] -> List[T]
551            fields.insert("to_list".into(), Ty::function(
552                vec![st()], EffectSet::empty(),
553                Ty::List(Box::new(Ty::Var(0)))));
554            // from_list :: List[T] -> Set[T]
555            fields.insert("from_list".into(), Ty::function(
556                vec![Ty::List(Box::new(Ty::Var(0)))],
557                EffectSet::empty(), st()));
558            // union :: Set[T], Set[T] -> Set[T]
559            fields.insert("union".into(), Ty::function(
560                vec![st(), st()], EffectSet::empty(), st()));
561            // intersect :: Set[T], Set[T] -> Set[T]
562            fields.insert("intersect".into(), Ty::function(
563                vec![st(), st()], EffectSet::empty(), st()));
564            // diff :: Set[T], Set[T] -> Set[T]
565            fields.insert("diff".into(), Ty::function(
566                vec![st(), st()], EffectSet::empty(), st()));
567            // is_empty :: Set[T] -> Bool
568            fields.insert("is_empty".into(), Ty::function(
569                vec![st()], EffectSet::empty(), Ty::bool()));
570            // is_subset :: Set[T], Set[T] -> Bool   (a is subset of b)
571            fields.insert("is_subset".into(), Ty::function(
572                vec![st(), st()], EffectSet::empty(), Ty::bool()));
573            Some(Ty::Record(fields))
574        }
575        "flow" => {
576            // Orchestration primitives (spec §11.2). Each takes one or
577            // more closures and returns a closure with a derived shape.
578            let mut fields = IndexMap::new();
579            // sequential[T, U, V](f: (T) -> U, g: (U) -> V) -> (T) -> V
580            fields.insert("sequential".into(), Ty::function(
581                vec![
582                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
583                    Ty::function(vec![Ty::Var(1)], EffectSet::empty(), Ty::Var(2)),
584                ],
585                EffectSet::empty(),
586                Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(2)),
587            ));
588            // branch[T, U](cond: (T) -> Bool, t: (T) -> U, f: (T) -> U) -> (T) -> U
589            fields.insert("branch".into(), Ty::function(
590                vec![
591                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::bool()),
592                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
593                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
594                ],
595                EffectSet::empty(),
596                Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
597            ));
598            // retry[T, U, E](f: (T) -> Result[U, E], n: Int) -> (T) -> Result[U, E]
599            let result_ty = Ty::Con("Result".into(), vec![Ty::Var(1), Ty::Var(2)]);
600            fields.insert("retry".into(), Ty::function(
601                vec![
602                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), result_ty.clone()),
603                    Ty::int(),
604                ],
605                EffectSet::empty(),
606                Ty::function(vec![Ty::Var(0)], EffectSet::empty(), result_ty),
607            ));
608            // parallel[A, B](fa: () -> A, fb: () -> B) -> () -> (A, B)
609            // Sequential implementation today; spec §11.2 reserves the
610            // option of a true-threaded scheduler. parallel_record is
611            // listed in the spec but not yet implemented — it needs row
612            // polymorphism over the input record's fields plus a
613            // record-iteration trampoline; tracked as follow-up.
614            fields.insert("parallel".into(), Ty::function(
615                vec![
616                    Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
617                    Ty::function(vec![], EffectSet::empty(), Ty::Var(1)),
618                ],
619                EffectSet::empty(),
620                Ty::function(vec![], EffectSet::empty(),
621                    Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])),
622            ));
623            // parallel_list[T](actions: List[() -> T]) -> List[T]
624            // Variadic counterpart to `parallel`. Runs each action and
625            // collects results in input order. Sequential under the
626            // hood (same caveat as `parallel`); spec §11.2 reserves
627            // true threading for a future scheduler. Unlike `parallel`,
628            // this returns the result list directly rather than a
629            // closure, since the input arity is dynamic.
630            fields.insert("parallel_list".into(), Ty::function(
631                vec![
632                    Ty::List(Box::new(
633                        Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
634                    )),
635                ],
636                EffectSet::empty(),
637                Ty::List(Box::new(Ty::Var(0))),
638            ));
639            Some(Ty::Record(fields))
640        }
641        "crypto" => {
642            let mut fields = IndexMap::new();
643            // Hashes: Bytes -> Bytes (digest as raw bytes)
644            for name in &["sha256", "sha512", "md5"] {
645                fields.insert((*name).into(), Ty::function(
646                    vec![Ty::bytes()],
647                    EffectSet::empty(),
648                    Ty::bytes(),
649                ));
650            }
651            // HMAC: (key :: Bytes, data :: Bytes) -> Bytes
652            for name in &["hmac_sha256", "hmac_sha512"] {
653                fields.insert((*name).into(), Ty::function(
654                    vec![Ty::bytes(), Ty::bytes()],
655                    EffectSet::empty(),
656                    Ty::bytes(),
657                ));
658            }
659            // base64 / hex
660            fields.insert("base64_encode".into(), Ty::function(
661                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
662            fields.insert("base64_decode".into(), Ty::function(
663                vec![Ty::str()], EffectSet::empty(),
664                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
665            fields.insert("hex_encode".into(), Ty::function(
666                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
667            fields.insert("hex_decode".into(), Ty::function(
668                vec![Ty::str()], EffectSet::empty(),
669                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
670            // constant-time equality (for HMAC verification etc.)
671            fields.insert("constant_time_eq".into(), Ty::function(
672                vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool()));
673            // Cryptographically-secure random bytes — OS RNG, not the
674            // deterministic `rand.int_in` stub. The new `[random]`
675            // effect is fine-grained on purpose so reviewers can find
676            // every token-generating call via `lex audit --effect
677            // random`.
678            fields.insert("random".into(), Ty::function(
679                vec![Ty::int()],
680                EffectSet::singleton("random"),
681                Ty::bytes(),
682            ));
683            Some(Ty::Record(fields))
684        }
685        "deque" => {
686            // Persistent double-ended queue. Push/pop O(1) on both
687            // ends; iteration order is front-to-back.
688            // Type variable: 0 = T.
689            let dt   = || Ty::Con("Deque".into(), vec![Ty::Var(0)]);
690            let pair = || Ty::Tuple(vec![Ty::Var(0), dt()]);
691            let mut fields = IndexMap::new();
692            // new :: () -> Deque[T]
693            fields.insert("new".into(), Ty::function(
694                vec![], EffectSet::empty(), dt()));
695            // size :: Deque[T] -> Int
696            fields.insert("size".into(), Ty::function(
697                vec![dt()], EffectSet::empty(), Ty::int()));
698            // is_empty :: Deque[T] -> Bool
699            fields.insert("is_empty".into(), Ty::function(
700                vec![dt()], EffectSet::empty(), Ty::bool()));
701            // push_back / push_front :: Deque[T], T -> Deque[T]
702            for n in &["push_back", "push_front"] {
703                fields.insert((*n).into(), Ty::function(
704                    vec![dt(), Ty::Var(0)], EffectSet::empty(), dt()));
705            }
706            // pop_back / pop_front :: Deque[T] -> Option[(T, Deque[T])]
707            for n in &["pop_back", "pop_front"] {
708                fields.insert((*n).into(), Ty::function(
709                    vec![dt()], EffectSet::empty(),
710                    Ty::Con("Option".into(), vec![pair()])));
711            }
712            // peek_back / peek_front :: Deque[T] -> Option[T]
713            for n in &["peek_back", "peek_front"] {
714                fields.insert((*n).into(), Ty::function(
715                    vec![dt()], EffectSet::empty(),
716                    Ty::Con("Option".into(), vec![Ty::Var(0)])));
717            }
718            // from_list :: List[T] -> Deque[T]
719            fields.insert("from_list".into(), Ty::function(
720                vec![Ty::List(Box::new(Ty::Var(0)))],
721                EffectSet::empty(), dt()));
722            // to_list :: Deque[T] -> List[T]
723            fields.insert("to_list".into(), Ty::function(
724                vec![dt()], EffectSet::empty(),
725                Ty::List(Box::new(Ty::Var(0)))));
726            Some(Ty::Record(fields))
727        }
728        "log" => {
729            // Structured logging behind a [log] effect. Emit ops route
730            // through a runtime-configured sink (stderr by default;
731            // can be redirected via set_sink). Configuration ops
732            // mutate the global sink and so are gated [io].
733            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
734            let mut fields = IndexMap::new();
735            for level in &["debug", "info", "warn", "error"] {
736                fields.insert((*level).into(), Ty::function(
737                    vec![Ty::str()],
738                    EffectSet::singleton("log"),
739                    Ty::Unit,
740                ));
741            }
742            // set_level :: Str -> [io] Result[Nil, Str]
743            fields.insert("set_level".into(), Ty::function(
744                vec![Ty::str()],
745                EffectSet::singleton("io"),
746                result_str(Ty::Unit)));
747            // set_format :: Str -> [io] Result[Nil, Str]
748            fields.insert("set_format".into(), Ty::function(
749                vec![Ty::str()],
750                EffectSet::singleton("io"),
751                result_str(Ty::Unit)));
752            // set_sink :: Str -> [io, fs_write] Result[Nil, Str]
753            fields.insert("set_sink".into(), Ty::function(
754                vec![Ty::str()],
755                EffectSet {
756                    concrete: ["io".to_string(), "fs_write".to_string()].into_iter().collect(),
757                    var: None,
758                },
759                result_str(Ty::Unit)));
760            Some(Ty::Record(fields))
761        }
762        "datetime" => {
763            // Instant and Duration are nominal opaque Ints under the
764            // hood (nanoseconds-since-UTC-epoch and signed nanoseconds
765            // respectively); the type checker tracks the distinction
766            // even though both values look like Int at runtime.
767            //
768            // Tz is the variant
769            //     Utc | Local | Offset(Int) | Iana(Str)
770            // registered as a built-in nominal type in
771            // `TypeEnv::new_with_builtins`. The pre-v1 stringly Tz
772            // ("UTC"/"Local"/IANA-name/"+05:30") is no longer accepted
773            // — passing a `Str` to `to_components` is now a type
774            // error.
775            let inst   = || Ty::Con("Instant".into(), vec![]);
776            let dur    = || Ty::Con("Duration".into(), vec![]);
777            let tz     = || Ty::Con("Tz".into(), vec![]);
778            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
779            let dt_t = || {
780                let mut fs = IndexMap::new();
781                fs.insert("year".into(),    Ty::int());
782                fs.insert("month".into(),   Ty::int());
783                fs.insert("day".into(),     Ty::int());
784                fs.insert("hour".into(),    Ty::int());
785                fs.insert("minute".into(),  Ty::int());
786                fs.insert("second".into(),  Ty::int());
787                fs.insert("nano".into(),    Ty::int());
788                fs.insert("tz_offset_minutes".into(), Ty::int());
789                Ty::Record(fs)
790            };
791            let mut fields = IndexMap::new();
792            fields.insert("now".into(), Ty::function(
793                vec![], EffectSet::singleton("time"), inst()));
794            fields.insert("parse_iso".into(), Ty::function(
795                vec![Ty::str()], EffectSet::empty(), result_str(inst())));
796            fields.insert("format_iso".into(), Ty::function(
797                vec![inst()], EffectSet::empty(), Ty::str()));
798            fields.insert("parse".into(), Ty::function(
799                vec![Ty::str(), Ty::str()], EffectSet::empty(), result_str(inst())));
800            fields.insert("format".into(), Ty::function(
801                vec![inst(), Ty::str()], EffectSet::empty(), Ty::str()));
802            fields.insert("to_components".into(), Ty::function(
803                vec![inst(), tz()], EffectSet::empty(), result_str(dt_t())));
804            fields.insert("from_components".into(), Ty::function(
805                vec![dt_t()], EffectSet::empty(), result_str(inst())));
806            fields.insert("add".into(), Ty::function(
807                vec![inst(), dur()], EffectSet::empty(), inst()));
808            fields.insert("diff".into(), Ty::function(
809                vec![inst(), inst()], EffectSet::empty(), dur()));
810            fields.insert("duration_seconds".into(), Ty::function(
811                vec![Ty::float()], EffectSet::empty(), dur()));
812            fields.insert("duration_minutes".into(), Ty::function(
813                vec![Ty::int()], EffectSet::empty(), dur()));
814            fields.insert("duration_days".into(), Ty::function(
815                vec![Ty::int()], EffectSet::empty(), dur()));
816            Some(Ty::Record(fields))
817        }
818        "process" => {
819            // Streaming subprocess. The opaque `ProcessHandle` type
820            // is an Int handle into a process-wide registry holding
821            // the `Child` plus its stdout/stderr `BufReader`s.
822            let ph = || Ty::Con("ProcessHandle".into(), vec![]);
823            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
824            let opts_t = || {
825                let mut fs = IndexMap::new();
826                fs.insert("cwd".into(),
827                    Ty::Con("Option".into(), vec![Ty::str()]));
828                fs.insert("env".into(),
829                    Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
830                fs.insert("stdin".into(),
831                    Ty::Con("Option".into(), vec![Ty::bytes()]));
832                Ty::Record(fs)
833            };
834            let exit_t = || {
835                let mut fs = IndexMap::new();
836                fs.insert("code".into(), Ty::int());
837                fs.insert("signaled".into(), Ty::bool());
838                Ty::Record(fs)
839            };
840            let output_t = || {
841                let mut fs = IndexMap::new();
842                fs.insert("stdout".into(), Ty::str());
843                fs.insert("stderr".into(), Ty::str());
844                fs.insert("exit_code".into(), Ty::int());
845                Ty::Record(fs)
846            };
847            let mut fields = IndexMap::new();
848            // spawn :: Str, List[Str], Opts -> [proc] Result[ProcessHandle, Str]
849            fields.insert("spawn".into(), Ty::function(
850                vec![Ty::str(), Ty::List(Box::new(Ty::str())), opts_t()],
851                EffectSet::singleton("proc"),
852                result_str(ph())));
853            // read_stdout_line / read_stderr_line :: ProcessHandle -> [proc] Option[Str]
854            for n in &["read_stdout_line", "read_stderr_line"] {
855                fields.insert((*n).into(), Ty::function(
856                    vec![ph()], EffectSet::singleton("proc"),
857                    Ty::Con("Option".into(), vec![Ty::str()])));
858            }
859            // wait :: ProcessHandle -> [proc] ProcessExit
860            fields.insert("wait".into(), Ty::function(
861                vec![ph()], EffectSet::singleton("proc"), exit_t()));
862            // kill :: ProcessHandle, Str -> [proc] Result[Nil, Str]
863            fields.insert("kill".into(), Ty::function(
864                vec![ph(), Ty::str()],
865                EffectSet::singleton("proc"),
866                result_str(Ty::Unit)));
867            // run :: Str, List[Str] -> [proc] Result[ProcessOutput, Str]
868            // Blocking convenience that captures stdout/stderr fully
869            // and returns once the child exits. For programs that
870            // need streaming, use spawn + read_*_line + wait.
871            fields.insert("run".into(), Ty::function(
872                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
873                EffectSet::singleton("proc"),
874                result_str(output_t())));
875            Some(Ty::Record(fields))
876        }
877        "fs" => {
878            // Filesystem walk + mutate. Walk-style ops (exists, walk,
879            // glob, …) declare [fs_walk] — distinct from [fs_read]
880            // (which is content reads via io.read), so reviewers can
881            // separately track directory traversal vs file-content
882            // exposure. Mutating ops (mkdir_p, remove, copy) declare
883            // [fs_write]. Path scoping uses --allow-fs-read for walk
884            // (a directory listing is an information disclosure on
885            // the same path tree) and --allow-fs-write for mutations.
886            let stat_t = || {
887                let mut fs = IndexMap::new();
888                fs.insert("size".into(), Ty::int());
889                fs.insert("mtime".into(), Ty::int());
890                fs.insert("is_dir".into(), Ty::bool());
891                fs.insert("is_file".into(), Ty::bool());
892                Ty::Record(fs)
893            };
894            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
895            let mut fields = IndexMap::new();
896            // Walk-style queries [fs_walk]
897            fields.insert("exists".into(), Ty::function(
898                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
899            fields.insert("is_file".into(), Ty::function(
900                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
901            fields.insert("is_dir".into(), Ty::function(
902                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
903            fields.insert("stat".into(), Ty::function(
904                vec![Ty::str()], EffectSet::singleton("fs_walk"),
905                result_str(stat_t())));
906            fields.insert("list_dir".into(), Ty::function(
907                vec![Ty::str()], EffectSet::singleton("fs_walk"),
908                result_str(Ty::List(Box::new(Ty::str())))));
909            fields.insert("walk".into(), Ty::function(
910                vec![Ty::str()], EffectSet::singleton("fs_walk"),
911                result_str(Ty::List(Box::new(Ty::str())))));
912            fields.insert("glob".into(), Ty::function(
913                vec![Ty::str()], EffectSet::singleton("fs_walk"),
914                result_str(Ty::List(Box::new(Ty::str())))));
915            // Mutations [fs_write]
916            fields.insert("mkdir_p".into(), Ty::function(
917                vec![Ty::str()], EffectSet::singleton("fs_write"),
918                result_str(Ty::Unit)));
919            fields.insert("remove".into(), Ty::function(
920                vec![Ty::str()], EffectSet::singleton("fs_write"),
921                result_str(Ty::Unit)));
922            fields.insert("copy".into(), Ty::function(
923                vec![Ty::str(), Ty::str()],
924                EffectSet {
925                    concrete: ["fs_walk".to_string(), "fs_write".to_string()].into_iter().collect(),
926                    var: None,
927                },
928                result_str(Ty::Unit)));
929            Some(Ty::Record(fields))
930        }
931        "kv" => {
932            // Embedded key-value store. The opaque `Kv` type is
933            // backed by an Int handle into a process-wide registry.
934            let kv_t = || Ty::Con("Kv".into(), vec![]);
935            let mut fields = IndexMap::new();
936            // open :: Str -> [kv, fs_write] Result[Kv, Str]
937            fields.insert("open".into(), Ty::function(
938                vec![Ty::str()],
939                EffectSet {
940                    concrete: ["kv".to_string(), "fs_write".to_string()].into_iter().collect(),
941                    var: None,
942                },
943                Ty::Con("Result".into(), vec![kv_t(), Ty::str()])));
944            // close :: Kv -> [kv] Nil
945            fields.insert("close".into(), Ty::function(
946                vec![kv_t()],
947                EffectSet::singleton("kv"),
948                Ty::Unit));
949            // get :: Kv, Str -> [kv] Option[Bytes]
950            fields.insert("get".into(), Ty::function(
951                vec![kv_t(), Ty::str()],
952                EffectSet::singleton("kv"),
953                Ty::Con("Option".into(), vec![Ty::bytes()])));
954            // put :: Kv, Str, Bytes -> [kv] Result[Nil, Str]
955            fields.insert("put".into(), Ty::function(
956                vec![kv_t(), Ty::str(), Ty::bytes()],
957                EffectSet::singleton("kv"),
958                Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
959            // delete :: Kv, Str -> [kv] Result[Nil, Str]
960            fields.insert("delete".into(), Ty::function(
961                vec![kv_t(), Ty::str()],
962                EffectSet::singleton("kv"),
963                Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
964            // contains :: Kv, Str -> [kv] Bool
965            fields.insert("contains".into(), Ty::function(
966                vec![kv_t(), Ty::str()],
967                EffectSet::singleton("kv"),
968                Ty::bool()));
969            // list_prefix :: Kv, Str -> [kv] List[Str]
970            fields.insert("list_prefix".into(), Ty::function(
971                vec![kv_t(), Ty::str()],
972                EffectSet::singleton("kv"),
973                Ty::List(Box::new(Ty::str()))));
974            Some(Ty::Record(fields))
975        }
976        "sql" => {
977            // Embedded SQL (SQLite). The opaque `Db` type is backed
978            // by an Int handle into a process-wide registry, same
979            // shape as `Kv`. v1 surface focuses on read-heavy and
980            // simple-write workloads — the kind that drove the
981            // requirement (audit history, "filter by verdict where
982            // score > 60", joins). Transactions, heterogeneous
983            // typed parameter binding, and named params are
984            // deferred to v1.5.
985            //
986            // Params are `List[Str]` for v1: callers stringify Int /
987            // Float values before binding, and SQLite's column type
988            // affinity coerces back at insert time. This is the one
989            // honest ergonomics caveat; the alternative (a tagged
990            // `SqlValue` variant) is forward-compatible but adds a
991            // type to the global scope that v1 doesn't need.
992            let db_t = || Ty::Con("Db".into(), vec![]);
993            let mut fields = IndexMap::new();
994            // open :: Str -> [sql, fs_write] Result[Db, Str]
995            // Path is the SQLite filename; ":memory:" works for
996            // ephemeral stores. fs_write is required because the
997            // DB file is created on first open.
998            fields.insert("open".into(), Ty::function(
999                vec![Ty::str()],
1000                EffectSet {
1001                    concrete: ["sql".to_string(), "fs_write".to_string()].into_iter().collect(),
1002                    var: None,
1003                },
1004                Ty::Con("Result".into(), vec![db_t(), Ty::str()])));
1005            // close :: Db -> [sql] Nil
1006            fields.insert("close".into(), Ty::function(
1007                vec![db_t()],
1008                EffectSet::singleton("sql"),
1009                Ty::Unit));
1010            // exec :: Db, Str, List[Str] -> [sql] Result[Int, Str]
1011            // Returns the affected row count (rusqlite's `execute`).
1012            // Suitable for INSERT / UPDATE / DELETE / DDL.
1013            fields.insert("exec".into(), Ty::function(
1014                vec![db_t(), Ty::str(), Ty::List(Box::new(Ty::str()))],
1015                EffectSet::singleton("sql"),
1016                Ty::Con("Result".into(), vec![Ty::int(), Ty::str()])));
1017            // query[T] :: Db, Str, List[Str] -> [sql] Result[List[T], Str]
1018            // Polymorphic on the row record shape. Each row is
1019            // decoded into a record keyed by column name, with
1020            // SQLite values mapped to the same Lex `Value` shape
1021            // as `json.parse` and `toml.parse` produce.
1022            fields.insert("query".into(), Ty::function(
1023                vec![db_t(), Ty::str(), Ty::List(Box::new(Ty::str()))],
1024                EffectSet::singleton("sql"),
1025                Ty::Con("Result".into(), vec![
1026                    Ty::List(Box::new(Ty::Var(0))),
1027                    Ty::str(),
1028                ])));
1029            Some(Ty::Record(fields))
1030        }
1031        "regex" => {
1032            // The compiled `Regex` is stored as a `Str` at runtime
1033            // (the pattern source) plus a process-wide cache of the
1034            // actual `regex::Regex`. So `Regex` is a nominal type at
1035            // the language level but its value is just the pattern.
1036            let regex_t = || Ty::Con("Regex".into(), vec![]);
1037            let match_t = || {
1038                let mut fs = IndexMap::new();
1039                fs.insert("text".into(), Ty::str());
1040                fs.insert("start".into(), Ty::int());
1041                fs.insert("end".into(), Ty::int());
1042                fs.insert("groups".into(), Ty::List(Box::new(Ty::str())));
1043                Ty::Record(fs)
1044            };
1045            let mut fields = IndexMap::new();
1046            // compile :: Str -> Result[Regex, Str]
1047            fields.insert("compile".into(), Ty::function(
1048                vec![Ty::str()], EffectSet::empty(),
1049                Ty::Con("Result".into(), vec![regex_t(), Ty::str()])));
1050            // is_match :: Regex, Str -> Bool
1051            fields.insert("is_match".into(), Ty::function(
1052                vec![regex_t(), Ty::str()], EffectSet::empty(), Ty::bool()));
1053            // find :: Regex, Str -> Option[Match]
1054            fields.insert("find".into(), Ty::function(
1055                vec![regex_t(), Ty::str()], EffectSet::empty(),
1056                Ty::Con("Option".into(), vec![match_t()])));
1057            // find_all :: Regex, Str -> List[Match]
1058            fields.insert("find_all".into(), Ty::function(
1059                vec![regex_t(), Ty::str()], EffectSet::empty(),
1060                Ty::List(Box::new(match_t()))));
1061            // replace :: Regex, Str, Str -> Str
1062            fields.insert("replace".into(), Ty::function(
1063                vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
1064            // replace_all :: Regex, Str, Str -> Str
1065            fields.insert("replace_all".into(), Ty::function(
1066                vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
1067            // split :: Regex, Str -> List[Str]
1068            fields.insert("split".into(), Ty::function(
1069                vec![regex_t(), Ty::str()], EffectSet::empty(),
1070                Ty::List(Box::new(Ty::str()))));
1071            Some(Ty::Record(fields))
1072        }
1073        "http" => {
1074            // Rich HTTP client. `[net]` for the wire ops, pure for
1075            // the builders / decoders. `--allow-net-host` gates per
1076            // request. Multipart upload + streaming response bodies
1077            // are deferred to v1.5; the v1 surface covers the
1078            // common cases (auth, headers, query, timeouts, JSON /
1079            // text decoding).
1080            let req_t  = || Ty::Con("HttpRequest".into(), vec![]);
1081            let resp_t = || Ty::Con("HttpResponse".into(), vec![]);
1082            let err_t  = || Ty::Con("HttpError".into(), vec![]);
1083            let result_he = |t: Ty| Ty::Con("Result".into(), vec![t, err_t()]);
1084            let str_str_map = || Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]);
1085            let mut fields = IndexMap::new();
1086            // -- wire ops (effectful) --
1087            // send :: HttpRequest -> [net] Result[HttpResponse, HttpError]
1088            fields.insert("send".into(), Ty::function(
1089                vec![req_t()],
1090                EffectSet::singleton("net"),
1091                result_he(resp_t()),
1092            ));
1093            // get :: Str -> [net] Result[HttpResponse, HttpError]
1094            fields.insert("get".into(), Ty::function(
1095                vec![Ty::str()],
1096                EffectSet::singleton("net"),
1097                result_he(resp_t()),
1098            ));
1099            // post :: Str, Bytes, Str -> [net] Result[HttpResponse, HttpError]
1100            fields.insert("post".into(), Ty::function(
1101                vec![Ty::str(), Ty::bytes(), Ty::str()],
1102                EffectSet::singleton("net"),
1103                result_he(resp_t()),
1104            ));
1105            // -- pure builders (record transforms) --
1106            // with_header :: HttpRequest, Str, Str -> HttpRequest
1107            fields.insert("with_header".into(), Ty::function(
1108                vec![req_t(), Ty::str(), Ty::str()],
1109                EffectSet::empty(),
1110                req_t(),
1111            ));
1112            // with_auth :: HttpRequest, Str, Str -> HttpRequest
1113            // (Renders `<scheme> <token>` into the `Authorization`
1114            // header — `Bearer <jwt>`, `Basic <b64>`, etc.)
1115            fields.insert("with_auth".into(), Ty::function(
1116                vec![req_t(), Ty::str(), Ty::str()],
1117                EffectSet::empty(),
1118                req_t(),
1119            ));
1120            // with_query :: HttpRequest, Map[Str, Str] -> HttpRequest
1121            // (Appends a `?k=v&...` query string; values are URL-
1122            // encoded so `&` / `=` / spaces in values don't escape.)
1123            fields.insert("with_query".into(), Ty::function(
1124                vec![req_t(), str_str_map()],
1125                EffectSet::empty(),
1126                req_t(),
1127            ));
1128            // with_timeout_ms :: HttpRequest, Int -> HttpRequest
1129            fields.insert("with_timeout_ms".into(), Ty::function(
1130                vec![req_t(), Ty::int()],
1131                EffectSet::empty(),
1132                req_t(),
1133            ));
1134            // -- pure decoders --
1135            // json_body[T] :: HttpResponse -> Result[T, HttpError]
1136            // Polymorphic on the parsed shape, matching `json.parse`.
1137            fields.insert("json_body".into(), Ty::function(
1138                vec![resp_t()],
1139                EffectSet::empty(),
1140                result_he(Ty::Var(0)),
1141            ));
1142            // text_body :: HttpResponse -> Result[Str, HttpError]
1143            fields.insert("text_body".into(), Ty::function(
1144                vec![resp_t()],
1145                EffectSet::empty(),
1146                result_he(Ty::str()),
1147            ));
1148            Some(Ty::Record(fields))
1149        }
1150        "yaml" => {
1151            // YAML config parser. Same shape as `std.toml`: parse
1152            // is polymorphic, output Value layout matches std.json
1153            // (Str/Int/Float/Bool/List/Record). Anchors and tags
1154            // are flattened by serde_yaml's deserializer.
1155            let mut fields = IndexMap::new();
1156            fields.insert("parse".into(), Ty::function(
1157                vec![Ty::str()], EffectSet::empty(),
1158                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1159            ));
1160            // Tactical fix for #168 — caller-supplied required-field
1161            // list. See std.json's parse_strict for context.
1162            fields.insert("parse_strict".into(), Ty::function(
1163                vec![Ty::str(), Ty::List(Box::new(Ty::str()))], EffectSet::empty(),
1164                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1165            ));
1166            fields.insert("stringify".into(), Ty::function(
1167                vec![Ty::Var(0)], EffectSet::empty(),
1168                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1169            ));
1170            Some(Ty::Record(fields))
1171        }
1172        "dotenv" => {
1173            // .env-style files. parse :: Str -> Result[Map[Str,Str], Str].
1174            // Returns a map (not a polymorphic record) because
1175            // dotenv files don't carry shape — every value is a
1176            // string and keys aren't statically known.
1177            let mut fields = IndexMap::new();
1178            fields.insert("parse".into(), Ty::function(
1179                vec![Ty::str()], EffectSet::empty(),
1180                Ty::Con("Result".into(), vec![
1181                    Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
1182                    Ty::str(),
1183                ]),
1184            ));
1185            Some(Ty::Record(fields))
1186        }
1187        "csv" => {
1188            // CSV rows-as-lists. parse :: Str -> Result[List[List[Str]], Str].
1189            // Header awareness is left to the caller — row 0 is
1190            // whatever the file has. A `parse_with_headers` that
1191            // returns List[Map[Str,Str]] is a natural follow-up.
1192            let row_ty = Ty::List(Box::new(Ty::str()));
1193            let rows_ty = Ty::List(Box::new(row_ty.clone()));
1194            let mut fields = IndexMap::new();
1195            fields.insert("parse".into(), Ty::function(
1196                vec![Ty::str()], EffectSet::empty(),
1197                Ty::Con("Result".into(), vec![rows_ty.clone(), Ty::str()]),
1198            ));
1199            fields.insert("stringify".into(), Ty::function(
1200                vec![rows_ty], EffectSet::empty(),
1201                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1202            ));
1203            Some(Ty::Record(fields))
1204        }
1205        "test" => {
1206            // Tiny assertion library (#proposed-stdlib). Each helper
1207            // returns Result[Unit, Str] so a test is itself a fn
1208            // returning Result. Callers compose suites in user code
1209            // (a List of (name, () -> Result[Unit, Str]) pairs +
1210            // list.fold to accumulate verdicts). Property generators
1211            // and a Rust-side Suite type are deferred to v2.
1212            let mut fields = IndexMap::new();
1213            // assert_eq[a, b] :: T -> T -> Result[Unit, Str]
1214            // (T constrained equal by unification on the two args)
1215            let unit_result = || Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]);
1216            fields.insert("assert_eq".into(), Ty::function(
1217                vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
1218            ));
1219            fields.insert("assert_ne".into(), Ty::function(
1220                vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
1221            ));
1222            fields.insert("assert_true".into(), Ty::function(
1223                vec![Ty::bool()], EffectSet::empty(), unit_result(),
1224            ));
1225            fields.insert("assert_false".into(), Ty::function(
1226                vec![Ty::bool()], EffectSet::empty(), unit_result(),
1227            ));
1228            Some(Ty::Record(fields))
1229        }
1230        "toml" => {
1231            // TOML config parser. Mirrors `std.json`'s shape: parse
1232            // is polymorphic so callers annotate the expected
1233            // record / list / scalar shape and the type checker
1234            // unifies. The parsed TOML maps to the same Lex Value
1235            // shape as JSON does:
1236            //
1237            //   TOML String   → Value::Str
1238            //   TOML Integer  → Value::Int
1239            //   TOML Float    → Value::Float
1240            //   TOML Boolean  → Value::Bool
1241            //   TOML Array    → Value::List
1242            //   TOML Table    → Value::Record
1243            //   TOML Datetime → Value::Str (RFC 3339, lossless)
1244            //
1245            // The Datetime → Str fallback is the one info-losing
1246            // step; callers who want a real `Instant` can pipe the
1247            // string through `datetime.parse_iso`.
1248            let mut fields = IndexMap::new();
1249            // parse :: Str -> Result[T, Str]
1250            fields.insert("parse".into(), Ty::function(
1251                vec![Ty::str()], EffectSet::empty(),
1252                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1253            ));
1254            // parse_strict :: (Str, List[Str]) -> Result[T, Str]
1255            // Tactical fix for #168 — caller passes the field
1256            // names T requires; runtime returns Err if any are
1257            // missing from the parsed table instead of letting
1258            // field access panic later. The full type-driven fix
1259            // (deriving `required` from T at type-check time so
1260            // plain `parse[T]` validates) is tracked in #168.
1261            fields.insert("parse_strict".into(), Ty::function(
1262                vec![Ty::str(), Ty::List(Box::new(Ty::str()))], EffectSet::empty(),
1263                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1264            ));
1265            // stringify :: T -> Result[Str, Str]
1266            // Returns Result (not Str) because not every Lex Value
1267            // has a TOML representation — top-level scalars,
1268            // closures, mixed-key maps etc. surface as Err rather
1269            // than panic.
1270            fields.insert("stringify".into(), Ty::function(
1271                vec![Ty::Var(0)], EffectSet::empty(),
1272                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1273            ));
1274            Some(Ty::Record(fields))
1275        }
1276        // `std.agent` (#184) — runtime primitives whose effects
1277        // separate (a) which LLM surface (`llm_local` vs
1278        // `llm_cloud`), (b) which peer protocol (`a2a`), and
1279        // (c) which tool boundary (`mcp`). The wire formats land
1280        // in downstream crates (`soft-agent`, `soft-a2a`) and
1281        // in #185 for MCP; what's typed here is the boundary
1282        // alone — agent code can be type-checked as
1283        // `[llm_local, a2a]` and will fail if it tries to reach
1284        // `[llm_cloud]` even before the wire layer is finished.
1285        "agent" => {
1286            let mut fields = IndexMap::new();
1287            // local_complete :: Str -> [llm_local] Result[Str, Str]
1288            fields.insert("local_complete".into(), Ty::function(
1289                vec![Ty::str()],
1290                EffectSet::singleton("llm_local"),
1291                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1292            ));
1293            // cloud_complete :: Str -> [llm_cloud] Result[Str, Str]
1294            fields.insert("cloud_complete".into(), Ty::function(
1295                vec![Ty::str()],
1296                EffectSet::singleton("llm_cloud"),
1297                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1298            ));
1299            // send_a2a :: (Str, Str) -> [a2a] Result[Str, Str]
1300            //              peer payload                   reply
1301            fields.insert("send_a2a".into(), Ty::function(
1302                vec![Ty::str(), Ty::str()],
1303                EffectSet::singleton("a2a"),
1304                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1305            ));
1306            // call_mcp :: (Str, Str, Str) -> [mcp] Result[Str, Str]
1307            //              server tool args_json         result_json
1308            fields.insert("call_mcp".into(), Ty::function(
1309                vec![Ty::str(), Ty::str(), Ty::str()],
1310                EffectSet::singleton("mcp"),
1311                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1312            ));
1313            Some(Ty::Record(fields))
1314        }
1315        _ => None,
1316    }
1317}
1318
1319/// Resolve `import "std.foo" as alias` to a module name (e.g. "io").
1320pub fn module_for_import(reference: &str) -> Option<&'static str> {
1321    let suffix = reference.strip_prefix("std.")?;
1322    Some(match suffix {
1323        "io" => "io",
1324        "str" => "str",
1325        "int" => "int",
1326        "float" => "float",
1327        "list" => "list",
1328        "result" => "result",
1329        "option" => "option",
1330        "json" => "json",
1331        "flow" => "flow",
1332        "tuple" => "tuple",
1333        "time" => "time",
1334        "rand" => "rand",
1335        "bytes" => "bytes",
1336        "net" => "net",
1337        "chat" => "chat",
1338        "math" => "math",
1339        "map" => "map",
1340        "set" => "set",
1341        "proc" => "proc",
1342        "crypto" => "crypto",
1343        "regex" => "regex",
1344        "deque" => "deque",
1345        "kv" => "kv",
1346        "sql" => "sql",
1347        "fs" => "fs",
1348        "process" => "process",
1349        "datetime" => "datetime",
1350        "log" => "log",
1351        "http" => "http",
1352        "toml" => "toml",
1353        "yaml" => "yaml",
1354        "dotenv" => "dotenv",
1355        "csv" => "csv",
1356        "test" => "test",
1357        "agent" => "agent",
1358        _ => return None,
1359    })
1360}