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
1157            // -- I/O (effect-gated) --
1158            // arrow.read_csv :: Str -> [fs_read] Result[Table, Str]
1159            // Header row required; schema inferred from the first 100 rows.
1160            // The `[fs_read]` effect surfaces in agent-tool policy gates
1161            // and `--allow-fs-read` per-path scoping, same as `io.read`.
1162            fields.insert("read_csv".into(), Ty::function(
1163                vec![str_t.clone()],
1164                EffectSet::singleton("fs_read"),
1165                res(table.clone())));
1166
1167            // arrow.read_parquet :: Str -> [fs_read] Result[Table, Str]
1168            // arrow.read_parquet_cols :: (Str, List[Str]) -> [fs_read] Result[Table, Str]
1169            // Same effect + path-scope rules as read_csv. _cols pushes
1170            // the projection into the Parquet reader (no decode of skipped
1171            // columns); missing column names surface as Err.
1172            fields.insert("read_parquet".into(), Ty::function(
1173                vec![str_t.clone()],
1174                EffectSet::singleton("fs_read"),
1175                res(table.clone())));
1176            fields.insert("read_parquet_cols".into(), Ty::function(
1177                vec![str_t.clone(), Ty::List(Box::new(str_t.clone()))],
1178                EffectSet::singleton("fs_read"),
1179                res(table.clone())));
1180
1181            // arrow.write_parquet :: (Table, Str) -> [fs_write] Result[Unit, Str]
1182            // arrow.write_csv     :: (Table, Str) -> [fs_write] Result[Unit, Str]
1183            // Path scope uses --allow-fs-write (symmetric with io.write).
1184            // Parquet default: Snappy compression, default page/row-group
1185            // sizes — sufficient for v1; a write_parquet_opts variant
1186            // can ride a later issue if knobs are needed.
1187            for name in &["write_parquet", "write_csv"] {
1188                fields.insert((*name).into(), Ty::function(
1189                    vec![table.clone(), str_t.clone()],
1190                    EffectSet::singleton("fs_write"),
1191                    res(Ty::Unit)));
1192            }
1193
1194            Some(Ty::Record(fields))
1195        }
1196        "df" => {
1197            // Polars-backed query ops over arrow.Table (#427). All pure
1198            // (no effects); the Polars DataFrame is internal plumbing,
1199            // never leaves the kernel.
1200            let table = Ty::Con("Table".into(), vec![]);
1201            let str_t = Ty::str();
1202            let int_t = Ty::int();
1203            let float_t = Ty::float();
1204            let bool_t = Ty::bool();
1205            let res = |ok: Ty| Ty::Con("Result".into(), vec![ok, Ty::str()]);
1206            let no_eff = EffectSet::empty();
1207
1208            let mut fields = IndexMap::new();
1209
1210            // df.filter_{eq,gt,lt}_int :: Table, Str, Int -> Result[Table, Str]
1211            for name in &["filter_eq_int", "filter_gt_int", "filter_lt_int"] {
1212                fields.insert((*name).into(), Ty::function(
1213                    vec![table.clone(), str_t.clone(), int_t.clone()],
1214                    no_eff.clone(), res(table.clone())));
1215            }
1216
1217            // #433 — string filters.
1218            // df.filter_eq_str  :: Table, Str, Str       -> Result[Table, Str]
1219            // df.filter_in_str  :: Table, Str, List[Str] -> Result[Table, Str]
1220            fields.insert("filter_eq_str".into(), Ty::function(
1221                vec![table.clone(), str_t.clone(), str_t.clone()],
1222                no_eff.clone(), res(table.clone())));
1223            fields.insert("filter_in_str".into(), Ty::function(
1224                vec![table.clone(), str_t.clone(), Ty::List(Box::new(str_t.clone()))],
1225                no_eff.clone(), res(table.clone())));
1226
1227            // #433 — float filters.
1228            // df.filter_{eq,lt,gt}_float :: Table, Str, Float -> Result[Table, Str]
1229            for name in &["filter_eq_float", "filter_lt_float", "filter_gt_float"] {
1230                fields.insert((*name).into(), Ty::function(
1231                    vec![table.clone(), str_t.clone(), float_t.clone()],
1232                    no_eff.clone(), res(table.clone())));
1233            }
1234
1235            // #433 — null handling.
1236            // df.filter_isnull  :: Table, Str       -> Result[Table, Str]
1237            // df.filter_notnull :: Table, Str       -> Result[Table, Str]
1238            // df.drop_nulls     :: Table, List[Str] -> Result[Table, Str]
1239            for name in &["filter_isnull", "filter_notnull"] {
1240                fields.insert((*name).into(), Ty::function(
1241                    vec![table.clone(), str_t.clone()],
1242                    no_eff.clone(), res(table.clone())));
1243            }
1244            fields.insert("drop_nulls".into(), Ty::function(
1245                vec![table.clone(), Ty::List(Box::new(str_t.clone()))],
1246                no_eff.clone(), res(table.clone())));
1247
1248            // df.sort_by :: Table, Str, Bool -> Result[Table, Str]
1249            fields.insert("sort_by".into(), Ty::function(
1250                vec![table.clone(), str_t.clone(), bool_t.clone()],
1251                no_eff.clone(), res(table.clone())));
1252
1253            // df.group_by_agg :: Table, List[Str], List[(Str, Str, Str)]
1254            //                    -> Result[Table, Str]
1255            // Spec tuple is (out_col, in_col, op). op ∈
1256            // "sum"|"mean"|"min"|"max"|"count"|"n_distinct".
1257            fields.insert("group_by_agg".into(), Ty::function(
1258                vec![
1259                    table.clone(),
1260                    Ty::List(Box::new(str_t.clone())),
1261                    Ty::List(Box::new(Ty::Tuple(vec![
1262                        str_t.clone(), str_t.clone(), str_t.clone(),
1263                    ]))),
1264                ],
1265                no_eff.clone(), res(table.clone())));
1266
1267            // df.inner_join / left_join :: Table, Table, Str -> Result[Table, Str]
1268            for name in &["inner_join", "left_join"] {
1269                fields.insert((*name).into(), Ty::function(
1270                    vec![table.clone(), table.clone(), str_t.clone()],
1271                    no_eff.clone(), res(table.clone())));
1272            }
1273
1274            Some(Ty::Record(fields))
1275        }
1276        // `std.proc` was removed in favour of `std.process` (#678): its
1277        // single op `proc.spawn(cmd, args)` was byte-for-byte equivalent to
1278        // `process.run(cmd, args)` — same `[proc]` effect, same
1279        // `{ stdout, stderr, exit_code }` result — and `std.process` is a
1280        // strict superset (streaming spawn / read / wait / kill). Callers
1281        // migrate `proc.spawn` → `process.run`.
1282        "json" => {
1283            let mut fields = IndexMap::new();
1284            // stringify :: T -> Str  (polymorphic on input)
1285            fields.insert("stringify".into(), Ty::function(
1286                vec![Ty::Var(0)], EffectSet::empty(), Ty::str(),
1287            ));
1288            // parse :: Str -> Result[T, Str]
1289            fields.insert("parse".into(), Ty::function(
1290                vec![Ty::str()], EffectSet::empty(),
1291                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1292            ));
1293            // parse_strict :: (Str, List[Str]) -> Result[T, Str]
1294            // Tactical fix for #168 — caller passes the field names
1295            // T requires; runtime returns Err if any are missing
1296            // from the parsed object instead of letting field
1297            // access panic later.
1298            fields.insert("parse_strict".into(), Ty::function(
1299                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
1300                EffectSet::empty(),
1301                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1302            ));
1303            Some(Ty::Record(fields))
1304        }
1305        "result" => {
1306            let mut fields = IndexMap::new();
1307            // result.map :: Result[T, E], (T) -> [E2] U -> [E2] Result[U, E]
1308            // Effect-polymorphic on the closure: result.map et al.
1309            // propagate the closure's effects to the surrounding call.
1310            fields.insert("map".into(), Ty::function(
1311                vec![
1312                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1313                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), Ty::Var(2)),
1314                ],
1315                EffectSet::open_var(3),
1316                Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
1317            ));
1318            fields.insert("and_then".into(), Ty::function(
1319                vec![
1320                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1321                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(4),
1322                        Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)])),
1323                ],
1324                EffectSet::open_var(4),
1325                Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
1326            ));
1327            fields.insert("map_err".into(), Ty::function(
1328                vec![
1329                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1330                    Ty::function(vec![Ty::Var(1)], EffectSet::open_var(5), Ty::Var(2)),
1331                ],
1332                EffectSet::open_var(5),
1333                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
1334            ));
1335            // result.or_else :: Result[T, E1], (E1) -> [E] Result[T, E2]
1336            //                                    -> [E] Result[T, E2]
1337            // Recovery combinator: closure runs only on Err and returns
1338            // the next Result (which itself may swap the error type).
1339            fields.insert("or_else".into(), Ty::function(
1340                vec![
1341                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1342                    Ty::function(vec![Ty::Var(1)], EffectSet::open_var(6),
1343                        Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)])),
1344                ],
1345                EffectSet::open_var(6),
1346                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
1347            ));
1348            // result.unwrap_or :: Result[T, E], T -> T
1349            // Eager fallback — the Ok payload, or the supplied default on
1350            // Err. Mirrors option.unwrap_or (#679).
1351            fields.insert("unwrap_or".into(), Ty::function(
1352                vec![Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]), Ty::Var(0)],
1353                EffectSet::empty(),
1354                Ty::Var(0),
1355            ));
1356            // result.unwrap_or_else :: Result[T, E], (E) -> [Eff] T -> [Eff] T
1357            // Lazy fallback — the closure runs only on Err and receives the
1358            // error payload (effect-polymorphic on the closure). Mirrors
1359            // option.unwrap_or_else (#679).
1360            fields.insert("unwrap_or_else".into(), Ty::function(
1361                vec![
1362                    Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1363                    Ty::function(vec![Ty::Var(1)], EffectSet::open_var(7), Ty::Var(0)),
1364                ],
1365                EffectSet::open_var(7),
1366                Ty::Var(0),
1367            ));
1368            // result.is_ok / is_err :: Result[T, E] -> Bool (#679)
1369            fields.insert("is_ok".into(), Ty::function(
1370                vec![Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)])],
1371                EffectSet::empty(), Ty::bool()));
1372            fields.insert("is_err".into(), Ty::function(
1373                vec![Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)])],
1374                EffectSet::empty(), Ty::bool()));
1375            Some(Ty::Record(fields))
1376        }
1377        "option" => {
1378            let mut fields = IndexMap::new();
1379            // option.map :: Option[T], (T) -> [E] U -> [E] Option[U]
1380            fields.insert("map".into(), Ty::function(
1381                vec![
1382                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
1383                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1384                ],
1385                EffectSet::open_var(2),
1386                Ty::Con("Option".into(), vec![Ty::Var(1)]),
1387            ));
1388            // option.and_then :: Option[T], (T) -> [E] Option[U] -> [E] Option[U]
1389            // The compiler entry has been wired since the result/option
1390            // variant_map work landed; this signature was missed,
1391            // making the call fail to type-check until now.
1392            fields.insert("and_then".into(), Ty::function(
1393                vec![
1394                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
1395                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
1396                        Ty::Con("Option".into(), vec![Ty::Var(1)])),
1397                ],
1398                EffectSet::open_var(3),
1399                Ty::Con("Option".into(), vec![Ty::Var(1)]),
1400            ));
1401            fields.insert("unwrap_or".into(), Ty::function(
1402                vec![Ty::Con("Option".into(), vec![Ty::Var(0)]), Ty::Var(0)],
1403                EffectSet::empty(),
1404                Ty::Var(0),
1405            ));
1406            // option.unwrap_or_else :: Option[T], () -> [E] T -> [E] T
1407            // Lazy variant of unwrap_or: the default is computed by a closure
1408            // only when the value is None (effect-polymorphic on the closure).
1409            fields.insert("unwrap_or_else".into(), Ty::function(
1410                vec![
1411                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
1412                    Ty::function(vec![], EffectSet::open_var(5), Ty::Var(0)),
1413                ],
1414                EffectSet::open_var(5),
1415                Ty::Var(0),
1416            ));
1417            // option.or_else :: Option[T], () -> [E] Option[T] -> [E] Option[T]
1418            // The closure takes no arguments because None has no payload to pass.
1419            fields.insert("or_else".into(), Ty::function(
1420                vec![
1421                    Ty::Con("Option".into(), vec![Ty::Var(0)]),
1422                    Ty::function(vec![], EffectSet::open_var(4),
1423                        Ty::Con("Option".into(), vec![Ty::Var(0)])),
1424                ],
1425                EffectSet::open_var(4),
1426                Ty::Con("Option".into(), vec![Ty::Var(0)]),
1427            ));
1428            // option.is_some / is_none :: Option[T] -> Bool (#679)
1429            fields.insert("is_some".into(), Ty::function(
1430                vec![Ty::Con("Option".into(), vec![Ty::Var(0)])],
1431                EffectSet::empty(), Ty::bool()));
1432            fields.insert("is_none".into(), Ty::function(
1433                vec![Ty::Con("Option".into(), vec![Ty::Var(0)])],
1434                EffectSet::empty(), Ty::bool()));
1435            // option.ok_or :: Option[T], E -> Result[T, E]
1436            // Cross from Option into Result, supplying the error for None (#679).
1437            fields.insert("ok_or".into(), Ty::function(
1438                vec![Ty::Con("Option".into(), vec![Ty::Var(0)]), Ty::Var(1)],
1439                EffectSet::empty(),
1440                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1441            ));
1442            Some(Ty::Record(fields))
1443        }
1444        "tuple" => {
1445            // Tuple accessors per §11.1. Polymorphic in the tuple's
1446            // element types; we use the same row-variable shape used
1447            // by list helpers. Tuples are heterogeneous, so each
1448            // accessor is statically typed via independent type
1449            // variables for each position.
1450            let mut fields = IndexMap::new();
1451            // fst :: (T0, T1) -> T0
1452            fields.insert("fst".into(), Ty::function(
1453                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
1454                EffectSet::empty(),
1455                Ty::Var(0),
1456            ));
1457            // snd :: (T0, T1) -> T1
1458            fields.insert("snd".into(), Ty::function(
1459                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
1460                EffectSet::empty(),
1461                Ty::Var(1),
1462            ));
1463            // third :: (T0, T1, T2) -> T2
1464            fields.insert("third".into(), Ty::function(
1465                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1), Ty::Var(2)])],
1466                EffectSet::empty(),
1467                Ty::Var(2),
1468            ));
1469            // len :: (T0, T1) -> Int  (covers any pair shape; Int back)
1470            fields.insert("len".into(), Ty::function(
1471                vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
1472                EffectSet::empty(),
1473                Ty::int(),
1474            ));
1475            Some(Ty::Record(fields))
1476        }
1477        "map" => {
1478            // Persistent map. Keys are `Str` or `Int` only — Lex's
1479            // type system tracks them polymorphically as Var(0)
1480            // ("K") and lets the runtime check the key shape; both
1481            // cases fit into `MapKey`.
1482            //
1483            // Type variables: 0 = K, 1 = V.
1484            let mt   = || Ty::Con("Map".into(), vec![Ty::Var(0), Ty::Var(1)]);
1485            let pair = || Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]);
1486            let mut fields = IndexMap::new();
1487            // new :: () -> Map[K, V]
1488            fields.insert("new".into(), Ty::function(
1489                vec![], EffectSet::empty(), mt()));
1490            // size :: Map[K, V] -> Int
1491            fields.insert("size".into(), Ty::function(
1492                vec![mt()], EffectSet::empty(), Ty::int()));
1493            // has :: Map[K, V], K -> Bool
1494            fields.insert("has".into(), Ty::function(
1495                vec![mt(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
1496            // get :: Map[K, V], K -> Option[V]
1497            fields.insert("get".into(), Ty::function(
1498                vec![mt(), Ty::Var(0)], EffectSet::empty(),
1499                Ty::Con("Option".into(), vec![Ty::Var(1)])));
1500            // set :: Map[K, V], K, V -> Map[K, V]
1501            fields.insert("set".into(), Ty::function(
1502                vec![mt(), Ty::Var(0), Ty::Var(1)],
1503                EffectSet::empty(), mt()));
1504            // delete :: Map[K, V], K -> Map[K, V]
1505            fields.insert("delete".into(), Ty::function(
1506                vec![mt(), Ty::Var(0)], EffectSet::empty(), mt()));
1507            // keys :: Map[K, V] -> List[K]
1508            fields.insert("keys".into(), Ty::function(
1509                vec![mt()], EffectSet::empty(),
1510                Ty::List(Box::new(Ty::Var(0)))));
1511            // values :: Map[K, V] -> List[V]
1512            fields.insert("values".into(), Ty::function(
1513                vec![mt()], EffectSet::empty(),
1514                Ty::List(Box::new(Ty::Var(1)))));
1515            // entries :: Map[K, V] -> List[(K, V)]
1516            fields.insert("entries".into(), Ty::function(
1517                vec![mt()], EffectSet::empty(),
1518                Ty::List(Box::new(pair()))));
1519            // from_list :: List[(K, V)] -> Map[K, V]
1520            fields.insert("from_list".into(), Ty::function(
1521                vec![Ty::List(Box::new(pair()))],
1522                EffectSet::empty(), mt()));
1523            // merge :: Map[K, V], Map[K, V] -> Map[K, V]   (b overrides a)
1524            fields.insert("merge".into(), Ty::function(
1525                vec![mt(), mt()], EffectSet::empty(), mt()));
1526            // is_empty :: Map[K, V] -> Bool
1527            fields.insert("is_empty".into(), Ty::function(
1528                vec![mt()], EffectSet::empty(), Ty::bool()));
1529            // fold :: Map[K, V], A, (A, K, V) -> [E] A -> [E] A
1530            // Iteration order matches `map.entries` (BTreeMap-sorted by
1531            // key). Effect-polymorphic on the combiner like `list.fold`.
1532            // Type variable 2 = A (accumulator), effect row 3.
1533            fields.insert("fold".into(), Ty::function(
1534                vec![
1535                    mt(),
1536                    Ty::Var(2),
1537                    Ty::function(
1538                        vec![Ty::Var(2), Ty::Var(0), Ty::Var(1)],
1539                        EffectSet::open_var(3),
1540                        Ty::Var(2),
1541                    ),
1542                ],
1543                EffectSet::open_var(3),
1544                Ty::Var(2),
1545            ));
1546            Some(Ty::Record(fields))
1547        }
1548        "set" => {
1549            // Persistent set with the same key-type discipline as map.
1550            // Type variable: 0 = T (the element type, also the key type).
1551            let st   = || Ty::Con("Set".into(), vec![Ty::Var(0)]);
1552            let mut fields = IndexMap::new();
1553            // new :: () -> Set[T]
1554            fields.insert("new".into(), Ty::function(
1555                vec![], EffectSet::empty(), st()));
1556            // size :: Set[T] -> Int
1557            fields.insert("size".into(), Ty::function(
1558                vec![st()], EffectSet::empty(), Ty::int()));
1559            // has :: Set[T], T -> Bool
1560            fields.insert("has".into(), Ty::function(
1561                vec![st(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
1562            // add :: Set[T], T -> Set[T]
1563            fields.insert("add".into(), Ty::function(
1564                vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
1565            // delete :: Set[T], T -> Set[T]
1566            fields.insert("delete".into(), Ty::function(
1567                vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
1568            // to_list :: Set[T] -> List[T]
1569            fields.insert("to_list".into(), Ty::function(
1570                vec![st()], EffectSet::empty(),
1571                Ty::List(Box::new(Ty::Var(0)))));
1572            // from_list :: List[T] -> Set[T]
1573            fields.insert("from_list".into(), Ty::function(
1574                vec![Ty::List(Box::new(Ty::Var(0)))],
1575                EffectSet::empty(), st()));
1576            // union :: Set[T], Set[T] -> Set[T]
1577            fields.insert("union".into(), Ty::function(
1578                vec![st(), st()], EffectSet::empty(), st()));
1579            // intersect :: Set[T], Set[T] -> Set[T]
1580            fields.insert("intersect".into(), Ty::function(
1581                vec![st(), st()], EffectSet::empty(), st()));
1582            // diff :: Set[T], Set[T] -> Set[T]
1583            fields.insert("diff".into(), Ty::function(
1584                vec![st(), st()], EffectSet::empty(), st()));
1585            // is_empty :: Set[T] -> Bool
1586            fields.insert("is_empty".into(), Ty::function(
1587                vec![st()], EffectSet::empty(), Ty::bool()));
1588            // is_subset :: Set[T], Set[T] -> Bool   (a is subset of b)
1589            fields.insert("is_subset".into(), Ty::function(
1590                vec![st(), st()], EffectSet::empty(), Ty::bool()));
1591            Some(Ty::Record(fields))
1592        }
1593        "iter" => {
1594            // Positional iterator (#364) + lazy variant via `iter.unfold`
1595            // (#376). Internal value shapes are `__IterEager(list, idx)` or
1596            // `__IterLazy(seed, step)`; all operations compile-inline and
1597            // dispatch on the variant tag at runtime.
1598            // Type var slots: 0 = T (element), 1 = U (mapped element) /
1599            // A (fold acc), 2 = S (unfold seed).
1600            let it = |n: u32| Ty::Con("Iter".into(), vec![Ty::Var(n)]);
1601            let mut fields = IndexMap::new();
1602            // from_list :: List[T] -> Iter[T]
1603            fields.insert("from_list".into(), Ty::function(
1604                vec![Ty::List(Box::new(Ty::Var(0)))],
1605                EffectSet::empty(), it(0)));
1606            // unfold[S, T] :: S, (S) -> Option[(T, S)] -> Iter[T] (#376)
1607            // The step closure may carry any effect row; the iterator
1608            // itself stays effect-free since the effects only fire when
1609            // the step is invoked via `iter.next` / `iter.to_list`.
1610            fields.insert("unfold".into(), Ty::function(
1611                vec![
1612                    Ty::Var(2), // seed S
1613                    Ty::function(
1614                        vec![Ty::Var(2)],
1615                        EffectSet::open_var(3),
1616                        Ty::Con("Option".into(), vec![
1617                            Ty::Tuple(vec![Ty::Var(0), Ty::Var(2)])
1618                        ]),
1619                    ),
1620                ],
1621                EffectSet::empty(), it(0)));
1622            // next :: Iter[T] -> Option[(T, Iter[T])]
1623            fields.insert("next".into(), Ty::function(
1624                vec![it(0)],
1625                EffectSet::empty(),
1626                Ty::Con("Option".into(), vec![
1627                    Ty::Tuple(vec![Ty::Var(0), it(0)])
1628                ])));
1629            // is_empty :: Iter[T] -> Bool
1630            fields.insert("is_empty".into(), Ty::function(
1631                vec![it(0)], EffectSet::empty(), Ty::bool()));
1632            // count :: Iter[T] -> Int   (remaining elements)
1633            fields.insert("count".into(), Ty::function(
1634                vec![it(0)], EffectSet::empty(), Ty::int()));
1635            // take :: Iter[T], Int -> Iter[T]
1636            fields.insert("take".into(), Ty::function(
1637                vec![it(0), Ty::int()], EffectSet::empty(), it(0)));
1638            // skip :: Iter[T], Int -> Iter[T]
1639            fields.insert("skip".into(), Ty::function(
1640                vec![it(0), Ty::int()], EffectSet::empty(), it(0)));
1641            // to_list :: Iter[T] -> List[T]
1642            fields.insert("to_list".into(), Ty::function(
1643                vec![it(0)], EffectSet::empty(),
1644                Ty::List(Box::new(Ty::Var(0)))));
1645            // collect :: Iter[T] -> List[T] — alias for `to_list`
1646            // (matches Rust / Python / Kotlin naming so call sites
1647            // coming from those languages don't have to re-learn).
1648            fields.insert("collect".into(), Ty::function(
1649                vec![it(0)], EffectSet::empty(),
1650                Ty::List(Box::new(Ty::Var(0)))));
1651            // map :: [E] Iter[T], (T) -> [E] U -> [E] Iter[U]
1652            fields.insert("map".into(), Ty::function(
1653                vec![
1654                    it(0),
1655                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1656                ],
1657                EffectSet::open_var(2), it(1)));
1658            // filter :: [E] Iter[T], (T) -> [E] Bool -> [E] Iter[T]
1659            fields.insert("filter".into(), Ty::function(
1660                vec![
1661                    it(0),
1662                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(1), Ty::bool()),
1663                ],
1664                EffectSet::open_var(1), it(0)));
1665            // fold :: [E] Iter[T], A, (A, T) -> [E] A -> [E] A
1666            fields.insert("fold".into(), Ty::function(
1667                vec![
1668                    it(0),
1669                    Ty::Var(1),
1670                    Ty::function(vec![Ty::Var(1), Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1671                ],
1672                EffectSet::open_var(2), Ty::Var(1)));
1673            Some(Ty::Record(fields))
1674        }
1675        "flow" => {
1676            // Orchestration primitives (spec §11.2). Each takes one or
1677            // more closures and returns a closure with a derived shape.
1678            let mut fields = IndexMap::new();
1679            // sequential[T, U, V](f: (T) -> U, g: (U) -> V) -> (T) -> V
1680            fields.insert("sequential".into(), Ty::function(
1681                vec![
1682                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1683                    Ty::function(vec![Ty::Var(1)], EffectSet::empty(), Ty::Var(2)),
1684                ],
1685                EffectSet::empty(),
1686                Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(2)),
1687            ));
1688            // branch[T, U](cond: (T) -> Bool, t: (T) -> U, f: (T) -> U) -> (T) -> U
1689            fields.insert("branch".into(), Ty::function(
1690                vec![
1691                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::bool()),
1692                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1693                    Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1694                ],
1695                EffectSet::empty(),
1696                Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1697            ));
1698            // retry[T, U, E, Eff](
1699            //   f: (T) -> [Eff] Result[U, E], n: Int
1700            // ) -> (T) -> [Eff] Result[U, E]
1701            // open_var(3) is the effect row carried by `f`; the
1702            // combinator itself is pure, so the outer EffectSet is
1703            // empty. The returned closure propagates Eff unchanged.
1704            let result_ty = Ty::Con("Result".into(), vec![Ty::Var(1), Ty::Var(2)]);
1705            fields.insert("retry".into(), Ty::function(
1706                vec![
1707                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
1708                    Ty::int(),
1709                ],
1710                EffectSet::empty(),
1711                Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
1712            ));
1713            // retry_with_backoff[T, U, E, Eff](
1714            //   f: (T) -> [Eff] Result[U, E], attempts: Int, base_ms: Int,
1715            // ) -> (T) -> [Eff, time] Result[U, E]
1716            // Same retry shape as `flow.retry` plus an exponential
1717            // backoff between attempts. The result function carries
1718            // `[time]` (from `time.sleep_ms`) unioned with the inner
1719            // closure's effect row Eff, so e.g. a `[net]` closure
1720            // produces a `[net, time]` result function. (#226)
1721            fields.insert("retry_with_backoff".into(), Ty::function(
1722                vec![
1723                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
1724                    Ty::int(),
1725                    Ty::int(),
1726                ],
1727                EffectSet::empty(),
1728                Ty::function(vec![Ty::Var(0)],
1729                    EffectSet::open_var(3).union(&EffectSet::singleton("time")), result_ty),
1730            ));
1731            // parallel[A, B](fa: () -> A, fb: () -> B) -> () -> (A, B)
1732            // Sequential implementation today; spec §11.2 reserves the
1733            // option of a true-threaded scheduler. parallel_record is
1734            // listed in the spec but not yet implemented — it needs row
1735            // polymorphism over the input record's fields plus a
1736            // record-iteration trampoline; tracked as follow-up.
1737            fields.insert("parallel".into(), Ty::function(
1738                vec![
1739                    Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
1740                    Ty::function(vec![], EffectSet::empty(), Ty::Var(1)),
1741                ],
1742                EffectSet::empty(),
1743                Ty::function(vec![], EffectSet::empty(),
1744                    Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])),
1745            ));
1746            // parallel_list[T](actions: List[() -> T]) -> List[T]
1747            // Variadic counterpart to `parallel`. Runs each action and
1748            // collects results in input order. Sequential under the
1749            // hood (same caveat as `parallel`); spec §11.2 reserves
1750            // true threading for a future scheduler. Unlike `parallel`,
1751            // this returns the result list directly rather than a
1752            // closure, since the input arity is dynamic.
1753            fields.insert("parallel_list".into(), Ty::function(
1754                vec![
1755                    Ty::List(Box::new(
1756                        Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
1757                    )),
1758                ],
1759                EffectSet::empty(),
1760                Ty::List(Box::new(Ty::Var(0))),
1761            ));
1762            Some(Ty::Record(fields))
1763        }
1764        "crypto" => {
1765            let mut fields = IndexMap::new();
1766            // Hashes: Bytes -> Bytes (digest as raw bytes).
1767            // SHA-256 / SHA-512 are vetted. MD5 is retained only for
1768            // interop with legacy systems — new code should not use it.
1769            // BLAKE2b (#382) is included as a faster alternative to
1770            // SHA-512 with the same security level.
1771            for name in &["sha256", "sha512", "md5", "blake2b"] {
1772                fields.insert((*name).into(), Ty::function(
1773                    vec![Ty::bytes()],
1774                    EffectSet::empty(),
1775                    Ty::bytes(),
1776                ));
1777            }
1778            // Hex-string convenience hashers (#382): hash a Str directly,
1779            // return the digest as a lowercase hex Str. Equivalent to
1780            // `crypto.hex_encode(crypto.shaN(bytes_from_str(s)))` but
1781            // saves the two-step incantation for the common case.
1782            for name in &["sha256_str", "sha512_str"] {
1783                fields.insert((*name).into(), Ty::function(
1784                    vec![Ty::str()],
1785                    EffectSet::empty(),
1786                    Ty::str(),
1787                ));
1788            }
1789            // HMAC: (key :: Bytes, data :: Bytes) -> Bytes
1790            for name in &["hmac_sha256", "hmac_sha512"] {
1791                fields.insert((*name).into(), Ty::function(
1792                    vec![Ty::bytes(), Ty::bytes()],
1793                    EffectSet::empty(),
1794                    Ty::bytes(),
1795                ));
1796            }
1797            // ed25519 asymmetric signatures (#643). A secret key is its 32-byte
1798            // seed (generate via `crypto.random(32)`); all three ops are pure.
1799            //   ed25519_public_key(secret :: Bytes) -> Result[Bytes, Str]
1800            //   ed25519_sign(secret :: Bytes, message :: Bytes) -> Result[Bytes, Str]
1801            //   ed25519_verify(public :: Bytes, message :: Bytes, sig :: Bytes) -> Bool
1802            fields.insert("ed25519_public_key".into(), Ty::function(
1803                vec![Ty::bytes()],
1804                EffectSet::empty(),
1805                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1806            ));
1807            fields.insert("ed25519_sign".into(), Ty::function(
1808                vec![Ty::bytes(), Ty::bytes()],
1809                EffectSet::empty(),
1810                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1811            ));
1812            fields.insert("ed25519_verify".into(), Ty::function(
1813                vec![Ty::bytes(), Ty::bytes(), Ty::bytes()],
1814                EffectSet::empty(),
1815                Ty::bool(),
1816            ));
1817            // Whether `bytes` decompresses to a valid point on the Edwards25519
1818            // curve (#93 follow-up: Solana Program Derived Address search --
1819            // a PDA is valid iff the candidate 32 bytes are NOT a valid point,
1820            // so callers try bump seeds until this returns false).
1821            //   ed25519_is_valid_point(bytes :: Bytes) -> Bool
1822            fields.insert("ed25519_is_valid_point".into(), Ty::function(
1823                vec![Ty::bytes()],
1824                EffectSet::empty(),
1825                Ty::bool(),
1826            ));
1827            // P-256 ECDSA / ES256 (#651). The JWT/SD-JWT signature
1828            // algorithm for AP2 agent keys; `lex-jose` builds the
1829            // token layer on top of these primitives. Key bytes are
1830            // raw: a secret key is the 32-byte scalar, a public key is
1831            // the 33-byte SEC1 compressed point; JWK serialization
1832            // lives downstream in `lex-jose`.
1833            //
1834            //   p256_generate()                       -> [random] Result[Bytes, Str]
1835            //   p256_public_key(sk :: Bytes)          -> Result[Bytes, Str]
1836            //   p256_sign(sk :: Bytes, msg :: Bytes)  -> Result[Bytes, Str]
1837            //   p256_verify(pk :: Bytes, msg :: Bytes, sig :: Bytes) -> Bool
1838            //
1839            // `p256_generate` mints fresh key material from the OS RNG,
1840            // so it carries the same fine-grained `[random]` effect as
1841            // `crypto.random` — every key-minting call stays visible to
1842            // `lex audit --effect random`. (The issue sketched `[env]`;
1843            // `[random]` is the dedicated effect for OS randomness in
1844            // this codebase, so we use that for consistency.)
1845            // `sign`/`verify` are pure: signing hashes `msg` with
1846            // SHA-256 internally (standard ES256) and the signature is
1847            // DER-encoded.
1848            fields.insert("p256_generate".into(), Ty::function(
1849                vec![],
1850                EffectSet::singleton("random"),
1851                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1852            ));
1853            fields.insert("p256_public_key".into(), Ty::function(
1854                vec![Ty::bytes()],
1855                EffectSet::empty(),
1856                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1857            ));
1858            fields.insert("p256_sign".into(), Ty::function(
1859                vec![Ty::bytes(), Ty::bytes()],
1860                EffectSet::empty(),
1861                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1862            ));
1863            fields.insert("p256_verify".into(), Ty::function(
1864                vec![Ty::bytes(), Ty::bytes(), Ty::bytes()],
1865                EffectSet::empty(),
1866                Ty::bool(),
1867            ));
1868            // secp256k1 ECDSA + recovery (#655). The EVM curve — backs
1869            // EIP-712 typed-data signing (EIP-3009 / x402 `exact`) and
1870            // Ethereum address derivation. Unlike `p256_*`/`ed25519_*`,
1871            // the sign/verify ops here take a **pre-hashed 32-byte
1872            // digest** (EIP-712 already hashes), hence the `_digest`
1873            // suffix — they do NOT hash the input again.
1874            //
1875            // - Secret key: 32-byte scalar.
1876            // - Public key: 65-byte UNCOMPRESSED SEC1 point (0x04‖X‖Y),
1877            //   so an address is `keccak256(pk[1..])[12..]` with no
1878            //   decompression step. (p256 returns compressed; the EVM
1879            //   convention is uncompressed.)
1880            // - Signature: 65 bytes `r(32)‖s(32)‖v(1)`, v ∈ {27,28}
1881            //   (Ethereum), low-S normalized (EIP-2).
1882            //
1883            //   keccak256(data :: Bytes) -> Bytes
1884            //   secp256k1_generate()                          -> [random] Result[Bytes, Str]
1885            //   secp256k1_public_key(sk :: Bytes)             -> Result[Bytes, Str]
1886            //   secp256k1_sign_digest(sk :: Bytes, digest :: Bytes) -> Result[Bytes, Str]
1887            //   secp256k1_recover(digest :: Bytes, sig :: Bytes)    -> Result[Bytes, Str]
1888            //   secp256k1_verify(pk :: Bytes, digest :: Bytes, sig :: Bytes) -> Bool
1889            //
1890            // `secp256k1_generate` mints from the OS RNG, so it carries
1891            // the same `[random]` effect as `crypto.random` / `p256_generate`.
1892            fields.insert("keccak256".into(), Ty::function(
1893                vec![Ty::bytes()],
1894                EffectSet::empty(),
1895                Ty::bytes(),
1896            ));
1897            fields.insert("secp256k1_generate".into(), Ty::function(
1898                vec![],
1899                EffectSet::singleton("random"),
1900                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1901            ));
1902            fields.insert("secp256k1_public_key".into(), Ty::function(
1903                vec![Ty::bytes()],
1904                EffectSet::empty(),
1905                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1906            ));
1907            fields.insert("secp256k1_sign_digest".into(), Ty::function(
1908                vec![Ty::bytes(), Ty::bytes()],
1909                EffectSet::empty(),
1910                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1911            ));
1912            fields.insert("secp256k1_recover".into(), Ty::function(
1913                vec![Ty::bytes(), Ty::bytes()],
1914                EffectSet::empty(),
1915                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1916            ));
1917            fields.insert("secp256k1_verify".into(), Ty::function(
1918                vec![Ty::bytes(), Ty::bytes(), Ty::bytes()],
1919                EffectSet::empty(),
1920                Ty::bool(),
1921            ));
1922            // base64 / hex
1923            fields.insert("base64_encode".into(), Ty::function(
1924                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1925            fields.insert("base64_decode".into(), Ty::function(
1926                vec![Ty::str()], EffectSet::empty(),
1927                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1928            // URL-safe base64 (#382): the alphabet swaps `+/` for `-_`
1929            // and omits padding. Required by JWT, signed-cookie, and
1930            // most token-bearing URL paths.
1931            fields.insert("base64url_encode".into(), Ty::function(
1932                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1933            fields.insert("base64url_decode".into(), Ty::function(
1934                vec![Ty::str()], EffectSet::empty(),
1935                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1936            fields.insert("hex_encode".into(), Ty::function(
1937                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1938            fields.insert("hex_decode".into(), Ty::function(
1939                vec![Ty::str()], EffectSet::empty(),
1940                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1941            // base58 (#658) — Bitcoin/Solana alphabet, no checksum. Solana
1942            // addresses, mints, signatures and the x402 `exact` payload are
1943            // base58; this is the Solana analog of keccak/secp256k1 (#655).
1944            fields.insert("base58_encode".into(), Ty::function(
1945                vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1946            fields.insert("base58_decode".into(), Ty::function(
1947                vec![Ty::str()], EffectSet::empty(),
1948                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1949            // Constant-time equality (for HMAC verification etc.).
1950            // `eq` / `eq_str` (#382) are the recommended spelling;
1951            // `constant_time_eq` stays as a deprecated alias.
1952            fields.insert("constant_time_eq".into(), Ty::function(
1953                vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool()));
1954            fields.insert("eq".into(), Ty::function(
1955                vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool()));
1956            fields.insert("eq_str".into(), Ty::function(
1957                vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
1958            // Cryptographically-secure random bytes — OS RNG, not the
1959            // deterministic `rand.int_in` stub. The new `[random]`
1960            // effect is fine-grained on purpose so reviewers can find
1961            // every token-generating call via `lex audit --effect
1962            // random`.
1963            fields.insert("random".into(), Ty::function(
1964                vec![Ty::int()],
1965                EffectSet::singleton("random"),
1966                Ty::bytes(),
1967            ));
1968            // random_str_hex (#382): the most common token-mint pattern
1969            // — N random bytes rendered as 2N lowercase hex chars.
1970            // Suitable for session ids, request ids, OAuth `state`,
1971            // CSRF tokens; not suitable as a JWT signing key (use raw
1972            // `random` for that).
1973            fields.insert("random_str_hex".into(), Ty::function(
1974                vec![Ty::int()],
1975                EffectSet::singleton("random"),
1976                Ty::str(),
1977            ));
1978
1979            // AEAD: authenticated encryption with associated data
1980            // (#382 AEAD slice). Both algorithms use a 12-byte nonce
1981            // and a 16-byte authentication tag. `seal` returns the
1982            // structured `AeadResult { ciphertext, tag }`; `open`
1983            // returns `Result[Bytes, Str]` so authentication failures
1984            // surface as `Err`, not a panic.
1985            //
1986            // - **AES-GCM** (`aes_gcm_seal/open`): AES-128/192/256-GCM,
1987            //   key length determined by the supplied key bytes (16, 24,
1988            //   or 32). NIST-recommended; hardware-accelerated on most CPUs.
1989            // - **ChaCha20-Poly1305** (`chacha20_poly1305_seal/open`):
1990            //   Always a 32-byte key. Equivalent security to AES-GCM
1991            //   without needing AES-NI hardware; preferred on constrained
1992            //   targets.
1993            let aead_t = || Ty::Con("AeadResult".into(), vec![]);
1994            // Seal: returns Result[AeadResult, Str] rather than bare
1995            // AeadResult so input-validation errors (wrong key length,
1996            // wrong nonce length) surface as `Err` to the Lex caller
1997            // instead of panicking the VM. AES-GCM expects 16/24/32-byte
1998            // keys; ChaCha20-Poly1305 expects exactly 32. Both expect a
1999            // 12-byte nonce.
2000            for name in &["aes_gcm_seal", "chacha20_poly1305_seal"] {
2001                fields.insert((*name).into(), Ty::function(
2002                    // (key, nonce, aad, plaintext) -> Result[AeadResult, Str]
2003                    vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes()],
2004                    EffectSet::empty(),
2005                    Ty::Con("Result".into(), vec![aead_t(), Ty::str()]),
2006                ));
2007            }
2008            for name in &["aes_gcm_open", "chacha20_poly1305_open"] {
2009                fields.insert((*name).into(), Ty::function(
2010                    // (key, nonce, aad, ciphertext, tag) -> Result[Bytes, Str]
2011                    vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes()],
2012                    EffectSet::empty(),
2013                    Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
2014                ));
2015            }
2016
2017            // KDFs: key-derivation functions (#382 KDF slice). All three
2018            // return `Result[Bytes, Str]` so caller-controlled inputs
2019            // (iteration count, output length, argon2id work factors)
2020            // that violate the underlying primitive's contract surface
2021            // as `Err` rather than panicking the VM. None require a new
2022            // effect — these are pure derivations.
2023            //
2024            // - **`pbkdf2_sha256(password, salt, iterations, len)`** —
2025            //   RFC 8018 PBKDF2 with HMAC-SHA256. Use ≥ 600_000 iterations
2026            //   for password storage (OWASP 2024). Older deployments
2027            //   pinning < 100_000 should rotate.
2028            // - **`hkdf_sha256(ikm, salt, info, len)`** — RFC 5869 extract+
2029            //   expand. Use for deriving multiple keys from a single
2030            //   high-entropy input (TLS, Noise, JWT-key rotation).
2031            //   Output length capped at 255 × 32 = 8160 bytes.
2032            // - **`argon2id(password, salt, t_cost, m_cost, len)`** —
2033            //   RFC 9106 Argon2id. Recommended for *new* password
2034            //   hashing. OWASP 2024 baseline: `t_cost=2, m_cost=19456`
2035            //   (19 MiB), or use `lex-crypto`'s vetted wrapper.
2036            fields.insert("pbkdf2_sha256".into(), Ty::function(
2037                // (password, salt, iterations, len) -> Result[Bytes, Str]
2038                vec![Ty::bytes(), Ty::bytes(), Ty::int(), Ty::int()],
2039                EffectSet::empty(),
2040                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
2041            ));
2042            fields.insert("hkdf_sha256".into(), Ty::function(
2043                // (ikm, salt, info, len) -> Result[Bytes, Str]
2044                vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::int()],
2045                EffectSet::empty(),
2046                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
2047            ));
2048            fields.insert("argon2id".into(), Ty::function(
2049                // (password, salt, t_cost, m_cost, len) -> Result[Bytes, Str]
2050                vec![Ty::bytes(), Ty::bytes(), Ty::int(), Ty::int(), Ty::int()],
2051                EffectSet::empty(),
2052                Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
2053            ));
2054
2055            Some(Ty::Record(fields))
2056        }
2057        "deque" => {
2058            // Persistent double-ended queue. Push/pop O(1) on both
2059            // ends; iteration order is front-to-back.
2060            // Type variable: 0 = T.
2061            let dt   = || Ty::Con("Deque".into(), vec![Ty::Var(0)]);
2062            let pair = || Ty::Tuple(vec![Ty::Var(0), dt()]);
2063            let mut fields = IndexMap::new();
2064            // new :: () -> Deque[T]
2065            fields.insert("new".into(), Ty::function(
2066                vec![], EffectSet::empty(), dt()));
2067            // size :: Deque[T] -> Int
2068            fields.insert("size".into(), Ty::function(
2069                vec![dt()], EffectSet::empty(), Ty::int()));
2070            // is_empty :: Deque[T] -> Bool
2071            fields.insert("is_empty".into(), Ty::function(
2072                vec![dt()], EffectSet::empty(), Ty::bool()));
2073            // push_back / push_front :: Deque[T], T -> Deque[T]
2074            for n in &["push_back", "push_front"] {
2075                fields.insert((*n).into(), Ty::function(
2076                    vec![dt(), Ty::Var(0)], EffectSet::empty(), dt()));
2077            }
2078            // pop_back / pop_front :: Deque[T] -> Option[(T, Deque[T])]
2079            for n in &["pop_back", "pop_front"] {
2080                fields.insert((*n).into(), Ty::function(
2081                    vec![dt()], EffectSet::empty(),
2082                    Ty::Con("Option".into(), vec![pair()])));
2083            }
2084            // peek_back / peek_front :: Deque[T] -> Option[T]
2085            for n in &["peek_back", "peek_front"] {
2086                fields.insert((*n).into(), Ty::function(
2087                    vec![dt()], EffectSet::empty(),
2088                    Ty::Con("Option".into(), vec![Ty::Var(0)])));
2089            }
2090            // from_list :: List[T] -> Deque[T]
2091            fields.insert("from_list".into(), Ty::function(
2092                vec![Ty::List(Box::new(Ty::Var(0)))],
2093                EffectSet::empty(), dt()));
2094            // to_list :: Deque[T] -> List[T]
2095            fields.insert("to_list".into(), Ty::function(
2096                vec![dt()], EffectSet::empty(),
2097                Ty::List(Box::new(Ty::Var(0)))));
2098            Some(Ty::Record(fields))
2099        }
2100        "log" => {
2101            // Structured logging behind a [log] effect. Emit ops route
2102            // through a runtime-configured sink (stderr by default;
2103            // can be redirected via set_sink). Configuration ops
2104            // mutate the global sink and so are gated [io].
2105            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
2106            let mut fields = IndexMap::new();
2107            for level in &["debug", "info", "warn", "error"] {
2108                fields.insert((*level).into(), Ty::function(
2109                    vec![Ty::str()],
2110                    EffectSet::singleton("log"),
2111                    Ty::Unit,
2112                ));
2113            }
2114            // set_level :: Str -> [io] Result[Unit, Str]
2115            fields.insert("set_level".into(), Ty::function(
2116                vec![Ty::str()],
2117                EffectSet::singleton("io"),
2118                result_str(Ty::Unit)));
2119            // set_format :: Str -> [io] Result[Unit, Str]
2120            fields.insert("set_format".into(), Ty::function(
2121                vec![Ty::str()],
2122                EffectSet::singleton("io"),
2123                result_str(Ty::Unit)));
2124            // set_sink :: Str -> [io, fs_write] Result[Unit, Str]
2125            fields.insert("set_sink".into(), Ty::function(
2126                vec![Ty::str()],
2127                EffectSet {
2128                    concrete: [crate::types::EffectKind::bare("io"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
2129                    var: None,
2130                },
2131                result_str(Ty::Unit)));
2132            Some(Ty::Record(fields))
2133        }
2134        "datetime" => {
2135            // Instant and Duration are nominal opaque Ints under the
2136            // hood (nanoseconds-since-UTC-epoch and signed nanoseconds
2137            // respectively); the type checker tracks the distinction
2138            // even though both values look like Int at runtime.
2139            //
2140            // Tz is the variant
2141            //     Utc | Local | Offset(Int) | Iana(Str)
2142            // registered as a built-in nominal type in
2143            // `TypeEnv::new_with_builtins`. The pre-v1 stringly Tz
2144            // ("UTC"/"Local"/IANA-name/"+05:30") is no longer accepted
2145            // — passing a `Str` to `to_components` is now a type
2146            // error.
2147            let inst   = || Ty::Con("Instant".into(), vec![]);
2148            let dur    = || Ty::Con("Duration".into(), vec![]);
2149            let tz     = || Ty::Con("Tz".into(), vec![]);
2150            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
2151            let dt_t = || {
2152                let mut fs = IndexMap::new();
2153                fs.insert("year".into(),    Ty::int());
2154                fs.insert("month".into(),   Ty::int());
2155                fs.insert("day".into(),     Ty::int());
2156                fs.insert("hour".into(),    Ty::int());
2157                fs.insert("minute".into(),  Ty::int());
2158                fs.insert("second".into(),  Ty::int());
2159                fs.insert("nano".into(),    Ty::int());
2160                fs.insert("tz_offset_minutes".into(), Ty::int());
2161                Ty::Record(fs)
2162            };
2163            let mut fields = IndexMap::new();
2164            fields.insert("now".into(), Ty::function(
2165                vec![], EffectSet::singleton("time"), inst()));
2166            fields.insert("parse_iso".into(), Ty::function(
2167                vec![Ty::str()], EffectSet::empty(), result_str(inst())));
2168            fields.insert("format_iso".into(), Ty::function(
2169                vec![inst()], EffectSet::empty(), Ty::str()));
2170            fields.insert("parse".into(), Ty::function(
2171                vec![Ty::str(), Ty::str()], EffectSet::empty(), result_str(inst())));
2172            fields.insert("format".into(), Ty::function(
2173                vec![inst(), Ty::str()], EffectSet::empty(), Ty::str()));
2174            fields.insert("to_components".into(), Ty::function(
2175                vec![inst(), tz()], EffectSet::empty(), result_str(dt_t())));
2176            fields.insert("from_components".into(), Ty::function(
2177                vec![dt_t()], EffectSet::empty(), result_str(inst())));
2178            fields.insert("add".into(), Ty::function(
2179                vec![inst(), dur()], EffectSet::empty(), inst()));
2180            fields.insert("diff".into(), Ty::function(
2181                vec![inst(), inst()], EffectSet::empty(), dur()));
2182            fields.insert("duration_seconds".into(), Ty::function(
2183                vec![Ty::float()], EffectSet::empty(), dur()));
2184            fields.insert("duration_minutes".into(), Ty::function(
2185                vec![Ty::int()], EffectSet::empty(), dur()));
2186            fields.insert("duration_days".into(), Ty::function(
2187                vec![Ty::int()], EffectSet::empty(), dur()));
2188            // #331: comparison ops on Instant.
2189            fields.insert("before".into(), Ty::function(
2190                vec![inst(), inst()], EffectSet::empty(), Ty::bool()));
2191            fields.insert("after".into(), Ty::function(
2192                vec![inst(), inst()], EffectSet::empty(), Ty::bool()));
2193            // compare :: Instant, Instant -> Int  (-1 / 0 / +1)
2194            fields.insert("compare".into(), Ty::function(
2195                vec![inst(), inst()], EffectSet::empty(), Ty::int()));
2196            Some(Ty::Record(fields))
2197        }
2198        // #331: duration module — scalar extraction from Duration values.
2199        "duration" => {
2200            let dur = || Ty::Con("Duration".into(), vec![]);
2201            let mut fields = IndexMap::new();
2202            // Scalar extraction from a Duration (nanoseconds under the
2203            // hood). Each truncates toward zero. `seconds` shipped with
2204            // #331; #681 rounds out the unit set so a Duration built in
2205            // days via `datetime.duration_days` can be read back in the
2206            // same units rather than only as seconds.
2207            // millis / seconds / minutes / hours / days :: Duration -> Int
2208            for name in &["millis", "seconds", "minutes", "hours", "days"] {
2209                fields.insert((*name).into(), Ty::function(
2210                    vec![dur()], EffectSet::empty(), Ty::int()));
2211            }
2212            Some(Ty::Record(fields))
2213        }
2214        "process" => {
2215            // Streaming subprocess. The opaque `ProcessHandle` type
2216            // is an Int handle into a process-wide registry holding
2217            // the `Child` plus its stdout/stderr `BufReader`s.
2218            let ph = || Ty::Con("ProcessHandle".into(), vec![]);
2219            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
2220            let opts_t = || {
2221                let mut fs = IndexMap::new();
2222                fs.insert("cwd".into(),
2223                    Ty::Con("Option".into(), vec![Ty::str()]));
2224                fs.insert("env".into(),
2225                    Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
2226                fs.insert("stdin".into(),
2227                    Ty::Con("Option".into(), vec![Ty::bytes()]));
2228                Ty::Record(fs)
2229            };
2230            let exit_t = || {
2231                let mut fs = IndexMap::new();
2232                fs.insert("code".into(), Ty::int());
2233                fs.insert("signaled".into(), Ty::bool());
2234                Ty::Record(fs)
2235            };
2236            let output_t = || {
2237                let mut fs = IndexMap::new();
2238                fs.insert("stdout".into(), Ty::str());
2239                fs.insert("stderr".into(), Ty::str());
2240                fs.insert("exit_code".into(), Ty::int());
2241                Ty::Record(fs)
2242            };
2243            let mut fields = IndexMap::new();
2244            // spawn :: Str, List[Str], Opts -> [proc] Result[ProcessHandle, Str]
2245            fields.insert("spawn".into(), Ty::function(
2246                vec![Ty::str(), Ty::List(Box::new(Ty::str())), opts_t()],
2247                EffectSet::singleton("proc"),
2248                result_str(ph())));
2249            // read_stdout_line / read_stderr_line :: ProcessHandle -> [proc] Option[Str]
2250            for n in &["read_stdout_line", "read_stderr_line"] {
2251                fields.insert((*n).into(), Ty::function(
2252                    vec![ph()], EffectSet::singleton("proc"),
2253                    Ty::Con("Option".into(), vec![Ty::str()])));
2254            }
2255            // wait :: ProcessHandle -> [proc] ProcessExit
2256            fields.insert("wait".into(), Ty::function(
2257                vec![ph()], EffectSet::singleton("proc"), exit_t()));
2258            // kill :: ProcessHandle, Str -> [proc] Result[Unit, Str]
2259            fields.insert("kill".into(), Ty::function(
2260                vec![ph(), Ty::str()],
2261                EffectSet::singleton("proc"),
2262                result_str(Ty::Unit)));
2263            // run :: Str, List[Str] -> [proc] Result[ProcessOutput, Str]
2264            // Blocking convenience that captures stdout/stderr fully
2265            // and returns once the child exits. For programs that
2266            // need streaming, use spawn + read_*_line + wait.
2267            fields.insert("run".into(), Ty::function(
2268                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
2269                EffectSet::singleton("proc"),
2270                result_str(output_t())));
2271            Some(Ty::Record(fields))
2272        }
2273        "fs" => {
2274            // Filesystem walk + mutate. Walk-style ops (exists, walk,
2275            // glob, …) declare [fs_walk] — distinct from [fs_read]
2276            // (which is content reads via io.read), so reviewers can
2277            // separately track directory traversal vs file-content
2278            // exposure. Mutating ops (mkdir_p, remove, copy) declare
2279            // [fs_write]. Path scoping uses --allow-fs-read for walk
2280            // (a directory listing is an information disclosure on
2281            // the same path tree) and --allow-fs-write for mutations.
2282            let stat_t = || {
2283                let mut fs = IndexMap::new();
2284                fs.insert("size".into(), Ty::int());
2285                fs.insert("mtime".into(), Ty::int());
2286                fs.insert("is_dir".into(), Ty::bool());
2287                fs.insert("is_file".into(), Ty::bool());
2288                Ty::Record(fs)
2289            };
2290            let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
2291            let mut fields = IndexMap::new();
2292            // Walk-style queries [fs_walk]
2293            fields.insert("exists".into(), Ty::function(
2294                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
2295            fields.insert("is_file".into(), Ty::function(
2296                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
2297            fields.insert("is_dir".into(), Ty::function(
2298                vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
2299            fields.insert("stat".into(), Ty::function(
2300                vec![Ty::str()], EffectSet::singleton("fs_walk"),
2301                result_str(stat_t())));
2302            fields.insert("list_dir".into(), Ty::function(
2303                vec![Ty::str()], EffectSet::singleton("fs_walk"),
2304                result_str(Ty::List(Box::new(Ty::str())))));
2305            fields.insert("walk".into(), Ty::function(
2306                vec![Ty::str()], EffectSet::singleton("fs_walk"),
2307                result_str(Ty::List(Box::new(Ty::str())))));
2308            fields.insert("glob".into(), Ty::function(
2309                vec![Ty::str()], EffectSet::singleton("fs_walk"),
2310                result_str(Ty::List(Box::new(Ty::str())))));
2311            // Mutations [fs_write]
2312            fields.insert("mkdir_p".into(), Ty::function(
2313                vec![Ty::str()], EffectSet::singleton("fs_write"),
2314                result_str(Ty::Unit)));
2315            fields.insert("remove".into(), Ty::function(
2316                vec![Ty::str()], EffectSet::singleton("fs_write"),
2317                result_str(Ty::Unit)));
2318            fields.insert("copy".into(), Ty::function(
2319                vec![Ty::str(), Ty::str()],
2320                EffectSet {
2321                    concrete: [crate::types::EffectKind::bare("fs_walk"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
2322                    var: None,
2323                },
2324                result_str(Ty::Unit)));
2325            Some(Ty::Record(fields))
2326        }
2327        "kv" => {
2328            // Embedded key-value store. The opaque `Kv` type is
2329            // backed by an Int handle into a process-wide registry.
2330            let kv_t = || Ty::Con("Kv".into(), vec![]);
2331            let mut fields = IndexMap::new();
2332            // open :: Str -> [kv, fs_write] Result[Kv, Str]
2333            fields.insert("open".into(), Ty::function(
2334                vec![Ty::str()],
2335                EffectSet {
2336                    concrete: [crate::types::EffectKind::bare("kv"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
2337                    var: None,
2338                },
2339                Ty::Con("Result".into(), vec![kv_t(), Ty::str()])));
2340            // close :: Kv -> [kv] Unit
2341            fields.insert("close".into(), Ty::function(
2342                vec![kv_t()],
2343                EffectSet::singleton("kv"),
2344                Ty::Unit));
2345            // get :: Kv, Str -> [kv] Option[Bytes]
2346            fields.insert("get".into(), Ty::function(
2347                vec![kv_t(), Ty::str()],
2348                EffectSet::singleton("kv"),
2349                Ty::Con("Option".into(), vec![Ty::bytes()])));
2350            // put :: Kv, Str, Bytes -> [kv] Result[Unit, Str]
2351            fields.insert("put".into(), Ty::function(
2352                vec![kv_t(), Ty::str(), Ty::bytes()],
2353                EffectSet::singleton("kv"),
2354                Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
2355            // delete :: Kv, Str -> [kv] Result[Unit, Str]
2356            fields.insert("delete".into(), Ty::function(
2357                vec![kv_t(), Ty::str()],
2358                EffectSet::singleton("kv"),
2359                Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
2360            // contains :: Kv, Str -> [kv] Bool
2361            fields.insert("contains".into(), Ty::function(
2362                vec![kv_t(), Ty::str()],
2363                EffectSet::singleton("kv"),
2364                Ty::bool()));
2365            // list_prefix :: Kv, Str -> [kv] List[Str]
2366            fields.insert("list_prefix".into(), Ty::function(
2367                vec![kv_t(), Ty::str()],
2368                EffectSet::singleton("kv"),
2369                Ty::List(Box::new(Ty::str()))));
2370            Some(Ty::Record(fields))
2371        }
2372        "vcs" => {
2373            // Content-addressed blob store (#5 / M6.1b). `put_blob` returns the
2374            // lowercase hex SHA-256 of the content — the SAME id as
2375            // `crypto.sha256_str`, so blobs are interchangeable with loom's
2376            // SQLite-backed artifacts by id. `ref_set`/`ref_get` bind a name
2377            // (namespace + key) to a blob sha, e.g. namespace "loom/sprint-{id}",
2378            // key = node id (branch-per-sprint, #5). The on-disk layout matches
2379            // lex-store's blob CAS (<root>/blobs, <root>/blobrefs).
2380            //
2381            // fs_write/fs_read appear in the effect rows so `lex audit` sees the
2382            // disk touch; the store root is internal (~/.lex/store or
2383            // $LEX_STORE_ROOT) so no user-path allowlist applies.
2384            let mut fields = IndexMap::new();
2385            let vcs_w = || EffectSet {
2386                concrete: [crate::types::EffectKind::bare("vcs"),
2387                           crate::types::EffectKind::bare("fs_write")]
2388                    .into_iter().collect(),
2389                var: None,
2390            };
2391            let vcs_r = || EffectSet {
2392                concrete: [crate::types::EffectKind::bare("vcs"),
2393                           crate::types::EffectKind::bare("fs_read")]
2394                    .into_iter().collect(),
2395                var: None,
2396            };
2397            let res = |ok: Ty| Ty::Con("Result".into(), vec![ok, Ty::str()]);
2398
2399            // put_blob :: Str -> [vcs, fs_write] Result[Str, Str]  (returns sha)
2400            fields.insert("put_blob".into(), Ty::function(
2401                vec![Ty::str()], vcs_w(), res(Ty::str())));
2402            // get_blob :: Str -> [vcs, fs_read] Result[Str, Str]
2403            fields.insert("get_blob".into(), Ty::function(
2404                vec![Ty::str()], vcs_r(), res(Ty::str())));
2405            // has_blob :: Str -> [vcs, fs_read] Bool
2406            fields.insert("has_blob".into(), Ty::function(
2407                vec![Ty::str()], vcs_r(), Ty::bool()));
2408            // ref_set :: Str, Str, Str -> [vcs, fs_write] Result[Unit, Str]
2409            fields.insert("ref_set".into(), Ty::function(
2410                vec![Ty::str(), Ty::str(), Ty::str()], vcs_w(), res(Ty::Unit)));
2411            // ref_get :: Str, Str -> [vcs, fs_read] Result[Str, Str]  (key -> sha)
2412            fields.insert("ref_get".into(), Ty::function(
2413                vec![Ty::str(), Ty::str()], vcs_r(), res(Ty::str())));
2414            Some(Ty::Record(fields))
2415        }
2416        "sql" => {
2417            // Embedded SQL (SQLite via rusqlite). The opaque `Db` type is
2418            // backed by an Int handle into a process-wide registry (#362).
2419            //
2420            // Params use the typed `SqlParam` ADT (PStr|PInt|PFloat|PBool|PNull)
2421            // registered in env.rs, so callers don't have to stringify values.
2422            //
2423            // Transactions: sql.begin(db) → SqlTx; sql.commit/rollback(tx).
2424            // exec_tx / query_tx mirror exec / query but operate on a SqlTx.
2425            //
2426            // Row decoders: get_str / get_int / get_float / get_bool extract
2427            // typed columns from a row record by name.
2428            let db_t  = || Ty::Con("Db".into(), vec![]);
2429            let tx_t  = || Ty::Con("SqlTx".into(), vec![]);
2430            let sp_t  = || Ty::Con("SqlParam".into(), vec![]);
2431            let params_t = || Ty::List(Box::new(sp_t()));
2432            let mut fields = IndexMap::new();
2433
2434            // SqlError = { message, code, detail } — populated with
2435            // SQLSTATE (Postgres) or symbolic SQLite error name (#380).
2436            let se_t = || Ty::Con("SqlError".into(), vec![]);
2437
2438            // open :: Str -> [sql, fs_write] Result[Db, SqlError]
2439            fields.insert("open".into(), Ty::function(
2440                vec![Ty::str()],
2441                EffectSet {
2442                    concrete: [crate::types::EffectKind::bare("sql"),
2443                               crate::types::EffectKind::bare("fs_write")]
2444                        .into_iter().collect(),
2445                    var: None,
2446                },
2447                Ty::Con("Result".into(), vec![db_t(), se_t()])));
2448
2449            // close :: Db -> [sql] Unit
2450            fields.insert("close".into(), Ty::function(
2451                vec![db_t()],
2452                EffectSet::singleton("sql"),
2453                Ty::Unit));
2454
2455            // exec :: Db, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
2456            fields.insert("exec".into(), Ty::function(
2457                vec![db_t(), Ty::str(), params_t()],
2458                EffectSet::singleton("sql"),
2459                Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
2460
2461            // query[T] :: Db, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
2462            fields.insert("query".into(), Ty::function(
2463                vec![db_t(), Ty::str(), params_t()],
2464                EffectSet::singleton("sql"),
2465                Ty::Con("Result".into(), vec![
2466                    Ty::List(Box::new(Ty::Var(0))),
2467                    se_t(),
2468                ])));
2469
2470            // query_iter[T] :: Db, Str, List[SqlParam] -> [sql] Result[Iter[T], SqlError]
2471            // Streaming variant of `query` (#379). Rows are pulled from
2472            // the server one at a time via an mpsc-backed cursor —
2473            // memory stays bounded regardless of result-set size.
2474            // Other ops on the same `Db` handle block until the cursor
2475            // is drained (single connection per Db).
2476            fields.insert("query_iter".into(), Ty::function(
2477                vec![db_t(), Ty::str(), params_t()],
2478                EffectSet::singleton("sql"),
2479                Ty::Con("Result".into(), vec![
2480                    Ty::Con("Iter".into(), vec![Ty::Var(0)]),
2481                    se_t(),
2482                ])));
2483
2484            // begin :: Db -> [sql] Result[SqlTx, SqlError]
2485            fields.insert("begin".into(), Ty::function(
2486                vec![db_t()],
2487                EffectSet::singleton("sql"),
2488                Ty::Con("Result".into(), vec![tx_t(), se_t()])));
2489
2490            // commit :: SqlTx -> [sql] Result[Unit, SqlError]
2491            fields.insert("commit".into(), Ty::function(
2492                vec![tx_t()],
2493                EffectSet::singleton("sql"),
2494                Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
2495
2496            // rollback :: SqlTx -> [sql] Result[Unit, SqlError]
2497            fields.insert("rollback".into(), Ty::function(
2498                vec![tx_t()],
2499                EffectSet::singleton("sql"),
2500                Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
2501
2502            // exec_tx :: SqlTx, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
2503            fields.insert("exec_tx".into(), Ty::function(
2504                vec![tx_t(), Ty::str(), params_t()],
2505                EffectSet::singleton("sql"),
2506                Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
2507
2508            // query_tx[T] :: SqlTx, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
2509            fields.insert("query_tx".into(), Ty::function(
2510                vec![tx_t(), Ty::str(), params_t()],
2511                EffectSet::singleton("sql"),
2512                Ty::Con("Result".into(), vec![
2513                    Ty::List(Box::new(Ty::Var(0))),
2514                    se_t(),
2515                ])));
2516
2517            // Row decoders: get_X[T] :: T, Str -> Option[X]
2518            // T is polymorphic so these work on any row record shape.
2519            fields.insert("get_str".into(), Ty::function(
2520                vec![Ty::Var(0), Ty::str()],
2521                EffectSet::empty(),
2522                Ty::Con("Option".into(), vec![Ty::str()])));
2523            fields.insert("get_int".into(), Ty::function(
2524                vec![Ty::Var(0), Ty::str()],
2525                EffectSet::empty(),
2526                Ty::Con("Option".into(), vec![Ty::int()])));
2527            fields.insert("get_float".into(), Ty::function(
2528                vec![Ty::Var(0), Ty::str()],
2529                EffectSet::empty(),
2530                Ty::Con("Option".into(), vec![Ty::float()])));
2531            fields.insert("get_bool".into(), Ty::function(
2532                vec![Ty::Var(0), Ty::str()],
2533                EffectSet::empty(),
2534                Ty::Con("Option".into(), vec![Ty::bool()])));
2535
2536            Some(Ty::Record(fields))
2537        }
2538        "redis" => {
2539            // Thin Redis client (#533). ConnRedis is an opaque handle backed by a
2540            // process-wide registry (same pattern as Db in std.sql). All ops carry
2541            // [net] — Redis is a TCP service; no separate [redis] effect.
2542            //
2543            // subscribe / psubscribe return Unit because they are blocking
2544            // infinite loops, consistent with net.serve_fn and ws.serve.
2545            //
2546            // subscribe/psubscribe open a *dedicated* connection internally —
2547            // Redis disallows non-Pub/Sub commands on a subscribed connection.
2548            let conn_t = || Ty::Con("ConnRedis".into(), vec![]);
2549            let mut fields = IndexMap::new();
2550
2551            // connect :: Str -> [net] Result[ConnRedis, Str]
2552            // url: "redis://host:6379" or "rediss://host:6380" (TLS)
2553            fields.insert("connect".into(), Ty::function(
2554                vec![Ty::str()],
2555                EffectSet::singleton("net"),
2556                Ty::Con("Result".into(), vec![conn_t(), Ty::str()])));
2557
2558            // close :: ConnRedis -> [net] Unit
2559            fields.insert("close".into(), Ty::function(
2560                vec![conn_t()],
2561                EffectSet::singleton("net"),
2562                Ty::Unit));
2563
2564            // ---- Key-value -----------------------------------------------
2565
2566            // get :: ConnRedis, Str -> [net] Option[Str]
2567            fields.insert("get".into(), Ty::function(
2568                vec![conn_t(), Ty::str()],
2569                EffectSet::singleton("net"),
2570                Ty::Con("Option".into(), vec![Ty::str()])));
2571
2572            // set :: ConnRedis, Str, Str -> [net] Unit
2573            fields.insert("set".into(), Ty::function(
2574                vec![conn_t(), Ty::str(), Ty::str()],
2575                EffectSet::singleton("net"),
2576                Ty::Unit));
2577
2578            // set_ex :: ConnRedis, Str, Str, Int -> [net] Unit
2579            fields.insert("set_ex".into(), Ty::function(
2580                vec![conn_t(), Ty::str(), Ty::str(), Ty::int()],
2581                EffectSet::singleton("net"),
2582                Ty::Unit));
2583
2584            // del :: ConnRedis, Str -> [net] Unit
2585            fields.insert("del".into(), Ty::function(
2586                vec![conn_t(), Ty::str()],
2587                EffectSet::singleton("net"),
2588                Ty::Unit));
2589
2590            // exists :: ConnRedis, Str -> [net] Bool
2591            fields.insert("exists".into(), Ty::function(
2592                vec![conn_t(), Ty::str()],
2593                EffectSet::singleton("net"),
2594                Ty::bool()));
2595
2596            // expire :: ConnRedis, Str, Int -> [net] Unit
2597            fields.insert("expire".into(), Ty::function(
2598                vec![conn_t(), Ty::str(), Ty::int()],
2599                EffectSet::singleton("net"),
2600                Ty::Unit));
2601
2602            // ---- Pub/Sub -------------------------------------------------
2603
2604            // publish :: ConnRedis, Str, Str -> [net] Int
2605            // Returns the number of subscribers that received the message.
2606            fields.insert("publish".into(), Ty::function(
2607                vec![conn_t(), Ty::str(), Ty::str()],
2608                EffectSet::singleton("net"),
2609                Ty::int()));
2610
2611            // subscribe :: ConnRedis, Str, (Str, Str ->[E] Unit) -> [net] Unit
2612            // Blocking loop; handler receives (channel, message) on each message.
2613            // Uses a dedicated connection — Redis disallows non-Pub/Sub commands
2614            // on a subscribed connection. Handler carries an open effect row so
2615            // callers can use io, net, sql, etc. inside the closure.
2616            let handler2 = Ty::function(
2617                vec![Ty::str(), Ty::str()],
2618                EffectSet::open_var(0),
2619                Ty::Unit);
2620            fields.insert("subscribe".into(), Ty::function(
2621                vec![conn_t(), Ty::str(), handler2],
2622                EffectSet::singleton("net"),
2623                Ty::Unit));  // Unit
2624
2625            // psubscribe :: ConnRedis, Str, (Str, Str, Str ->[E] Unit) -> [net] Unit
2626            // Pattern-subscribe; handler receives (pattern, channel, message).
2627            // Handler carries an open effect row (same rationale as subscribe).
2628            let handler3 = Ty::function(
2629                vec![Ty::str(), Ty::str(), Ty::str()],
2630                EffectSet::open_var(1),
2631                Ty::Unit);
2632            fields.insert("psubscribe".into(), Ty::function(
2633                vec![conn_t(), Ty::str(), handler3],
2634                EffectSet::singleton("net"),
2635                Ty::Unit));  // Unit
2636
2637            // ---- List ----------------------------------------------------
2638
2639            // lpush :: ConnRedis, Str, Str -> [net] Int
2640            fields.insert("lpush".into(), Ty::function(
2641                vec![conn_t(), Ty::str(), Ty::str()],
2642                EffectSet::singleton("net"),
2643                Ty::int()));
2644
2645            // rpush :: ConnRedis, Str, Str -> [net] Int
2646            fields.insert("rpush".into(), Ty::function(
2647                vec![conn_t(), Ty::str(), Ty::str()],
2648                EffectSet::singleton("net"),
2649                Ty::int()));
2650
2651            // brpop :: ConnRedis, Str, Int -> [net] Option[Str]
2652            // Blocking right-pop; returns None on timeout. timeout=0 blocks
2653            // indefinitely (the runtime does not treat this as a hung effect).
2654            fields.insert("brpop".into(), Ty::function(
2655                vec![conn_t(), Ty::str(), Ty::int()],
2656                EffectSet::singleton("net"),
2657                Ty::Con("Option".into(), vec![Ty::str()])));
2658
2659            // llen :: ConnRedis, Str -> [net] Int
2660            fields.insert("llen".into(), Ty::function(
2661                vec![conn_t(), Ty::str()],
2662                EffectSet::singleton("net"),
2663                Ty::int()));
2664
2665            // ---- Hash ----------------------------------------------------
2666
2667            // hset :: ConnRedis, Str, Str, Str -> [net] Unit
2668            fields.insert("hset".into(), Ty::function(
2669                vec![conn_t(), Ty::str(), Ty::str(), Ty::str()],
2670                EffectSet::singleton("net"),
2671                Ty::Unit));
2672
2673            // hget :: ConnRedis, Str, Str -> [net] Option[Str]
2674            fields.insert("hget".into(), Ty::function(
2675                vec![conn_t(), Ty::str(), Ty::str()],
2676                EffectSet::singleton("net"),
2677                Ty::Con("Option".into(), vec![Ty::str()])));
2678
2679            // hdel :: ConnRedis, Str, Str -> [net] Unit
2680            fields.insert("hdel".into(), Ty::function(
2681                vec![conn_t(), Ty::str(), Ty::str()],
2682                EffectSet::singleton("net"),
2683                Ty::Unit));
2684
2685            // hgetall :: ConnRedis, Str -> [net] List[(Str, Str)]
2686            fields.insert("hgetall".into(), Ty::function(
2687                vec![conn_t(), Ty::str()],
2688                EffectSet::singleton("net"),
2689                Ty::List(Box::new(Ty::Tuple(vec![Ty::str(), Ty::str()])))));
2690
2691            Some(Ty::Record(fields))
2692        }
2693        "parser" => {
2694            // #217: structured parser combinators. Parser values are
2695            // tagged Records at runtime (`{ kind, ... }`), opaque at
2696            // the language level via `Ty::Con("Parser", [T])`.
2697            //
2698            // Surface:
2699            //   - primitives: char, string, digit, alpha, whitespace, eof
2700            //   - combinators: seq, alt, many, optional, map, and_then
2701            //   - run :: Parser[T], Str -> Result[T, ParseErr]
2702            //
2703            // `map` and `and_then` were deferred from #217's v1 because
2704            // their closure arguments carried call-site identity that
2705            // broke the canonical-parsers acceptance criterion. With
2706            // closure body-hash equality landed in #222, that concern
2707            // is gone, and #221 wires them in. The interpreter for
2708            // `parser.run` has been moved to `lex-bytecode::parser_runtime`
2709            // so it can invoke closures from `Map` / `AndThen` nodes.
2710            let pt = |t: Ty| Ty::Con("Parser".into(), vec![t]);
2711            let parse_err = || {
2712                let mut fs = IndexMap::new();
2713                fs.insert("pos".into(), Ty::int());
2714                fs.insert("message".into(), Ty::str());
2715                Ty::Record(fs)
2716            };
2717            let mut fields = IndexMap::new();
2718            // char :: Str -> Parser[Str] (single-char Str literal)
2719            fields.insert("char".into(), Ty::function(
2720                vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
2721            // string :: Str -> Parser[Str]
2722            fields.insert("string".into(), Ty::function(
2723                vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
2724            // digit :: () -> Parser[Str]
2725            fields.insert("digit".into(), Ty::function(
2726                vec![], EffectSet::empty(), pt(Ty::str())));
2727            // alpha :: () -> Parser[Str]
2728            fields.insert("alpha".into(), Ty::function(
2729                vec![], EffectSet::empty(), pt(Ty::str())));
2730            // whitespace :: () -> Parser[Str]
2731            fields.insert("whitespace".into(), Ty::function(
2732                vec![], EffectSet::empty(), pt(Ty::str())));
2733            // eof :: () -> Parser[Unit]
2734            fields.insert("eof".into(), Ty::function(
2735                vec![], EffectSet::empty(), pt(Ty::Unit)));
2736            // seq :: Parser[A], Parser[B] -> Parser[(A, B)]
2737            fields.insert("seq".into(), Ty::function(
2738                vec![pt(Ty::Var(0)), pt(Ty::Var(1))],
2739                EffectSet::empty(),
2740                pt(Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]))));
2741            // alt :: Parser[T], Parser[T] -> Parser[T]
2742            // PEG-style ordered choice: the second alternative is
2743            // tried only if the first fails.
2744            fields.insert("alt".into(), Ty::function(
2745                vec![pt(Ty::Var(0)), pt(Ty::Var(0))],
2746                EffectSet::empty(),
2747                pt(Ty::Var(0))));
2748            // many :: Parser[T] -> Parser[List[T]]
2749            // Zero-or-more. Stops as soon as the inner parser fails
2750            // OR doesn't advance the position (avoids infinite loop
2751            // on empty matches).
2752            fields.insert("many".into(), Ty::function(
2753                vec![pt(Ty::Var(0))],
2754                EffectSet::empty(),
2755                pt(Ty::List(Box::new(Ty::Var(0))))));
2756            // optional :: Parser[T] -> Parser[Option[T]]
2757            fields.insert("optional".into(), Ty::function(
2758                vec![pt(Ty::Var(0))],
2759                EffectSet::empty(),
2760                pt(Ty::Con("Option".into(), vec![Ty::Var(0)]))));
2761            // map :: Parser[T], (T) -> [E] U -> [E] Parser[U]
2762            // The closure runs at parse time when the Parser is run.
2763            // Effect-polymorphic on the closure: any effect the
2764            // closure declares propagates to the surrounding `run`.
2765            fields.insert("map".into(), Ty::function(
2766                vec![
2767                    pt(Ty::Var(0)),
2768                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
2769                ],
2770                EffectSet::open_var(2),
2771                pt(Ty::Var(1))));
2772            // and_then :: Parser[T], (T) -> [E] Parser[U] -> [E] Parser[U]
2773            // Monadic bind: closure inspects the parsed value and
2774            // returns the next parser to run.
2775            fields.insert("and_then".into(), Ty::function(
2776                vec![
2777                    pt(Ty::Var(0)),
2778                    Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
2779                        pt(Ty::Var(1))),
2780                ],
2781                EffectSet::open_var(3),
2782                pt(Ty::Var(1))));
2783            // run :: Parser[T], Str -> Result[T, ParseErr]
2784            // ParseErr = { pos :: Int, message :: Str }
2785            fields.insert("run".into(), Ty::function(
2786                vec![pt(Ty::Var(0)), Ty::str()],
2787                EffectSet::empty(),
2788                Ty::Con("Result".into(), vec![Ty::Var(0), parse_err()])));
2789            Some(Ty::Record(fields))
2790        }
2791        "cli" => {
2792            // #224 Rubric port: argparse-equivalent for end-user
2793            // programs. Spec values are tagged `Json` records (opaque
2794            // to the language but inspectable). Construction via the
2795            // `flag` / `option` / `positional` / `spec` builders;
2796            // parse + introspection / help via the remaining ops.
2797            let json = || Ty::Con("Json".into(), vec![]);
2798            let opt_str = || Ty::Con("Option".into(), vec![Ty::str()]);
2799            let mut fields = IndexMap::new();
2800            // flag :: Str -> Option[Str] -> Str -> Json
2801            //   long_name -> short -> help -> CliArg
2802            fields.insert("flag".into(), Ty::function(
2803                vec![Ty::str(), opt_str(), Ty::str()],
2804                EffectSet::empty(),
2805                json()));
2806            // option :: Str -> Option[Str] -> Str -> Option[Str] -> Json
2807            //   long_name -> short -> help -> default -> CliArg
2808            fields.insert("option".into(), Ty::function(
2809                vec![Ty::str(), opt_str(), Ty::str(), opt_str()],
2810                EffectSet::empty(),
2811                json()));
2812            // positional :: Str -> Str -> Bool -> Json
2813            //   name -> help -> required -> CliArg
2814            fields.insert("positional".into(), Ty::function(
2815                vec![Ty::str(), Ty::str(), Ty::bool()],
2816                EffectSet::empty(),
2817                json()));
2818            // spec :: Str -> Str -> List[Json] -> List[Json] -> Json
2819            //   name -> help -> args -> subcommands -> CliSpec
2820            fields.insert("spec".into(), Ty::function(
2821                vec![Ty::str(), Ty::str(),
2822                     Ty::List(Box::new(json())),
2823                     Ty::List(Box::new(json()))],
2824                EffectSet::empty(),
2825                json()));
2826            // parse :: Json -> List[Str] -> Result[Json, Str]
2827            //   spec -> argv -> Result[CliParsed, error]
2828            fields.insert("parse".into(), Ty::function(
2829                vec![json(), Ty::List(Box::new(Ty::str()))],
2830                EffectSet::empty(),
2831                Ty::Con("Result".into(), vec![json(), Ty::str()])));
2832            // envelope :: Bool -> Str -> T -> Json
2833            //   ok -> command -> data -> ACLI-shaped envelope.
2834            // `data` is polymorphic so callers don't have to round-
2835            // trip through `json.parse` for trivial payloads.
2836            fields.insert("envelope".into(), Ty::function(
2837                vec![Ty::bool(), Ty::str(), Ty::Var(0)],
2838                EffectSet::empty(),
2839                json()));
2840            // describe :: Json -> Json — machine-readable spec dump
2841            fields.insert("describe".into(), Ty::function(
2842                vec![json()],
2843                EffectSet::empty(),
2844                json()));
2845            // help :: Json -> Str — human-readable help text
2846            fields.insert("help".into(), Ty::function(
2847                vec![json()],
2848                EffectSet::empty(),
2849                Ty::str()));
2850            Some(Ty::Record(fields))
2851        }
2852        "regex" => {
2853            // The compiled `Regex` is stored as a `Str` at runtime
2854            // (the pattern source) plus a process-wide cache of the
2855            // actual `regex::Regex`. So `Regex` is a nominal type at
2856            // the language level but its value is just the pattern.
2857            let regex_t = || Ty::Con("Regex".into(), vec![]);
2858            let match_t = || {
2859                let mut fs = IndexMap::new();
2860                fs.insert("text".into(), Ty::str());
2861                fs.insert("start".into(), Ty::int());
2862                fs.insert("end".into(), Ty::int());
2863                fs.insert("groups".into(), Ty::List(Box::new(Ty::str())));
2864                Ty::Record(fs)
2865            };
2866            let mut fields = IndexMap::new();
2867            // compile :: Str -> Result[Regex, Str]
2868            fields.insert("compile".into(), Ty::function(
2869                vec![Ty::str()], EffectSet::empty(),
2870                Ty::Con("Result".into(), vec![regex_t(), Ty::str()])));
2871            // is_match :: Regex, Str -> Bool
2872            fields.insert("is_match".into(), Ty::function(
2873                vec![regex_t(), Ty::str()], EffectSet::empty(), Ty::bool()));
2874            // is_match_str :: Str, Str -> Bool
2875            // Compiles the first argument as a pattern and matches against the second.
2876            // Returns false on invalid pattern instead of propagating an error.
2877            fields.insert("is_match_str".into(), Ty::function(
2878                vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
2879            // find :: Regex, Str -> Option[Match]
2880            fields.insert("find".into(), Ty::function(
2881                vec![regex_t(), Ty::str()], EffectSet::empty(),
2882                Ty::Con("Option".into(), vec![match_t()])));
2883            // find_all :: Regex, Str -> List[Match]
2884            fields.insert("find_all".into(), Ty::function(
2885                vec![regex_t(), Ty::str()], EffectSet::empty(),
2886                Ty::List(Box::new(match_t()))));
2887            // replace :: Regex, Str, Str -> Str
2888            fields.insert("replace".into(), Ty::function(
2889                vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
2890            // replace_all :: Regex, Str, Str -> Str
2891            fields.insert("replace_all".into(), Ty::function(
2892                vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
2893            // split :: Regex, Str -> List[Str]
2894            fields.insert("split".into(), Ty::function(
2895                vec![regex_t(), Ty::str()], EffectSet::empty(),
2896                Ty::List(Box::new(Ty::str()))));
2897            Some(Ty::Record(fields))
2898        }
2899        "http" => {
2900            // Rich HTTP client. `[net]` for the wire ops, pure for
2901            // the builders / decoders. `--allow-net-host` gates per
2902            // request. Multipart upload + streaming response bodies
2903            // are deferred to v1.5; the v1 surface covers the
2904            // common cases (auth, headers, query, timeouts, JSON /
2905            // text decoding).
2906            let req_t  = || Ty::Con("HttpRequest".into(), vec![]);
2907            let resp_t = || Ty::Con("HttpResponse".into(), vec![]);
2908            let err_t  = || Ty::Con("HttpError".into(), vec![]);
2909            let result_he = |t: Ty| Ty::Con("Result".into(), vec![t, err_t()]);
2910            let str_str_map = || Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]);
2911            let mut fields = IndexMap::new();
2912            // -- wire ops (effectful) --
2913            // send :: HttpRequest -> [net] Result[HttpResponse, HttpError]
2914            fields.insert("send".into(), Ty::function(
2915                vec![req_t()],
2916                EffectSet::singleton("net"),
2917                result_he(resp_t()),
2918            ));
2919            // get :: Str -> [net] Result[HttpResponse, HttpError]
2920            fields.insert("get".into(), Ty::function(
2921                vec![Ty::str()],
2922                EffectSet::singleton("net"),
2923                result_he(resp_t()),
2924            ));
2925            // post :: Str, Bytes, Str -> [net] Result[HttpResponse, HttpError]
2926            fields.insert("post".into(), Ty::function(
2927                vec![Ty::str(), Ty::bytes(), Ty::str()],
2928                EffectSet::singleton("net"),
2929                result_he(resp_t()),
2930            ));
2931            // -- pure builders (record transforms) --
2932            // with_header :: HttpRequest, Str, Str -> HttpRequest
2933            fields.insert("with_header".into(), Ty::function(
2934                vec![req_t(), Ty::str(), Ty::str()],
2935                EffectSet::empty(),
2936                req_t(),
2937            ));
2938            // with_auth :: HttpRequest, Str, Str -> HttpRequest
2939            // (Renders `<scheme> <token>` into the `Authorization`
2940            // header — `Bearer <jwt>`, `Basic <b64>`, etc.)
2941            fields.insert("with_auth".into(), Ty::function(
2942                vec![req_t(), Ty::str(), Ty::str()],
2943                EffectSet::empty(),
2944                req_t(),
2945            ));
2946            // with_query :: HttpRequest, Map[Str, Str] -> HttpRequest
2947            // (Appends a `?k=v&...` query string; values are URL-
2948            // encoded so `&` / `=` / spaces in values don't escape.)
2949            fields.insert("with_query".into(), Ty::function(
2950                vec![req_t(), str_str_map()],
2951                EffectSet::empty(),
2952                req_t(),
2953            ));
2954            // with_timeout_ms :: HttpRequest, Int -> HttpRequest
2955            fields.insert("with_timeout_ms".into(), Ty::function(
2956                vec![req_t(), Ty::int()],
2957                EffectSet::empty(),
2958                req_t(),
2959            ));
2960            // -- pure decoders --
2961            // json_body[T] :: HttpResponse -> Result[T, HttpError]
2962            // Polymorphic on the parsed shape, matching `json.parse`.
2963            fields.insert("json_body".into(), Ty::function(
2964                vec![resp_t()],
2965                EffectSet::empty(),
2966                result_he(Ty::Var(0)),
2967            ));
2968            // text_body :: HttpResponse -> Result[Str, HttpError]
2969            fields.insert("text_body".into(), Ty::function(
2970                vec![resp_t()],
2971                EffectSet::empty(),
2972                result_he(Ty::str()),
2973            ));
2974            // stream_lines :: Str, Map[Str, Str], Str -> [net] Result[Stream[Str], Str]
2975            // Streaming HTTP POST that yields the response body line-by-line
2976            // for SSE / NDJSON endpoints. Returns a lazy `Stream[Str]` (#683):
2977            // each `stream.next` pulls exactly one line off the socket as it
2978            // arrives, so an endpoint that holds the connection open and emits
2979            // events over time is consumed incrementally instead of blocking
2980            // until close. Connection errors at request time surface as
2981            // `Err(Str)`; a mid-stream read error / close ends the stream
2982            // (next `stream.next` returns `None`). Consume with `std.stream`
2983            // (`stream.next` / `stream.collect`), which carries `[stream]`.
2984            fields.insert("stream_lines".into(), Ty::function(
2985                vec![
2986                    Ty::str(),
2987                    Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
2988                    Ty::str(),
2989                ],
2990                EffectSet::singleton("net"),
2991                Ty::Con("Result".into(), vec![
2992                    Ty::Con("Stream".into(), vec![Ty::str()]),
2993                    Ty::str(),
2994                ]),
2995            ));
2996            Some(Ty::Record(fields))
2997        }
2998        "yaml" => {
2999            // YAML config parser. Same shape as `std.toml`: parse
3000            // is polymorphic, output Value layout matches std.json
3001            // (Str/Int/Float/Bool/List/Record). Anchors and tags
3002            // are flattened by serde_yaml's deserializer.
3003            let mut fields = IndexMap::new();
3004            fields.insert("parse".into(), Ty::function(
3005                vec![Ty::str()], EffectSet::empty(),
3006                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3007            ));
3008            // Tactical fix for #168 — caller-supplied required-field
3009            // list. See std.json's parse_strict for context.
3010            fields.insert("parse_strict".into(), Ty::function(
3011                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
3012                EffectSet::empty(),
3013                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3014            ));
3015            fields.insert("stringify".into(), Ty::function(
3016                vec![Ty::Var(0)], EffectSet::empty(),
3017                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3018            ));
3019            Some(Ty::Record(fields))
3020        }
3021        "dotenv" => {
3022            // .env-style files. parse :: Str -> Result[Map[Str,Str], Str].
3023            // Returns a map (not a polymorphic record) because
3024            // dotenv files don't carry shape — every value is a
3025            // string and keys aren't statically known.
3026            let mut fields = IndexMap::new();
3027            fields.insert("parse".into(), Ty::function(
3028                vec![Ty::str()], EffectSet::empty(),
3029                Ty::Con("Result".into(), vec![
3030                    Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
3031                    Ty::str(),
3032                ]),
3033            ));
3034            Some(Ty::Record(fields))
3035        }
3036        "csv" => {
3037            // CSV rows-as-lists. parse :: Str -> Result[List[List[Str]], Str].
3038            // Header awareness is left to the caller — row 0 is
3039            // whatever the file has. A `parse_with_headers` that
3040            // returns List[Map[Str,Str]] is a natural follow-up.
3041            let row_ty = Ty::List(Box::new(Ty::str()));
3042            let rows_ty = Ty::List(Box::new(row_ty.clone()));
3043            let mut fields = IndexMap::new();
3044            fields.insert("parse".into(), Ty::function(
3045                vec![Ty::str()], EffectSet::empty(),
3046                Ty::Con("Result".into(), vec![rows_ty.clone(), Ty::str()]),
3047            ));
3048            fields.insert("stringify".into(), Ty::function(
3049                vec![rows_ty], EffectSet::empty(),
3050                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3051            ));
3052            Some(Ty::Record(fields))
3053        }
3054        "test" => {
3055            // Tiny assertion library (#proposed-stdlib). Each helper
3056            // returns Result[Unit, Str] so a test is itself a fn
3057            // returning Result. Callers compose suites in user code
3058            // (a List of (name, () -> Result[Unit, Str]) pairs +
3059            // list.fold to accumulate verdicts). Property generators
3060            // and a Rust-side Suite type are deferred to v2.
3061            let mut fields = IndexMap::new();
3062            // assert_eq[a, b] :: T -> T -> Result[Unit, Str]
3063            // (T constrained equal by unification on the two args)
3064            let unit_result = || Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]);
3065            fields.insert("assert_eq".into(), Ty::function(
3066                vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
3067            ));
3068            fields.insert("assert_ne".into(), Ty::function(
3069                vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
3070            ));
3071            fields.insert("assert_true".into(), Ty::function(
3072                vec![Ty::bool()], EffectSet::empty(), unit_result(),
3073            ));
3074            fields.insert("assert_false".into(), Ty::function(
3075                vec![Ty::bool()], EffectSet::empty(), unit_result(),
3076            ));
3077            Some(Ty::Record(fields))
3078        }
3079        "toml" => {
3080            // TOML config parser. Mirrors `std.json`'s shape: parse
3081            // is polymorphic so callers annotate the expected
3082            // record / list / scalar shape and the type checker
3083            // unifies. The parsed TOML maps to the same Lex Value
3084            // shape as JSON does:
3085            //
3086            //   TOML String   → Value::Str
3087            //   TOML Integer  → Value::Int
3088            //   TOML Float    → Value::Float
3089            //   TOML Boolean  → Value::Bool
3090            //   TOML Array    → Value::List
3091            //   TOML Table    → Value::Record
3092            //   TOML Datetime → Value::Str (RFC 3339, lossless)
3093            //
3094            // The Datetime → Str fallback is the one info-losing
3095            // step; callers who want a real `Instant` can pipe the
3096            // string through `datetime.parse_iso`.
3097            let mut fields = IndexMap::new();
3098            // parse :: Str -> Result[T, Str]
3099            fields.insert("parse".into(), Ty::function(
3100                vec![Ty::str()], EffectSet::empty(),
3101                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3102            ));
3103            // parse_strict :: (Str, List[Str]) -> Result[T, Str]
3104            // Tactical fix for #168 — caller passes the field
3105            // names T requires; runtime returns Err if any are
3106            // missing from the parsed table instead of letting
3107            // field access panic later.
3108            fields.insert("parse_strict".into(), Ty::function(
3109                vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
3110                EffectSet::empty(),
3111                Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3112            ));
3113            // stringify :: T -> Result[Str, Str]
3114            // Returns Result (not Str) because not every Lex Value
3115            // has a TOML representation — top-level scalars,
3116            // closures, mixed-key maps etc. surface as Err rather
3117            // than panic.
3118            fields.insert("stringify".into(), Ty::function(
3119                vec![Ty::Var(0)], EffectSet::empty(),
3120                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3121            ));
3122            Some(Ty::Record(fields))
3123        }
3124        // `std.agent` (#184) — runtime primitives whose effects
3125        // separate (a) which LLM surface (`llm_local` vs
3126        // `llm_cloud`), (b) which peer protocol (`a2a`), and
3127        // (c) which tool boundary (`mcp`). The wire formats land
3128        // in downstream crates (`soft-agent`, `soft-a2a`) and
3129        // in #185 for MCP; what's typed here is the boundary
3130        // alone — agent code can be type-checked as
3131        // `[llm_local, a2a]` and will fail if it tries to reach
3132        // `[llm_cloud]` even before the wire layer is finished.
3133        "agent" => {
3134            let mut fields = IndexMap::new();
3135            // local_complete :: Str -> [llm_local] Result[Str, Str]
3136            fields.insert("local_complete".into(), Ty::function(
3137                vec![Ty::str()],
3138                EffectSet::singleton("llm_local"),
3139                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3140            ));
3141            // cloud_complete :: Str -> [llm_cloud] Result[Str, Str]
3142            fields.insert("cloud_complete".into(), Ty::function(
3143                vec![Ty::str()],
3144                EffectSet::singleton("llm_cloud"),
3145                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3146            ));
3147            // send_a2a :: (Str, Str) -> [a2a] Result[Str, Str]
3148            //              peer payload                   reply
3149            fields.insert("send_a2a".into(), Ty::function(
3150                vec![Ty::str(), Ty::str()],
3151                EffectSet::singleton("a2a"),
3152                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3153            ));
3154            // call_mcp :: (Str, Str, Str) -> [mcp] Result[Str, Str]
3155            //              server tool args_json         result_json
3156            fields.insert("call_mcp".into(), Ty::function(
3157                vec![Ty::str(), Ty::str(), Ty::str()],
3158                EffectSet::singleton("mcp"),
3159                Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3160            ));
3161            // cloud_stream :: Str -> [llm_cloud] Result[Stream[Str], Str]
3162            // (#305 slice 3). Streaming counterpart to cloud_complete.
3163            // The result is `Result[Stream[Str], Str]` rather than a
3164            // bare Stream so transport errors surface synchronously
3165            // at handshake time; per-chunk errors collapse the
3166            // stream to early termination.
3167            fields.insert("cloud_stream".into(), Ty::function(
3168                vec![Ty::str()],
3169                EffectSet::singleton("llm_cloud"),
3170                Ty::Con("Result".into(), vec![
3171                    Ty::Con("Stream".into(), vec![Ty::str()]),
3172                    Ty::str(),
3173                ]),
3174            ));
3175            Some(Ty::Record(fields))
3176        }
3177        "stream" => {
3178            // #305 slice 3: opaque consumer-side operations on
3179            // `Stream[T]`. Producers live elsewhere (`agent.cloud_stream`
3180            // for now); future producers (`http.get_stream`, etc.)
3181            // will register the same Stream[T] surface.
3182            let mut fields = IndexMap::new();
3183            // next :: Stream[T] -> [stream] Option[T]
3184            // One pull. `None` signals end-of-stream (consumed by
3185            // the producer's lazy generator).
3186            fields.insert("next".into(), Ty::function(
3187                vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
3188                EffectSet::singleton("stream"),
3189                Ty::Con("Option".into(), vec![Ty::Var(0)]),
3190            ));
3191            // collect :: Stream[T] -> [stream] List[T]
3192            // Drain to a list. Eager; blocks until the producer
3193            // signals end-of-stream.
3194            fields.insert("collect".into(), Ty::function(
3195                vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
3196                EffectSet::singleton("stream"),
3197                Ty::List(Box::new(Ty::Var(0))),
3198            ));
3199            Some(Ty::Record(fields))
3200        }
3201        // -- std.decimal (#574): exact decimal arithmetic with explicit rounding.
3202        // `Decimal = { coefficient :: Int, exponent :: Int }` where the value
3203        // is `coefficient × 10^exponent`.  All arithmetic is exact (no IEEE 754
3204        // approximation); rounding only happens at `round_to`, which demands an
3205        // explicit mode string ("HalfUp" | "HalfDown" | "HalfEven" |
3206        // "Down" | "Up" | "Ceiling" | "Floor").
3207        "decimal" => {
3208            // Local helper: the Decimal record type.
3209            let decimal_ty = || {
3210                let mut f = IndexMap::new();
3211                f.insert("coefficient".into(), Ty::int());
3212                f.insert("exponent".into(), Ty::int());
3213                Ty::Record(f)
3214            };
3215            let mut fields = IndexMap::new();
3216            // Constructors
3217            // decimal :: (Int, Int) -> Decimal — coefficient, exponent
3218            fields.insert("decimal".into(), Ty::function(
3219                vec![Ty::int(), Ty::int()], EffectSet::empty(), decimal_ty()));
3220            // zero :: () -> Decimal — 0 × 10^0
3221            fields.insert("zero".into(), Ty::function(
3222                vec![], EffectSet::empty(), decimal_ty()));
3223            // one :: () -> Decimal — 1 × 10^0
3224            fields.insert("one".into(), Ty::function(
3225                vec![], EffectSet::empty(), decimal_ty()));
3226            // from_int :: Int -> Decimal — lift integer, exponent=0
3227            fields.insert("from_int".into(), Ty::function(
3228                vec![Ty::int()], EffectSet::empty(), decimal_ty()));
3229            // Arithmetic — all exact, no rounding
3230            // add :: (Decimal, Decimal) -> Decimal
3231            fields.insert("add".into(), Ty::function(
3232                vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3233            // sub :: (Decimal, Decimal) -> Decimal
3234            fields.insert("sub".into(), Ty::function(
3235                vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3236            // mul :: (Decimal, Decimal) -> Decimal — exponents add
3237            fields.insert("mul".into(), Ty::function(
3238                vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3239            // Comparison — three-way: -1 / 0 / 1
3240            // compare :: (Decimal, Decimal) -> Int
3241            fields.insert("compare".into(), Ty::function(
3242                vec![decimal_ty(), decimal_ty()], EffectSet::empty(), Ty::int()));
3243            // Predicates
3244            fields.insert("is_zero".into(), Ty::function(
3245                vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3246            fields.insert("is_positive".into(), Ty::function(
3247                vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3248            fields.insert("is_negative".into(), Ty::function(
3249                vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3250            // Transformers
3251            // normalize :: Decimal -> Decimal — remove trailing zeros
3252            fields.insert("normalize".into(), Ty::function(
3253                vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3254            // negate :: Decimal -> Decimal
3255            fields.insert("negate".into(), Ty::function(
3256                vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3257            // abs :: Decimal -> Decimal
3258            fields.insert("abs".into(), Ty::function(
3259                vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3260            // round_to :: (Decimal, Int, Str) -> Decimal
3261            //   target_exp: the exponent to round to (e.g. -2 → 2 decimal places)
3262            //   mode: "HalfUp" | "HalfDown" | "HalfEven" | "Down" | "Up" | "Ceiling" | "Floor"
3263            fields.insert("round_to".into(), Ty::function(
3264                vec![decimal_ty(), Ty::int(), Ty::str()],
3265                EffectSet::empty(), decimal_ty()));
3266            // to_str :: Decimal -> Str — decimal notation, e.g. "123.45"
3267            fields.insert("to_str".into(), Ty::function(
3268                vec![decimal_ty()], EffectSet::empty(), Ty::str()));
3269            // pow10 :: Int -> Int — 10^n; n must be in [0, 18]
3270            fields.insert("pow10".into(), Ty::function(
3271                vec![Ty::int()], EffectSet::empty(), Ty::int()));
3272            Some(Ty::Record(fields))
3273        }
3274        _ => None,
3275    }
3276}
3277
3278/// Resolve `import "std.foo" as alias` to a module name (e.g. "io").
3279pub fn module_for_import(reference: &str) -> Option<&'static str> {
3280    let suffix = reference.strip_prefix("std.")?;
3281    Some(match suffix {
3282        "io" => "io",
3283        "str" => "str",
3284        "int" => "int",
3285        "float" => "float",
3286        "list" => "list",
3287        "result" => "result",
3288        "option" => "option",
3289        "json" => "json",
3290        "flow" => "flow",
3291        "tuple" => "tuple",
3292        "time" => "time",
3293        "rand" => "rand",
3294        "random" => "random",
3295        "env" => "env",
3296        "bytes" => "bytes",
3297        "net" => "net",
3298        "tls" => "tls",
3299        "chat" => "chat",
3300        "math" => "math",
3301        "map" => "map",
3302        "set" => "set",
3303        "iter" => "iter",
3304        "crypto" => "crypto",
3305        "regex" => "regex",
3306        "parser" => "parser",
3307        "deque" => "deque",
3308        "kv" => "kv",
3309        "sql" => "sql",
3310        "fs" => "fs",
3311        "process" => "process",
3312        "datetime" => "datetime",
3313        "duration" => "duration",
3314        "log" => "log",
3315        "http" => "http",
3316        "toml" => "toml",
3317        "yaml" => "yaml",
3318        "dotenv" => "dotenv",
3319        "csv" => "csv",
3320        "test" => "test",
3321        "agent" => "agent",
3322        "cli" => "cli",
3323        "stream" => "stream",
3324        "conc" => "conc",
3325        "arrow" => "arrow",
3326        "df" => "df",
3327        "redis" => "redis",
3328        "decimal" => "decimal",
3329        "vcs" => "vcs",
3330        _ => return None,
3331    })
3332}