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] Unit
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            // io.readline() -> [io] Option[Str]
35            fields.insert("readline".into(), Ty::function(
36                vec![],
37                EffectSet::singleton("io"),
38                Ty::Con("Option".into(), vec![Ty::str()]),
39            ));
40            // io.argv() -> [io] List[Str]
41            fields.insert("argv".into(), Ty::function(
42                vec![],
43                EffectSet::singleton("io"),
44                Ty::List(Box::new(Ty::str())),
45            ));
46            Some(Ty::Record(fields))
47        }
48        "str" => {
49            let mut fields = IndexMap::new();
50            fields.insert("is_empty".into(), Ty::function(vec![Ty::str()], EffectSet::empty(), Ty::bool()));
51            fields.insert("to_int".into(), Ty::function(vec![Ty::str()], EffectSet::empty(),
52                Ty::Con("Option".into(), vec![Ty::int()])));
53            fields.insert("to_float".into(), Ty::function(vec![Ty::str()], EffectSet::empty(),
54                Ty::Con("Option".into(), vec![Ty::float()])));
55            fields.insert("concat".into(), Ty::function(vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
56            fields.insert("len".into(), Ty::function(vec![Ty::str()], EffectSet::empty(), Ty::int()));
57            // char_at :: (Str, Int) -> Str  — O(1) single-char access; "" if out of range.
58            fields.insert("char_at".into(), Ty::function(vec![Ty::str(), Ty::int()], EffectSet::empty(), Ty::str()));
59            fields.insert("split".into(), Ty::function(
60                vec![Ty::str(), Ty::str()],
61                EffectSet::empty(),
62                Ty::List(Box::new(Ty::str())),
63            ));
64            fields.insert("join".into(), Ty::function(
65                vec![Ty::List(Box::new(Ty::str())), Ty::str()],
66                EffectSet::empty(),
67                Ty::str(),
68            ));
69            // -- predicates --
70            for name in &["starts_with", "ends_with", "contains"] {
71                fields.insert((*name).into(), Ty::function(
72                    vec![Ty::str(), Ty::str()],
73                    EffectSet::empty(),
74                    Ty::bool(),
75                ));
76            }
77            // str.cmp :: (Str, Str) -> Int — -1 / 0 / 1, lex-byte order.
78            // Three-way comparator usable as a sort-by closure value;
79            // boolean comparisons stay on the `<`/`<=`/`>`/`>=` operators
80            // (which already work on Str via `bin_ord`), so `str.lt`
81            // etc. are deliberately not exposed — keep the surface
82            // minimal (#440).
83            fields.insert("cmp".into(), Ty::function(
84                vec![Ty::str(), Ty::str()],
85                EffectSet::empty(),
86                Ty::int(),
87            ));
88            // -- transformers --
89            fields.insert("replace".into(), Ty::function(
90                vec![Ty::str(), Ty::str(), Ty::str()],
91                EffectSet::empty(),
92                Ty::str(),
93            ));
94            for name in &["trim", "to_upper", "to_lower"] {
95                fields.insert((*name).into(), Ty::function(
96                    vec![Ty::str()], EffectSet::empty(), Ty::str(),
97                ));
98            }
99            for name in &["strip_prefix", "strip_suffix"] {
100                fields.insert((*name).into(), Ty::function(
101                    vec![Ty::str(), Ty::str()],
102                    EffectSet::empty(),
103                    Ty::Con("Option".into(), vec![Ty::str()]),
104                ));
105            }
106            // slice :: (Str, Int, Int) -> Str  — byte-range half-open
107            fields.insert("slice".into(), Ty::function(
108                vec![Ty::str(), Ty::int(), Ty::int()],
109                EffectSet::empty(),
110                Ty::str(),
111            ));
112            Some(Ty::Record(fields))
113        }
114        "int" => {
115            let mut fields = IndexMap::new();
116            fields.insert("to_str".into(), Ty::function(vec![Ty::int()], EffectSet::empty(), Ty::str()));
117            fields.insert("to_float".into(), Ty::function(vec![Ty::int()], EffectSet::empty(), Ty::float()));
118            // Int scalar helpers (#681). math.{min,max,abs} are Float-only;
119            // these are the integer equivalents so Int callers don't have to
120            // round-trip through Float (which is lossy for large ints).
121            fields.insert("abs".into(), Ty::function(vec![Ty::int()], EffectSet::empty(), Ty::int()));
122            for name in &["min", "max"] {
123                fields.insert((*name).into(), Ty::function(
124                    vec![Ty::int(), Ty::int()], EffectSet::empty(), Ty::int()));
125            }
126            Some(Ty::Record(fields))
127        }
128        "math" => {
129            let mut fields = IndexMap::new();
130            // Matrix is registered as a built-in type alias in
131            // TypeEnv::new_with_builtins; refer to it nominally so call
132            // sites unify against the user's `:: Matrix` annotations.
133            let mat = || Ty::Con("Matrix".into(), Vec::new());
134            // Scalar floats — single-arg `Float -> Float`.
135            for name in &[
136                "exp", "log", "log2", "log10", "sqrt", "abs",
137                "sin", "cos", "tan", "asin", "acos", "atan",
138                "floor", "ceil", "round", "trunc",
139            ] {
140                fields.insert((*name).into(), Ty::function(
141                    vec![Ty::float()], EffectSet::empty(), Ty::float(),
142                ));
143            }
144            // Two-arg `Float, Float -> Float`.
145            for name in &["pow", "atan2", "min", "max"] {
146                fields.insert((*name).into(), Ty::function(
147                    vec![Ty::float(), Ty::float()], EffectSet::empty(), Ty::float(),
148                ));
149            }
150            // Constructors.
151            fields.insert("zeros".into(), Ty::function(
152                vec![Ty::int(), Ty::int()], EffectSet::empty(), mat(),
153            ));
154            fields.insert("ones".into(), Ty::function(
155                vec![Ty::int(), Ty::int()], EffectSet::empty(), mat(),
156            ));
157            fields.insert("from_lists".into(), Ty::function(
158                vec![Ty::List(Box::new(Ty::List(Box::new(Ty::float()))))],
159                EffectSet::empty(),
160                mat(),
161            ));
162            fields.insert("from_flat".into(), Ty::function(
163                vec![Ty::int(), Ty::int(), Ty::List(Box::new(Ty::float()))],
164                EffectSet::empty(),
165                mat(),
166            ));
167            // Accessors.
168            fields.insert("rows".into(), Ty::function(vec![mat()], EffectSet::empty(), Ty::int()));
169            fields.insert("cols".into(), Ty::function(vec![mat()], EffectSet::empty(), Ty::int()));
170            fields.insert("get".into(), Ty::function(
171                vec![mat(), Ty::int(), Ty::int()], EffectSet::empty(), Ty::float(),
172            ));
173            fields.insert("to_flat".into(), Ty::function(
174                vec![mat()], EffectSet::empty(),
175                Ty::List(Box::new(Ty::float())),
176            ));
177            // Linalg ops.
178            fields.insert("transpose".into(), Ty::function(
179                vec![mat()], EffectSet::empty(), mat(),
180            ));
181            fields.insert("matmul".into(), Ty::function(
182                vec![mat(), mat()], EffectSet::empty(), mat(),
183            ));
184            fields.insert("scale".into(), Ty::function(
185                vec![Ty::float(), mat()], EffectSet::empty(), mat(),
186            ));
187            for name in &["add", "sub"] {
188                fields.insert((*name).into(), Ty::function(
189                    vec![mat(), mat()], EffectSet::empty(), mat(),
190                ));
191            }
192            fields.insert("sigmoid".into(), Ty::function(
193                vec![mat()], EffectSet::empty(), mat(),
194            ));
195            Some(Ty::Record(fields))
196        }
197        "float" => {
198            let mut fields = IndexMap::new();
199            fields.insert("to_int".into(), Ty::function(vec![Ty::float()], EffectSet::empty(), Ty::int()));
200            fields.insert("to_str".into(), Ty::function(vec![Ty::float()], EffectSet::empty(), Ty::str()));
201            Some(Ty::Record(fields))
202        }
203        "list" => {
204            // list polymorphic functions need fresh vars at use sites; we
205            // encode them with placeholder Var ids that get instantiated.
206            let mut fields = IndexMap::new();
207            // Effect polymorphism: each HOF carries an effect-row
208            // variable so an effectful closure (e.g. one that calls
209            // net.get inside list.map's lambda) propagates its
210            // effects to the result type. Spec §7.3.
211            //
212            // map :: [E] List[a], (a) -> [E] b -> [E] List[b]
213            fields.insert("map".into(), Ty::function(
214                vec![
215                    Ty::List(Box::new(Ty::Var(0))),
216                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
217                ],
218                EffectSet::open_var(2),
219                Ty::List(Box::new(Ty::Var(1))),
220            ));
221            // #305 slice 1: parallel map. Same signature shape as
222            // `map`; the runtime spawns OS threads (capped by
223            // LEX_PAR_MAX_CONCURRENCY) to apply the closure
224            // concurrently. Effect row stays open so a closure with
225            // declared effects still type-checks against
226            // par_map — though slice 1's runtime currently refuses
227            // effectful closures at execution (queued as slice 2).
228            fields.insert("par_map".into(), Ty::function(
229                vec![
230                    Ty::List(Box::new(Ty::Var(0))),
231                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(7), Ty::Var(1)),
232                ],
233                EffectSet::open_var(7),
234                Ty::List(Box::new(Ty::Var(1))),
235            ));
236            // #338: sort_by :: List[T], (T) -> [E] K -> [E] List[T]
237            // Stable sort by the key the closure derives from each
238            // element. K is intended to be one of Int / Float / Str
239            // (the runtime comparator falls back to equality for
240            // other shapes, preserving original order via the
241            // stable sort) but the type system doesn't enforce that
242            // — keep the signature minimal so callers can pass any
243            // K and trust the comparator.
244            fields.insert("sort_by".into(), Ty::function(
245                vec![
246                    Ty::List(Box::new(Ty::Var(0))),
247                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(8), Ty::Var(1)),
248                ],
249                EffectSet::open_var(8),
250                Ty::List(Box::new(Ty::Var(0))),
251            ));
252            fields.insert("filter".into(), Ty::function(
253                vec![
254                    Ty::List(Box::new(Ty::Var(0))),
255                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), Ty::bool()),
256                ],
257                EffectSet::open_var(3),
258                Ty::List(Box::new(Ty::Var(0))),
259            ));
260            fields.insert("fold".into(), Ty::function(
261                vec![
262                    Ty::List(Box::new(Ty::Var(0))),
263                    Ty::Var(1),
264                    Ty::function(vec![Ty::Var(1), Ty::Var(0)], EffectSet::open_var(4), Ty::Var(1)),
265                ],
266                EffectSet::open_var(4),
267                Ty::Var(1),
268            ));
269            fields.insert("len".into(), Ty::function(
270                vec![Ty::List(Box::new(Ty::Var(0)))],
271                EffectSet::empty(),
272                Ty::int(),
273            ));
274            fields.insert("is_empty".into(), Ty::function(
275                vec![Ty::List(Box::new(Ty::Var(0)))],
276                EffectSet::empty(),
277                Ty::bool(),
278            ));
279            fields.insert("range".into(), Ty::function(
280                vec![Ty::int(), Ty::int()],
281                EffectSet::empty(),
282                Ty::List(Box::new(Ty::int())),
283            ));
284            fields.insert("head".into(), Ty::function(
285                vec![Ty::List(Box::new(Ty::Var(0)))],
286                EffectSet::empty(),
287                Ty::Con("Option".into(), vec![Ty::Var(0)]),
288            ));
289            fields.insert("tail".into(), Ty::function(
290                vec![Ty::List(Box::new(Ty::Var(0)))],
291                EffectSet::empty(),
292                Ty::List(Box::new(Ty::Var(0))),
293            ));
294            fields.insert("concat".into(), Ty::function(
295                vec![Ty::List(Box::new(Ty::Var(0))), Ty::List(Box::new(Ty::Var(0)))],
296                EffectSet::empty(),
297                Ty::List(Box::new(Ty::Var(0))),
298            ));
299            // reverse :: List[T] -> List[T]
300            fields.insert("reverse".into(), Ty::function(
301                vec![Ty::List(Box::new(Ty::Var(0)))],
302                EffectSet::empty(),
303                Ty::List(Box::new(Ty::Var(0))),
304            ));
305            // #334: cons :: T, List[T] -> List[T]  — O(1)-amortised prepend.
306            fields.insert("cons".into(), Ty::function(
307                vec![Ty::Var(0), Ty::List(Box::new(Ty::Var(0)))],
308                EffectSet::empty(),
309                Ty::List(Box::new(Ty::Var(0))),
310            ));
311            // enumerate :: List[T] -> List[(Int, T)]
312            // Pairs each element with its zero-based index.
313            fields.insert("enumerate".into(), Ty::function(
314                vec![Ty::List(Box::new(Ty::Var(0)))],
315                EffectSet::empty(),
316                Ty::List(Box::new(Ty::Tuple(vec![Ty::int(), Ty::Var(0)]))),
317            ));
318            Some(Ty::Record(fields))
319        }
320        "bytes" => {
321            let mut fields = IndexMap::new();
322            fields.insert("len".into(), Ty::function(
323                vec![Ty::bytes()], EffectSet::empty(), Ty::int(),
324            ));
325            fields.insert("is_empty".into(), Ty::function(
326                vec![Ty::bytes()], EffectSet::empty(), Ty::bool(),
327            ));
328            fields.insert("eq".into(), Ty::function(
329                vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool(),
330            ));
331            fields.insert("from_str".into(), Ty::function(
332                vec![Ty::str()], EffectSet::empty(), Ty::bytes(),
333            ));
334            fields.insert("to_str".into(), Ty::function(
335                vec![Ty::bytes()], EffectSet::empty(),
336                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
337            ));
338            fields.insert("slice".into(), Ty::function(
339                vec![Ty::bytes(), Ty::int(), Ty::int()],
340                EffectSet::empty(), Ty::bytes(),
341            ));
342            Some(Ty::Record(fields))
343        }
344        "time" => {
345            // time.now() -> [time] Int — unix timestamp seconds.
346            // Reading the clock is an effect for two reasons: it's
347            // non-deterministic (replay needs the captured value) and
348            // it's a side-channel surface (see "Capability ≠
349            // correctness" on the landing page).
350            let mut fields = IndexMap::new();
351            fields.insert("now".into(), Ty::function(
352                vec![],
353                EffectSet::singleton("time"),
354                Ty::int(),
355            ));
356            // now_ms :: () -> [time] Int — unix milliseconds (#378).
357            // Resolution beyond what `time.now` (seconds) offers, for
358            // request-latency measurement / rate-limiter windows.
359            // Honors `LEX_TEST_NOW` for deterministic tests.
360            fields.insert("now_ms".into(), Ty::function(
361                vec![],
362                EffectSet::singleton("time"),
363                Ty::int(),
364            ));
365            // now_str :: () -> [time] Str — wall-clock instant rendered
366            // as an ISO-8601 / RFC 3339 string in UTC (#378). Suitable
367            // for auto-managed `created_at` / `updated_at` timestamps
368            // and structured log lines. Honors `LEX_TEST_NOW`.
369            fields.insert("now_str".into(), Ty::function(
370                vec![],
371                EffectSet::singleton("time"),
372                Ty::str(),
373            ));
374            // mono_ns :: () -> [time] Int — monotonic-clock nanoseconds
375            // since process start (#378). Use for *duration*
376            // measurement (`end - start`); the value carries no wall-
377            // clock meaning and the clock can never go backwards
378            // (unlike `time.now_ms` under NTP jitter). Not affected by
379            // `LEX_TEST_NOW` — pinning a monotonic clock would defeat
380            // its purpose; tests that need a fake monotonic clock
381            // should inject one through `EffectHandler`.
382            fields.insert("mono_ns".into(), Ty::function(
383                vec![],
384                EffectSet::singleton("time"),
385                Ty::int(),
386            ));
387            // sleep_ms :: Int -> [time] Unit (#226).
388            // Used internally by flow.retry_with_backoff for
389            // exponential-backoff delays; also available to user
390            // code under `--allow-effects time`.
391            fields.insert("sleep_ms".into(), Ty::function(
392                vec![Ty::int()],
393                EffectSet::singleton("time"),
394                Ty::Unit,
395            ));
396            // sleep :: Duration -> [time] Unit (#445).
397            // Duration-typed sleep — pairs with the
398            // `datetime.duration_seconds` / `duration_minutes` /
399            // `duration_days` constructors so periodic-task code
400            // expresses the period in units of meaning rather than
401            // raw milliseconds. Backed by `std::thread::sleep` at
402            // runtime — blocks the calling thread, which is the right
403            // semantics for the agent-driven workloads this exists
404            // for. Inside a `net.serve` worker the same caveat as
405            // `LEX_NET_INLINE_VM=1` applies (worker is stalled for `d`).
406            fields.insert("sleep".into(), Ty::function(
407                vec![Ty::Con("Duration".into(), vec![])],
408                EffectSet::singleton("time"),
409                Ty::Unit,
410            ));
411            Some(Ty::Record(fields))
412        }
413        "rand" => {
414            // rand.int_in(lo, hi) -> [random] Int — honest uniform draw in
415            // [lo, hi] (inclusive) from the OS RNG (#677). Carries the same
416            // `[random]` effect as `crypto.random` rather than a separate
417            // `rand` effect, so a reviewer auditing `--effect random` sees
418            // every non-deterministic draw in one place. For deterministic,
419            // replayable randomness thread a seed through `std.random`
420            // instead; for cryptographic strength use `crypto.random`.
421            let mut fields = IndexMap::new();
422            fields.insert("int_in".into(), Ty::function(
423                vec![Ty::int(), Ty::int()],
424                EffectSet::singleton("random"),
425                Ty::int(),
426            ));
427            Some(Ty::Record(fields))
428        }
429        "random" => {
430            // #219: pure, seeded RNG. The caller threads the `Rng`
431            // value through computations explicitly — there is no
432            // global state and no effect tag, because the seed is
433            // visible in the program's value flow and replay is
434            // therefore deterministic by construction.
435            //
436            // Backed at runtime by SplitMix64 (deterministic across
437            // platforms, single-u64 state). The proposal mentioned
438            // `rand_chacha` for cryptographic-strength bias, but the
439            // acceptance criterion is just "byte-identical sequence
440            // across platforms," and SplitMix64 satisfies that with
441            // a state shape that fits in `Value::Int` cleanly.
442            let rng_t = || Ty::Con("Rng".into(), vec![]);
443            let mut fields = IndexMap::new();
444            // seed :: Int -> Rng
445            fields.insert("seed".into(), Ty::function(
446                vec![Ty::int()], EffectSet::empty(), rng_t()));
447            // int :: Rng, Int, Int -> (Int, Rng)
448            // Uniform in [lo, hi] inclusive at both ends. Returns
449            // the drawn value and the advanced Rng.
450            fields.insert("int".into(), Ty::function(
451                vec![rng_t(), Ty::int(), Ty::int()],
452                EffectSet::empty(),
453                Ty::Tuple(vec![Ty::int(), rng_t()])));
454            // float :: Rng -> (Float, Rng)
455            // Uniform in [0.0, 1.0).
456            fields.insert("float".into(), Ty::function(
457                vec![rng_t()], EffectSet::empty(),
458                Ty::Tuple(vec![Ty::float(), rng_t()])));
459            // choose :: Rng, List[T] -> Option[(T, Rng)]
460            // Returns None if the list is empty.
461            fields.insert("choose".into(), Ty::function(
462                vec![rng_t(), Ty::List(Box::new(Ty::Var(0)))],
463                EffectSet::empty(),
464                Ty::Con("Option".into(), vec![
465                    Ty::Tuple(vec![Ty::Var(0), rng_t()]),
466                ]),
467            ));
468            Some(Ty::Record(fields))
469        }
470        "env" => {
471            // #216: env.get(name) -> [env] Option[Str].
472            // Per-var scoping (`[env(NAME)]`) lands with the
473            // per-capability effect parameterization work (#207); the
474            // flat `[env]` is the v1 surface.
475            let mut fields = IndexMap::new();
476            fields.insert("get".into(), Ty::function(
477                vec![Ty::str()],
478                EffectSet::singleton("env"),
479                Ty::Con("Option".into(), vec![Ty::str()]),
480            ));
481            Some(Ty::Record(fields))
482        }
483        "net" => {
484            let mut fields = IndexMap::new();
485            // get :: Str -> [net] Result[Str, Str]
486            fields.insert("get".into(), Ty::function(
487                vec![Ty::str()],
488                EffectSet::singleton("net"),
489                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
490            ));
491            fields.insert("post".into(), Ty::function(
492                vec![Ty::str(), Ty::str()],
493                EffectSet::singleton("net"),
494                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
495            ));
496            // serve :: (Int, Str) -> [net] Unit  (blocks; never returns
497            // under normal use). Handler's signature isn't carried in
498            // the type system here — looked up by name at runtime.
499            fields.insert("serve".into(), Ty::function(
500                vec![Ty::int(), Ty::str()],
501                EffectSet::singleton("net"),
502                Ty::Unit,
503            ));
504            // serve_tls :: (Int, Str, Str, Str) -> [net] Unit
505            //              port  cert  key   handler
506            // cert and key are filesystem paths to PEM-encoded files.
507            fields.insert("serve_tls".into(), Ty::function(
508                vec![Ty::int(), Ty::str(), Ty::str(), Ty::str()],
509                EffectSet::singleton("net"),
510                Ty::Unit,
511            ));
512            // serve_ws :: (Int, Str) -> [net] Unit
513            //             port  on_message_handler_name
514            // The handler is looked up by name at runtime.
515            fields.insert("serve_ws".into(), Ty::function(
516                vec![Ty::int(), Ty::str()],
517                EffectSet::singleton("net"),
518                Ty::Unit,
519            ));
520            // serve_ws_fn[Eff] :: (Int, Str, (WsConn, WsMessage) -> [Eff] WsAction)
521            //                      -> [net, Eff] Unit
522            // Effect-polymorphic WebSocket server that accepts a handler closure.
523            // The second argument is the subprotocol string for the
524            // Sec-WebSocket-Protocol handshake header ("" for none).
525            // open_var(0) propagates the handler's effect row to the call site.
526            fields.insert("serve_ws_fn".into(), Ty::function(
527                vec![
528                    Ty::int(),
529                    Ty::str(), // subprotocol
530                    Ty::function(
531                        vec![
532                            Ty::Con("WsConn".into(), vec![]),
533                            Ty::Con("WsMessage".into(), vec![]),
534                        ],
535                        EffectSet::open_var(0),
536                        Ty::Con("WsAction".into(), vec![]),
537                    ),
538                ],
539                EffectSet::open_var(0).union(&EffectSet::singleton("net")),
540                Ty::Unit,
541            ));
542            // serve_ws_fn_auth[Eff] :: (Int, Str,
543            //   (Str, List[{name :: Str, value :: Str}]) -> [Eff] Result[Unit, Str],
544            //   (WsConn, WsMessage) -> [Eff] WsAction)
545            //   -> [net, Eff] Unit
546            // Variant of serve_ws_fn that runs a pre-handshake auth
547            // callback against the upgrade request's path + headers.
548            // `Err(msg)` from the callback responds 401 Unauthorized
549            // and skips the WS upgrade entirely (#423). The auth and
550            // message-handler closures share the same effect row, so
551            // a caller using e.g. `[sql]` to look up a password hash
552            // in auth can use `[sql]` in subsequent handlers without
553            // duplicating the declaration.
554            let header_entry = || {
555                let mut fs = IndexMap::new();
556                fs.insert("name".into(),  Ty::str());
557                fs.insert("value".into(), Ty::str());
558                Ty::Record(fs)
559            };
560            fields.insert("serve_ws_fn_auth".into(), Ty::function(
561                vec![
562                    Ty::int(),
563                    Ty::str(), // subprotocol
564                    // auth callback: (path, headers) -> [Eff] Result[Unit, Str]
565                    Ty::function(
566                        vec![
567                            Ty::str(),
568                            Ty::List(Box::new(header_entry())),
569                        ],
570                        EffectSet::open_var(0),
571                        Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]),
572                    ),
573                    // on_message: same shape as serve_ws_fn
574                    Ty::function(
575                        vec![
576                            Ty::Con("WsConn".into(), vec![]),
577                            Ty::Con("WsMessage".into(), vec![]),
578                        ],
579                        EffectSet::open_var(0),
580                        Ty::Con("WsAction".into(), vec![]),
581                    ),
582                ],
583                EffectSet::open_var(0).union(&EffectSet::singleton("net")),
584                Ty::Unit,
585            ));
586            // serve_ws_fn_actor[Eff] ::
587            //   (Int, Str,
588            //    (WsConn) -> Str,                        # name_of, registry name
589            //    (WsConn, WsMessage) -> [Eff] WsAction)  # on_message
590            //   -> [net, concurrent, Eff] Unit
591            //
592            // Variant of serve_ws_fn that registers each accepted
593            // connection as a named actor in conc_registry. Non-WS
594            // callers can then `conc.lookup(name) |> conc.tell(frame)`
595            // to push outbound frames into the socket from arbitrary
596            // [concurrent]-tagged code (HTTP webhooks, scheduled tasks,
597            // broadcast loops). Documented in #459.
598            //
599            // name_of is intentionally pure: it inspects the WsConn
600            // record (id / path / subprotocol) and decides what
601            // name to register the connection under. Empty string
602            // means "don't register this connection" — `on_message`
603            // still runs but no outbound handle is exposed.
604            //
605            // The result row carries `concurrent` because the runtime
606            // registers an `ActorHandler::Native` bridge in the conc
607            // registry; lookups from non-WS callers are themselves
608            // `[concurrent]` effects.
609            fields.insert("serve_ws_fn_actor".into(), Ty::function(
610                vec![
611                    Ty::int(),
612                    Ty::str(), // subprotocol
613                    Ty::function(
614                        vec![Ty::Con("WsConn".into(), vec![])],
615                        EffectSet::empty(),
616                        Ty::str(),
617                    ),
618                    Ty::function(
619                        vec![
620                            Ty::Con("WsConn".into(), vec![]),
621                            Ty::Con("WsMessage".into(), vec![]),
622                        ],
623                        EffectSet::open_var(0),
624                        Ty::Con("WsAction".into(), vec![]),
625                    ),
626                ],
627                EffectSet::open_var(0)
628                    .union(&EffectSet::singleton("net"))
629                    .union(&EffectSet::singleton("concurrent")),
630                Ty::Unit,
631            ));
632            // dial_ws[Eff] :: (Str, Str, () -> [Eff] WsAction,
633            //                  (WsMessage) -> [Eff] WsAction)
634            //                  -> [net, Eff] Result[Unit, Str]
635            //
636            // WebSocket *client* — the inverse of serve_ws_fn (#390).
637            // Connects to `url` (ws:// or wss://) with the given
638            // subprotocol header, calls `on_open` once after the
639            // handshake completes, then loops invoking `on_message`
640            // for every inbound frame. Each callback returns a
641            // `WsAction` that gets applied to the socket — same enum
642            // as the server side, same semantics for `WsSend` /
643            // `WsSendBinary` / `WsNoOp`. open_var(0) propagates the
644            // handler effects so callers that touch [io], [time],
645            // [random] etc. inside their handlers see those propagate
646            // out of the dial_ws call.
647            //
648            // Returns `Result[Unit, Str]` rather than the bare `Unit`
649            // that serve_ws_fn returns: a dial can fail on connect
650            // (DNS, refused, bad TLS) or mid-stream (read error,
651            // unexpected close) and the caller usually wants to know.
652            fields.insert("dial_ws".into(), Ty::function(
653                vec![
654                    Ty::str(), // url (ws:// or wss://)
655                    Ty::str(), // subprotocol (Sec-WebSocket-Protocol)
656                    Ty::function(
657                        vec![],
658                        EffectSet::open_var(0),
659                        Ty::Con("WsAction".into(), vec![]),
660                    ),
661                    Ty::function(
662                        vec![Ty::Con("WsMessage".into(), vec![])],
663                        EffectSet::open_var(0),
664                        Ty::Con("WsAction".into(), vec![]),
665                    ),
666                ],
667                EffectSet::open_var(0).union(&EffectSet::singleton("net")),
668                Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]),
669            ));
670            // dial_ws_actor[Eff] :: (Str, Str, Str,
671            //                        () -> [Eff] WsAction,
672            //                        (WsMessage) -> [Eff] WsAction)
673            //                        -> [net, Eff] Result[Unit, Str]
674            //
675            // Variant of dial_ws that registers the outgoing connection in the
676            // conc registry under `name`. conc.tell(actor, frame_str) enqueues
677            // a frame for delivery, enabling proactive sends (heartbeats,
678            // meter values) from any other actor without changing the
679            // reactive on_message signature.
680            fields.insert("dial_ws_actor".into(), Ty::function(
681                vec![
682                    Ty::str(), // url
683                    Ty::str(), // subprotocol ("" for none)
684                    Ty::str(), // conc registry name ("" to skip registration)
685                    Ty::function(
686                        vec![],
687                        EffectSet::open_var(0),
688                        Ty::Con("WsAction".into(), vec![]),
689                    ),
690                    Ty::function(
691                        vec![Ty::Con("WsMessage".into(), vec![])],
692                        EffectSet::open_var(0),
693                        Ty::Con("WsAction".into(), vec![]),
694                    ),
695                ],
696                EffectSet::open_var(0).union(&EffectSet::singleton("net")),
697                Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]),
698            ));
699            // serve_fn[Eff] :: (Int, (Request) -> [Eff] Response) -> [net, Eff] Unit
700            // Effect-polymorphic variant of serve that accepts a first-class closure
701            // instead of a handler name. open_var(0) captures the handler's effect row
702            // so callers that invoke e.g. [io] effects inside the closure propagate them
703            // to the serve_fn call site.
704            fields.insert("serve_fn".into(), Ty::function(
705                vec![
706                    Ty::int(),
707                    Ty::function(
708                        vec![Ty::Con("Request".into(), vec![])],
709                        EffectSet::open_var(0),
710                        Ty::Con("Response".into(), vec![]),
711                    ),
712                ],
713                EffectSet::open_var(0).union(&EffectSet::singleton("net")),
714                Ty::Unit,
715            ));
716            // serve_routed[Eff] :: (
717            //     Int,
718            //     List[(Str, Str, (Request) -> [Eff] Response)],
719            //     (Request) -> [Eff] Response
720            //   ) -> [net, Eff] Unit
721            //
722            // Pattern-matched dispatch over `serve_fn`. Each route is a
723            // (method, path-pattern, handler) triple — method is an
724            // HTTP verb (case-insensitive) or "*" for any; path-patterns
725            // use `:name` segments (e.g. "/users/:id") and matched values
726            // are stamped onto `req.path_params` before the handler runs.
727            // Routes are tried in registration order; the first match
728            // wins. `fallback` runs when no route matches — typically a
729            // 404 responder. Same `open_var(0)` effect-row trick as
730            // `serve_fn` so handler effects propagate to the call site.
731            fields.insert("serve_routed".into(), Ty::function(
732                vec![
733                    Ty::int(),
734                    Ty::List(Box::new(Ty::Tuple(vec![
735                        Ty::str(),
736                        Ty::str(),
737                        Ty::function(
738                            vec![Ty::Con("Request".into(), vec![])],
739                            EffectSet::open_var(0),
740                            Ty::Con("Response".into(), vec![]),
741                        ),
742                    ]))),
743                    Ty::function(
744                        vec![Ty::Con("Request".into(), vec![])],
745                        EffectSet::open_var(0),
746                        Ty::Con("Response".into(), vec![]),
747                    ),
748                ],
749                EffectSet::open_var(0).union(&EffectSet::singleton("net")),
750                Ty::Unit,
751            ));
752
753            // ServeOpts is a structural record literal — callers build
754            // it with `{ http2: ..., inline_vm: ..., host: ... }`. Used
755            // by `serve_with` / `serve_fn_with` / `serve_routed_with`
756            // to replace the legacy LEX_NET_HTTP2 / LEX_NET_INLINE_VM
757            // env-var gates with a first-class, type-checked config.
758            // See lex-lang#497.
759            let serve_opts_t = || {
760                let mut fs = IndexMap::new();
761                fs.insert("http2".into(),     Ty::bool());
762                fs.insert("inline_vm".into(), Ty::bool());
763                fs.insert("host".into(),      Ty::str());
764                Ty::Record(fs)
765            };
766
767            // default_opts :: () -> ServeOpts
768            // Returns the same defaults the legacy serve* paths use —
769            // http2=false, inline_vm=false, host="0.0.0.0". Pure; the
770            // env-var fallback only applies on the legacy serve* path,
771            // not here.
772            fields.insert("default_opts".into(), Ty::function(
773                vec![],
774                EffectSet::empty(),
775                serve_opts_t(),
776            ));
777
778            // serve_with :: (Int, Str, ServeOpts) -> [net] Unit
779            fields.insert("serve_with".into(), Ty::function(
780                vec![Ty::int(), Ty::str(), serve_opts_t()],
781                EffectSet::singleton("net"),
782                Ty::Unit,
783            ));
784
785            // serve_fn_with[Eff] :: (Int, (Request) -> [Eff] Response, ServeOpts)
786            //                       -> [net, Eff] Unit
787            fields.insert("serve_fn_with".into(), Ty::function(
788                vec![
789                    Ty::int(),
790                    Ty::function(
791                        vec![Ty::Con("Request".into(), vec![])],
792                        EffectSet::open_var(0),
793                        Ty::Con("Response".into(), vec![]),
794                    ),
795                    serve_opts_t(),
796                ],
797                EffectSet::open_var(0).union(&EffectSet::singleton("net")),
798                Ty::Unit,
799            ));
800
801            // serve_routed_with[Eff] :: (
802            //   Int, List[(Str, Str, (Request) -> [Eff] Response)],
803            //   (Request) -> [Eff] Response, ServeOpts
804            // ) -> [net, Eff] Unit
805            fields.insert("serve_routed_with".into(), Ty::function(
806                vec![
807                    Ty::int(),
808                    Ty::List(Box::new(Ty::Tuple(vec![
809                        Ty::str(),
810                        Ty::str(),
811                        Ty::function(
812                            vec![Ty::Con("Request".into(), vec![])],
813                            EffectSet::open_var(0),
814                            Ty::Con("Response".into(), vec![]),
815                        ),
816                    ]))),
817                    Ty::function(
818                        vec![Ty::Con("Request".into(), vec![])],
819                        EffectSet::open_var(0),
820                        Ty::Con("Response".into(), vec![]),
821                    ),
822                    serve_opts_t(),
823                ],
824                EffectSet::open_var(0).union(&EffectSet::singleton("net")),
825                Ty::Unit,
826            ));
827
828            // serve_quic / serve_quic_fn / serve_quic_routed (#496).
829            // HTTP/3 over QUIC. TlsConfig is an opaque value built by
830            // `tls.from_pem_files` or `tls.self_signed` — it carries the
831            // server certificate chain + private key needed for the
832            // QUIC handshake (TLS is mandatory for HTTP/3). Effect row
833            // stays `[net]` for symmetry with `serve` / `serve_fn`;
834            // policy gates don't distinguish HTTP/1.1+2 (TCP) from
835            // HTTP/3 (UDP) at the effect level.
836            //
837            // serve_quic :: (Int, TlsConfig, Str) -> [net] Unit
838            fields.insert("serve_quic".into(), Ty::function(
839                vec![Ty::int(), Ty::Con("TlsConfig".into(), vec![]), Ty::str()],
840                EffectSet::singleton("net"),
841                Ty::Unit,
842            ));
843
844            // serve_quic_fn[Eff] :: (Int, TlsConfig,
845            //                        (Request) -> [Eff] Response)
846            //                       -> [net, Eff] Unit
847            fields.insert("serve_quic_fn".into(), Ty::function(
848                vec![
849                    Ty::int(),
850                    Ty::Con("TlsConfig".into(), vec![]),
851                    Ty::function(
852                        vec![Ty::Con("Request".into(), vec![])],
853                        EffectSet::open_var(0),
854                        Ty::Con("Response".into(), vec![]),
855                    ),
856                ],
857                EffectSet::open_var(0).union(&EffectSet::singleton("net")),
858                Ty::Unit,
859            ));
860
861            // serve_quic_routed[Eff] :: (
862            //   Int, TlsConfig,
863            //   List[(Str, Str, (Request) -> [Eff] Response)],
864            //   (Request) -> [Eff] Response
865            // ) -> [net, Eff] Unit
866            fields.insert("serve_quic_routed".into(), Ty::function(
867                vec![
868                    Ty::int(),
869                    Ty::Con("TlsConfig".into(), vec![]),
870                    Ty::List(Box::new(Ty::Tuple(vec![
871                        Ty::str(),
872                        Ty::str(),
873                        Ty::function(
874                            vec![Ty::Con("Request".into(), vec![])],
875                            EffectSet::open_var(0),
876                            Ty::Con("Response".into(), vec![]),
877                        ),
878                    ]))),
879                    Ty::function(
880                        vec![Ty::Con("Request".into(), vec![])],
881                        EffectSet::open_var(0),
882                        Ty::Con("Response".into(), vec![]),
883                    ),
884                ],
885                EffectSet::open_var(0).union(&EffectSet::singleton("net")),
886                Ty::Unit,
887            ));
888
889            Some(Ty::Record(fields))
890        }
891        // `tls` — TLS certificate handling for `net.serve_quic` (#496).
892        // `TlsConfig` is opaque to user code; the only ways to obtain
893        // one are these constructors. The runtime keeps the certificate
894        // chain + private key behind that opaque type so we can change
895        // the internal representation (record-of-bytes today, possibly
896        // a Resource handle tomorrow) without breaking source code.
897        "tls" => {
898            let mut fields = IndexMap::new();
899            // from_pem_files :: (Str, Str) -> [fs_read] Result[TlsConfig, Str]
900            //                    cert  key
901            // Load a PEM-encoded certificate chain + private key from
902            // disk. Both paths are read with the `[fs_read]` effect so
903            // policy gates can restrict where certs may come from.
904            fields.insert("from_pem_files".into(), Ty::function(
905                vec![Ty::str(), Ty::str()],
906                EffectSet::singleton("fs_read"),
907                Ty::Con("Result".into(), vec![
908                    Ty::Con("TlsConfig".into(), vec![]),
909                    Ty::str(),
910                ]),
911            ));
912            // self_signed :: Str -> Result[TlsConfig, Str]
913            // Generate a self-signed certificate for the given hostname
914            // (or "localhost"). Pure — no effects needed. Intended for
915            // local development and integration tests only; real
916            // deployments should use a CA-signed cert via from_pem_files.
917            fields.insert("self_signed".into(), Ty::function(
918                vec![Ty::str()],
919                EffectSet::empty(),
920                Ty::Con("Result".into(), vec![
921                    Ty::Con("TlsConfig".into(), vec![]),
922                    Ty::str(),
923                ]),
924            ));
925            Some(Ty::Record(fields))
926        }
927        "chat" => {
928            let mut fields = IndexMap::new();
929            fields.insert("broadcast".into(), Ty::function(
930                vec![Ty::str(), Ty::str()],
931                EffectSet::singleton("chat"),
932                Ty::Unit,
933            ));
934            fields.insert("send".into(), Ty::function(
935                vec![Ty::int(), Ty::str()],
936                EffectSet::singleton("chat"),
937                Ty::bool(),
938            ));
939            Some(Ty::Record(fields))
940        }
941        "conc" => {
942            // Actor model (#381). Effect: [concurrent].
943            // spawn :: S, (S, M) -> [E] (S, R) -> [concurrent] Actor[S]
944            // ask   :: Actor[S], M -> [concurrent] R
945            // tell  :: Actor[S], M -> [concurrent] Unit
946            //
947            // The type variables used here are fresh placeholders;
948            // the checker instantiates them at each call site.
949            //   0 = S (state), 1 = M (message), 2 = R (reply), 3 = E (effect row)
950            let actor_t = |s: Ty| Ty::Con("Actor".into(), vec![s]);
951            let mut fields = IndexMap::new();
952            // spawn :: S, (S, M -> [E] (S, R)) -> [concurrent] Actor[S]
953            fields.insert("spawn".into(), Ty::function(
954                vec![
955                    Ty::Var(0),
956                    Ty::Function {
957                        params: vec![Ty::Var(0), Ty::Var(1)],
958                        effects: EffectSet::open_var(3),
959                        ret: Box::new(Ty::Tuple(vec![Ty::Var(0), Ty::Var(2)])),
960                    },
961                ],
962                EffectSet::singleton("concurrent"),
963                actor_t(Ty::Var(0)),
964            ));
965            // ask :: Actor[S], M -> [concurrent] R
966            fields.insert("ask".into(), Ty::function(
967                vec![actor_t(Ty::Var(0)), Ty::Var(1)],
968                EffectSet::singleton("concurrent"),
969                Ty::Var(2),
970            ));
971            // tell :: Actor[S], M -> [concurrent] Unit
972            fields.insert("tell".into(), Ty::function(
973                vec![actor_t(Ty::Var(0)), Ty::Var(1)],
974                EffectSet::singleton("concurrent"),
975                Ty::Unit,
976            ));
977            // #444 — named-actor discovery within a process.
978            //
979            // register :: Actor[S], Str -> [concurrent] Result[Unit, ConcError]
980            //   Returns Err(AlreadyRegistered(name)) if the name is
981            //   taken — registration is exclusive so name collisions
982            //   surface at the source level, not as silent overwrites.
983            //
984            // lookup :: Str -> [concurrent] Option[Actor[S]]
985            //   Returns Some(actor) if registered, None otherwise. The
986            //   static `[S]` parametrisation isn't checked at runtime in
987            //   v1; the caller is responsible for matching the
988            //   registration site's type. SigId-tagged variant deferred —
989            //   see `conc_registry.rs` in lex-bytecode.
990            //
991            // unregister :: Str -> [concurrent] Result[Unit, ConcError]
992            //   Returns Err(NotRegistered(name)) if absent. Existing
993            //   `Actor[S]` handles held by callers continue to work
994            //   after unregistration; the cell is reclaimed when the
995            //   last handle drops.
996            //
997            // registered :: () -> [concurrent] List[Str]
998            //   Sorted snapshot of currently registered names. Debug /
999            //   introspection — not part of the steady-state agent flow.
1000            let conc_err = || Ty::Con("ConcError".into(), vec![]);
1001            let result_ce = |ok: Ty| Ty::Con("Result".into(), vec![ok, conc_err()]);
1002            fields.insert("register".into(), Ty::function(
1003                vec![actor_t(Ty::Var(0)), Ty::str()],
1004                EffectSet::singleton("concurrent"),
1005                result_ce(Ty::Unit),
1006            ));
1007            fields.insert("lookup".into(), Ty::function(
1008                vec![Ty::str()],
1009                EffectSet::singleton("concurrent"),
1010                Ty::Con("Option".into(), vec![actor_t(Ty::Var(0))]),
1011            ));
1012            fields.insert("unregister".into(), Ty::function(
1013                vec![Ty::str()],
1014                EffectSet::singleton("concurrent"),
1015                result_ce(Ty::Unit),
1016            ));
1017            fields.insert("registered".into(), Ty::function(
1018                vec![],
1019                EffectSet::singleton("concurrent"),
1020                Ty::List(Box::new(Ty::str())),
1021            ));
1022            Some(Ty::Record(fields))
1023        }
1024        "arrow" => {
1025            // Apache Arrow tables (#426). All ops are pure (no effects);
1026            // tables are immutable and conversions / reductions all run as
1027            // one Rust call over the flat buffer.
1028            //
1029            // `arrow.Table` is opaque from the type system's point of view —
1030            // the runtime variant `Value::ArrowTable` is the only producer
1031            // and consumer, so we model it as a 0-arity type constructor.
1032            let table = Ty::Con("Table".into(), vec![]);
1033            let str_t   = Ty::str();
1034            let int_t   = Ty::int();
1035            let float_t = Ty::float();
1036            let opt = |inner: Ty| Ty::Con("Option".into(), vec![inner]);
1037            let res = |ok: Ty| Ty::Con("Result".into(), vec![ok, Ty::str()]);
1038            let no_eff = EffectSet::empty();
1039
1040            let mut fields = IndexMap::new();
1041
1042            // -- constructors --
1043            // arrow.from_int_columns   :: List[(Str, List[Int])]   -> Result[Table, Str]
1044            // arrow.from_float_columns :: List[(Str, List[Float])] -> Result[Table, Str]
1045            // arrow.from_str_columns   :: List[(Str, List[Str])]   -> Result[Table, Str]
1046            for (name, elem) in [
1047                ("from_int_columns",   int_t.clone()),
1048                ("from_float_columns", float_t.clone()),
1049                ("from_str_columns",   str_t.clone()),
1050            ] {
1051                fields.insert(name.into(), Ty::function(
1052                    vec![Ty::List(Box::new(Ty::Tuple(vec![
1053                        str_t.clone(),
1054                        Ty::List(Box::new(elem)),
1055                    ])))],
1056                    no_eff.clone(),
1057                    res(table.clone()),
1058                ));
1059            }
1060
1061            // -- introspection --
1062            // arrow.nrows / arrow.ncols :: Table -> Int
1063            fields.insert("nrows".into(), Ty::function(
1064                vec![table.clone()], no_eff.clone(), int_t.clone()));
1065            fields.insert("ncols".into(), Ty::function(
1066                vec![table.clone()], no_eff.clone(), int_t.clone()));
1067            // arrow.col_names :: Table -> List[Str]
1068            fields.insert("col_names".into(), Ty::function(
1069                vec![table.clone()], no_eff.clone(),
1070                Ty::List(Box::new(str_t.clone()))));
1071            // arrow.col_type :: Table, Str -> Option[Str]
1072            fields.insert("col_type".into(), Ty::function(
1073                vec![table.clone(), str_t.clone()],
1074                no_eff.clone(), opt(str_t.clone())));
1075
1076            // -- column reductions --
1077            // arrow.col_sum_int   :: Table, Str -> Result[Int, Str]
1078            // arrow.col_sum_float :: Table, Str -> Result[Float, Str]
1079            // arrow.col_mean      :: Table, Str -> Result[Option[Float], Str]
1080            // arrow.col_min_int   :: Table, Str -> Result[Option[Int], Str]
1081            // arrow.col_max_int   :: Table, Str -> Result[Option[Int], Str]
1082            // arrow.col_count     :: Table, Str -> Result[Int, Str]
1083            for (name, ret_ok) in [
1084                ("col_sum_int",   int_t.clone()),
1085                ("col_sum_float", float_t.clone()),
1086                ("col_mean",      opt(float_t.clone())),
1087                ("col_min_int",   opt(int_t.clone())),
1088                ("col_max_int",   opt(int_t.clone())),
1089                ("col_count",     int_t.clone()),
1090            ] {
1091                fields.insert(name.into(), Ty::function(
1092                    vec![table.clone(), str_t.clone()],
1093                    no_eff.clone(), res(ret_ok)));
1094            }
1095
1096            // -- slicing --
1097            // arrow.head / tail :: Table, Int -> Table
1098            for name in &["head", "tail"] {
1099                fields.insert((*name).into(), Ty::function(
1100                    vec![table.clone(), int_t.clone()],
1101                    no_eff.clone(), table.clone()));
1102            }
1103            // arrow.slice :: Table, Int, Int -> Table
1104            fields.insert("slice".into(), Ty::function(
1105                vec![table.clone(), int_t.clone(), int_t.clone()],
1106                no_eff.clone(), table.clone()));
1107            // arrow.select_cols :: Table, List[Str] -> Result[Table, Str]
1108            fields.insert("select_cols".into(), Ty::function(
1109                vec![table.clone(), Ty::List(Box::new(str_t.clone()))],
1110                no_eff.clone(), res(table.clone())));
1111            // arrow.drop_col :: Table, Str -> Result[Table, Str]
1112            fields.insert("drop_col".into(), Ty::function(
1113                vec![table.clone(), str_t.clone()],
1114                no_eff.clone(), res(table.clone())));
1115
1116            // -- I/O (effect-gated) --
1117            // arrow.read_csv :: Str -> [fs_read] Result[Table, Str]
1118            // Header row required; schema inferred from the first 100 rows.
1119            // The `[fs_read]` effect surfaces in agent-tool policy gates
1120            // and `--allow-fs-read` per-path scoping, same as `io.read`.
1121            fields.insert("read_csv".into(), Ty::function(
1122                vec![str_t.clone()],
1123                EffectSet::singleton("fs_read"),
1124                res(table.clone())));
1125
1126            // arrow.read_parquet :: Str -> [fs_read] Result[Table, Str]
1127            // arrow.read_parquet_cols :: (Str, List[Str]) -> [fs_read] Result[Table, Str]
1128            // Same effect + path-scope rules as read_csv. _cols pushes
1129            // the projection into the Parquet reader (no decode of skipped
1130            // columns); missing column names surface as Err.
1131            fields.insert("read_parquet".into(), Ty::function(
1132                vec![str_t.clone()],
1133                EffectSet::singleton("fs_read"),
1134                res(table.clone())));
1135            fields.insert("read_parquet_cols".into(), Ty::function(
1136                vec![str_t.clone(), Ty::List(Box::new(str_t.clone()))],
1137                EffectSet::singleton("fs_read"),
1138                res(table.clone())));
1139
1140            // arrow.write_parquet :: (Table, Str) -> [fs_write] Result[Unit, Str]
1141            // arrow.write_csv     :: (Table, Str) -> [fs_write] Result[Unit, Str]
1142            // Path scope uses --allow-fs-write (symmetric with io.write).
1143            // Parquet default: Snappy compression, default page/row-group
1144            // sizes — sufficient for v1; a write_parquet_opts variant
1145            // can ride a later issue if knobs are needed.
1146            for name in &["write_parquet", "write_csv"] {
1147                fields.insert((*name).into(), Ty::function(
1148                    vec![table.clone(), str_t.clone()],
1149                    EffectSet::singleton("fs_write"),
1150                    res(Ty::Unit)));
1151            }
1152
1153            Some(Ty::Record(fields))
1154        }
1155        "df" => {
1156            // Polars-backed query ops over arrow.Table (#427). All pure
1157            // (no effects); the Polars DataFrame is internal plumbing,
1158            // never leaves the kernel.
1159            let table = Ty::Con("Table".into(), vec![]);
1160            let str_t = Ty::str();
1161            let int_t = Ty::int();
1162            let float_t = Ty::float();
1163            let bool_t = Ty::bool();
1164            let res = |ok: Ty| Ty::Con("Result".into(), vec![ok, Ty::str()]);
1165            let no_eff = EffectSet::empty();
1166
1167            let mut fields = IndexMap::new();
1168
1169            // df.filter_{eq,gt,lt}_int :: Table, Str, Int -> Result[Table, Str]
1170            for name in &["filter_eq_int", "filter_gt_int", "filter_lt_int"] {
1171                fields.insert((*name).into(), Ty::function(
1172                    vec![table.clone(), str_t.clone(), int_t.clone()],
1173                    no_eff.clone(), res(table.clone())));
1174            }
1175
1176            // #433 — string filters.
1177            // df.filter_eq_str  :: Table, Str, Str       -> Result[Table, Str]
1178            // df.filter_in_str  :: Table, Str, List[Str] -> Result[Table, Str]
1179            fields.insert("filter_eq_str".into(), Ty::function(
1180                vec![table.clone(), str_t.clone(), str_t.clone()],
1181                no_eff.clone(), res(table.clone())));
1182            fields.insert("filter_in_str".into(), Ty::function(
1183                vec![table.clone(), str_t.clone(), Ty::List(Box::new(str_t.clone()))],
1184                no_eff.clone(), res(table.clone())));
1185
1186            // #433 — float filters.
1187            // df.filter_{eq,lt,gt}_float :: Table, Str, Float -> Result[Table, Str]
1188            for name in &["filter_eq_float", "filter_lt_float", "filter_gt_float"] {
1189                fields.insert((*name).into(), Ty::function(
1190                    vec![table.clone(), str_t.clone(), float_t.clone()],
1191                    no_eff.clone(), res(table.clone())));
1192            }
1193
1194            // #433 — null handling.
1195            // df.filter_isnull  :: Table, Str       -> Result[Table, Str]
1196            // df.filter_notnull :: Table, Str       -> Result[Table, Str]
1197            // df.drop_nulls     :: Table, List[Str] -> Result[Table, Str]
1198            for name in &["filter_isnull", "filter_notnull"] {
1199                fields.insert((*name).into(), Ty::function(
1200                    vec![table.clone(), str_t.clone()],
1201                    no_eff.clone(), res(table.clone())));
1202            }
1203            fields.insert("drop_nulls".into(), Ty::function(
1204                vec![table.clone(), Ty::List(Box::new(str_t.clone()))],
1205                no_eff.clone(), res(table.clone())));
1206
1207            // df.sort_by :: Table, Str, Bool -> Result[Table, Str]
1208            fields.insert("sort_by".into(), Ty::function(
1209                vec![table.clone(), str_t.clone(), bool_t.clone()],
1210                no_eff.clone(), res(table.clone())));
1211
1212            // df.group_by_agg :: Table, List[Str], List[(Str, Str, Str)]
1213            //                    -> Result[Table, Str]
1214            // Spec tuple is (out_col, in_col, op). op ∈
1215            // "sum"|"mean"|"min"|"max"|"count"|"n_distinct".
1216            fields.insert("group_by_agg".into(), Ty::function(
1217                vec![
1218                    table.clone(),
1219                    Ty::List(Box::new(str_t.clone())),
1220                    Ty::List(Box::new(Ty::Tuple(vec![
1221                        str_t.clone(), str_t.clone(), str_t.clone(),
1222                    ]))),
1223                ],
1224                no_eff.clone(), res(table.clone())));
1225
1226            // df.inner_join / left_join :: Table, Table, Str -> Result[Table, Str]
1227            for name in &["inner_join", "left_join"] {
1228                fields.insert((*name).into(), Ty::function(
1229                    vec![table.clone(), table.clone(), str_t.clone()],
1230                    no_eff.clone(), res(table.clone())));
1231            }
1232
1233            Some(Ty::Record(fields))
1234        }
1235        // `std.proc` was removed in favour of `std.process` (#678): its
1236        // single op `proc.spawn(cmd, args)` was byte-for-byte equivalent to
1237        // `process.run(cmd, args)` — same `[proc]` effect, same
1238        // `{ stdout, stderr, exit_code }` result — and `std.process` is a
1239        // strict superset (streaming spawn / read / wait / kill). Callers
1240        // migrate `proc.spawn` → `process.run`.
1241        "json" => {
1242            let mut fields = IndexMap::new();
1243            // stringify :: T -> Str  (polymorphic on input)
1244            fields.insert("stringify".into(), Ty::function(
1245                vec![Ty::Var(0)], EffectSet::empty(), Ty::str(),
1246            ));
1247            // parse :: Str -> Result[T, Str]
1248            fields.insert("parse".into(), Ty::function(
1249                vec![Ty::str()], EffectSet::empty(),
1250                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1251            ));
1252            // parse_strict :: (Str, List[Str]) -> Result[T, Str]
1253            // Tactical fix for #168 — caller passes the field names
1254            // T requires; runtime returns Err if any are missing
1255            // from the parsed object instead of letting field
1256            // access panic later.
1257            fields.insert("parse_strict".into(), Ty::function(
1258                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
1259                EffectSet::empty(),
1260                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1261            ));
1262            Some(Ty::Record(fields))
1263        }
1264        "result" => {
1265            let mut fields = IndexMap::new();
1266            // result.map :: Result[T, E], (T) -> [E2] U -> [E2] Result[U, E]
1267            // Effect-polymorphic on the closure: result.map et al.
1268            // propagate the closure's effects to the surrounding call.
1269            fields.insert("map".into(), Ty::function(
1270                vec![
1271                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1272                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), Ty::Var(2)),
1273                ],
1274                EffectSet::open_var(3),
1275                Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
1276            ));
1277            fields.insert("and_then".into(), Ty::function(
1278                vec![
1279                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1280                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(4),
1281                        Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)])),
1282                ],
1283                EffectSet::open_var(4),
1284                Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
1285            ));
1286            fields.insert("map_err".into(), Ty::function(
1287                vec![
1288                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1289                    Ty::function(vec![Ty::Var(1)], EffectSet::open_var(5), Ty::Var(2)),
1290                ],
1291                EffectSet::open_var(5),
1292                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
1293            ));
1294            // result.or_else :: Result[T, E1], (E1) -> [E] Result[T, E2]
1295            //                                    -> [E] Result[T, E2]
1296            // Recovery combinator: closure runs only on Err and returns
1297            // the next Result (which itself may swap the error type).
1298            fields.insert("or_else".into(), Ty::function(
1299                vec![
1300                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1301                    Ty::function(vec![Ty::Var(1)], EffectSet::open_var(6),
1302                        Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)])),
1303                ],
1304                EffectSet::open_var(6),
1305                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
1306            ));
1307            // result.unwrap_or :: Result[T, E], T -> T
1308            // Eager fallback — the Ok payload, or the supplied default on
1309            // Err. Mirrors option.unwrap_or (#679).
1310            fields.insert("unwrap_or".into(), Ty::function(
1311                vec![Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]), Ty::Var(0)],
1312                EffectSet::empty(),
1313                Ty::Var(0),
1314            ));
1315            // result.unwrap_or_else :: Result[T, E], (E) -> [Eff] T -> [Eff] T
1316            // Lazy fallback — the closure runs only on Err and receives the
1317            // error payload (effect-polymorphic on the closure). Mirrors
1318            // option.unwrap_or_else (#679).
1319            fields.insert("unwrap_or_else".into(), Ty::function(
1320                vec![
1321                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1322                    Ty::function(vec![Ty::Var(1)], EffectSet::open_var(7), Ty::Var(0)),
1323                ],
1324                EffectSet::open_var(7),
1325                Ty::Var(0),
1326            ));
1327            // result.is_ok / is_err :: Result[T, E] -> Bool (#679)
1328            fields.insert("is_ok".into(), Ty::function(
1329                vec![Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)])],
1330                EffectSet::empty(), Ty::bool()));
1331            fields.insert("is_err".into(), Ty::function(
1332                vec![Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)])],
1333                EffectSet::empty(), Ty::bool()));
1334            Some(Ty::Record(fields))
1335        }
1336        "option" => {
1337            let mut fields = IndexMap::new();
1338            // option.map :: Option[T], (T) -> [E] U -> [E] Option[U]
1339            fields.insert("map".into(), Ty::function(
1340                vec![
1341                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
1342                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1343                ],
1344                EffectSet::open_var(2),
1345                Ty::Con("Option".into(), vec![Ty::Var(1)]),
1346            ));
1347            // option.and_then :: Option[T], (T) -> [E] Option[U] -> [E] Option[U]
1348            // The compiler entry has been wired since the result/option
1349            // variant_map work landed; this signature was missed,
1350            // making the call fail to type-check until now.
1351            fields.insert("and_then".into(), Ty::function(
1352                vec![
1353                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
1354                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
1355                        Ty::Con("Option".into(), vec![Ty::Var(1)])),
1356                ],
1357                EffectSet::open_var(3),
1358                Ty::Con("Option".into(), vec![Ty::Var(1)]),
1359            ));
1360            fields.insert("unwrap_or".into(), Ty::function(
1361                vec![Ty::Con("Option".into(), vec![Ty::Var(0)]), Ty::Var(0)],
1362                EffectSet::empty(),
1363                Ty::Var(0),
1364            ));
1365            // option.unwrap_or_else :: Option[T], () -> [E] T -> [E] T
1366            // Lazy variant of unwrap_or: the default is computed by a closure
1367            // only when the value is None (effect-polymorphic on the closure).
1368            fields.insert("unwrap_or_else".into(), Ty::function(
1369                vec![
1370                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
1371                    Ty::function(vec![], EffectSet::open_var(5), Ty::Var(0)),
1372                ],
1373                EffectSet::open_var(5),
1374                Ty::Var(0),
1375            ));
1376            // option.or_else :: Option[T], () -> [E] Option[T] -> [E] Option[T]
1377            // The closure takes no arguments because None has no payload to pass.
1378            fields.insert("or_else".into(), Ty::function(
1379                vec![
1380                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
1381                    Ty::function(vec![], EffectSet::open_var(4),
1382                        Ty::Con("Option".into(), vec![Ty::Var(0)])),
1383                ],
1384                EffectSet::open_var(4),
1385                Ty::Con("Option".into(), vec![Ty::Var(0)]),
1386            ));
1387            // option.is_some / is_none :: Option[T] -> Bool (#679)
1388            fields.insert("is_some".into(), Ty::function(
1389                vec![Ty::Con("Option".into(), vec![Ty::Var(0)])],
1390                EffectSet::empty(), Ty::bool()));
1391            fields.insert("is_none".into(), Ty::function(
1392                vec![Ty::Con("Option".into(), vec![Ty::Var(0)])],
1393                EffectSet::empty(), Ty::bool()));
1394            // option.ok_or :: Option[T], E -> Result[T, E]
1395            // Cross from Option into Result, supplying the error for None (#679).
1396            fields.insert("ok_or".into(), Ty::function(
1397                vec![Ty::Con("Option".into(), vec![Ty::Var(0)]), Ty::Var(1)],
1398                EffectSet::empty(),
1399                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1400            ));
1401            Some(Ty::Record(fields))
1402        }
1403        "tuple" => {
1404            // Tuple accessors per §11.1. Polymorphic in the tuple's
1405            // element types; we use the same row-variable shape used
1406            // by list helpers. Tuples are heterogeneous, so each
1407            // accessor is statically typed via independent type
1408            // variables for each position.
1409            let mut fields = IndexMap::new();
1410            // fst :: (T0, T1) -> T0
1411            fields.insert("fst".into(), Ty::function(
1412                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
1413                EffectSet::empty(),
1414                Ty::Var(0),
1415            ));
1416            // snd :: (T0, T1) -> T1
1417            fields.insert("snd".into(), Ty::function(
1418                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
1419                EffectSet::empty(),
1420                Ty::Var(1),
1421            ));
1422            // third :: (T0, T1, T2) -> T2
1423            fields.insert("third".into(), Ty::function(
1424                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1), Ty::Var(2)])],
1425                EffectSet::empty(),
1426                Ty::Var(2),
1427            ));
1428            // len :: (T0, T1) -> Int  (covers any pair shape; Int back)
1429            fields.insert("len".into(), Ty::function(
1430                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
1431                EffectSet::empty(),
1432                Ty::int(),
1433            ));
1434            Some(Ty::Record(fields))
1435        }
1436        "map" => {
1437            // Persistent map. Keys are `Str` or `Int` only — Lex's
1438            // type system tracks them polymorphically as Var(0)
1439            // ("K") and lets the runtime check the key shape; both
1440            // cases fit into `MapKey`.
1441            //
1442            // Type variables: 0 = K, 1 = V.
1443            let mt   = || Ty::Con("Map".into(), vec![Ty::Var(0), Ty::Var(1)]);
1444            let pair = || Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]);
1445            let mut fields = IndexMap::new();
1446            // new :: () -> Map[K, V]
1447            fields.insert("new".into(), Ty::function(
1448                vec![], EffectSet::empty(), mt()));
1449            // size :: Map[K, V] -> Int
1450            fields.insert("size".into(), Ty::function(
1451                vec![mt()], EffectSet::empty(), Ty::int()));
1452            // has :: Map[K, V], K -> Bool
1453            fields.insert("has".into(), Ty::function(
1454                vec![mt(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
1455            // get :: Map[K, V], K -> Option[V]
1456            fields.insert("get".into(), Ty::function(
1457                vec![mt(), Ty::Var(0)], EffectSet::empty(),
1458                Ty::Con("Option".into(), vec![Ty::Var(1)])));
1459            // set :: Map[K, V], K, V -> Map[K, V]
1460            fields.insert("set".into(), Ty::function(
1461                vec![mt(), Ty::Var(0), Ty::Var(1)],
1462                EffectSet::empty(), mt()));
1463            // delete :: Map[K, V], K -> Map[K, V]
1464            fields.insert("delete".into(), Ty::function(
1465                vec![mt(), Ty::Var(0)], EffectSet::empty(), mt()));
1466            // keys :: Map[K, V] -> List[K]
1467            fields.insert("keys".into(), Ty::function(
1468                vec![mt()], EffectSet::empty(),
1469                Ty::List(Box::new(Ty::Var(0)))));
1470            // values :: Map[K, V] -> List[V]
1471            fields.insert("values".into(), Ty::function(
1472                vec![mt()], EffectSet::empty(),
1473                Ty::List(Box::new(Ty::Var(1)))));
1474            // entries :: Map[K, V] -> List[(K, V)]
1475            fields.insert("entries".into(), Ty::function(
1476                vec![mt()], EffectSet::empty(),
1477                Ty::List(Box::new(pair()))));
1478            // from_list :: List[(K, V)] -> Map[K, V]
1479            fields.insert("from_list".into(), Ty::function(
1480                vec![Ty::List(Box::new(pair()))],
1481                EffectSet::empty(), mt()));
1482            // merge :: Map[K, V], Map[K, V] -> Map[K, V]   (b overrides a)
1483            fields.insert("merge".into(), Ty::function(
1484                vec![mt(), mt()], EffectSet::empty(), mt()));
1485            // is_empty :: Map[K, V] -> Bool
1486            fields.insert("is_empty".into(), Ty::function(
1487                vec![mt()], EffectSet::empty(), Ty::bool()));
1488            // fold :: Map[K, V], A, (A, K, V) -> [E] A -> [E] A
1489            // Iteration order matches `map.entries` (BTreeMap-sorted by
1490            // key). Effect-polymorphic on the combiner like `list.fold`.
1491            // Type variable 2 = A (accumulator), effect row 3.
1492            fields.insert("fold".into(), Ty::function(
1493                vec![
1494                    mt(),
1495                    Ty::Var(2),
1496                    Ty::function(
1497                        vec![Ty::Var(2), Ty::Var(0), Ty::Var(1)],
1498                        EffectSet::open_var(3),
1499                        Ty::Var(2),
1500                    ),
1501                ],
1502                EffectSet::open_var(3),
1503                Ty::Var(2),
1504            ));
1505            Some(Ty::Record(fields))
1506        }
1507        "set" => {
1508            // Persistent set with the same key-type discipline as map.
1509            // Type variable: 0 = T (the element type, also the key type).
1510            let st   = || Ty::Con("Set".into(), vec![Ty::Var(0)]);
1511            let mut fields = IndexMap::new();
1512            // new :: () -> Set[T]
1513            fields.insert("new".into(), Ty::function(
1514                vec![], EffectSet::empty(), st()));
1515            // size :: Set[T] -> Int
1516            fields.insert("size".into(), Ty::function(
1517                vec![st()], EffectSet::empty(), Ty::int()));
1518            // has :: Set[T], T -> Bool
1519            fields.insert("has".into(), Ty::function(
1520                vec![st(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
1521            // add :: Set[T], T -> Set[T]
1522            fields.insert("add".into(), Ty::function(
1523                vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
1524            // delete :: Set[T], T -> Set[T]
1525            fields.insert("delete".into(), Ty::function(
1526                vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
1527            // to_list :: Set[T] -> List[T]
1528            fields.insert("to_list".into(), Ty::function(
1529                vec![st()], EffectSet::empty(),
1530                Ty::List(Box::new(Ty::Var(0)))));
1531            // from_list :: List[T] -> Set[T]
1532            fields.insert("from_list".into(), Ty::function(
1533                vec![Ty::List(Box::new(Ty::Var(0)))],
1534                EffectSet::empty(), st()));
1535            // union :: Set[T], Set[T] -> Set[T]
1536            fields.insert("union".into(), Ty::function(
1537                vec![st(), st()], EffectSet::empty(), st()));
1538            // intersect :: Set[T], Set[T] -> Set[T]
1539            fields.insert("intersect".into(), Ty::function(
1540                vec![st(), st()], EffectSet::empty(), st()));
1541            // diff :: Set[T], Set[T] -> Set[T]
1542            fields.insert("diff".into(), Ty::function(
1543                vec![st(), st()], EffectSet::empty(), st()));
1544            // is_empty :: Set[T] -> Bool
1545            fields.insert("is_empty".into(), Ty::function(
1546                vec![st()], EffectSet::empty(), Ty::bool()));
1547            // is_subset :: Set[T], Set[T] -> Bool   (a is subset of b)
1548            fields.insert("is_subset".into(), Ty::function(
1549                vec![st(), st()], EffectSet::empty(), Ty::bool()));
1550            Some(Ty::Record(fields))
1551        }
1552        "iter" => {
1553            // Positional iterator (#364) + lazy variant via `iter.unfold`
1554            // (#376). Internal value shapes are `__IterEager(list, idx)` or
1555            // `__IterLazy(seed, step)`; all operations compile-inline and
1556            // dispatch on the variant tag at runtime.
1557            // Type var slots: 0 = T (element), 1 = U (mapped element) /
1558            // A (fold acc), 2 = S (unfold seed).
1559            let it = |n: u32| Ty::Con("Iter".into(), vec![Ty::Var(n)]);
1560            let mut fields = IndexMap::new();
1561            // from_list :: List[T] -> Iter[T]
1562            fields.insert("from_list".into(), Ty::function(
1563                vec![Ty::List(Box::new(Ty::Var(0)))],
1564                EffectSet::empty(), it(0)));
1565            // unfold[S, T] :: S, (S) -> Option[(T, S)] -> Iter[T] (#376)
1566            // The step closure may carry any effect row; the iterator
1567            // itself stays effect-free since the effects only fire when
1568            // the step is invoked via `iter.next` / `iter.to_list`.
1569            fields.insert("unfold".into(), Ty::function(
1570                vec![
1571                    Ty::Var(2), // seed S
1572                    Ty::function(
1573                        vec![Ty::Var(2)],
1574                        EffectSet::open_var(3),
1575                        Ty::Con("Option".into(), vec![
1576                            Ty::Tuple(vec![Ty::Var(0), Ty::Var(2)])
1577                        ]),
1578                    ),
1579                ],
1580                EffectSet::empty(), it(0)));
1581            // next :: Iter[T] -> Option[(T, Iter[T])]
1582            fields.insert("next".into(), Ty::function(
1583                vec![it(0)],
1584                EffectSet::empty(),
1585                Ty::Con("Option".into(), vec![
1586                    Ty::Tuple(vec![Ty::Var(0), it(0)])
1587                ])));
1588            // is_empty :: Iter[T] -> Bool
1589            fields.insert("is_empty".into(), Ty::function(
1590                vec![it(0)], EffectSet::empty(), Ty::bool()));
1591            // count :: Iter[T] -> Int   (remaining elements)
1592            fields.insert("count".into(), Ty::function(
1593                vec![it(0)], EffectSet::empty(), Ty::int()));
1594            // take :: Iter[T], Int -> Iter[T]
1595            fields.insert("take".into(), Ty::function(
1596                vec![it(0), Ty::int()], EffectSet::empty(), it(0)));
1597            // skip :: Iter[T], Int -> Iter[T]
1598            fields.insert("skip".into(), Ty::function(
1599                vec![it(0), Ty::int()], EffectSet::empty(), it(0)));
1600            // to_list :: Iter[T] -> List[T]
1601            fields.insert("to_list".into(), Ty::function(
1602                vec![it(0)], EffectSet::empty(),
1603                Ty::List(Box::new(Ty::Var(0)))));
1604            // collect :: Iter[T] -> List[T] — alias for `to_list`
1605            // (matches Rust / Python / Kotlin naming so call sites
1606            // coming from those languages don't have to re-learn).
1607            fields.insert("collect".into(), Ty::function(
1608                vec![it(0)], EffectSet::empty(),
1609                Ty::List(Box::new(Ty::Var(0)))));
1610            // map :: [E] Iter[T], (T) -> [E] U -> [E] Iter[U]
1611            fields.insert("map".into(), Ty::function(
1612                vec![
1613                    it(0),
1614                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1615                ],
1616                EffectSet::open_var(2), it(1)));
1617            // filter :: [E] Iter[T], (T) -> [E] Bool -> [E] Iter[T]
1618            fields.insert("filter".into(), Ty::function(
1619                vec![
1620                    it(0),
1621                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(1), Ty::bool()),
1622                ],
1623                EffectSet::open_var(1), it(0)));
1624            // fold :: [E] Iter[T], A, (A, T) -> [E] A -> [E] A
1625            fields.insert("fold".into(), Ty::function(
1626                vec![
1627                    it(0),
1628                    Ty::Var(1),
1629                    Ty::function(vec![Ty::Var(1), Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1630                ],
1631                EffectSet::open_var(2), Ty::Var(1)));
1632            Some(Ty::Record(fields))
1633        }
1634        "flow" => {
1635            // Orchestration primitives (spec §11.2). Each takes one or
1636            // more closures and returns a closure with a derived shape.
1637            let mut fields = IndexMap::new();
1638            // sequential[T, U, V](f: (T) -> U, g: (U) -> V) -> (T) -> V
1639            fields.insert("sequential".into(), Ty::function(
1640                vec![
1641                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1642                    Ty::function(vec![Ty::Var(1)], EffectSet::empty(), Ty::Var(2)),
1643                ],
1644                EffectSet::empty(),
1645                Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(2)),
1646            ));
1647            // branch[T, U](cond: (T) -> Bool, t: (T) -> U, f: (T) -> U) -> (T) -> U
1648            fields.insert("branch".into(), Ty::function(
1649                vec![
1650                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::bool()),
1651                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1652                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1653                ],
1654                EffectSet::empty(),
1655                Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1656            ));
1657            // retry[T, U, E, Eff](
1658            //   f: (T) -> [Eff] Result[U, E], n: Int
1659            // ) -> (T) -> [Eff] Result[U, E]
1660            // open_var(3) is the effect row carried by `f`; the
1661            // combinator itself is pure, so the outer EffectSet is
1662            // empty. The returned closure propagates Eff unchanged.
1663            let result_ty = Ty::Con("Result".into(), vec![Ty::Var(1), Ty::Var(2)]);
1664            fields.insert("retry".into(), Ty::function(
1665                vec![
1666                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
1667                    Ty::int(),
1668                ],
1669                EffectSet::empty(),
1670                Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
1671            ));
1672            // retry_with_backoff[T, U, E, Eff](
1673            //   f: (T) -> [Eff] Result[U, E], attempts: Int, base_ms: Int,
1674            // ) -> (T) -> [Eff, time] Result[U, E]
1675            // Same retry shape as `flow.retry` plus an exponential
1676            // backoff between attempts. The result function carries
1677            // `[time]` (from `time.sleep_ms`) unioned with the inner
1678            // closure's effect row Eff, so e.g. a `[net]` closure
1679            // produces a `[net, time]` result function. (#226)
1680            fields.insert("retry_with_backoff".into(), Ty::function(
1681                vec![
1682                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
1683                    Ty::int(),
1684                    Ty::int(),
1685                ],
1686                EffectSet::empty(),
1687                Ty::function(vec![Ty::Var(0)],
1688                    EffectSet::open_var(3).union(&EffectSet::singleton("time")), result_ty),
1689            ));
1690            // parallel[A, B](fa: () -> A, fb: () -> B) -> () -> (A, B)
1691            // Sequential implementation today; spec §11.2 reserves the
1692            // option of a true-threaded scheduler. parallel_record is
1693            // listed in the spec but not yet implemented — it needs row
1694            // polymorphism over the input record's fields plus a
1695            // record-iteration trampoline; tracked as follow-up.
1696            fields.insert("parallel".into(), Ty::function(
1697                vec![
1698                    Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
1699                    Ty::function(vec![], EffectSet::empty(), Ty::Var(1)),
1700                ],
1701                EffectSet::empty(),
1702                Ty::function(vec![], EffectSet::empty(),
1703                    Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])),
1704            ));
1705            // parallel_list[T](actions: List[() -> T]) -> List[T]
1706            // Variadic counterpart to `parallel`. Runs each action and
1707            // collects results in input order. Sequential under the
1708            // hood (same caveat as `parallel`); spec §11.2 reserves
1709            // true threading for a future scheduler. Unlike `parallel`,
1710            // this returns the result list directly rather than a
1711            // closure, since the input arity is dynamic.
1712            fields.insert("parallel_list".into(), Ty::function(
1713                vec![
1714                    Ty::List(Box::new(
1715                        Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
1716                    )),
1717                ],
1718                EffectSet::empty(),
1719                Ty::List(Box::new(Ty::Var(0))),
1720            ));
1721            Some(Ty::Record(fields))
1722        }
1723        "crypto" => {
1724            let mut fields = IndexMap::new();
1725            // Hashes: Bytes -> Bytes (digest as raw bytes).
1726            // SHA-256 / SHA-512 are vetted. MD5 is retained only for
1727            // interop with legacy systems — new code should not use it.
1728            // BLAKE2b (#382) is included as a faster alternative to
1729            // SHA-512 with the same security level.
1730            for name in &["sha256", "sha512", "md5", "blake2b"] {
1731                fields.insert((*name).into(), Ty::function(
1732                    vec![Ty::bytes()],
1733                    EffectSet::empty(),
1734                    Ty::bytes(),
1735                ));
1736            }
1737            // Hex-string convenience hashers (#382): hash a Str directly,
1738            // return the digest as a lowercase hex Str. Equivalent to
1739            // `crypto.hex_encode(crypto.shaN(bytes_from_str(s)))` but
1740            // saves the two-step incantation for the common case.
1741            for name in &["sha256_str", "sha512_str"] {
1742                fields.insert((*name).into(), Ty::function(
1743                    vec![Ty::str()],
1744                    EffectSet::empty(),
1745                    Ty::str(),
1746                ));
1747            }
1748            // HMAC: (key :: Bytes, data :: Bytes) -> Bytes
1749            for name in &["hmac_sha256", "hmac_sha512"] {
1750                fields.insert((*name).into(), Ty::function(
1751                    vec![Ty::bytes(), Ty::bytes()],
1752                    EffectSet::empty(),
1753                    Ty::bytes(),
1754                ));
1755            }
1756            // ed25519 asymmetric signatures (#643). A secret key is its 32-byte
1757            // seed (generate via `crypto.random(32)`); all three ops are pure.
1758            //   ed25519_public_key(secret :: Bytes) -> Result[Bytes, Str]
1759            //   ed25519_sign(secret :: Bytes, message :: Bytes) -> Result[Bytes, Str]
1760            //   ed25519_verify(public :: Bytes, message :: Bytes, sig :: Bytes) -> Bool
1761            fields.insert("ed25519_public_key".into(), Ty::function(
1762                vec![Ty::bytes()],
1763                EffectSet::empty(),
1764                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1765            ));
1766            fields.insert("ed25519_sign".into(), Ty::function(
1767                vec![Ty::bytes(), Ty::bytes()],
1768                EffectSet::empty(),
1769                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1770            ));
1771            fields.insert("ed25519_verify".into(), Ty::function(
1772                vec![Ty::bytes(), Ty::bytes(), Ty::bytes()],
1773                EffectSet::empty(),
1774                Ty::bool(),
1775            ));
1776            // P-256 ECDSA / ES256 (#651). The JWT/SD-JWT signature
1777            // algorithm for AP2 agent keys; `lex-jose` builds the
1778            // token layer on top of these primitives. Key bytes are
1779            // raw: a secret key is the 32-byte scalar, a public key is
1780            // the 33-byte SEC1 compressed point; JWK serialization
1781            // lives downstream in `lex-jose`.
1782            //
1783            //   p256_generate()                       -> [random] Result[Bytes, Str]
1784            //   p256_public_key(sk :: Bytes)          -> Result[Bytes, Str]
1785            //   p256_sign(sk :: Bytes, msg :: Bytes)  -> Result[Bytes, Str]
1786            //   p256_verify(pk :: Bytes, msg :: Bytes, sig :: Bytes) -> Bool
1787            //
1788            // `p256_generate` mints fresh key material from the OS RNG,
1789            // so it carries the same fine-grained `[random]` effect as
1790            // `crypto.random` — every key-minting call stays visible to
1791            // `lex audit --effect random`. (The issue sketched `[env]`;
1792            // `[random]` is the dedicated effect for OS randomness in
1793            // this codebase, so we use that for consistency.)
1794            // `sign`/`verify` are pure: signing hashes `msg` with
1795            // SHA-256 internally (standard ES256) and the signature is
1796            // DER-encoded.
1797            fields.insert("p256_generate".into(), Ty::function(
1798                vec![],
1799                EffectSet::singleton("random"),
1800                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1801            ));
1802            fields.insert("p256_public_key".into(), Ty::function(
1803                vec![Ty::bytes()],
1804                EffectSet::empty(),
1805                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1806            ));
1807            fields.insert("p256_sign".into(), Ty::function(
1808                vec![Ty::bytes(), Ty::bytes()],
1809                EffectSet::empty(),
1810                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1811            ));
1812            fields.insert("p256_verify".into(), Ty::function(
1813                vec![Ty::bytes(), Ty::bytes(), Ty::bytes()],
1814                EffectSet::empty(),
1815                Ty::bool(),
1816            ));
1817            // secp256k1 ECDSA + recovery (#655). The EVM curve — backs
1818            // EIP-712 typed-data signing (EIP-3009 / x402 `exact`) and
1819            // Ethereum address derivation. Unlike `p256_*`/`ed25519_*`,
1820            // the sign/verify ops here take a **pre-hashed 32-byte
1821            // digest** (EIP-712 already hashes), hence the `_digest`
1822            // suffix — they do NOT hash the input again.
1823            //
1824            // - Secret key: 32-byte scalar.
1825            // - Public key: 65-byte UNCOMPRESSED SEC1 point (0x04‖X‖Y),
1826            //   so an address is `keccak256(pk[1..])[12..]` with no
1827            //   decompression step. (p256 returns compressed; the EVM
1828            //   convention is uncompressed.)
1829            // - Signature: 65 bytes `r(32)‖s(32)‖v(1)`, v ∈ {27,28}
1830            //   (Ethereum), low-S normalized (EIP-2).
1831            //
1832            //   keccak256(data :: Bytes) -> Bytes
1833            //   secp256k1_generate()                          -> [random] Result[Bytes, Str]
1834            //   secp256k1_public_key(sk :: Bytes)             -> Result[Bytes, Str]
1835            //   secp256k1_sign_digest(sk :: Bytes, digest :: Bytes) -> Result[Bytes, Str]
1836            //   secp256k1_recover(digest :: Bytes, sig :: Bytes)    -> Result[Bytes, Str]
1837            //   secp256k1_verify(pk :: Bytes, digest :: Bytes, sig :: Bytes) -> Bool
1838            //
1839            // `secp256k1_generate` mints from the OS RNG, so it carries
1840            // the same `[random]` effect as `crypto.random` / `p256_generate`.
1841            fields.insert("keccak256".into(), Ty::function(
1842                vec![Ty::bytes()],
1843                EffectSet::empty(),
1844                Ty::bytes(),
1845            ));
1846            fields.insert("secp256k1_generate".into(), Ty::function(
1847                vec![],
1848                EffectSet::singleton("random"),
1849                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1850            ));
1851            fields.insert("secp256k1_public_key".into(), Ty::function(
1852                vec![Ty::bytes()],
1853                EffectSet::empty(),
1854                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1855            ));
1856            fields.insert("secp256k1_sign_digest".into(), Ty::function(
1857                vec![Ty::bytes(), Ty::bytes()],
1858                EffectSet::empty(),
1859                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1860            ));
1861            fields.insert("secp256k1_recover".into(), Ty::function(
1862                vec![Ty::bytes(), Ty::bytes()],
1863                EffectSet::empty(),
1864                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1865            ));
1866            fields.insert("secp256k1_verify".into(), Ty::function(
1867                vec![Ty::bytes(), Ty::bytes(), Ty::bytes()],
1868                EffectSet::empty(),
1869                Ty::bool(),
1870            ));
1871            // base64 / hex
1872            fields.insert("base64_encode".into(), Ty::function(
1873                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1874            fields.insert("base64_decode".into(), Ty::function(
1875                vec![Ty::str()], EffectSet::empty(),
1876                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1877            // URL-safe base64 (#382): the alphabet swaps `+/` for `-_`
1878            // and omits padding. Required by JWT, signed-cookie, and
1879            // most token-bearing URL paths.
1880            fields.insert("base64url_encode".into(), Ty::function(
1881                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1882            fields.insert("base64url_decode".into(), Ty::function(
1883                vec![Ty::str()], EffectSet::empty(),
1884                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1885            fields.insert("hex_encode".into(), Ty::function(
1886                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1887            fields.insert("hex_decode".into(), Ty::function(
1888                vec![Ty::str()], EffectSet::empty(),
1889                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1890            // base58 (#658) — Bitcoin/Solana alphabet, no checksum. Solana
1891            // addresses, mints, signatures and the x402 `exact` payload are
1892            // base58; this is the Solana analog of keccak/secp256k1 (#655).
1893            fields.insert("base58_encode".into(), Ty::function(
1894                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1895            fields.insert("base58_decode".into(), Ty::function(
1896                vec![Ty::str()], EffectSet::empty(),
1897                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1898            // Constant-time equality (for HMAC verification etc.).
1899            // `eq` / `eq_str` (#382) are the recommended spelling;
1900            // `constant_time_eq` stays as a deprecated alias.
1901            fields.insert("constant_time_eq".into(), Ty::function(
1902                vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool()));
1903            fields.insert("eq".into(), Ty::function(
1904                vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool()));
1905            fields.insert("eq_str".into(), Ty::function(
1906                vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
1907            // Cryptographically-secure random bytes — OS RNG, not the
1908            // deterministic `rand.int_in` stub. The new `[random]`
1909            // effect is fine-grained on purpose so reviewers can find
1910            // every token-generating call via `lex audit --effect
1911            // random`.
1912            fields.insert("random".into(), Ty::function(
1913                vec![Ty::int()],
1914                EffectSet::singleton("random"),
1915                Ty::bytes(),
1916            ));
1917            // random_str_hex (#382): the most common token-mint pattern
1918            // — N random bytes rendered as 2N lowercase hex chars.
1919            // Suitable for session ids, request ids, OAuth `state`,
1920            // CSRF tokens; not suitable as a JWT signing key (use raw
1921            // `random` for that).
1922            fields.insert("random_str_hex".into(), Ty::function(
1923                vec![Ty::int()],
1924                EffectSet::singleton("random"),
1925                Ty::str(),
1926            ));
1927
1928            // AEAD: authenticated encryption with associated data
1929            // (#382 AEAD slice). Both algorithms use a 12-byte nonce
1930            // and a 16-byte authentication tag. `seal` returns the
1931            // structured `AeadResult { ciphertext, tag }`; `open`
1932            // returns `Result[Bytes, Str]` so authentication failures
1933            // surface as `Err`, not a panic.
1934            //
1935            // - **AES-GCM** (`aes_gcm_seal/open`): AES-128/192/256-GCM,
1936            //   key length determined by the supplied key bytes (16, 24,
1937            //   or 32). NIST-recommended; hardware-accelerated on most CPUs.
1938            // - **ChaCha20-Poly1305** (`chacha20_poly1305_seal/open`):
1939            //   Always a 32-byte key. Equivalent security to AES-GCM
1940            //   without needing AES-NI hardware; preferred on constrained
1941            //   targets.
1942            let aead_t = || Ty::Con("AeadResult".into(), vec![]);
1943            // Seal: returns Result[AeadResult, Str] rather than bare
1944            // AeadResult so input-validation errors (wrong key length,
1945            // wrong nonce length) surface as `Err` to the Lex caller
1946            // instead of panicking the VM. AES-GCM expects 16/24/32-byte
1947            // keys; ChaCha20-Poly1305 expects exactly 32. Both expect a
1948            // 12-byte nonce.
1949            for name in &["aes_gcm_seal", "chacha20_poly1305_seal"] {
1950                fields.insert((*name).into(), Ty::function(
1951                    // (key, nonce, aad, plaintext) -> Result[AeadResult, Str]
1952                    vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes()],
1953                    EffectSet::empty(),
1954                    Ty::Con("Result".into(), vec![aead_t(), Ty::str()]),
1955                ));
1956            }
1957            for name in &["aes_gcm_open", "chacha20_poly1305_open"] {
1958                fields.insert((*name).into(), Ty::function(
1959                    // (key, nonce, aad, ciphertext, tag) -> Result[Bytes, Str]
1960                    vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes()],
1961                    EffectSet::empty(),
1962                    Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1963                ));
1964            }
1965
1966            // KDFs: key-derivation functions (#382 KDF slice). All three
1967            // return `Result[Bytes, Str]` so caller-controlled inputs
1968            // (iteration count, output length, argon2id work factors)
1969            // that violate the underlying primitive's contract surface
1970            // as `Err` rather than panicking the VM. None require a new
1971            // effect — these are pure derivations.
1972            //
1973            // - **`pbkdf2_sha256(password, salt, iterations, len)`** —
1974            //   RFC 8018 PBKDF2 with HMAC-SHA256. Use ≥ 600_000 iterations
1975            //   for password storage (OWASP 2024). Older deployments
1976            //   pinning < 100_000 should rotate.
1977            // - **`hkdf_sha256(ikm, salt, info, len)`** — RFC 5869 extract+
1978            //   expand. Use for deriving multiple keys from a single
1979            //   high-entropy input (TLS, Noise, JWT-key rotation).
1980            //   Output length capped at 255 × 32 = 8160 bytes.
1981            // - **`argon2id(password, salt, t_cost, m_cost, len)`** —
1982            //   RFC 9106 Argon2id. Recommended for *new* password
1983            //   hashing. OWASP 2024 baseline: `t_cost=2, m_cost=19456`
1984            //   (19 MiB), or use `lex-crypto`'s vetted wrapper.
1985            fields.insert("pbkdf2_sha256".into(), Ty::function(
1986                // (password, salt, iterations, len) -> Result[Bytes, Str]
1987                vec![Ty::bytes(), Ty::bytes(), Ty::int(), Ty::int()],
1988                EffectSet::empty(),
1989                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1990            ));
1991            fields.insert("hkdf_sha256".into(), Ty::function(
1992                // (ikm, salt, info, len) -> Result[Bytes, Str]
1993                vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::int()],
1994                EffectSet::empty(),
1995                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1996            ));
1997            fields.insert("argon2id".into(), Ty::function(
1998                // (password, salt, t_cost, m_cost, len) -> Result[Bytes, Str]
1999                vec![Ty::bytes(), Ty::bytes(), Ty::int(), Ty::int(), Ty::int()],
2000                EffectSet::empty(),
2001                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
2002            ));
2003
2004            Some(Ty::Record(fields))
2005        }
2006        "deque" => {
2007            // Persistent double-ended queue. Push/pop O(1) on both
2008            // ends; iteration order is front-to-back.
2009            // Type variable: 0 = T.
2010            let dt   = || Ty::Con("Deque".into(), vec![Ty::Var(0)]);
2011            let pair = || Ty::Tuple(vec![Ty::Var(0), dt()]);
2012            let mut fields = IndexMap::new();
2013            // new :: () -> Deque[T]
2014            fields.insert("new".into(), Ty::function(
2015                vec![], EffectSet::empty(), dt()));
2016            // size :: Deque[T] -> Int
2017            fields.insert("size".into(), Ty::function(
2018                vec![dt()], EffectSet::empty(), Ty::int()));
2019            // is_empty :: Deque[T] -> Bool
2020            fields.insert("is_empty".into(), Ty::function(
2021                vec![dt()], EffectSet::empty(), Ty::bool()));
2022            // push_back / push_front :: Deque[T], T -> Deque[T]
2023            for n in &["push_back", "push_front"] {
2024                fields.insert((*n).into(), Ty::function(
2025                    vec![dt(), Ty::Var(0)], EffectSet::empty(), dt()));
2026            }
2027            // pop_back / pop_front :: Deque[T] -> Option[(T, Deque[T])]
2028            for n in &["pop_back", "pop_front"] {
2029                fields.insert((*n).into(), Ty::function(
2030                    vec![dt()], EffectSet::empty(),
2031                    Ty::Con("Option".into(), vec![pair()])));
2032            }
2033            // peek_back / peek_front :: Deque[T] -> Option[T]
2034            for n in &["peek_back", "peek_front"] {
2035                fields.insert((*n).into(), Ty::function(
2036                    vec![dt()], EffectSet::empty(),
2037                    Ty::Con("Option".into(), vec![Ty::Var(0)])));
2038            }
2039            // from_list :: List[T] -> Deque[T]
2040            fields.insert("from_list".into(), Ty::function(
2041                vec![Ty::List(Box::new(Ty::Var(0)))],
2042                EffectSet::empty(), dt()));
2043            // to_list :: Deque[T] -> List[T]
2044            fields.insert("to_list".into(), Ty::function(
2045                vec![dt()], EffectSet::empty(),
2046                Ty::List(Box::new(Ty::Var(0)))));
2047            Some(Ty::Record(fields))
2048        }
2049        "log" => {
2050            // Structured logging behind a [log] effect. Emit ops route
2051            // through a runtime-configured sink (stderr by default;
2052            // can be redirected via set_sink). Configuration ops
2053            // mutate the global sink and so are gated [io].
2054            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
2055            let mut fields = IndexMap::new();
2056            for level in &["debug", "info", "warn", "error"] {
2057                fields.insert((*level).into(), Ty::function(
2058                    vec![Ty::str()],
2059                    EffectSet::singleton("log"),
2060                    Ty::Unit,
2061                ));
2062            }
2063            // set_level :: Str -> [io] Result[Unit, Str]
2064            fields.insert("set_level".into(), Ty::function(
2065                vec![Ty::str()],
2066                EffectSet::singleton("io"),
2067                result_str(Ty::Unit)));
2068            // set_format :: Str -> [io] Result[Unit, Str]
2069            fields.insert("set_format".into(), Ty::function(
2070                vec![Ty::str()],
2071                EffectSet::singleton("io"),
2072                result_str(Ty::Unit)));
2073            // set_sink :: Str -> [io, fs_write] Result[Unit, Str]
2074            fields.insert("set_sink".into(), Ty::function(
2075                vec![Ty::str()],
2076                EffectSet {
2077                    concrete: [crate::types::EffectKind::bare("io"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
2078                    var: None,
2079                },
2080                result_str(Ty::Unit)));
2081            Some(Ty::Record(fields))
2082        }
2083        "datetime" => {
2084            // Instant and Duration are nominal opaque Ints under the
2085            // hood (nanoseconds-since-UTC-epoch and signed nanoseconds
2086            // respectively); the type checker tracks the distinction
2087            // even though both values look like Int at runtime.
2088            //
2089            // Tz is the variant
2090            //     Utc | Local | Offset(Int) | Iana(Str)
2091            // registered as a built-in nominal type in
2092            // `TypeEnv::new_with_builtins`. The pre-v1 stringly Tz
2093            // ("UTC"/"Local"/IANA-name/"+05:30") is no longer accepted
2094            // — passing a `Str` to `to_components` is now a type
2095            // error.
2096            let inst   = || Ty::Con("Instant".into(), vec![]);
2097            let dur    = || Ty::Con("Duration".into(), vec![]);
2098            let tz     = || Ty::Con("Tz".into(), vec![]);
2099            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
2100            let dt_t = || {
2101                let mut fs = IndexMap::new();
2102                fs.insert("year".into(),    Ty::int());
2103                fs.insert("month".into(),   Ty::int());
2104                fs.insert("day".into(),     Ty::int());
2105                fs.insert("hour".into(),    Ty::int());
2106                fs.insert("minute".into(),  Ty::int());
2107                fs.insert("second".into(),  Ty::int());
2108                fs.insert("nano".into(),    Ty::int());
2109                fs.insert("tz_offset_minutes".into(), Ty::int());
2110                Ty::Record(fs)
2111            };
2112            let mut fields = IndexMap::new();
2113            fields.insert("now".into(), Ty::function(
2114                vec![], EffectSet::singleton("time"), inst()));
2115            fields.insert("parse_iso".into(), Ty::function(
2116                vec![Ty::str()], EffectSet::empty(), result_str(inst())));
2117            fields.insert("format_iso".into(), Ty::function(
2118                vec![inst()], EffectSet::empty(), Ty::str()));
2119            fields.insert("parse".into(), Ty::function(
2120                vec![Ty::str(), Ty::str()], EffectSet::empty(), result_str(inst())));
2121            fields.insert("format".into(), Ty::function(
2122                vec![inst(), Ty::str()], EffectSet::empty(), Ty::str()));
2123            fields.insert("to_components".into(), Ty::function(
2124                vec![inst(), tz()], EffectSet::empty(), result_str(dt_t())));
2125            fields.insert("from_components".into(), Ty::function(
2126                vec![dt_t()], EffectSet::empty(), result_str(inst())));
2127            fields.insert("add".into(), Ty::function(
2128                vec![inst(), dur()], EffectSet::empty(), inst()));
2129            fields.insert("diff".into(), Ty::function(
2130                vec![inst(), inst()], EffectSet::empty(), dur()));
2131            fields.insert("duration_seconds".into(), Ty::function(
2132                vec![Ty::float()], EffectSet::empty(), dur()));
2133            fields.insert("duration_minutes".into(), Ty::function(
2134                vec![Ty::int()], EffectSet::empty(), dur()));
2135            fields.insert("duration_days".into(), Ty::function(
2136                vec![Ty::int()], EffectSet::empty(), dur()));
2137            // #331: comparison ops on Instant.
2138            fields.insert("before".into(), Ty::function(
2139                vec![inst(), inst()], EffectSet::empty(), Ty::bool()));
2140            fields.insert("after".into(), Ty::function(
2141                vec![inst(), inst()], EffectSet::empty(), Ty::bool()));
2142            // compare :: Instant, Instant -> Int  (-1 / 0 / +1)
2143            fields.insert("compare".into(), Ty::function(
2144                vec![inst(), inst()], EffectSet::empty(), Ty::int()));
2145            Some(Ty::Record(fields))
2146        }
2147        // #331: duration module — scalar extraction from Duration values.
2148        "duration" => {
2149            let dur = || Ty::Con("Duration".into(), vec![]);
2150            let mut fields = IndexMap::new();
2151            // Scalar extraction from a Duration (nanoseconds under the
2152            // hood). Each truncates toward zero. `seconds` shipped with
2153            // #331; #681 rounds out the unit set so a Duration built in
2154            // days via `datetime.duration_days` can be read back in the
2155            // same units rather than only as seconds.
2156            // millis / seconds / minutes / hours / days :: Duration -> Int
2157            for name in &["millis", "seconds", "minutes", "hours", "days"] {
2158                fields.insert((*name).into(), Ty::function(
2159                    vec![dur()], EffectSet::empty(), Ty::int()));
2160            }
2161            Some(Ty::Record(fields))
2162        }
2163        "process" => {
2164            // Streaming subprocess. The opaque `ProcessHandle` type
2165            // is an Int handle into a process-wide registry holding
2166            // the `Child` plus its stdout/stderr `BufReader`s.
2167            let ph = || Ty::Con("ProcessHandle".into(), vec![]);
2168            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
2169            let opts_t = || {
2170                let mut fs = IndexMap::new();
2171                fs.insert("cwd".into(),
2172                    Ty::Con("Option".into(), vec![Ty::str()]));
2173                fs.insert("env".into(),
2174                    Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
2175                fs.insert("stdin".into(),
2176                    Ty::Con("Option".into(), vec![Ty::bytes()]));
2177                Ty::Record(fs)
2178            };
2179            let exit_t = || {
2180                let mut fs = IndexMap::new();
2181                fs.insert("code".into(), Ty::int());
2182                fs.insert("signaled".into(), Ty::bool());
2183                Ty::Record(fs)
2184            };
2185            let output_t = || {
2186                let mut fs = IndexMap::new();
2187                fs.insert("stdout".into(), Ty::str());
2188                fs.insert("stderr".into(), Ty::str());
2189                fs.insert("exit_code".into(), Ty::int());
2190                Ty::Record(fs)
2191            };
2192            let mut fields = IndexMap::new();
2193            // spawn :: Str, List[Str], Opts -> [proc] Result[ProcessHandle, Str]
2194            fields.insert("spawn".into(), Ty::function(
2195                vec![Ty::str(), Ty::List(Box::new(Ty::str())), opts_t()],
2196                EffectSet::singleton("proc"),
2197                result_str(ph())));
2198            // read_stdout_line / read_stderr_line :: ProcessHandle -> [proc] Option[Str]
2199            for n in &["read_stdout_line", "read_stderr_line"] {
2200                fields.insert((*n).into(), Ty::function(
2201                    vec![ph()], EffectSet::singleton("proc"),
2202                    Ty::Con("Option".into(), vec![Ty::str()])));
2203            }
2204            // wait :: ProcessHandle -> [proc] ProcessExit
2205            fields.insert("wait".into(), Ty::function(
2206                vec![ph()], EffectSet::singleton("proc"), exit_t()));
2207            // kill :: ProcessHandle, Str -> [proc] Result[Unit, Str]
2208            fields.insert("kill".into(), Ty::function(
2209                vec![ph(), Ty::str()],
2210                EffectSet::singleton("proc"),
2211                result_str(Ty::Unit)));
2212            // run :: Str, List[Str] -> [proc] Result[ProcessOutput, Str]
2213            // Blocking convenience that captures stdout/stderr fully
2214            // and returns once the child exits. For programs that
2215            // need streaming, use spawn + read_*_line + wait.
2216            fields.insert("run".into(), Ty::function(
2217                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
2218                EffectSet::singleton("proc"),
2219                result_str(output_t())));
2220            Some(Ty::Record(fields))
2221        }
2222        "fs" => {
2223            // Filesystem walk + mutate. Walk-style ops (exists, walk,
2224            // glob, …) declare [fs_walk] — distinct from [fs_read]
2225            // (which is content reads via io.read), so reviewers can
2226            // separately track directory traversal vs file-content
2227            // exposure. Mutating ops (mkdir_p, remove, copy) declare
2228            // [fs_write]. Path scoping uses --allow-fs-read for walk
2229            // (a directory listing is an information disclosure on
2230            // the same path tree) and --allow-fs-write for mutations.
2231            let stat_t = || {
2232                let mut fs = IndexMap::new();
2233                fs.insert("size".into(), Ty::int());
2234                fs.insert("mtime".into(), Ty::int());
2235                fs.insert("is_dir".into(), Ty::bool());
2236                fs.insert("is_file".into(), Ty::bool());
2237                Ty::Record(fs)
2238            };
2239            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
2240            let mut fields = IndexMap::new();
2241            // Walk-style queries [fs_walk]
2242            fields.insert("exists".into(), Ty::function(
2243                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
2244            fields.insert("is_file".into(), Ty::function(
2245                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
2246            fields.insert("is_dir".into(), Ty::function(
2247                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
2248            fields.insert("stat".into(), Ty::function(
2249                vec![Ty::str()], EffectSet::singleton("fs_walk"),
2250                result_str(stat_t())));
2251            fields.insert("list_dir".into(), Ty::function(
2252                vec![Ty::str()], EffectSet::singleton("fs_walk"),
2253                result_str(Ty::List(Box::new(Ty::str())))));
2254            fields.insert("walk".into(), Ty::function(
2255                vec![Ty::str()], EffectSet::singleton("fs_walk"),
2256                result_str(Ty::List(Box::new(Ty::str())))));
2257            fields.insert("glob".into(), Ty::function(
2258                vec![Ty::str()], EffectSet::singleton("fs_walk"),
2259                result_str(Ty::List(Box::new(Ty::str())))));
2260            // Mutations [fs_write]
2261            fields.insert("mkdir_p".into(), Ty::function(
2262                vec![Ty::str()], EffectSet::singleton("fs_write"),
2263                result_str(Ty::Unit)));
2264            fields.insert("remove".into(), Ty::function(
2265                vec![Ty::str()], EffectSet::singleton("fs_write"),
2266                result_str(Ty::Unit)));
2267            fields.insert("copy".into(), Ty::function(
2268                vec![Ty::str(), Ty::str()],
2269                EffectSet {
2270                    concrete: [crate::types::EffectKind::bare("fs_walk"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
2271                    var: None,
2272                },
2273                result_str(Ty::Unit)));
2274            Some(Ty::Record(fields))
2275        }
2276        "kv" => {
2277            // Embedded key-value store. The opaque `Kv` type is
2278            // backed by an Int handle into a process-wide registry.
2279            let kv_t = || Ty::Con("Kv".into(), vec![]);
2280            let mut fields = IndexMap::new();
2281            // open :: Str -> [kv, fs_write] Result[Kv, Str]
2282            fields.insert("open".into(), Ty::function(
2283                vec![Ty::str()],
2284                EffectSet {
2285                    concrete: [crate::types::EffectKind::bare("kv"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
2286                    var: None,
2287                },
2288                Ty::Con("Result".into(), vec![kv_t(), Ty::str()])));
2289            // close :: Kv -> [kv] Unit
2290            fields.insert("close".into(), Ty::function(
2291                vec![kv_t()],
2292                EffectSet::singleton("kv"),
2293                Ty::Unit));
2294            // get :: Kv, Str -> [kv] Option[Bytes]
2295            fields.insert("get".into(), Ty::function(
2296                vec![kv_t(), Ty::str()],
2297                EffectSet::singleton("kv"),
2298                Ty::Con("Option".into(), vec![Ty::bytes()])));
2299            // put :: Kv, Str, Bytes -> [kv] Result[Unit, Str]
2300            fields.insert("put".into(), Ty::function(
2301                vec![kv_t(), Ty::str(), Ty::bytes()],
2302                EffectSet::singleton("kv"),
2303                Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
2304            // delete :: Kv, Str -> [kv] Result[Unit, Str]
2305            fields.insert("delete".into(), Ty::function(
2306                vec![kv_t(), Ty::str()],
2307                EffectSet::singleton("kv"),
2308                Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
2309            // contains :: Kv, Str -> [kv] Bool
2310            fields.insert("contains".into(), Ty::function(
2311                vec![kv_t(), Ty::str()],
2312                EffectSet::singleton("kv"),
2313                Ty::bool()));
2314            // list_prefix :: Kv, Str -> [kv] List[Str]
2315            fields.insert("list_prefix".into(), Ty::function(
2316                vec![kv_t(), Ty::str()],
2317                EffectSet::singleton("kv"),
2318                Ty::List(Box::new(Ty::str()))));
2319            Some(Ty::Record(fields))
2320        }
2321        "sql" => {
2322            // Embedded SQL (SQLite via rusqlite). The opaque `Db` type is
2323            // backed by an Int handle into a process-wide registry (#362).
2324            //
2325            // Params use the typed `SqlParam` ADT (PStr|PInt|PFloat|PBool|PNull)
2326            // registered in env.rs, so callers don't have to stringify values.
2327            //
2328            // Transactions: sql.begin(db) → SqlTx; sql.commit/rollback(tx).
2329            // exec_tx / query_tx mirror exec / query but operate on a SqlTx.
2330            //
2331            // Row decoders: get_str / get_int / get_float / get_bool extract
2332            // typed columns from a row record by name.
2333            let db_t  = || Ty::Con("Db".into(), vec![]);
2334            let tx_t  = || Ty::Con("SqlTx".into(), vec![]);
2335            let sp_t  = || Ty::Con("SqlParam".into(), vec![]);
2336            let params_t = || Ty::List(Box::new(sp_t()));
2337            let mut fields = IndexMap::new();
2338
2339            // SqlError = { message, code, detail } — populated with
2340            // SQLSTATE (Postgres) or symbolic SQLite error name (#380).
2341            let se_t = || Ty::Con("SqlError".into(), vec![]);
2342
2343            // open :: Str -> [sql, fs_write] Result[Db, SqlError]
2344            fields.insert("open".into(), Ty::function(
2345                vec![Ty::str()],
2346                EffectSet {
2347                    concrete: [crate::types::EffectKind::bare("sql"),
2348                               crate::types::EffectKind::bare("fs_write")]
2349                        .into_iter().collect(),
2350                    var: None,
2351                },
2352                Ty::Con("Result".into(), vec![db_t(), se_t()])));
2353
2354            // close :: Db -> [sql] Unit
2355            fields.insert("close".into(), Ty::function(
2356                vec![db_t()],
2357                EffectSet::singleton("sql"),
2358                Ty::Unit));
2359
2360            // exec :: Db, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
2361            fields.insert("exec".into(), Ty::function(
2362                vec![db_t(), Ty::str(), params_t()],
2363                EffectSet::singleton("sql"),
2364                Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
2365
2366            // query[T] :: Db, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
2367            fields.insert("query".into(), Ty::function(
2368                vec![db_t(), Ty::str(), params_t()],
2369                EffectSet::singleton("sql"),
2370                Ty::Con("Result".into(), vec![
2371                    Ty::List(Box::new(Ty::Var(0))),
2372                    se_t(),
2373                ])));
2374
2375            // query_iter[T] :: Db, Str, List[SqlParam] -> [sql] Result[Iter[T], SqlError]
2376            // Streaming variant of `query` (#379). Rows are pulled from
2377            // the server one at a time via an mpsc-backed cursor —
2378            // memory stays bounded regardless of result-set size.
2379            // Other ops on the same `Db` handle block until the cursor
2380            // is drained (single connection per Db).
2381            fields.insert("query_iter".into(), Ty::function(
2382                vec![db_t(), Ty::str(), params_t()],
2383                EffectSet::singleton("sql"),
2384                Ty::Con("Result".into(), vec![
2385                    Ty::Con("Iter".into(), vec![Ty::Var(0)]),
2386                    se_t(),
2387                ])));
2388
2389            // begin :: Db -> [sql] Result[SqlTx, SqlError]
2390            fields.insert("begin".into(), Ty::function(
2391                vec![db_t()],
2392                EffectSet::singleton("sql"),
2393                Ty::Con("Result".into(), vec![tx_t(), se_t()])));
2394
2395            // commit :: SqlTx -> [sql] Result[Unit, SqlError]
2396            fields.insert("commit".into(), Ty::function(
2397                vec![tx_t()],
2398                EffectSet::singleton("sql"),
2399                Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
2400
2401            // rollback :: SqlTx -> [sql] Result[Unit, SqlError]
2402            fields.insert("rollback".into(), Ty::function(
2403                vec![tx_t()],
2404                EffectSet::singleton("sql"),
2405                Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
2406
2407            // exec_tx :: SqlTx, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
2408            fields.insert("exec_tx".into(), Ty::function(
2409                vec![tx_t(), Ty::str(), params_t()],
2410                EffectSet::singleton("sql"),
2411                Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
2412
2413            // query_tx[T] :: SqlTx, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
2414            fields.insert("query_tx".into(), Ty::function(
2415                vec![tx_t(), Ty::str(), params_t()],
2416                EffectSet::singleton("sql"),
2417                Ty::Con("Result".into(), vec![
2418                    Ty::List(Box::new(Ty::Var(0))),
2419                    se_t(),
2420                ])));
2421
2422            // Row decoders: get_X[T] :: T, Str -> Option[X]
2423            // T is polymorphic so these work on any row record shape.
2424            fields.insert("get_str".into(), Ty::function(
2425                vec![Ty::Var(0), Ty::str()],
2426                EffectSet::empty(),
2427                Ty::Con("Option".into(), vec![Ty::str()])));
2428            fields.insert("get_int".into(), Ty::function(
2429                vec![Ty::Var(0), Ty::str()],
2430                EffectSet::empty(),
2431                Ty::Con("Option".into(), vec![Ty::int()])));
2432            fields.insert("get_float".into(), Ty::function(
2433                vec![Ty::Var(0), Ty::str()],
2434                EffectSet::empty(),
2435                Ty::Con("Option".into(), vec![Ty::float()])));
2436            fields.insert("get_bool".into(), Ty::function(
2437                vec![Ty::Var(0), Ty::str()],
2438                EffectSet::empty(),
2439                Ty::Con("Option".into(), vec![Ty::bool()])));
2440
2441            Some(Ty::Record(fields))
2442        }
2443        "redis" => {
2444            // Thin Redis client (#533). ConnRedis is an opaque handle backed by a
2445            // process-wide registry (same pattern as Db in std.sql). All ops carry
2446            // [net] — Redis is a TCP service; no separate [redis] effect.
2447            //
2448            // subscribe / psubscribe return Unit because they are blocking
2449            // infinite loops, consistent with net.serve_fn and ws.serve.
2450            //
2451            // subscribe/psubscribe open a *dedicated* connection internally —
2452            // Redis disallows non-Pub/Sub commands on a subscribed connection.
2453            let conn_t = || Ty::Con("ConnRedis".into(), vec![]);
2454            let mut fields = IndexMap::new();
2455
2456            // connect :: Str -> [net] Result[ConnRedis, Str]
2457            // url: "redis://host:6379" or "rediss://host:6380" (TLS)
2458            fields.insert("connect".into(), Ty::function(
2459                vec![Ty::str()],
2460                EffectSet::singleton("net"),
2461                Ty::Con("Result".into(), vec![conn_t(), Ty::str()])));
2462
2463            // close :: ConnRedis -> [net] Unit
2464            fields.insert("close".into(), Ty::function(
2465                vec![conn_t()],
2466                EffectSet::singleton("net"),
2467                Ty::Unit));
2468
2469            // ---- Key-value -----------------------------------------------
2470
2471            // get :: ConnRedis, Str -> [net] Option[Str]
2472            fields.insert("get".into(), Ty::function(
2473                vec![conn_t(), Ty::str()],
2474                EffectSet::singleton("net"),
2475                Ty::Con("Option".into(), vec![Ty::str()])));
2476
2477            // set :: ConnRedis, Str, Str -> [net] Unit
2478            fields.insert("set".into(), Ty::function(
2479                vec![conn_t(), Ty::str(), Ty::str()],
2480                EffectSet::singleton("net"),
2481                Ty::Unit));
2482
2483            // set_ex :: ConnRedis, Str, Str, Int -> [net] Unit
2484            fields.insert("set_ex".into(), Ty::function(
2485                vec![conn_t(), Ty::str(), Ty::str(), Ty::int()],
2486                EffectSet::singleton("net"),
2487                Ty::Unit));
2488
2489            // del :: ConnRedis, Str -> [net] Unit
2490            fields.insert("del".into(), Ty::function(
2491                vec![conn_t(), Ty::str()],
2492                EffectSet::singleton("net"),
2493                Ty::Unit));
2494
2495            // exists :: ConnRedis, Str -> [net] Bool
2496            fields.insert("exists".into(), Ty::function(
2497                vec![conn_t(), Ty::str()],
2498                EffectSet::singleton("net"),
2499                Ty::bool()));
2500
2501            // expire :: ConnRedis, Str, Int -> [net] Unit
2502            fields.insert("expire".into(), Ty::function(
2503                vec![conn_t(), Ty::str(), Ty::int()],
2504                EffectSet::singleton("net"),
2505                Ty::Unit));
2506
2507            // ---- Pub/Sub -------------------------------------------------
2508
2509            // publish :: ConnRedis, Str, Str -> [net] Int
2510            // Returns the number of subscribers that received the message.
2511            fields.insert("publish".into(), Ty::function(
2512                vec![conn_t(), Ty::str(), Ty::str()],
2513                EffectSet::singleton("net"),
2514                Ty::int()));
2515
2516            // subscribe :: ConnRedis, Str, (Str, Str ->[E] Unit) -> [net] Unit
2517            // Blocking loop; handler receives (channel, message) on each message.
2518            // Uses a dedicated connection — Redis disallows non-Pub/Sub commands
2519            // on a subscribed connection. Handler carries an open effect row so
2520            // callers can use io, net, sql, etc. inside the closure.
2521            let handler2 = Ty::function(
2522                vec![Ty::str(), Ty::str()],
2523                EffectSet::open_var(0),
2524                Ty::Unit);
2525            fields.insert("subscribe".into(), Ty::function(
2526                vec![conn_t(), Ty::str(), handler2],
2527                EffectSet::singleton("net"),
2528                Ty::Unit));  // Unit
2529
2530            // psubscribe :: ConnRedis, Str, (Str, Str, Str ->[E] Unit) -> [net] Unit
2531            // Pattern-subscribe; handler receives (pattern, channel, message).
2532            // Handler carries an open effect row (same rationale as subscribe).
2533            let handler3 = Ty::function(
2534                vec![Ty::str(), Ty::str(), Ty::str()],
2535                EffectSet::open_var(1),
2536                Ty::Unit);
2537            fields.insert("psubscribe".into(), Ty::function(
2538                vec![conn_t(), Ty::str(), handler3],
2539                EffectSet::singleton("net"),
2540                Ty::Unit));  // Unit
2541
2542            // ---- List ----------------------------------------------------
2543
2544            // lpush :: ConnRedis, Str, Str -> [net] Int
2545            fields.insert("lpush".into(), Ty::function(
2546                vec![conn_t(), Ty::str(), Ty::str()],
2547                EffectSet::singleton("net"),
2548                Ty::int()));
2549
2550            // rpush :: ConnRedis, Str, Str -> [net] Int
2551            fields.insert("rpush".into(), Ty::function(
2552                vec![conn_t(), Ty::str(), Ty::str()],
2553                EffectSet::singleton("net"),
2554                Ty::int()));
2555
2556            // brpop :: ConnRedis, Str, Int -> [net] Option[Str]
2557            // Blocking right-pop; returns None on timeout. timeout=0 blocks
2558            // indefinitely (the runtime does not treat this as a hung effect).
2559            fields.insert("brpop".into(), Ty::function(
2560                vec![conn_t(), Ty::str(), Ty::int()],
2561                EffectSet::singleton("net"),
2562                Ty::Con("Option".into(), vec![Ty::str()])));
2563
2564            // llen :: ConnRedis, Str -> [net] Int
2565            fields.insert("llen".into(), Ty::function(
2566                vec![conn_t(), Ty::str()],
2567                EffectSet::singleton("net"),
2568                Ty::int()));
2569
2570            // ---- Hash ----------------------------------------------------
2571
2572            // hset :: ConnRedis, Str, Str, Str -> [net] Unit
2573            fields.insert("hset".into(), Ty::function(
2574                vec![conn_t(), Ty::str(), Ty::str(), Ty::str()],
2575                EffectSet::singleton("net"),
2576                Ty::Unit));
2577
2578            // hget :: ConnRedis, Str, Str -> [net] Option[Str]
2579            fields.insert("hget".into(), Ty::function(
2580                vec![conn_t(), Ty::str(), Ty::str()],
2581                EffectSet::singleton("net"),
2582                Ty::Con("Option".into(), vec![Ty::str()])));
2583
2584            // hdel :: ConnRedis, Str, Str -> [net] Unit
2585            fields.insert("hdel".into(), Ty::function(
2586                vec![conn_t(), Ty::str(), Ty::str()],
2587                EffectSet::singleton("net"),
2588                Ty::Unit));
2589
2590            // hgetall :: ConnRedis, Str -> [net] List[(Str, Str)]
2591            fields.insert("hgetall".into(), Ty::function(
2592                vec![conn_t(), Ty::str()],
2593                EffectSet::singleton("net"),
2594                Ty::List(Box::new(Ty::Tuple(vec![Ty::str(), Ty::str()])))));
2595
2596            Some(Ty::Record(fields))
2597        }
2598        "parser" => {
2599            // #217: structured parser combinators. Parser values are
2600            // tagged Records at runtime (`{ kind, ... }`), opaque at
2601            // the language level via `Ty::Con("Parser", [T])`.
2602            //
2603            // Surface:
2604            //   - primitives: char, string, digit, alpha, whitespace, eof
2605            //   - combinators: seq, alt, many, optional, map, and_then
2606            //   - run :: Parser[T], Str -> Result[T, ParseErr]
2607            //
2608            // `map` and `and_then` were deferred from #217's v1 because
2609            // their closure arguments carried call-site identity that
2610            // broke the canonical-parsers acceptance criterion. With
2611            // closure body-hash equality landed in #222, that concern
2612            // is gone, and #221 wires them in. The interpreter for
2613            // `parser.run` has been moved to `lex-bytecode::parser_runtime`
2614            // so it can invoke closures from `Map` / `AndThen` nodes.
2615            let pt = |t: Ty| Ty::Con("Parser".into(), vec![t]);
2616            let parse_err = || {
2617                let mut fs = IndexMap::new();
2618                fs.insert("pos".into(), Ty::int());
2619                fs.insert("message".into(), Ty::str());
2620                Ty::Record(fs)
2621            };
2622            let mut fields = IndexMap::new();
2623            // char :: Str -> Parser[Str] (single-char Str literal)
2624            fields.insert("char".into(), Ty::function(
2625                vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
2626            // string :: Str -> Parser[Str]
2627            fields.insert("string".into(), Ty::function(
2628                vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
2629            // digit :: () -> Parser[Str]
2630            fields.insert("digit".into(), Ty::function(
2631                vec![], EffectSet::empty(), pt(Ty::str())));
2632            // alpha :: () -> Parser[Str]
2633            fields.insert("alpha".into(), Ty::function(
2634                vec![], EffectSet::empty(), pt(Ty::str())));
2635            // whitespace :: () -> Parser[Str]
2636            fields.insert("whitespace".into(), Ty::function(
2637                vec![], EffectSet::empty(), pt(Ty::str())));
2638            // eof :: () -> Parser[Unit]
2639            fields.insert("eof".into(), Ty::function(
2640                vec![], EffectSet::empty(), pt(Ty::Unit)));
2641            // seq :: Parser[A], Parser[B] -> Parser[(A, B)]
2642            fields.insert("seq".into(), Ty::function(
2643                vec![pt(Ty::Var(0)), pt(Ty::Var(1))],
2644                EffectSet::empty(),
2645                pt(Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]))));
2646            // alt :: Parser[T], Parser[T] -> Parser[T]
2647            // PEG-style ordered choice: the second alternative is
2648            // tried only if the first fails.
2649            fields.insert("alt".into(), Ty::function(
2650                vec![pt(Ty::Var(0)), pt(Ty::Var(0))],
2651                EffectSet::empty(),
2652                pt(Ty::Var(0))));
2653            // many :: Parser[T] -> Parser[List[T]]
2654            // Zero-or-more. Stops as soon as the inner parser fails
2655            // OR doesn't advance the position (avoids infinite loop
2656            // on empty matches).
2657            fields.insert("many".into(), Ty::function(
2658                vec![pt(Ty::Var(0))],
2659                EffectSet::empty(),
2660                pt(Ty::List(Box::new(Ty::Var(0))))));
2661            // optional :: Parser[T] -> Parser[Option[T]]
2662            fields.insert("optional".into(), Ty::function(
2663                vec![pt(Ty::Var(0))],
2664                EffectSet::empty(),
2665                pt(Ty::Con("Option".into(), vec![Ty::Var(0)]))));
2666            // map :: Parser[T], (T) -> [E] U -> [E] Parser[U]
2667            // The closure runs at parse time when the Parser is run.
2668            // Effect-polymorphic on the closure: any effect the
2669            // closure declares propagates to the surrounding `run`.
2670            fields.insert("map".into(), Ty::function(
2671                vec![
2672                    pt(Ty::Var(0)),
2673                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
2674                ],
2675                EffectSet::open_var(2),
2676                pt(Ty::Var(1))));
2677            // and_then :: Parser[T], (T) -> [E] Parser[U] -> [E] Parser[U]
2678            // Monadic bind: closure inspects the parsed value and
2679            // returns the next parser to run.
2680            fields.insert("and_then".into(), Ty::function(
2681                vec![
2682                    pt(Ty::Var(0)),
2683                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
2684                        pt(Ty::Var(1))),
2685                ],
2686                EffectSet::open_var(3),
2687                pt(Ty::Var(1))));
2688            // run :: Parser[T], Str -> Result[T, ParseErr]
2689            // ParseErr = { pos :: Int, message :: Str }
2690            fields.insert("run".into(), Ty::function(
2691                vec![pt(Ty::Var(0)), Ty::str()],
2692                EffectSet::empty(),
2693                Ty::Con("Result".into(), vec![Ty::Var(0), parse_err()])));
2694            Some(Ty::Record(fields))
2695        }
2696        "cli" => {
2697            // #224 Rubric port: argparse-equivalent for end-user
2698            // programs. Spec values are tagged `Json` records (opaque
2699            // to the language but inspectable). Construction via the
2700            // `flag` / `option` / `positional` / `spec` builders;
2701            // parse + introspection / help via the remaining ops.
2702            let json = || Ty::Con("Json".into(), vec![]);
2703            let opt_str = || Ty::Con("Option".into(), vec![Ty::str()]);
2704            let mut fields = IndexMap::new();
2705            // flag :: Str -> Option[Str] -> Str -> Json
2706            //   long_name -> short -> help -> CliArg
2707            fields.insert("flag".into(), Ty::function(
2708                vec![Ty::str(), opt_str(), Ty::str()],
2709                EffectSet::empty(),
2710                json()));
2711            // option :: Str -> Option[Str] -> Str -> Option[Str] -> Json
2712            //   long_name -> short -> help -> default -> CliArg
2713            fields.insert("option".into(), Ty::function(
2714                vec![Ty::str(), opt_str(), Ty::str(), opt_str()],
2715                EffectSet::empty(),
2716                json()));
2717            // positional :: Str -> Str -> Bool -> Json
2718            //   name -> help -> required -> CliArg
2719            fields.insert("positional".into(), Ty::function(
2720                vec![Ty::str(), Ty::str(), Ty::bool()],
2721                EffectSet::empty(),
2722                json()));
2723            // spec :: Str -> Str -> List[Json] -> List[Json] -> Json
2724            //   name -> help -> args -> subcommands -> CliSpec
2725            fields.insert("spec".into(), Ty::function(
2726                vec![Ty::str(), Ty::str(),
2727                     Ty::List(Box::new(json())),
2728                     Ty::List(Box::new(json()))],
2729                EffectSet::empty(),
2730                json()));
2731            // parse :: Json -> List[Str] -> Result[Json, Str]
2732            //   spec -> argv -> Result[CliParsed, error]
2733            fields.insert("parse".into(), Ty::function(
2734                vec![json(), Ty::List(Box::new(Ty::str()))],
2735                EffectSet::empty(),
2736                Ty::Con("Result".into(), vec![json(), Ty::str()])));
2737            // envelope :: Bool -> Str -> T -> Json
2738            //   ok -> command -> data -> ACLI-shaped envelope.
2739            // `data` is polymorphic so callers don't have to round-
2740            // trip through `json.parse` for trivial payloads.
2741            fields.insert("envelope".into(), Ty::function(
2742                vec![Ty::bool(), Ty::str(), Ty::Var(0)],
2743                EffectSet::empty(),
2744                json()));
2745            // describe :: Json -> Json — machine-readable spec dump
2746            fields.insert("describe".into(), Ty::function(
2747                vec![json()],
2748                EffectSet::empty(),
2749                json()));
2750            // help :: Json -> Str — human-readable help text
2751            fields.insert("help".into(), Ty::function(
2752                vec![json()],
2753                EffectSet::empty(),
2754                Ty::str()));
2755            Some(Ty::Record(fields))
2756        }
2757        "regex" => {
2758            // The compiled `Regex` is stored as a `Str` at runtime
2759            // (the pattern source) plus a process-wide cache of the
2760            // actual `regex::Regex`. So `Regex` is a nominal type at
2761            // the language level but its value is just the pattern.
2762            let regex_t = || Ty::Con("Regex".into(), vec![]);
2763            let match_t = || {
2764                let mut fs = IndexMap::new();
2765                fs.insert("text".into(), Ty::str());
2766                fs.insert("start".into(), Ty::int());
2767                fs.insert("end".into(), Ty::int());
2768                fs.insert("groups".into(), Ty::List(Box::new(Ty::str())));
2769                Ty::Record(fs)
2770            };
2771            let mut fields = IndexMap::new();
2772            // compile :: Str -> Result[Regex, Str]
2773            fields.insert("compile".into(), Ty::function(
2774                vec![Ty::str()], EffectSet::empty(),
2775                Ty::Con("Result".into(), vec![regex_t(), Ty::str()])));
2776            // is_match :: Regex, Str -> Bool
2777            fields.insert("is_match".into(), Ty::function(
2778                vec![regex_t(), Ty::str()], EffectSet::empty(), Ty::bool()));
2779            // is_match_str :: Str, Str -> Bool
2780            // Compiles the first argument as a pattern and matches against the second.
2781            // Returns false on invalid pattern instead of propagating an error.
2782            fields.insert("is_match_str".into(), Ty::function(
2783                vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
2784            // find :: Regex, Str -> Option[Match]
2785            fields.insert("find".into(), Ty::function(
2786                vec![regex_t(), Ty::str()], EffectSet::empty(),
2787                Ty::Con("Option".into(), vec![match_t()])));
2788            // find_all :: Regex, Str -> List[Match]
2789            fields.insert("find_all".into(), Ty::function(
2790                vec![regex_t(), Ty::str()], EffectSet::empty(),
2791                Ty::List(Box::new(match_t()))));
2792            // replace :: Regex, Str, Str -> Str
2793            fields.insert("replace".into(), Ty::function(
2794                vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
2795            // replace_all :: Regex, Str, Str -> Str
2796            fields.insert("replace_all".into(), Ty::function(
2797                vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
2798            // split :: Regex, Str -> List[Str]
2799            fields.insert("split".into(), Ty::function(
2800                vec![regex_t(), Ty::str()], EffectSet::empty(),
2801                Ty::List(Box::new(Ty::str()))));
2802            Some(Ty::Record(fields))
2803        }
2804        "http" => {
2805            // Rich HTTP client. `[net]` for the wire ops, pure for
2806            // the builders / decoders. `--allow-net-host` gates per
2807            // request. Multipart upload + streaming response bodies
2808            // are deferred to v1.5; the v1 surface covers the
2809            // common cases (auth, headers, query, timeouts, JSON /
2810            // text decoding).
2811            let req_t  = || Ty::Con("HttpRequest".into(), vec![]);
2812            let resp_t = || Ty::Con("HttpResponse".into(), vec![]);
2813            let err_t  = || Ty::Con("HttpError".into(), vec![]);
2814            let result_he = |t: Ty| Ty::Con("Result".into(), vec![t, err_t()]);
2815            let str_str_map = || Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]);
2816            let mut fields = IndexMap::new();
2817            // -- wire ops (effectful) --
2818            // send :: HttpRequest -> [net] Result[HttpResponse, HttpError]
2819            fields.insert("send".into(), Ty::function(
2820                vec![req_t()],
2821                EffectSet::singleton("net"),
2822                result_he(resp_t()),
2823            ));
2824            // get :: Str -> [net] Result[HttpResponse, HttpError]
2825            fields.insert("get".into(), Ty::function(
2826                vec![Ty::str()],
2827                EffectSet::singleton("net"),
2828                result_he(resp_t()),
2829            ));
2830            // post :: Str, Bytes, Str -> [net] Result[HttpResponse, HttpError]
2831            fields.insert("post".into(), Ty::function(
2832                vec![Ty::str(), Ty::bytes(), Ty::str()],
2833                EffectSet::singleton("net"),
2834                result_he(resp_t()),
2835            ));
2836            // -- pure builders (record transforms) --
2837            // with_header :: HttpRequest, Str, Str -> HttpRequest
2838            fields.insert("with_header".into(), Ty::function(
2839                vec![req_t(), Ty::str(), Ty::str()],
2840                EffectSet::empty(),
2841                req_t(),
2842            ));
2843            // with_auth :: HttpRequest, Str, Str -> HttpRequest
2844            // (Renders `<scheme> <token>` into the `Authorization`
2845            // header — `Bearer <jwt>`, `Basic <b64>`, etc.)
2846            fields.insert("with_auth".into(), Ty::function(
2847                vec![req_t(), Ty::str(), Ty::str()],
2848                EffectSet::empty(),
2849                req_t(),
2850            ));
2851            // with_query :: HttpRequest, Map[Str, Str] -> HttpRequest
2852            // (Appends a `?k=v&...` query string; values are URL-
2853            // encoded so `&` / `=` / spaces in values don't escape.)
2854            fields.insert("with_query".into(), Ty::function(
2855                vec![req_t(), str_str_map()],
2856                EffectSet::empty(),
2857                req_t(),
2858            ));
2859            // with_timeout_ms :: HttpRequest, Int -> HttpRequest
2860            fields.insert("with_timeout_ms".into(), Ty::function(
2861                vec![req_t(), Ty::int()],
2862                EffectSet::empty(),
2863                req_t(),
2864            ));
2865            // -- pure decoders --
2866            // json_body[T] :: HttpResponse -> Result[T, HttpError]
2867            // Polymorphic on the parsed shape, matching `json.parse`.
2868            fields.insert("json_body".into(), Ty::function(
2869                vec![resp_t()],
2870                EffectSet::empty(),
2871                result_he(Ty::Var(0)),
2872            ));
2873            // text_body :: HttpResponse -> Result[Str, HttpError]
2874            fields.insert("text_body".into(), Ty::function(
2875                vec![resp_t()],
2876                EffectSet::empty(),
2877                result_he(Ty::str()),
2878            ));
2879            // stream_lines :: Str, Map[Str, Str], Str -> [net] Result[Stream[Str], Str]
2880            // Streaming HTTP POST that yields the response body line-by-line
2881            // for SSE / NDJSON endpoints. Returns a lazy `Stream[Str]` (#683):
2882            // each `stream.next` pulls exactly one line off the socket as it
2883            // arrives, so an endpoint that holds the connection open and emits
2884            // events over time is consumed incrementally instead of blocking
2885            // until close. Connection errors at request time surface as
2886            // `Err(Str)`; a mid-stream read error / close ends the stream
2887            // (next `stream.next` returns `None`). Consume with `std.stream`
2888            // (`stream.next` / `stream.collect`), which carries `[stream]`.
2889            fields.insert("stream_lines".into(), Ty::function(
2890                vec![
2891                    Ty::str(),
2892                    Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
2893                    Ty::str(),
2894                ],
2895                EffectSet::singleton("net"),
2896                Ty::Con("Result".into(), vec![
2897                    Ty::Con("Stream".into(), vec![Ty::str()]),
2898                    Ty::str(),
2899                ]),
2900            ));
2901            Some(Ty::Record(fields))
2902        }
2903        "yaml" => {
2904            // YAML config parser. Same shape as `std.toml`: parse
2905            // is polymorphic, output Value layout matches std.json
2906            // (Str/Int/Float/Bool/List/Record). Anchors and tags
2907            // are flattened by serde_yaml's deserializer.
2908            let mut fields = IndexMap::new();
2909            fields.insert("parse".into(), Ty::function(
2910                vec![Ty::str()], EffectSet::empty(),
2911                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
2912            ));
2913            // Tactical fix for #168 — caller-supplied required-field
2914            // list. See std.json's parse_strict for context.
2915            fields.insert("parse_strict".into(), Ty::function(
2916                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
2917                EffectSet::empty(),
2918                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
2919            ));
2920            fields.insert("stringify".into(), Ty::function(
2921                vec![Ty::Var(0)], EffectSet::empty(),
2922                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2923            ));
2924            Some(Ty::Record(fields))
2925        }
2926        "dotenv" => {
2927            // .env-style files. parse :: Str -> Result[Map[Str,Str], Str].
2928            // Returns a map (not a polymorphic record) because
2929            // dotenv files don't carry shape — every value is a
2930            // string and keys aren't statically known.
2931            let mut fields = IndexMap::new();
2932            fields.insert("parse".into(), Ty::function(
2933                vec![Ty::str()], EffectSet::empty(),
2934                Ty::Con("Result".into(), vec![
2935                    Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
2936                    Ty::str(),
2937                ]),
2938            ));
2939            Some(Ty::Record(fields))
2940        }
2941        "csv" => {
2942            // CSV rows-as-lists. parse :: Str -> Result[List[List[Str]], Str].
2943            // Header awareness is left to the caller — row 0 is
2944            // whatever the file has. A `parse_with_headers` that
2945            // returns List[Map[Str,Str]] is a natural follow-up.
2946            let row_ty = Ty::List(Box::new(Ty::str()));
2947            let rows_ty = Ty::List(Box::new(row_ty.clone()));
2948            let mut fields = IndexMap::new();
2949            fields.insert("parse".into(), Ty::function(
2950                vec![Ty::str()], EffectSet::empty(),
2951                Ty::Con("Result".into(), vec![rows_ty.clone(), Ty::str()]),
2952            ));
2953            fields.insert("stringify".into(), Ty::function(
2954                vec![rows_ty], EffectSet::empty(),
2955                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2956            ));
2957            Some(Ty::Record(fields))
2958        }
2959        "test" => {
2960            // Tiny assertion library (#proposed-stdlib). Each helper
2961            // returns Result[Unit, Str] so a test is itself a fn
2962            // returning Result. Callers compose suites in user code
2963            // (a List of (name, () -> Result[Unit, Str]) pairs +
2964            // list.fold to accumulate verdicts). Property generators
2965            // and a Rust-side Suite type are deferred to v2.
2966            let mut fields = IndexMap::new();
2967            // assert_eq[a, b] :: T -> T -> Result[Unit, Str]
2968            // (T constrained equal by unification on the two args)
2969            let unit_result = || Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]);
2970            fields.insert("assert_eq".into(), Ty::function(
2971                vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
2972            ));
2973            fields.insert("assert_ne".into(), Ty::function(
2974                vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
2975            ));
2976            fields.insert("assert_true".into(), Ty::function(
2977                vec![Ty::bool()], EffectSet::empty(), unit_result(),
2978            ));
2979            fields.insert("assert_false".into(), Ty::function(
2980                vec![Ty::bool()], EffectSet::empty(), unit_result(),
2981            ));
2982            Some(Ty::Record(fields))
2983        }
2984        "toml" => {
2985            // TOML config parser. Mirrors `std.json`'s shape: parse
2986            // is polymorphic so callers annotate the expected
2987            // record / list / scalar shape and the type checker
2988            // unifies. The parsed TOML maps to the same Lex Value
2989            // shape as JSON does:
2990            //
2991            //   TOML String   → Value::Str
2992            //   TOML Integer  → Value::Int
2993            //   TOML Float    → Value::Float
2994            //   TOML Boolean  → Value::Bool
2995            //   TOML Array    → Value::List
2996            //   TOML Table    → Value::Record
2997            //   TOML Datetime → Value::Str (RFC 3339, lossless)
2998            //
2999            // The Datetime → Str fallback is the one info-losing
3000            // step; callers who want a real `Instant` can pipe the
3001            // string through `datetime.parse_iso`.
3002            let mut fields = IndexMap::new();
3003            // parse :: Str -> Result[T, Str]
3004            fields.insert("parse".into(), Ty::function(
3005                vec![Ty::str()], EffectSet::empty(),
3006                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3007            ));
3008            // parse_strict :: (Str, List[Str]) -> Result[T, Str]
3009            // Tactical fix for #168 — caller passes the field
3010            // names T requires; runtime returns Err if any are
3011            // missing from the parsed table instead of letting
3012            // field access panic later.
3013            fields.insert("parse_strict".into(), Ty::function(
3014                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
3015                EffectSet::empty(),
3016                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3017            ));
3018            // stringify :: T -> Result[Str, Str]
3019            // Returns Result (not Str) because not every Lex Value
3020            // has a TOML representation — top-level scalars,
3021            // closures, mixed-key maps etc. surface as Err rather
3022            // than panic.
3023            fields.insert("stringify".into(), Ty::function(
3024                vec![Ty::Var(0)], EffectSet::empty(),
3025                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3026            ));
3027            Some(Ty::Record(fields))
3028        }
3029        // `std.agent` (#184) — runtime primitives whose effects
3030        // separate (a) which LLM surface (`llm_local` vs
3031        // `llm_cloud`), (b) which peer protocol (`a2a`), and
3032        // (c) which tool boundary (`mcp`). The wire formats land
3033        // in downstream crates (`soft-agent`, `soft-a2a`) and
3034        // in #185 for MCP; what's typed here is the boundary
3035        // alone — agent code can be type-checked as
3036        // `[llm_local, a2a]` and will fail if it tries to reach
3037        // `[llm_cloud]` even before the wire layer is finished.
3038        "agent" => {
3039            let mut fields = IndexMap::new();
3040            // local_complete :: Str -> [llm_local] Result[Str, Str]
3041            fields.insert("local_complete".into(), Ty::function(
3042                vec![Ty::str()],
3043                EffectSet::singleton("llm_local"),
3044                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3045            ));
3046            // cloud_complete :: Str -> [llm_cloud] Result[Str, Str]
3047            fields.insert("cloud_complete".into(), Ty::function(
3048                vec![Ty::str()],
3049                EffectSet::singleton("llm_cloud"),
3050                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3051            ));
3052            // send_a2a :: (Str, Str) -> [a2a] Result[Str, Str]
3053            //              peer payload                   reply
3054            fields.insert("send_a2a".into(), Ty::function(
3055                vec![Ty::str(), Ty::str()],
3056                EffectSet::singleton("a2a"),
3057                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3058            ));
3059            // call_mcp :: (Str, Str, Str) -> [mcp] Result[Str, Str]
3060            //              server tool args_json         result_json
3061            fields.insert("call_mcp".into(), Ty::function(
3062                vec![Ty::str(), Ty::str(), Ty::str()],
3063                EffectSet::singleton("mcp"),
3064                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3065            ));
3066            // cloud_stream :: Str -> [llm_cloud] Result[Stream[Str], Str]
3067            // (#305 slice 3). Streaming counterpart to cloud_complete.
3068            // The result is `Result[Stream[Str], Str]` rather than a
3069            // bare Stream so transport errors surface synchronously
3070            // at handshake time; per-chunk errors collapse the
3071            // stream to early termination.
3072            fields.insert("cloud_stream".into(), Ty::function(
3073                vec![Ty::str()],
3074                EffectSet::singleton("llm_cloud"),
3075                Ty::Con("Result".into(), vec![
3076                    Ty::Con("Stream".into(), vec![Ty::str()]),
3077                    Ty::str(),
3078                ]),
3079            ));
3080            Some(Ty::Record(fields))
3081        }
3082        "stream" => {
3083            // #305 slice 3: opaque consumer-side operations on
3084            // `Stream[T]`. Producers live elsewhere (`agent.cloud_stream`
3085            // for now); future producers (`http.get_stream`, etc.)
3086            // will register the same Stream[T] surface.
3087            let mut fields = IndexMap::new();
3088            // next :: Stream[T] -> [stream] Option[T]
3089            // One pull. `None` signals end-of-stream (consumed by
3090            // the producer's lazy generator).
3091            fields.insert("next".into(), Ty::function(
3092                vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
3093                EffectSet::singleton("stream"),
3094                Ty::Con("Option".into(), vec![Ty::Var(0)]),
3095            ));
3096            // collect :: Stream[T] -> [stream] List[T]
3097            // Drain to a list. Eager; blocks until the producer
3098            // signals end-of-stream.
3099            fields.insert("collect".into(), Ty::function(
3100                vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
3101                EffectSet::singleton("stream"),
3102                Ty::List(Box::new(Ty::Var(0))),
3103            ));
3104            Some(Ty::Record(fields))
3105        }
3106        // -- std.decimal (#574): exact decimal arithmetic with explicit rounding.
3107        // `Decimal = { coefficient :: Int, exponent :: Int }` where the value
3108        // is `coefficient × 10^exponent`.  All arithmetic is exact (no IEEE 754
3109        // approximation); rounding only happens at `round_to`, which demands an
3110        // explicit mode string ("HalfUp" | "HalfDown" | "HalfEven" |
3111        // "Down" | "Up" | "Ceiling" | "Floor").
3112        "decimal" => {
3113            // Local helper: the Decimal record type.
3114            let decimal_ty = || {
3115                let mut f = IndexMap::new();
3116                f.insert("coefficient".into(), Ty::int());
3117                f.insert("exponent".into(), Ty::int());
3118                Ty::Record(f)
3119            };
3120            let mut fields = IndexMap::new();
3121            // Constructors
3122            // decimal :: (Int, Int) -> Decimal — coefficient, exponent
3123            fields.insert("decimal".into(), Ty::function(
3124                vec![Ty::int(), Ty::int()], EffectSet::empty(), decimal_ty()));
3125            // zero :: () -> Decimal — 0 × 10^0
3126            fields.insert("zero".into(), Ty::function(
3127                vec![], EffectSet::empty(), decimal_ty()));
3128            // one :: () -> Decimal — 1 × 10^0
3129            fields.insert("one".into(), Ty::function(
3130                vec![], EffectSet::empty(), decimal_ty()));
3131            // from_int :: Int -> Decimal — lift integer, exponent=0
3132            fields.insert("from_int".into(), Ty::function(
3133                vec![Ty::int()], EffectSet::empty(), decimal_ty()));
3134            // Arithmetic — all exact, no rounding
3135            // add :: (Decimal, Decimal) -> Decimal
3136            fields.insert("add".into(), Ty::function(
3137                vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3138            // sub :: (Decimal, Decimal) -> Decimal
3139            fields.insert("sub".into(), Ty::function(
3140                vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3141            // mul :: (Decimal, Decimal) -> Decimal — exponents add
3142            fields.insert("mul".into(), Ty::function(
3143                vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3144            // Comparison — three-way: -1 / 0 / 1
3145            // compare :: (Decimal, Decimal) -> Int
3146            fields.insert("compare".into(), Ty::function(
3147                vec![decimal_ty(), decimal_ty()], EffectSet::empty(), Ty::int()));
3148            // Predicates
3149            fields.insert("is_zero".into(), Ty::function(
3150                vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3151            fields.insert("is_positive".into(), Ty::function(
3152                vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3153            fields.insert("is_negative".into(), Ty::function(
3154                vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3155            // Transformers
3156            // normalize :: Decimal -> Decimal — remove trailing zeros
3157            fields.insert("normalize".into(), Ty::function(
3158                vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3159            // negate :: Decimal -> Decimal
3160            fields.insert("negate".into(), Ty::function(
3161                vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3162            // abs :: Decimal -> Decimal
3163            fields.insert("abs".into(), Ty::function(
3164                vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3165            // round_to :: (Decimal, Int, Str) -> Decimal
3166            //   target_exp: the exponent to round to (e.g. -2 → 2 decimal places)
3167            //   mode: "HalfUp" | "HalfDown" | "HalfEven" | "Down" | "Up" | "Ceiling" | "Floor"
3168            fields.insert("round_to".into(), Ty::function(
3169                vec![decimal_ty(), Ty::int(), Ty::str()],
3170                EffectSet::empty(), decimal_ty()));
3171            // to_str :: Decimal -> Str — decimal notation, e.g. "123.45"
3172            fields.insert("to_str".into(), Ty::function(
3173                vec![decimal_ty()], EffectSet::empty(), Ty::str()));
3174            // pow10 :: Int -> Int — 10^n; n must be in [0, 18]
3175            fields.insert("pow10".into(), Ty::function(
3176                vec![Ty::int()], EffectSet::empty(), Ty::int()));
3177            Some(Ty::Record(fields))
3178        }
3179        _ => None,
3180    }
3181}
3182
3183/// Resolve `import "std.foo" as alias` to a module name (e.g. "io").
3184pub fn module_for_import(reference: &str) -> Option<&'static str> {
3185    let suffix = reference.strip_prefix("std.")?;
3186    Some(match suffix {
3187        "io" => "io",
3188        "str" => "str",
3189        "int" => "int",
3190        "float" => "float",
3191        "list" => "list",
3192        "result" => "result",
3193        "option" => "option",
3194        "json" => "json",
3195        "flow" => "flow",
3196        "tuple" => "tuple",
3197        "time" => "time",
3198        "rand" => "rand",
3199        "random" => "random",
3200        "env" => "env",
3201        "bytes" => "bytes",
3202        "net" => "net",
3203        "tls" => "tls",
3204        "chat" => "chat",
3205        "math" => "math",
3206        "map" => "map",
3207        "set" => "set",
3208        "iter" => "iter",
3209        "crypto" => "crypto",
3210        "regex" => "regex",
3211        "parser" => "parser",
3212        "deque" => "deque",
3213        "kv" => "kv",
3214        "sql" => "sql",
3215        "fs" => "fs",
3216        "process" => "process",
3217        "datetime" => "datetime",
3218        "duration" => "duration",
3219        "log" => "log",
3220        "http" => "http",
3221        "toml" => "toml",
3222        "yaml" => "yaml",
3223        "dotenv" => "dotenv",
3224        "csv" => "csv",
3225        "test" => "test",
3226        "agent" => "agent",
3227        "cli" => "cli",
3228        "stream" => "stream",
3229        "conc" => "conc",
3230        "arrow" => "arrow",
3231        "df" => "df",
3232        "redis" => "redis",
3233        "decimal" => "decimal",
3234        _ => return None,
3235    })
3236}