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