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