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