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        "vcs" => {
2322            // Content-addressed blob store (#5 / M6.1b). `put_blob` returns the
2323            // lowercase hex SHA-256 of the content — the SAME id as
2324            // `crypto.sha256_str`, so blobs are interchangeable with loom's
2325            // SQLite-backed artifacts by id. `ref_set`/`ref_get` bind a name
2326            // (namespace + key) to a blob sha, e.g. namespace "loom/sprint-{id}",
2327            // key = node id (branch-per-sprint, #5). The on-disk layout matches
2328            // lex-store's blob CAS (<root>/blobs, <root>/blobrefs).
2329            //
2330            // fs_write/fs_read appear in the effect rows so `lex audit` sees the
2331            // disk touch; the store root is internal (~/.lex/store or
2332            // $LEX_STORE_ROOT) so no user-path allowlist applies.
2333            let mut fields = IndexMap::new();
2334            let vcs_w = || EffectSet {
2335                concrete: [crate::types::EffectKind::bare("vcs"),
2336                           crate::types::EffectKind::bare("fs_write")]
2337                    .into_iter().collect(),
2338                var: None,
2339            };
2340            let vcs_r = || EffectSet {
2341                concrete: [crate::types::EffectKind::bare("vcs"),
2342                           crate::types::EffectKind::bare("fs_read")]
2343                    .into_iter().collect(),
2344                var: None,
2345            };
2346            let res = |ok: Ty| Ty::Con("Result".into(), vec![ok, Ty::str()]);
2347
2348            // put_blob :: Str -> [vcs, fs_write] Result[Str, Str]  (returns sha)
2349            fields.insert("put_blob".into(), Ty::function(
2350                vec![Ty::str()], vcs_w(), res(Ty::str())));
2351            // get_blob :: Str -> [vcs, fs_read] Result[Str, Str]
2352            fields.insert("get_blob".into(), Ty::function(
2353                vec![Ty::str()], vcs_r(), res(Ty::str())));
2354            // has_blob :: Str -> [vcs, fs_read] Bool
2355            fields.insert("has_blob".into(), Ty::function(
2356                vec![Ty::str()], vcs_r(), Ty::bool()));
2357            // ref_set :: Str, Str, Str -> [vcs, fs_write] Result[Unit, Str]
2358            fields.insert("ref_set".into(), Ty::function(
2359                vec![Ty::str(), Ty::str(), Ty::str()], vcs_w(), res(Ty::Unit)));
2360            // ref_get :: Str, Str -> [vcs, fs_read] Result[Str, Str]  (key -> sha)
2361            fields.insert("ref_get".into(), Ty::function(
2362                vec![Ty::str(), Ty::str()], vcs_r(), res(Ty::str())));
2363            Some(Ty::Record(fields))
2364        }
2365        "sql" => {
2366            // Embedded SQL (SQLite via rusqlite). The opaque `Db` type is
2367            // backed by an Int handle into a process-wide registry (#362).
2368            //
2369            // Params use the typed `SqlParam` ADT (PStr|PInt|PFloat|PBool|PNull)
2370            // registered in env.rs, so callers don't have to stringify values.
2371            //
2372            // Transactions: sql.begin(db) → SqlTx; sql.commit/rollback(tx).
2373            // exec_tx / query_tx mirror exec / query but operate on a SqlTx.
2374            //
2375            // Row decoders: get_str / get_int / get_float / get_bool extract
2376            // typed columns from a row record by name.
2377            let db_t  = || Ty::Con("Db".into(), vec![]);
2378            let tx_t  = || Ty::Con("SqlTx".into(), vec![]);
2379            let sp_t  = || Ty::Con("SqlParam".into(), vec![]);
2380            let params_t = || Ty::List(Box::new(sp_t()));
2381            let mut fields = IndexMap::new();
2382
2383            // SqlError = { message, code, detail } — populated with
2384            // SQLSTATE (Postgres) or symbolic SQLite error name (#380).
2385            let se_t = || Ty::Con("SqlError".into(), vec![]);
2386
2387            // open :: Str -> [sql, fs_write] Result[Db, SqlError]
2388            fields.insert("open".into(), Ty::function(
2389                vec![Ty::str()],
2390                EffectSet {
2391                    concrete: [crate::types::EffectKind::bare("sql"),
2392                               crate::types::EffectKind::bare("fs_write")]
2393                        .into_iter().collect(),
2394                    var: None,
2395                },
2396                Ty::Con("Result".into(), vec![db_t(), se_t()])));
2397
2398            // close :: Db -> [sql] Unit
2399            fields.insert("close".into(), Ty::function(
2400                vec![db_t()],
2401                EffectSet::singleton("sql"),
2402                Ty::Unit));
2403
2404            // exec :: Db, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
2405            fields.insert("exec".into(), Ty::function(
2406                vec![db_t(), Ty::str(), params_t()],
2407                EffectSet::singleton("sql"),
2408                Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
2409
2410            // query[T] :: Db, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
2411            fields.insert("query".into(), Ty::function(
2412                vec![db_t(), Ty::str(), params_t()],
2413                EffectSet::singleton("sql"),
2414                Ty::Con("Result".into(), vec![
2415                    Ty::List(Box::new(Ty::Var(0))),
2416                    se_t(),
2417                ])));
2418
2419            // query_iter[T] :: Db, Str, List[SqlParam] -> [sql] Result[Iter[T], SqlError]
2420            // Streaming variant of `query` (#379). Rows are pulled from
2421            // the server one at a time via an mpsc-backed cursor —
2422            // memory stays bounded regardless of result-set size.
2423            // Other ops on the same `Db` handle block until the cursor
2424            // is drained (single connection per Db).
2425            fields.insert("query_iter".into(), Ty::function(
2426                vec![db_t(), Ty::str(), params_t()],
2427                EffectSet::singleton("sql"),
2428                Ty::Con("Result".into(), vec![
2429                    Ty::Con("Iter".into(), vec![Ty::Var(0)]),
2430                    se_t(),
2431                ])));
2432
2433            // begin :: Db -> [sql] Result[SqlTx, SqlError]
2434            fields.insert("begin".into(), Ty::function(
2435                vec![db_t()],
2436                EffectSet::singleton("sql"),
2437                Ty::Con("Result".into(), vec![tx_t(), se_t()])));
2438
2439            // commit :: SqlTx -> [sql] Result[Unit, SqlError]
2440            fields.insert("commit".into(), Ty::function(
2441                vec![tx_t()],
2442                EffectSet::singleton("sql"),
2443                Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
2444
2445            // rollback :: SqlTx -> [sql] Result[Unit, SqlError]
2446            fields.insert("rollback".into(), Ty::function(
2447                vec![tx_t()],
2448                EffectSet::singleton("sql"),
2449                Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
2450
2451            // exec_tx :: SqlTx, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
2452            fields.insert("exec_tx".into(), Ty::function(
2453                vec![tx_t(), Ty::str(), params_t()],
2454                EffectSet::singleton("sql"),
2455                Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
2456
2457            // query_tx[T] :: SqlTx, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
2458            fields.insert("query_tx".into(), Ty::function(
2459                vec![tx_t(), Ty::str(), params_t()],
2460                EffectSet::singleton("sql"),
2461                Ty::Con("Result".into(), vec![
2462                    Ty::List(Box::new(Ty::Var(0))),
2463                    se_t(),
2464                ])));
2465
2466            // Row decoders: get_X[T] :: T, Str -> Option[X]
2467            // T is polymorphic so these work on any row record shape.
2468            fields.insert("get_str".into(), Ty::function(
2469                vec![Ty::Var(0), Ty::str()],
2470                EffectSet::empty(),
2471                Ty::Con("Option".into(), vec![Ty::str()])));
2472            fields.insert("get_int".into(), Ty::function(
2473                vec![Ty::Var(0), Ty::str()],
2474                EffectSet::empty(),
2475                Ty::Con("Option".into(), vec![Ty::int()])));
2476            fields.insert("get_float".into(), Ty::function(
2477                vec![Ty::Var(0), Ty::str()],
2478                EffectSet::empty(),
2479                Ty::Con("Option".into(), vec![Ty::float()])));
2480            fields.insert("get_bool".into(), Ty::function(
2481                vec![Ty::Var(0), Ty::str()],
2482                EffectSet::empty(),
2483                Ty::Con("Option".into(), vec![Ty::bool()])));
2484
2485            Some(Ty::Record(fields))
2486        }
2487        "redis" => {
2488            // Thin Redis client (#533). ConnRedis is an opaque handle backed by a
2489            // process-wide registry (same pattern as Db in std.sql). All ops carry
2490            // [net] — Redis is a TCP service; no separate [redis] effect.
2491            //
2492            // subscribe / psubscribe return Unit because they are blocking
2493            // infinite loops, consistent with net.serve_fn and ws.serve.
2494            //
2495            // subscribe/psubscribe open a *dedicated* connection internally —
2496            // Redis disallows non-Pub/Sub commands on a subscribed connection.
2497            let conn_t = || Ty::Con("ConnRedis".into(), vec![]);
2498            let mut fields = IndexMap::new();
2499
2500            // connect :: Str -> [net] Result[ConnRedis, Str]
2501            // url: "redis://host:6379" or "rediss://host:6380" (TLS)
2502            fields.insert("connect".into(), Ty::function(
2503                vec![Ty::str()],
2504                EffectSet::singleton("net"),
2505                Ty::Con("Result".into(), vec![conn_t(), Ty::str()])));
2506
2507            // close :: ConnRedis -> [net] Unit
2508            fields.insert("close".into(), Ty::function(
2509                vec![conn_t()],
2510                EffectSet::singleton("net"),
2511                Ty::Unit));
2512
2513            // ---- Key-value -----------------------------------------------
2514
2515            // get :: ConnRedis, Str -> [net] Option[Str]
2516            fields.insert("get".into(), Ty::function(
2517                vec![conn_t(), Ty::str()],
2518                EffectSet::singleton("net"),
2519                Ty::Con("Option".into(), vec![Ty::str()])));
2520
2521            // set :: ConnRedis, Str, Str -> [net] Unit
2522            fields.insert("set".into(), Ty::function(
2523                vec![conn_t(), Ty::str(), Ty::str()],
2524                EffectSet::singleton("net"),
2525                Ty::Unit));
2526
2527            // set_ex :: ConnRedis, Str, Str, Int -> [net] Unit
2528            fields.insert("set_ex".into(), Ty::function(
2529                vec![conn_t(), Ty::str(), Ty::str(), Ty::int()],
2530                EffectSet::singleton("net"),
2531                Ty::Unit));
2532
2533            // del :: ConnRedis, Str -> [net] Unit
2534            fields.insert("del".into(), Ty::function(
2535                vec![conn_t(), Ty::str()],
2536                EffectSet::singleton("net"),
2537                Ty::Unit));
2538
2539            // exists :: ConnRedis, Str -> [net] Bool
2540            fields.insert("exists".into(), Ty::function(
2541                vec![conn_t(), Ty::str()],
2542                EffectSet::singleton("net"),
2543                Ty::bool()));
2544
2545            // expire :: ConnRedis, Str, Int -> [net] Unit
2546            fields.insert("expire".into(), Ty::function(
2547                vec![conn_t(), Ty::str(), Ty::int()],
2548                EffectSet::singleton("net"),
2549                Ty::Unit));
2550
2551            // ---- Pub/Sub -------------------------------------------------
2552
2553            // publish :: ConnRedis, Str, Str -> [net] Int
2554            // Returns the number of subscribers that received the message.
2555            fields.insert("publish".into(), Ty::function(
2556                vec![conn_t(), Ty::str(), Ty::str()],
2557                EffectSet::singleton("net"),
2558                Ty::int()));
2559
2560            // subscribe :: ConnRedis, Str, (Str, Str ->[E] Unit) -> [net] Unit
2561            // Blocking loop; handler receives (channel, message) on each message.
2562            // Uses a dedicated connection — Redis disallows non-Pub/Sub commands
2563            // on a subscribed connection. Handler carries an open effect row so
2564            // callers can use io, net, sql, etc. inside the closure.
2565            let handler2 = Ty::function(
2566                vec![Ty::str(), Ty::str()],
2567                EffectSet::open_var(0),
2568                Ty::Unit);
2569            fields.insert("subscribe".into(), Ty::function(
2570                vec![conn_t(), Ty::str(), handler2],
2571                EffectSet::singleton("net"),
2572                Ty::Unit));  // Unit
2573
2574            // psubscribe :: ConnRedis, Str, (Str, Str, Str ->[E] Unit) -> [net] Unit
2575            // Pattern-subscribe; handler receives (pattern, channel, message).
2576            // Handler carries an open effect row (same rationale as subscribe).
2577            let handler3 = Ty::function(
2578                vec![Ty::str(), Ty::str(), Ty::str()],
2579                EffectSet::open_var(1),
2580                Ty::Unit);
2581            fields.insert("psubscribe".into(), Ty::function(
2582                vec![conn_t(), Ty::str(), handler3],
2583                EffectSet::singleton("net"),
2584                Ty::Unit));  // Unit
2585
2586            // ---- List ----------------------------------------------------
2587
2588            // lpush :: ConnRedis, Str, Str -> [net] Int
2589            fields.insert("lpush".into(), Ty::function(
2590                vec![conn_t(), Ty::str(), Ty::str()],
2591                EffectSet::singleton("net"),
2592                Ty::int()));
2593
2594            // rpush :: ConnRedis, Str, Str -> [net] Int
2595            fields.insert("rpush".into(), Ty::function(
2596                vec![conn_t(), Ty::str(), Ty::str()],
2597                EffectSet::singleton("net"),
2598                Ty::int()));
2599
2600            // brpop :: ConnRedis, Str, Int -> [net] Option[Str]
2601            // Blocking right-pop; returns None on timeout. timeout=0 blocks
2602            // indefinitely (the runtime does not treat this as a hung effect).
2603            fields.insert("brpop".into(), Ty::function(
2604                vec![conn_t(), Ty::str(), Ty::int()],
2605                EffectSet::singleton("net"),
2606                Ty::Con("Option".into(), vec![Ty::str()])));
2607
2608            // llen :: ConnRedis, Str -> [net] Int
2609            fields.insert("llen".into(), Ty::function(
2610                vec![conn_t(), Ty::str()],
2611                EffectSet::singleton("net"),
2612                Ty::int()));
2613
2614            // ---- Hash ----------------------------------------------------
2615
2616            // hset :: ConnRedis, Str, Str, Str -> [net] Unit
2617            fields.insert("hset".into(), Ty::function(
2618                vec![conn_t(), Ty::str(), Ty::str(), Ty::str()],
2619                EffectSet::singleton("net"),
2620                Ty::Unit));
2621
2622            // hget :: ConnRedis, Str, Str -> [net] Option[Str]
2623            fields.insert("hget".into(), Ty::function(
2624                vec![conn_t(), Ty::str(), Ty::str()],
2625                EffectSet::singleton("net"),
2626                Ty::Con("Option".into(), vec![Ty::str()])));
2627
2628            // hdel :: ConnRedis, Str, Str -> [net] Unit
2629            fields.insert("hdel".into(), Ty::function(
2630                vec![conn_t(), Ty::str(), Ty::str()],
2631                EffectSet::singleton("net"),
2632                Ty::Unit));
2633
2634            // hgetall :: ConnRedis, Str -> [net] List[(Str, Str)]
2635            fields.insert("hgetall".into(), Ty::function(
2636                vec![conn_t(), Ty::str()],
2637                EffectSet::singleton("net"),
2638                Ty::List(Box::new(Ty::Tuple(vec![Ty::str(), Ty::str()])))));
2639
2640            Some(Ty::Record(fields))
2641        }
2642        "parser" => {
2643            // #217: structured parser combinators. Parser values are
2644            // tagged Records at runtime (`{ kind, ... }`), opaque at
2645            // the language level via `Ty::Con("Parser", [T])`.
2646            //
2647            // Surface:
2648            //   - primitives: char, string, digit, alpha, whitespace, eof
2649            //   - combinators: seq, alt, many, optional, map, and_then
2650            //   - run :: Parser[T], Str -> Result[T, ParseErr]
2651            //
2652            // `map` and `and_then` were deferred from #217's v1 because
2653            // their closure arguments carried call-site identity that
2654            // broke the canonical-parsers acceptance criterion. With
2655            // closure body-hash equality landed in #222, that concern
2656            // is gone, and #221 wires them in. The interpreter for
2657            // `parser.run` has been moved to `lex-bytecode::parser_runtime`
2658            // so it can invoke closures from `Map` / `AndThen` nodes.
2659            let pt = |t: Ty| Ty::Con("Parser".into(), vec![t]);
2660            let parse_err = || {
2661                let mut fs = IndexMap::new();
2662                fs.insert("pos".into(), Ty::int());
2663                fs.insert("message".into(), Ty::str());
2664                Ty::Record(fs)
2665            };
2666            let mut fields = IndexMap::new();
2667            // char :: Str -> Parser[Str] (single-char Str literal)
2668            fields.insert("char".into(), Ty::function(
2669                vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
2670            // string :: Str -> Parser[Str]
2671            fields.insert("string".into(), Ty::function(
2672                vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
2673            // digit :: () -> Parser[Str]
2674            fields.insert("digit".into(), Ty::function(
2675                vec![], EffectSet::empty(), pt(Ty::str())));
2676            // alpha :: () -> Parser[Str]
2677            fields.insert("alpha".into(), Ty::function(
2678                vec![], EffectSet::empty(), pt(Ty::str())));
2679            // whitespace :: () -> Parser[Str]
2680            fields.insert("whitespace".into(), Ty::function(
2681                vec![], EffectSet::empty(), pt(Ty::str())));
2682            // eof :: () -> Parser[Unit]
2683            fields.insert("eof".into(), Ty::function(
2684                vec![], EffectSet::empty(), pt(Ty::Unit)));
2685            // seq :: Parser[A], Parser[B] -> Parser[(A, B)]
2686            fields.insert("seq".into(), Ty::function(
2687                vec![pt(Ty::Var(0)), pt(Ty::Var(1))],
2688                EffectSet::empty(),
2689                pt(Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]))));
2690            // alt :: Parser[T], Parser[T] -> Parser[T]
2691            // PEG-style ordered choice: the second alternative is
2692            // tried only if the first fails.
2693            fields.insert("alt".into(), Ty::function(
2694                vec![pt(Ty::Var(0)), pt(Ty::Var(0))],
2695                EffectSet::empty(),
2696                pt(Ty::Var(0))));
2697            // many :: Parser[T] -> Parser[List[T]]
2698            // Zero-or-more. Stops as soon as the inner parser fails
2699            // OR doesn't advance the position (avoids infinite loop
2700            // on empty matches).
2701            fields.insert("many".into(), Ty::function(
2702                vec![pt(Ty::Var(0))],
2703                EffectSet::empty(),
2704                pt(Ty::List(Box::new(Ty::Var(0))))));
2705            // optional :: Parser[T] -> Parser[Option[T]]
2706            fields.insert("optional".into(), Ty::function(
2707                vec![pt(Ty::Var(0))],
2708                EffectSet::empty(),
2709                pt(Ty::Con("Option".into(), vec![Ty::Var(0)]))));
2710            // map :: Parser[T], (T) -> [E] U -> [E] Parser[U]
2711            // The closure runs at parse time when the Parser is run.
2712            // Effect-polymorphic on the closure: any effect the
2713            // closure declares propagates to the surrounding `run`.
2714            fields.insert("map".into(), Ty::function(
2715                vec![
2716                    pt(Ty::Var(0)),
2717                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
2718                ],
2719                EffectSet::open_var(2),
2720                pt(Ty::Var(1))));
2721            // and_then :: Parser[T], (T) -> [E] Parser[U] -> [E] Parser[U]
2722            // Monadic bind: closure inspects the parsed value and
2723            // returns the next parser to run.
2724            fields.insert("and_then".into(), Ty::function(
2725                vec![
2726                    pt(Ty::Var(0)),
2727                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
2728                        pt(Ty::Var(1))),
2729                ],
2730                EffectSet::open_var(3),
2731                pt(Ty::Var(1))));
2732            // run :: Parser[T], Str -> Result[T, ParseErr]
2733            // ParseErr = { pos :: Int, message :: Str }
2734            fields.insert("run".into(), Ty::function(
2735                vec![pt(Ty::Var(0)), Ty::str()],
2736                EffectSet::empty(),
2737                Ty::Con("Result".into(), vec![Ty::Var(0), parse_err()])));
2738            Some(Ty::Record(fields))
2739        }
2740        "cli" => {
2741            // #224 Rubric port: argparse-equivalent for end-user
2742            // programs. Spec values are tagged `Json` records (opaque
2743            // to the language but inspectable). Construction via the
2744            // `flag` / `option` / `positional` / `spec` builders;
2745            // parse + introspection / help via the remaining ops.
2746            let json = || Ty::Con("Json".into(), vec![]);
2747            let opt_str = || Ty::Con("Option".into(), vec![Ty::str()]);
2748            let mut fields = IndexMap::new();
2749            // flag :: Str -> Option[Str] -> Str -> Json
2750            //   long_name -> short -> help -> CliArg
2751            fields.insert("flag".into(), Ty::function(
2752                vec![Ty::str(), opt_str(), Ty::str()],
2753                EffectSet::empty(),
2754                json()));
2755            // option :: Str -> Option[Str] -> Str -> Option[Str] -> Json
2756            //   long_name -> short -> help -> default -> CliArg
2757            fields.insert("option".into(), Ty::function(
2758                vec![Ty::str(), opt_str(), Ty::str(), opt_str()],
2759                EffectSet::empty(),
2760                json()));
2761            // positional :: Str -> Str -> Bool -> Json
2762            //   name -> help -> required -> CliArg
2763            fields.insert("positional".into(), Ty::function(
2764                vec![Ty::str(), Ty::str(), Ty::bool()],
2765                EffectSet::empty(),
2766                json()));
2767            // spec :: Str -> Str -> List[Json] -> List[Json] -> Json
2768            //   name -> help -> args -> subcommands -> CliSpec
2769            fields.insert("spec".into(), Ty::function(
2770                vec![Ty::str(), Ty::str(),
2771                     Ty::List(Box::new(json())),
2772                     Ty::List(Box::new(json()))],
2773                EffectSet::empty(),
2774                json()));
2775            // parse :: Json -> List[Str] -> Result[Json, Str]
2776            //   spec -> argv -> Result[CliParsed, error]
2777            fields.insert("parse".into(), Ty::function(
2778                vec![json(), Ty::List(Box::new(Ty::str()))],
2779                EffectSet::empty(),
2780                Ty::Con("Result".into(), vec![json(), Ty::str()])));
2781            // envelope :: Bool -> Str -> T -> Json
2782            //   ok -> command -> data -> ACLI-shaped envelope.
2783            // `data` is polymorphic so callers don't have to round-
2784            // trip through `json.parse` for trivial payloads.
2785            fields.insert("envelope".into(), Ty::function(
2786                vec![Ty::bool(), Ty::str(), Ty::Var(0)],
2787                EffectSet::empty(),
2788                json()));
2789            // describe :: Json -> Json — machine-readable spec dump
2790            fields.insert("describe".into(), Ty::function(
2791                vec![json()],
2792                EffectSet::empty(),
2793                json()));
2794            // help :: Json -> Str — human-readable help text
2795            fields.insert("help".into(), Ty::function(
2796                vec![json()],
2797                EffectSet::empty(),
2798                Ty::str()));
2799            Some(Ty::Record(fields))
2800        }
2801        "regex" => {
2802            // The compiled `Regex` is stored as a `Str` at runtime
2803            // (the pattern source) plus a process-wide cache of the
2804            // actual `regex::Regex`. So `Regex` is a nominal type at
2805            // the language level but its value is just the pattern.
2806            let regex_t = || Ty::Con("Regex".into(), vec![]);
2807            let match_t = || {
2808                let mut fs = IndexMap::new();
2809                fs.insert("text".into(), Ty::str());
2810                fs.insert("start".into(), Ty::int());
2811                fs.insert("end".into(), Ty::int());
2812                fs.insert("groups".into(), Ty::List(Box::new(Ty::str())));
2813                Ty::Record(fs)
2814            };
2815            let mut fields = IndexMap::new();
2816            // compile :: Str -> Result[Regex, Str]
2817            fields.insert("compile".into(), Ty::function(
2818                vec![Ty::str()], EffectSet::empty(),
2819                Ty::Con("Result".into(), vec![regex_t(), Ty::str()])));
2820            // is_match :: Regex, Str -> Bool
2821            fields.insert("is_match".into(), Ty::function(
2822                vec![regex_t(), Ty::str()], EffectSet::empty(), Ty::bool()));
2823            // is_match_str :: Str, Str -> Bool
2824            // Compiles the first argument as a pattern and matches against the second.
2825            // Returns false on invalid pattern instead of propagating an error.
2826            fields.insert("is_match_str".into(), Ty::function(
2827                vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
2828            // find :: Regex, Str -> Option[Match]
2829            fields.insert("find".into(), Ty::function(
2830                vec![regex_t(), Ty::str()], EffectSet::empty(),
2831                Ty::Con("Option".into(), vec![match_t()])));
2832            // find_all :: Regex, Str -> List[Match]
2833            fields.insert("find_all".into(), Ty::function(
2834                vec![regex_t(), Ty::str()], EffectSet::empty(),
2835                Ty::List(Box::new(match_t()))));
2836            // replace :: Regex, Str, Str -> Str
2837            fields.insert("replace".into(), Ty::function(
2838                vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
2839            // replace_all :: Regex, Str, Str -> Str
2840            fields.insert("replace_all".into(), Ty::function(
2841                vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
2842            // split :: Regex, Str -> List[Str]
2843            fields.insert("split".into(), Ty::function(
2844                vec![regex_t(), Ty::str()], EffectSet::empty(),
2845                Ty::List(Box::new(Ty::str()))));
2846            Some(Ty::Record(fields))
2847        }
2848        "http" => {
2849            // Rich HTTP client. `[net]` for the wire ops, pure for
2850            // the builders / decoders. `--allow-net-host` gates per
2851            // request. Multipart upload + streaming response bodies
2852            // are deferred to v1.5; the v1 surface covers the
2853            // common cases (auth, headers, query, timeouts, JSON /
2854            // text decoding).
2855            let req_t  = || Ty::Con("HttpRequest".into(), vec![]);
2856            let resp_t = || Ty::Con("HttpResponse".into(), vec![]);
2857            let err_t  = || Ty::Con("HttpError".into(), vec![]);
2858            let result_he = |t: Ty| Ty::Con("Result".into(), vec![t, err_t()]);
2859            let str_str_map = || Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]);
2860            let mut fields = IndexMap::new();
2861            // -- wire ops (effectful) --
2862            // send :: HttpRequest -> [net] Result[HttpResponse, HttpError]
2863            fields.insert("send".into(), Ty::function(
2864                vec![req_t()],
2865                EffectSet::singleton("net"),
2866                result_he(resp_t()),
2867            ));
2868            // get :: Str -> [net] Result[HttpResponse, HttpError]
2869            fields.insert("get".into(), Ty::function(
2870                vec![Ty::str()],
2871                EffectSet::singleton("net"),
2872                result_he(resp_t()),
2873            ));
2874            // post :: Str, Bytes, Str -> [net] Result[HttpResponse, HttpError]
2875            fields.insert("post".into(), Ty::function(
2876                vec![Ty::str(), Ty::bytes(), Ty::str()],
2877                EffectSet::singleton("net"),
2878                result_he(resp_t()),
2879            ));
2880            // -- pure builders (record transforms) --
2881            // with_header :: HttpRequest, Str, Str -> HttpRequest
2882            fields.insert("with_header".into(), Ty::function(
2883                vec![req_t(), Ty::str(), Ty::str()],
2884                EffectSet::empty(),
2885                req_t(),
2886            ));
2887            // with_auth :: HttpRequest, Str, Str -> HttpRequest
2888            // (Renders `<scheme> <token>` into the `Authorization`
2889            // header — `Bearer <jwt>`, `Basic <b64>`, etc.)
2890            fields.insert("with_auth".into(), Ty::function(
2891                vec![req_t(), Ty::str(), Ty::str()],
2892                EffectSet::empty(),
2893                req_t(),
2894            ));
2895            // with_query :: HttpRequest, Map[Str, Str] -> HttpRequest
2896            // (Appends a `?k=v&...` query string; values are URL-
2897            // encoded so `&` / `=` / spaces in values don't escape.)
2898            fields.insert("with_query".into(), Ty::function(
2899                vec![req_t(), str_str_map()],
2900                EffectSet::empty(),
2901                req_t(),
2902            ));
2903            // with_timeout_ms :: HttpRequest, Int -> HttpRequest
2904            fields.insert("with_timeout_ms".into(), Ty::function(
2905                vec![req_t(), Ty::int()],
2906                EffectSet::empty(),
2907                req_t(),
2908            ));
2909            // -- pure decoders --
2910            // json_body[T] :: HttpResponse -> Result[T, HttpError]
2911            // Polymorphic on the parsed shape, matching `json.parse`.
2912            fields.insert("json_body".into(), Ty::function(
2913                vec![resp_t()],
2914                EffectSet::empty(),
2915                result_he(Ty::Var(0)),
2916            ));
2917            // text_body :: HttpResponse -> Result[Str, HttpError]
2918            fields.insert("text_body".into(), Ty::function(
2919                vec![resp_t()],
2920                EffectSet::empty(),
2921                result_he(Ty::str()),
2922            ));
2923            // stream_lines :: Str, Map[Str, Str], Str -> [net] Result[Stream[Str], Str]
2924            // Streaming HTTP POST that yields the response body line-by-line
2925            // for SSE / NDJSON endpoints. Returns a lazy `Stream[Str]` (#683):
2926            // each `stream.next` pulls exactly one line off the socket as it
2927            // arrives, so an endpoint that holds the connection open and emits
2928            // events over time is consumed incrementally instead of blocking
2929            // until close. Connection errors at request time surface as
2930            // `Err(Str)`; a mid-stream read error / close ends the stream
2931            // (next `stream.next` returns `None`). Consume with `std.stream`
2932            // (`stream.next` / `stream.collect`), which carries `[stream]`.
2933            fields.insert("stream_lines".into(), Ty::function(
2934                vec![
2935                    Ty::str(),
2936                    Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
2937                    Ty::str(),
2938                ],
2939                EffectSet::singleton("net"),
2940                Ty::Con("Result".into(), vec![
2941                    Ty::Con("Stream".into(), vec![Ty::str()]),
2942                    Ty::str(),
2943                ]),
2944            ));
2945            Some(Ty::Record(fields))
2946        }
2947        "yaml" => {
2948            // YAML config parser. Same shape as `std.toml`: parse
2949            // is polymorphic, output Value layout matches std.json
2950            // (Str/Int/Float/Bool/List/Record). Anchors and tags
2951            // are flattened by serde_yaml's deserializer.
2952            let mut fields = IndexMap::new();
2953            fields.insert("parse".into(), Ty::function(
2954                vec![Ty::str()], EffectSet::empty(),
2955                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
2956            ));
2957            // Tactical fix for #168 — caller-supplied required-field
2958            // list. See std.json's parse_strict for context.
2959            fields.insert("parse_strict".into(), Ty::function(
2960                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
2961                EffectSet::empty(),
2962                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
2963            ));
2964            fields.insert("stringify".into(), Ty::function(
2965                vec![Ty::Var(0)], EffectSet::empty(),
2966                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2967            ));
2968            Some(Ty::Record(fields))
2969        }
2970        "dotenv" => {
2971            // .env-style files. parse :: Str -> Result[Map[Str,Str], Str].
2972            // Returns a map (not a polymorphic record) because
2973            // dotenv files don't carry shape — every value is a
2974            // string and keys aren't statically known.
2975            let mut fields = IndexMap::new();
2976            fields.insert("parse".into(), Ty::function(
2977                vec![Ty::str()], EffectSet::empty(),
2978                Ty::Con("Result".into(), vec![
2979                    Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
2980                    Ty::str(),
2981                ]),
2982            ));
2983            Some(Ty::Record(fields))
2984        }
2985        "csv" => {
2986            // CSV rows-as-lists. parse :: Str -> Result[List[List[Str]], Str].
2987            // Header awareness is left to the caller — row 0 is
2988            // whatever the file has. A `parse_with_headers` that
2989            // returns List[Map[Str,Str]] is a natural follow-up.
2990            let row_ty = Ty::List(Box::new(Ty::str()));
2991            let rows_ty = Ty::List(Box::new(row_ty.clone()));
2992            let mut fields = IndexMap::new();
2993            fields.insert("parse".into(), Ty::function(
2994                vec![Ty::str()], EffectSet::empty(),
2995                Ty::Con("Result".into(), vec![rows_ty.clone(), Ty::str()]),
2996            ));
2997            fields.insert("stringify".into(), Ty::function(
2998                vec![rows_ty], EffectSet::empty(),
2999                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3000            ));
3001            Some(Ty::Record(fields))
3002        }
3003        "test" => {
3004            // Tiny assertion library (#proposed-stdlib). Each helper
3005            // returns Result[Unit, Str] so a test is itself a fn
3006            // returning Result. Callers compose suites in user code
3007            // (a List of (name, () -> Result[Unit, Str]) pairs +
3008            // list.fold to accumulate verdicts). Property generators
3009            // and a Rust-side Suite type are deferred to v2.
3010            let mut fields = IndexMap::new();
3011            // assert_eq[a, b] :: T -> T -> Result[Unit, Str]
3012            // (T constrained equal by unification on the two args)
3013            let unit_result = || Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]);
3014            fields.insert("assert_eq".into(), Ty::function(
3015                vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
3016            ));
3017            fields.insert("assert_ne".into(), Ty::function(
3018                vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
3019            ));
3020            fields.insert("assert_true".into(), Ty::function(
3021                vec![Ty::bool()], EffectSet::empty(), unit_result(),
3022            ));
3023            fields.insert("assert_false".into(), Ty::function(
3024                vec![Ty::bool()], EffectSet::empty(), unit_result(),
3025            ));
3026            Some(Ty::Record(fields))
3027        }
3028        "toml" => {
3029            // TOML config parser. Mirrors `std.json`'s shape: parse
3030            // is polymorphic so callers annotate the expected
3031            // record / list / scalar shape and the type checker
3032            // unifies. The parsed TOML maps to the same Lex Value
3033            // shape as JSON does:
3034            //
3035            //   TOML String   → Value::Str
3036            //   TOML Integer  → Value::Int
3037            //   TOML Float    → Value::Float
3038            //   TOML Boolean  → Value::Bool
3039            //   TOML Array    → Value::List
3040            //   TOML Table    → Value::Record
3041            //   TOML Datetime → Value::Str (RFC 3339, lossless)
3042            //
3043            // The Datetime → Str fallback is the one info-losing
3044            // step; callers who want a real `Instant` can pipe the
3045            // string through `datetime.parse_iso`.
3046            let mut fields = IndexMap::new();
3047            // parse :: Str -> Result[T, Str]
3048            fields.insert("parse".into(), Ty::function(
3049                vec![Ty::str()], EffectSet::empty(),
3050                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3051            ));
3052            // parse_strict :: (Str, List[Str]) -> Result[T, Str]
3053            // Tactical fix for #168 — caller passes the field
3054            // names T requires; runtime returns Err if any are
3055            // missing from the parsed table instead of letting
3056            // field access panic later.
3057            fields.insert("parse_strict".into(), Ty::function(
3058                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
3059                EffectSet::empty(),
3060                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3061            ));
3062            // stringify :: T -> Result[Str, Str]
3063            // Returns Result (not Str) because not every Lex Value
3064            // has a TOML representation — top-level scalars,
3065            // closures, mixed-key maps etc. surface as Err rather
3066            // than panic.
3067            fields.insert("stringify".into(), Ty::function(
3068                vec![Ty::Var(0)], EffectSet::empty(),
3069                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3070            ));
3071            Some(Ty::Record(fields))
3072        }
3073        // `std.agent` (#184) — runtime primitives whose effects
3074        // separate (a) which LLM surface (`llm_local` vs
3075        // `llm_cloud`), (b) which peer protocol (`a2a`), and
3076        // (c) which tool boundary (`mcp`). The wire formats land
3077        // in downstream crates (`soft-agent`, `soft-a2a`) and
3078        // in #185 for MCP; what's typed here is the boundary
3079        // alone — agent code can be type-checked as
3080        // `[llm_local, a2a]` and will fail if it tries to reach
3081        // `[llm_cloud]` even before the wire layer is finished.
3082        "agent" => {
3083            let mut fields = IndexMap::new();
3084            // local_complete :: Str -> [llm_local] Result[Str, Str]
3085            fields.insert("local_complete".into(), Ty::function(
3086                vec![Ty::str()],
3087                EffectSet::singleton("llm_local"),
3088                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3089            ));
3090            // cloud_complete :: Str -> [llm_cloud] Result[Str, Str]
3091            fields.insert("cloud_complete".into(), Ty::function(
3092                vec![Ty::str()],
3093                EffectSet::singleton("llm_cloud"),
3094                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3095            ));
3096            // send_a2a :: (Str, Str) -> [a2a] Result[Str, Str]
3097            //              peer payload                   reply
3098            fields.insert("send_a2a".into(), Ty::function(
3099                vec![Ty::str(), Ty::str()],
3100                EffectSet::singleton("a2a"),
3101                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3102            ));
3103            // call_mcp :: (Str, Str, Str) -> [mcp] Result[Str, Str]
3104            //              server tool args_json         result_json
3105            fields.insert("call_mcp".into(), Ty::function(
3106                vec![Ty::str(), Ty::str(), Ty::str()],
3107                EffectSet::singleton("mcp"),
3108                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3109            ));
3110            // cloud_stream :: Str -> [llm_cloud] Result[Stream[Str], Str]
3111            // (#305 slice 3). Streaming counterpart to cloud_complete.
3112            // The result is `Result[Stream[Str], Str]` rather than a
3113            // bare Stream so transport errors surface synchronously
3114            // at handshake time; per-chunk errors collapse the
3115            // stream to early termination.
3116            fields.insert("cloud_stream".into(), Ty::function(
3117                vec![Ty::str()],
3118                EffectSet::singleton("llm_cloud"),
3119                Ty::Con("Result".into(), vec![
3120                    Ty::Con("Stream".into(), vec![Ty::str()]),
3121                    Ty::str(),
3122                ]),
3123            ));
3124            Some(Ty::Record(fields))
3125        }
3126        "stream" => {
3127            // #305 slice 3: opaque consumer-side operations on
3128            // `Stream[T]`. Producers live elsewhere (`agent.cloud_stream`
3129            // for now); future producers (`http.get_stream`, etc.)
3130            // will register the same Stream[T] surface.
3131            let mut fields = IndexMap::new();
3132            // next :: Stream[T] -> [stream] Option[T]
3133            // One pull. `None` signals end-of-stream (consumed by
3134            // the producer's lazy generator).
3135            fields.insert("next".into(), Ty::function(
3136                vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
3137                EffectSet::singleton("stream"),
3138                Ty::Con("Option".into(), vec![Ty::Var(0)]),
3139            ));
3140            // collect :: Stream[T] -> [stream] List[T]
3141            // Drain to a list. Eager; blocks until the producer
3142            // signals end-of-stream.
3143            fields.insert("collect".into(), Ty::function(
3144                vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
3145                EffectSet::singleton("stream"),
3146                Ty::List(Box::new(Ty::Var(0))),
3147            ));
3148            Some(Ty::Record(fields))
3149        }
3150        // -- std.decimal (#574): exact decimal arithmetic with explicit rounding.
3151        // `Decimal = { coefficient :: Int, exponent :: Int }` where the value
3152        // is `coefficient × 10^exponent`.  All arithmetic is exact (no IEEE 754
3153        // approximation); rounding only happens at `round_to`, which demands an
3154        // explicit mode string ("HalfUp" | "HalfDown" | "HalfEven" |
3155        // "Down" | "Up" | "Ceiling" | "Floor").
3156        "decimal" => {
3157            // Local helper: the Decimal record type.
3158            let decimal_ty = || {
3159                let mut f = IndexMap::new();
3160                f.insert("coefficient".into(), Ty::int());
3161                f.insert("exponent".into(), Ty::int());
3162                Ty::Record(f)
3163            };
3164            let mut fields = IndexMap::new();
3165            // Constructors
3166            // decimal :: (Int, Int) -> Decimal — coefficient, exponent
3167            fields.insert("decimal".into(), Ty::function(
3168                vec![Ty::int(), Ty::int()], EffectSet::empty(), decimal_ty()));
3169            // zero :: () -> Decimal — 0 × 10^0
3170            fields.insert("zero".into(), Ty::function(
3171                vec![], EffectSet::empty(), decimal_ty()));
3172            // one :: () -> Decimal — 1 × 10^0
3173            fields.insert("one".into(), Ty::function(
3174                vec![], EffectSet::empty(), decimal_ty()));
3175            // from_int :: Int -> Decimal — lift integer, exponent=0
3176            fields.insert("from_int".into(), Ty::function(
3177                vec![Ty::int()], EffectSet::empty(), decimal_ty()));
3178            // Arithmetic — all exact, no rounding
3179            // add :: (Decimal, Decimal) -> Decimal
3180            fields.insert("add".into(), Ty::function(
3181                vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3182            // sub :: (Decimal, Decimal) -> Decimal
3183            fields.insert("sub".into(), Ty::function(
3184                vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3185            // mul :: (Decimal, Decimal) -> Decimal — exponents add
3186            fields.insert("mul".into(), Ty::function(
3187                vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3188            // Comparison — three-way: -1 / 0 / 1
3189            // compare :: (Decimal, Decimal) -> Int
3190            fields.insert("compare".into(), Ty::function(
3191                vec![decimal_ty(), decimal_ty()], EffectSet::empty(), Ty::int()));
3192            // Predicates
3193            fields.insert("is_zero".into(), Ty::function(
3194                vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3195            fields.insert("is_positive".into(), Ty::function(
3196                vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3197            fields.insert("is_negative".into(), Ty::function(
3198                vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3199            // Transformers
3200            // normalize :: Decimal -> Decimal — remove trailing zeros
3201            fields.insert("normalize".into(), Ty::function(
3202                vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3203            // negate :: Decimal -> Decimal
3204            fields.insert("negate".into(), Ty::function(
3205                vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3206            // abs :: Decimal -> Decimal
3207            fields.insert("abs".into(), Ty::function(
3208                vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3209            // round_to :: (Decimal, Int, Str) -> Decimal
3210            //   target_exp: the exponent to round to (e.g. -2 → 2 decimal places)
3211            //   mode: "HalfUp" | "HalfDown" | "HalfEven" | "Down" | "Up" | "Ceiling" | "Floor"
3212            fields.insert("round_to".into(), Ty::function(
3213                vec![decimal_ty(), Ty::int(), Ty::str()],
3214                EffectSet::empty(), decimal_ty()));
3215            // to_str :: Decimal -> Str — decimal notation, e.g. "123.45"
3216            fields.insert("to_str".into(), Ty::function(
3217                vec![decimal_ty()], EffectSet::empty(), Ty::str()));
3218            // pow10 :: Int -> Int — 10^n; n must be in [0, 18]
3219            fields.insert("pow10".into(), Ty::function(
3220                vec![Ty::int()], EffectSet::empty(), Ty::int()));
3221            Some(Ty::Record(fields))
3222        }
3223        _ => None,
3224    }
3225}
3226
3227/// Resolve `import "std.foo" as alias` to a module name (e.g. "io").
3228pub fn module_for_import(reference: &str) -> Option<&'static str> {
3229    let suffix = reference.strip_prefix("std.")?;
3230    Some(match suffix {
3231        "io" => "io",
3232        "str" => "str",
3233        "int" => "int",
3234        "float" => "float",
3235        "list" => "list",
3236        "result" => "result",
3237        "option" => "option",
3238        "json" => "json",
3239        "flow" => "flow",
3240        "tuple" => "tuple",
3241        "time" => "time",
3242        "rand" => "rand",
3243        "random" => "random",
3244        "env" => "env",
3245        "bytes" => "bytes",
3246        "net" => "net",
3247        "tls" => "tls",
3248        "chat" => "chat",
3249        "math" => "math",
3250        "map" => "map",
3251        "set" => "set",
3252        "iter" => "iter",
3253        "crypto" => "crypto",
3254        "regex" => "regex",
3255        "parser" => "parser",
3256        "deque" => "deque",
3257        "kv" => "kv",
3258        "sql" => "sql",
3259        "fs" => "fs",
3260        "process" => "process",
3261        "datetime" => "datetime",
3262        "duration" => "duration",
3263        "log" => "log",
3264        "http" => "http",
3265        "toml" => "toml",
3266        "yaml" => "yaml",
3267        "dotenv" => "dotenv",
3268        "csv" => "csv",
3269        "test" => "test",
3270        "agent" => "agent",
3271        "cli" => "cli",
3272        "stream" => "stream",
3273        "conc" => "conc",
3274        "arrow" => "arrow",
3275        "df" => "df",
3276        "redis" => "redis",
3277        "decimal" => "decimal",
3278        "vcs" => "vcs",
3279        _ => return None,
3280    })
3281}