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] Nil
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 Some(Ty::Record(fields))
35 }
36 "str" => {
37 let mut fields = IndexMap::new();
38 fields.insert("is_empty".into(), Ty::function(vec![Ty::str()], EffectSet::empty(), Ty::bool()));
39 fields.insert("to_int".into(), Ty::function(vec![Ty::str()], EffectSet::empty(),
40 Ty::Con("Option".into(), vec![Ty::int()])));
41 fields.insert("to_float".into(), Ty::function(vec![Ty::str()], EffectSet::empty(),
42 Ty::Con("Option".into(), vec![Ty::float()])));
43 fields.insert("concat".into(), Ty::function(vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
44 fields.insert("len".into(), Ty::function(vec![Ty::str()], EffectSet::empty(), Ty::int()));
45 fields.insert("split".into(), Ty::function(
46 vec![Ty::str(), Ty::str()],
47 EffectSet::empty(),
48 Ty::List(Box::new(Ty::str())),
49 ));
50 fields.insert("join".into(), Ty::function(
51 vec![Ty::List(Box::new(Ty::str())), Ty::str()],
52 EffectSet::empty(),
53 Ty::str(),
54 ));
55 // -- predicates --
56 for name in &["starts_with", "ends_with", "contains"] {
57 fields.insert((*name).into(), Ty::function(
58 vec![Ty::str(), Ty::str()],
59 EffectSet::empty(),
60 Ty::bool(),
61 ));
62 }
63 // str.cmp :: (Str, Str) -> Int — -1 / 0 / 1, lex-byte order.
64 // Three-way comparator usable as a sort-by closure value;
65 // boolean comparisons stay on the `<`/`<=`/`>`/`>=` operators
66 // (which already work on Str via `bin_ord`), so `str.lt`
67 // etc. are deliberately not exposed — keep the surface
68 // minimal (#440).
69 fields.insert("cmp".into(), Ty::function(
70 vec![Ty::str(), Ty::str()],
71 EffectSet::empty(),
72 Ty::int(),
73 ));
74 // -- transformers --
75 fields.insert("replace".into(), Ty::function(
76 vec![Ty::str(), Ty::str(), Ty::str()],
77 EffectSet::empty(),
78 Ty::str(),
79 ));
80 for name in &["trim", "to_upper", "to_lower"] {
81 fields.insert((*name).into(), Ty::function(
82 vec![Ty::str()], EffectSet::empty(), Ty::str(),
83 ));
84 }
85 for name in &["strip_prefix", "strip_suffix"] {
86 fields.insert((*name).into(), Ty::function(
87 vec![Ty::str(), Ty::str()],
88 EffectSet::empty(),
89 Ty::Con("Option".into(), vec![Ty::str()]),
90 ));
91 }
92 // slice :: (Str, Int, Int) -> Str — byte-range half-open
93 fields.insert("slice".into(), Ty::function(
94 vec![Ty::str(), Ty::int(), Ty::int()],
95 EffectSet::empty(),
96 Ty::str(),
97 ));
98 Some(Ty::Record(fields))
99 }
100 "int" => {
101 let mut fields = IndexMap::new();
102 fields.insert("to_str".into(), Ty::function(vec![Ty::int()], EffectSet::empty(), Ty::str()));
103 fields.insert("to_float".into(), Ty::function(vec![Ty::int()], EffectSet::empty(), Ty::float()));
104 Some(Ty::Record(fields))
105 }
106 "math" => {
107 let mut fields = IndexMap::new();
108 // Matrix is registered as a built-in type alias in
109 // TypeEnv::new_with_builtins; refer to it nominally so call
110 // sites unify against the user's `:: Matrix` annotations.
111 let mat = || Ty::Con("Matrix".into(), Vec::new());
112 // Scalar floats — single-arg `Float -> Float`.
113 for name in &[
114 "exp", "log", "log2", "log10", "sqrt", "abs",
115 "sin", "cos", "tan", "asin", "acos", "atan",
116 "floor", "ceil", "round", "trunc",
117 ] {
118 fields.insert((*name).into(), Ty::function(
119 vec![Ty::float()], EffectSet::empty(), Ty::float(),
120 ));
121 }
122 // Two-arg `Float, Float -> Float`.
123 for name in &["pow", "atan2", "min", "max"] {
124 fields.insert((*name).into(), Ty::function(
125 vec![Ty::float(), Ty::float()], EffectSet::empty(), Ty::float(),
126 ));
127 }
128 // Constructors.
129 fields.insert("zeros".into(), Ty::function(
130 vec![Ty::int(), Ty::int()], EffectSet::empty(), mat(),
131 ));
132 fields.insert("ones".into(), Ty::function(
133 vec![Ty::int(), Ty::int()], EffectSet::empty(), mat(),
134 ));
135 fields.insert("from_lists".into(), Ty::function(
136 vec![Ty::List(Box::new(Ty::List(Box::new(Ty::float()))))],
137 EffectSet::empty(),
138 mat(),
139 ));
140 fields.insert("from_flat".into(), Ty::function(
141 vec![Ty::int(), Ty::int(), Ty::List(Box::new(Ty::float()))],
142 EffectSet::empty(),
143 mat(),
144 ));
145 // Accessors.
146 fields.insert("rows".into(), Ty::function(vec![mat()], EffectSet::empty(), Ty::int()));
147 fields.insert("cols".into(), Ty::function(vec![mat()], EffectSet::empty(), Ty::int()));
148 fields.insert("get".into(), Ty::function(
149 vec![mat(), Ty::int(), Ty::int()], EffectSet::empty(), Ty::float(),
150 ));
151 fields.insert("to_flat".into(), Ty::function(
152 vec![mat()], EffectSet::empty(),
153 Ty::List(Box::new(Ty::float())),
154 ));
155 // Linalg ops.
156 fields.insert("transpose".into(), Ty::function(
157 vec![mat()], EffectSet::empty(), mat(),
158 ));
159 fields.insert("matmul".into(), Ty::function(
160 vec![mat(), mat()], EffectSet::empty(), mat(),
161 ));
162 fields.insert("scale".into(), Ty::function(
163 vec![Ty::float(), mat()], EffectSet::empty(), mat(),
164 ));
165 for name in &["add", "sub"] {
166 fields.insert((*name).into(), Ty::function(
167 vec![mat(), mat()], EffectSet::empty(), mat(),
168 ));
169 }
170 fields.insert("sigmoid".into(), Ty::function(
171 vec![mat()], EffectSet::empty(), mat(),
172 ));
173 Some(Ty::Record(fields))
174 }
175 "float" => {
176 let mut fields = IndexMap::new();
177 fields.insert("to_int".into(), Ty::function(vec![Ty::float()], EffectSet::empty(), Ty::int()));
178 fields.insert("to_str".into(), Ty::function(vec![Ty::float()], EffectSet::empty(), Ty::str()));
179 Some(Ty::Record(fields))
180 }
181 "list" => {
182 // list polymorphic functions need fresh vars at use sites; we
183 // encode them with placeholder Var ids that get instantiated.
184 let mut fields = IndexMap::new();
185 // Effect polymorphism: each HOF carries an effect-row
186 // variable so an effectful closure (e.g. one that calls
187 // net.get inside list.map's lambda) propagates its
188 // effects to the result type. Spec §7.3.
189 //
190 // map :: [E] List[a], (a) -> [E] b -> [E] List[b]
191 fields.insert("map".into(), Ty::function(
192 vec![
193 Ty::List(Box::new(Ty::Var(0))),
194 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
195 ],
196 EffectSet::open_var(2),
197 Ty::List(Box::new(Ty::Var(1))),
198 ));
199 // #305 slice 1: parallel map. Same signature shape as
200 // `map`; the runtime spawns OS threads (capped by
201 // LEX_PAR_MAX_CONCURRENCY) to apply the closure
202 // concurrently. Effect row stays open so a closure with
203 // declared effects still type-checks against
204 // par_map — though slice 1's runtime currently refuses
205 // effectful closures at execution (queued as slice 2).
206 fields.insert("par_map".into(), Ty::function(
207 vec![
208 Ty::List(Box::new(Ty::Var(0))),
209 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(7), Ty::Var(1)),
210 ],
211 EffectSet::open_var(7),
212 Ty::List(Box::new(Ty::Var(1))),
213 ));
214 // #338: sort_by :: List[T], (T) -> [E] K -> [E] List[T]
215 // Stable sort by the key the closure derives from each
216 // element. K is intended to be one of Int / Float / Str
217 // (the runtime comparator falls back to equality for
218 // other shapes, preserving original order via the
219 // stable sort) but the type system doesn't enforce that
220 // — keep the signature minimal so callers can pass any
221 // K and trust the comparator.
222 fields.insert("sort_by".into(), Ty::function(
223 vec![
224 Ty::List(Box::new(Ty::Var(0))),
225 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(8), Ty::Var(1)),
226 ],
227 EffectSet::open_var(8),
228 Ty::List(Box::new(Ty::Var(0))),
229 ));
230 fields.insert("filter".into(), Ty::function(
231 vec![
232 Ty::List(Box::new(Ty::Var(0))),
233 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), Ty::bool()),
234 ],
235 EffectSet::open_var(3),
236 Ty::List(Box::new(Ty::Var(0))),
237 ));
238 fields.insert("fold".into(), Ty::function(
239 vec![
240 Ty::List(Box::new(Ty::Var(0))),
241 Ty::Var(1),
242 Ty::function(vec![Ty::Var(1), Ty::Var(0)], EffectSet::open_var(4), Ty::Var(1)),
243 ],
244 EffectSet::open_var(4),
245 Ty::Var(1),
246 ));
247 fields.insert("len".into(), Ty::function(
248 vec![Ty::List(Box::new(Ty::Var(0)))],
249 EffectSet::empty(),
250 Ty::int(),
251 ));
252 fields.insert("is_empty".into(), Ty::function(
253 vec![Ty::List(Box::new(Ty::Var(0)))],
254 EffectSet::empty(),
255 Ty::bool(),
256 ));
257 fields.insert("range".into(), Ty::function(
258 vec![Ty::int(), Ty::int()],
259 EffectSet::empty(),
260 Ty::List(Box::new(Ty::int())),
261 ));
262 fields.insert("head".into(), Ty::function(
263 vec![Ty::List(Box::new(Ty::Var(0)))],
264 EffectSet::empty(),
265 Ty::Con("Option".into(), vec![Ty::Var(0)]),
266 ));
267 fields.insert("tail".into(), Ty::function(
268 vec![Ty::List(Box::new(Ty::Var(0)))],
269 EffectSet::empty(),
270 Ty::List(Box::new(Ty::Var(0))),
271 ));
272 fields.insert("concat".into(), Ty::function(
273 vec![Ty::List(Box::new(Ty::Var(0))), Ty::List(Box::new(Ty::Var(0)))],
274 EffectSet::empty(),
275 Ty::List(Box::new(Ty::Var(0))),
276 ));
277 // reverse :: List[T] -> List[T]
278 fields.insert("reverse".into(), Ty::function(
279 vec![Ty::List(Box::new(Ty::Var(0)))],
280 EffectSet::empty(),
281 Ty::List(Box::new(Ty::Var(0))),
282 ));
283 // #334: cons :: T, List[T] -> List[T] — O(1)-amortised prepend.
284 fields.insert("cons".into(), Ty::function(
285 vec![Ty::Var(0), Ty::List(Box::new(Ty::Var(0)))],
286 EffectSet::empty(),
287 Ty::List(Box::new(Ty::Var(0))),
288 ));
289 // enumerate :: List[T] -> List[(Int, T)]
290 // Pairs each element with its zero-based index.
291 fields.insert("enumerate".into(), Ty::function(
292 vec![Ty::List(Box::new(Ty::Var(0)))],
293 EffectSet::empty(),
294 Ty::List(Box::new(Ty::Tuple(vec![Ty::int(), Ty::Var(0)]))),
295 ));
296 Some(Ty::Record(fields))
297 }
298 "bytes" => {
299 let mut fields = IndexMap::new();
300 fields.insert("len".into(), Ty::function(
301 vec![Ty::bytes()], EffectSet::empty(), Ty::int(),
302 ));
303 fields.insert("is_empty".into(), Ty::function(
304 vec![Ty::bytes()], EffectSet::empty(), Ty::bool(),
305 ));
306 fields.insert("eq".into(), Ty::function(
307 vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool(),
308 ));
309 fields.insert("from_str".into(), Ty::function(
310 vec![Ty::str()], EffectSet::empty(), Ty::bytes(),
311 ));
312 fields.insert("to_str".into(), Ty::function(
313 vec![Ty::bytes()], EffectSet::empty(),
314 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
315 ));
316 fields.insert("slice".into(), Ty::function(
317 vec![Ty::bytes(), Ty::int(), Ty::int()],
318 EffectSet::empty(), Ty::bytes(),
319 ));
320 Some(Ty::Record(fields))
321 }
322 "time" => {
323 // time.now() -> [time] Int — unix timestamp seconds.
324 // Reading the clock is an effect for two reasons: it's
325 // non-deterministic (replay needs the captured value) and
326 // it's a side-channel surface (see "Capability ≠
327 // correctness" on the landing page).
328 let mut fields = IndexMap::new();
329 fields.insert("now".into(), Ty::function(
330 vec![],
331 EffectSet::singleton("time"),
332 Ty::int(),
333 ));
334 // now_ms :: () -> [time] Int — unix milliseconds (#378).
335 // Resolution beyond what `time.now` (seconds) offers, for
336 // request-latency measurement / rate-limiter windows.
337 // Honors `LEX_TEST_NOW` for deterministic tests.
338 fields.insert("now_ms".into(), Ty::function(
339 vec![],
340 EffectSet::singleton("time"),
341 Ty::int(),
342 ));
343 // now_str :: () -> [time] Str — wall-clock instant rendered
344 // as an ISO-8601 / RFC 3339 string in UTC (#378). Suitable
345 // for auto-managed `created_at` / `updated_at` timestamps
346 // and structured log lines. Honors `LEX_TEST_NOW`.
347 fields.insert("now_str".into(), Ty::function(
348 vec![],
349 EffectSet::singleton("time"),
350 Ty::str(),
351 ));
352 // mono_ns :: () -> [time] Int — monotonic-clock nanoseconds
353 // since process start (#378). Use for *duration*
354 // measurement (`end - start`); the value carries no wall-
355 // clock meaning and the clock can never go backwards
356 // (unlike `time.now_ms` under NTP jitter). Not affected by
357 // `LEX_TEST_NOW` — pinning a monotonic clock would defeat
358 // its purpose; tests that need a fake monotonic clock
359 // should inject one through `EffectHandler`.
360 fields.insert("mono_ns".into(), Ty::function(
361 vec![],
362 EffectSet::singleton("time"),
363 Ty::int(),
364 ));
365 // sleep_ms :: Int -> [time] Unit (#226).
366 // Used internally by flow.retry_with_backoff for
367 // exponential-backoff delays; also available to user
368 // code under `--allow-effects time`.
369 fields.insert("sleep_ms".into(), Ty::function(
370 vec![Ty::int()],
371 EffectSet::singleton("time"),
372 Ty::Unit,
373 ));
374 // sleep :: Duration -> [time] Unit (#445).
375 // Duration-typed sleep — pairs with the
376 // `datetime.duration_seconds` / `duration_minutes` /
377 // `duration_days` constructors so periodic-task code
378 // expresses the period in units of meaning rather than
379 // raw milliseconds. Backed by `std::thread::sleep` at
380 // runtime — blocks the calling thread, which is the right
381 // semantics for the agent-driven workloads this exists
382 // for. Inside a `net.serve` worker the same caveat as
383 // `LEX_NET_INLINE_VM=1` applies (worker is stalled for `d`).
384 fields.insert("sleep".into(), Ty::function(
385 vec![Ty::Con("Duration".into(), vec![])],
386 EffectSet::singleton("time"),
387 Ty::Unit,
388 ));
389 Some(Ty::Record(fields))
390 }
391 "rand" => {
392 // rand.int_in(lo, hi) -> [rand] Int — currently a deterministic
393 // stub (midpoint) per spec §13; replaced when randomness lands.
394 let mut fields = IndexMap::new();
395 fields.insert("int_in".into(), Ty::function(
396 vec![Ty::int(), Ty::int()],
397 EffectSet::singleton("rand"),
398 Ty::int(),
399 ));
400 Some(Ty::Record(fields))
401 }
402 "random" => {
403 // #219: pure, seeded RNG. The caller threads the `Rng`
404 // value through computations explicitly — there is no
405 // global state and no effect tag, because the seed is
406 // visible in the program's value flow and replay is
407 // therefore deterministic by construction.
408 //
409 // Backed at runtime by SplitMix64 (deterministic across
410 // platforms, single-u64 state). The proposal mentioned
411 // `rand_chacha` for cryptographic-strength bias, but the
412 // acceptance criterion is just "byte-identical sequence
413 // across platforms," and SplitMix64 satisfies that with
414 // a state shape that fits in `Value::Int` cleanly.
415 let rng_t = || Ty::Con("Rng".into(), vec![]);
416 let mut fields = IndexMap::new();
417 // seed :: Int -> Rng
418 fields.insert("seed".into(), Ty::function(
419 vec![Ty::int()], EffectSet::empty(), rng_t()));
420 // int :: Rng, Int, Int -> (Int, Rng)
421 // Uniform in [lo, hi] inclusive at both ends. Returns
422 // the drawn value and the advanced Rng.
423 fields.insert("int".into(), Ty::function(
424 vec![rng_t(), Ty::int(), Ty::int()],
425 EffectSet::empty(),
426 Ty::Tuple(vec![Ty::int(), rng_t()])));
427 // float :: Rng -> (Float, Rng)
428 // Uniform in [0.0, 1.0).
429 fields.insert("float".into(), Ty::function(
430 vec![rng_t()], EffectSet::empty(),
431 Ty::Tuple(vec![Ty::float(), rng_t()])));
432 // choose :: Rng, List[T] -> Option[(T, Rng)]
433 // Returns None if the list is empty.
434 fields.insert("choose".into(), Ty::function(
435 vec![rng_t(), Ty::List(Box::new(Ty::Var(0)))],
436 EffectSet::empty(),
437 Ty::Con("Option".into(), vec![
438 Ty::Tuple(vec![Ty::Var(0), rng_t()]),
439 ]),
440 ));
441 Some(Ty::Record(fields))
442 }
443 "env" => {
444 // #216: env.get(name) -> [env] Option[Str].
445 // Per-var scoping (`[env(NAME)]`) lands with the
446 // per-capability effect parameterization work (#207); the
447 // flat `[env]` is the v1 surface.
448 let mut fields = IndexMap::new();
449 fields.insert("get".into(), Ty::function(
450 vec![Ty::str()],
451 EffectSet::singleton("env"),
452 Ty::Con("Option".into(), vec![Ty::str()]),
453 ));
454 Some(Ty::Record(fields))
455 }
456 "net" => {
457 let mut fields = IndexMap::new();
458 // get :: Str -> [net] Result[Str, Str]
459 fields.insert("get".into(), Ty::function(
460 vec![Ty::str()],
461 EffectSet::singleton("net"),
462 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
463 ));
464 fields.insert("post".into(), Ty::function(
465 vec![Ty::str(), Ty::str()],
466 EffectSet::singleton("net"),
467 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
468 ));
469 // serve :: (Int, Str) -> [net] Unit (blocks; never returns
470 // under normal use). Handler's signature isn't carried in
471 // the type system here — looked up by name at runtime.
472 fields.insert("serve".into(), Ty::function(
473 vec![Ty::int(), Ty::str()],
474 EffectSet::singleton("net"),
475 Ty::Unit,
476 ));
477 // serve_tls :: (Int, Str, Str, Str) -> [net] Unit
478 // port cert key handler
479 // cert and key are filesystem paths to PEM-encoded files.
480 fields.insert("serve_tls".into(), Ty::function(
481 vec![Ty::int(), Ty::str(), Ty::str(), Ty::str()],
482 EffectSet::singleton("net"),
483 Ty::Unit,
484 ));
485 // serve_ws :: (Int, Str) -> [net] Unit
486 // port on_message_handler_name
487 // The handler is looked up by name at runtime.
488 fields.insert("serve_ws".into(), Ty::function(
489 vec![Ty::int(), Ty::str()],
490 EffectSet::singleton("net"),
491 Ty::Unit,
492 ));
493 // serve_ws_fn[Eff] :: (Int, Str, (WsConn, WsMessage) -> [Eff] WsAction)
494 // -> [net, Eff] Unit
495 // Effect-polymorphic WebSocket server that accepts a handler closure.
496 // The second argument is the subprotocol string for the
497 // Sec-WebSocket-Protocol handshake header ("" for none).
498 // open_var(0) propagates the handler's effect row to the call site.
499 fields.insert("serve_ws_fn".into(), Ty::function(
500 vec![
501 Ty::int(),
502 Ty::str(), // subprotocol
503 Ty::function(
504 vec![
505 Ty::Con("WsConn".into(), vec![]),
506 Ty::Con("WsMessage".into(), vec![]),
507 ],
508 EffectSet::open_var(0),
509 Ty::Con("WsAction".into(), vec![]),
510 ),
511 ],
512 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
513 Ty::Unit,
514 ));
515 // serve_ws_fn_auth[Eff] :: (Int, Str,
516 // (Str, List[{name :: Str, value :: Str}]) -> [Eff] Result[Unit, Str],
517 // (WsConn, WsMessage) -> [Eff] WsAction)
518 // -> [net, Eff] Unit
519 // Variant of serve_ws_fn that runs a pre-handshake auth
520 // callback against the upgrade request's path + headers.
521 // `Err(msg)` from the callback responds 401 Unauthorized
522 // and skips the WS upgrade entirely (#423). The auth and
523 // message-handler closures share the same effect row, so
524 // a caller using e.g. `[sql]` to look up a password hash
525 // in auth can use `[sql]` in subsequent handlers without
526 // duplicating the declaration.
527 let header_entry = || {
528 let mut fs = IndexMap::new();
529 fs.insert("name".into(), Ty::str());
530 fs.insert("value".into(), Ty::str());
531 Ty::Record(fs)
532 };
533 fields.insert("serve_ws_fn_auth".into(), Ty::function(
534 vec![
535 Ty::int(),
536 Ty::str(), // subprotocol
537 // auth callback: (path, headers) -> [Eff] Result[Unit, Str]
538 Ty::function(
539 vec![
540 Ty::str(),
541 Ty::List(Box::new(header_entry())),
542 ],
543 EffectSet::open_var(0),
544 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]),
545 ),
546 // on_message: same shape as serve_ws_fn
547 Ty::function(
548 vec![
549 Ty::Con("WsConn".into(), vec![]),
550 Ty::Con("WsMessage".into(), vec![]),
551 ],
552 EffectSet::open_var(0),
553 Ty::Con("WsAction".into(), vec![]),
554 ),
555 ],
556 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
557 Ty::Unit,
558 ));
559 // serve_ws_fn_actor[Eff] ::
560 // (Int, Str,
561 // (WsConn) -> Str, # name_of, registry name
562 // (WsConn, WsMessage) -> [Eff] WsAction) # on_message
563 // -> [net, concurrent, Eff] Unit
564 //
565 // Variant of serve_ws_fn that registers each accepted
566 // connection as a named actor in conc_registry. Non-WS
567 // callers can then `conc.lookup(name) |> conc.tell(frame)`
568 // to push outbound frames into the socket from arbitrary
569 // [concurrent]-tagged code (HTTP webhooks, scheduled tasks,
570 // broadcast loops). Documented in #459.
571 //
572 // name_of is intentionally pure: it inspects the WsConn
573 // record (id / path / subprotocol) and decides what
574 // name to register the connection under. Empty string
575 // means "don't register this connection" — `on_message`
576 // still runs but no outbound handle is exposed.
577 //
578 // The result row carries `concurrent` because the runtime
579 // registers an `ActorHandler::Native` bridge in the conc
580 // registry; lookups from non-WS callers are themselves
581 // `[concurrent]` effects.
582 fields.insert("serve_ws_fn_actor".into(), Ty::function(
583 vec![
584 Ty::int(),
585 Ty::str(), // subprotocol
586 Ty::function(
587 vec![Ty::Con("WsConn".into(), vec![])],
588 EffectSet::empty(),
589 Ty::str(),
590 ),
591 Ty::function(
592 vec![
593 Ty::Con("WsConn".into(), vec![]),
594 Ty::Con("WsMessage".into(), vec![]),
595 ],
596 EffectSet::open_var(0),
597 Ty::Con("WsAction".into(), vec![]),
598 ),
599 ],
600 EffectSet::open_var(0)
601 .union(&EffectSet::singleton("net"))
602 .union(&EffectSet::singleton("concurrent")),
603 Ty::Unit,
604 ));
605 // dial_ws[Eff] :: (Str, Str, () -> [Eff] WsAction,
606 // (WsMessage) -> [Eff] WsAction)
607 // -> [net, Eff] Result[Unit, Str]
608 //
609 // WebSocket *client* — the inverse of serve_ws_fn (#390).
610 // Connects to `url` (ws:// or wss://) with the given
611 // subprotocol header, calls `on_open` once after the
612 // handshake completes, then loops invoking `on_message`
613 // for every inbound frame. Each callback returns a
614 // `WsAction` that gets applied to the socket — same enum
615 // as the server side, same semantics for `WsSend` /
616 // `WsSendBinary` / `WsNoOp`. open_var(0) propagates the
617 // handler effects so callers that touch [io], [time],
618 // [random] etc. inside their handlers see those propagate
619 // out of the dial_ws call.
620 //
621 // Returns `Result[Unit, Str]` rather than the bare `Unit`
622 // that serve_ws_fn returns: a dial can fail on connect
623 // (DNS, refused, bad TLS) or mid-stream (read error,
624 // unexpected close) and the caller usually wants to know.
625 fields.insert("dial_ws".into(), Ty::function(
626 vec![
627 Ty::str(), // url (ws:// or wss://)
628 Ty::str(), // subprotocol (Sec-WebSocket-Protocol)
629 Ty::function(
630 vec![],
631 EffectSet::open_var(0),
632 Ty::Con("WsAction".into(), vec![]),
633 ),
634 Ty::function(
635 vec![Ty::Con("WsMessage".into(), vec![])],
636 EffectSet::open_var(0),
637 Ty::Con("WsAction".into(), vec![]),
638 ),
639 ],
640 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
641 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]),
642 ));
643 // serve_fn[Eff] :: (Int, (Request) -> [Eff] Response) -> [net, Eff] Unit
644 // Effect-polymorphic variant of serve that accepts a first-class closure
645 // instead of a handler name. open_var(0) captures the handler's effect row
646 // so callers that invoke e.g. [io] effects inside the closure propagate them
647 // to the serve_fn call site.
648 fields.insert("serve_fn".into(), Ty::function(
649 vec![
650 Ty::int(),
651 Ty::function(
652 vec![Ty::Con("Request".into(), vec![])],
653 EffectSet::open_var(0),
654 Ty::Con("Response".into(), vec![]),
655 ),
656 ],
657 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
658 Ty::Unit,
659 ));
660 // serve_routed[Eff] :: (
661 // Int,
662 // List[(Str, Str, (Request) -> [Eff] Response)],
663 // (Request) -> [Eff] Response
664 // ) -> [net, Eff] Unit
665 //
666 // Pattern-matched dispatch over `serve_fn`. Each route is a
667 // (method, path-pattern, handler) triple — method is an
668 // HTTP verb (case-insensitive) or "*" for any; path-patterns
669 // use `:name` segments (e.g. "/users/:id") and matched values
670 // are stamped onto `req.path_params` before the handler runs.
671 // Routes are tried in registration order; the first match
672 // wins. `fallback` runs when no route matches — typically a
673 // 404 responder. Same `open_var(0)` effect-row trick as
674 // `serve_fn` so handler effects propagate to the call site.
675 fields.insert("serve_routed".into(), Ty::function(
676 vec![
677 Ty::int(),
678 Ty::List(Box::new(Ty::Tuple(vec![
679 Ty::str(),
680 Ty::str(),
681 Ty::function(
682 vec![Ty::Con("Request".into(), vec![])],
683 EffectSet::open_var(0),
684 Ty::Con("Response".into(), vec![]),
685 ),
686 ]))),
687 Ty::function(
688 vec![Ty::Con("Request".into(), vec![])],
689 EffectSet::open_var(0),
690 Ty::Con("Response".into(), vec![]),
691 ),
692 ],
693 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
694 Ty::Unit,
695 ));
696 Some(Ty::Record(fields))
697 }
698 "chat" => {
699 let mut fields = IndexMap::new();
700 fields.insert("broadcast".into(), Ty::function(
701 vec![Ty::str(), Ty::str()],
702 EffectSet::singleton("chat"),
703 Ty::Unit,
704 ));
705 fields.insert("send".into(), Ty::function(
706 vec![Ty::int(), Ty::str()],
707 EffectSet::singleton("chat"),
708 Ty::bool(),
709 ));
710 Some(Ty::Record(fields))
711 }
712 "conc" => {
713 // Actor model (#381). Effect: [concurrent].
714 // spawn :: S, (S, M) -> [E] (S, R) -> [concurrent] Actor[S]
715 // ask :: Actor[S], M -> [concurrent] R
716 // tell :: Actor[S], M -> [concurrent] Unit
717 //
718 // The type variables used here are fresh placeholders;
719 // the checker instantiates them at each call site.
720 // 0 = S (state), 1 = M (message), 2 = R (reply), 3 = E (effect row)
721 let actor_t = |s: Ty| Ty::Con("Actor".into(), vec![s]);
722 let mut fields = IndexMap::new();
723 // spawn :: S, (S, M -> [E] (S, R)) -> [concurrent] Actor[S]
724 fields.insert("spawn".into(), Ty::function(
725 vec![
726 Ty::Var(0),
727 Ty::Function {
728 params: vec![Ty::Var(0), Ty::Var(1)],
729 effects: EffectSet::open_var(3),
730 ret: Box::new(Ty::Tuple(vec![Ty::Var(0), Ty::Var(2)])),
731 },
732 ],
733 EffectSet::singleton("concurrent"),
734 actor_t(Ty::Var(0)),
735 ));
736 // ask :: Actor[S], M -> [concurrent] R
737 fields.insert("ask".into(), Ty::function(
738 vec![actor_t(Ty::Var(0)), Ty::Var(1)],
739 EffectSet::singleton("concurrent"),
740 Ty::Var(2),
741 ));
742 // tell :: Actor[S], M -> [concurrent] Unit
743 fields.insert("tell".into(), Ty::function(
744 vec![actor_t(Ty::Var(0)), Ty::Var(1)],
745 EffectSet::singleton("concurrent"),
746 Ty::Unit,
747 ));
748 // #444 — named-actor discovery within a process.
749 //
750 // register :: Actor[S], Str -> [concurrent] Result[Unit, ConcError]
751 // Returns Err(AlreadyRegistered(name)) if the name is
752 // taken — registration is exclusive so name collisions
753 // surface at the source level, not as silent overwrites.
754 //
755 // lookup :: Str -> [concurrent] Option[Actor[S]]
756 // Returns Some(actor) if registered, None otherwise. The
757 // static `[S]` parametrisation isn't checked at runtime in
758 // v1; the caller is responsible for matching the
759 // registration site's type. SigId-tagged variant deferred —
760 // see `conc_registry.rs` in lex-bytecode.
761 //
762 // unregister :: Str -> [concurrent] Result[Unit, ConcError]
763 // Returns Err(NotRegistered(name)) if absent. Existing
764 // `Actor[S]` handles held by callers continue to work
765 // after unregistration; the cell is reclaimed when the
766 // last handle drops.
767 //
768 // registered :: () -> [concurrent] List[Str]
769 // Sorted snapshot of currently registered names. Debug /
770 // introspection — not part of the steady-state agent flow.
771 let conc_err = || Ty::Con("ConcError".into(), vec![]);
772 let result_ce = |ok: Ty| Ty::Con("Result".into(), vec![ok, conc_err()]);
773 fields.insert("register".into(), Ty::function(
774 vec![actor_t(Ty::Var(0)), Ty::str()],
775 EffectSet::singleton("concurrent"),
776 result_ce(Ty::Unit),
777 ));
778 fields.insert("lookup".into(), Ty::function(
779 vec![Ty::str()],
780 EffectSet::singleton("concurrent"),
781 Ty::Con("Option".into(), vec![actor_t(Ty::Var(0))]),
782 ));
783 fields.insert("unregister".into(), Ty::function(
784 vec![Ty::str()],
785 EffectSet::singleton("concurrent"),
786 result_ce(Ty::Unit),
787 ));
788 fields.insert("registered".into(), Ty::function(
789 vec![],
790 EffectSet::singleton("concurrent"),
791 Ty::List(Box::new(Ty::str())),
792 ));
793 Some(Ty::Record(fields))
794 }
795 "arrow" => {
796 // Apache Arrow tables (#426). All ops are pure (no effects);
797 // tables are immutable and conversions / reductions all run as
798 // one Rust call over the flat buffer.
799 //
800 // `arrow.Table` is opaque from the type system's point of view —
801 // the runtime variant `Value::ArrowTable` is the only producer
802 // and consumer, so we model it as a 0-arity type constructor.
803 let table = Ty::Con("Table".into(), vec![]);
804 let str_t = Ty::str();
805 let int_t = Ty::int();
806 let float_t = Ty::float();
807 let opt = |inner: Ty| Ty::Con("Option".into(), vec![inner]);
808 let res = |ok: Ty| Ty::Con("Result".into(), vec![ok, Ty::str()]);
809 let no_eff = EffectSet::empty();
810
811 let mut fields = IndexMap::new();
812
813 // -- constructors --
814 // arrow.from_int_columns :: List[(Str, List[Int])] -> Result[Table, Str]
815 // arrow.from_float_columns :: List[(Str, List[Float])] -> Result[Table, Str]
816 // arrow.from_str_columns :: List[(Str, List[Str])] -> Result[Table, Str]
817 for (name, elem) in [
818 ("from_int_columns", int_t.clone()),
819 ("from_float_columns", float_t.clone()),
820 ("from_str_columns", str_t.clone()),
821 ] {
822 fields.insert(name.into(), Ty::function(
823 vec![Ty::List(Box::new(Ty::Tuple(vec![
824 str_t.clone(),
825 Ty::List(Box::new(elem)),
826 ])))],
827 no_eff.clone(),
828 res(table.clone()),
829 ));
830 }
831
832 // -- introspection --
833 // arrow.nrows / arrow.ncols :: Table -> Int
834 fields.insert("nrows".into(), Ty::function(
835 vec![table.clone()], no_eff.clone(), int_t.clone()));
836 fields.insert("ncols".into(), Ty::function(
837 vec![table.clone()], no_eff.clone(), int_t.clone()));
838 // arrow.col_names :: Table -> List[Str]
839 fields.insert("col_names".into(), Ty::function(
840 vec![table.clone()], no_eff.clone(),
841 Ty::List(Box::new(str_t.clone()))));
842 // arrow.col_type :: Table, Str -> Option[Str]
843 fields.insert("col_type".into(), Ty::function(
844 vec![table.clone(), str_t.clone()],
845 no_eff.clone(), opt(str_t.clone())));
846
847 // -- column reductions --
848 // arrow.col_sum_int :: Table, Str -> Result[Int, Str]
849 // arrow.col_sum_float :: Table, Str -> Result[Float, Str]
850 // arrow.col_mean :: Table, Str -> Result[Option[Float], Str]
851 // arrow.col_min_int :: Table, Str -> Result[Option[Int], Str]
852 // arrow.col_max_int :: Table, Str -> Result[Option[Int], Str]
853 // arrow.col_count :: Table, Str -> Result[Int, Str]
854 for (name, ret_ok) in [
855 ("col_sum_int", int_t.clone()),
856 ("col_sum_float", float_t.clone()),
857 ("col_mean", opt(float_t.clone())),
858 ("col_min_int", opt(int_t.clone())),
859 ("col_max_int", opt(int_t.clone())),
860 ("col_count", int_t.clone()),
861 ] {
862 fields.insert(name.into(), Ty::function(
863 vec![table.clone(), str_t.clone()],
864 no_eff.clone(), res(ret_ok)));
865 }
866
867 // -- slicing --
868 // arrow.head / tail :: Table, Int -> Table
869 for name in &["head", "tail"] {
870 fields.insert((*name).into(), Ty::function(
871 vec![table.clone(), int_t.clone()],
872 no_eff.clone(), table.clone()));
873 }
874 // arrow.slice :: Table, Int, Int -> Table
875 fields.insert("slice".into(), Ty::function(
876 vec![table.clone(), int_t.clone(), int_t.clone()],
877 no_eff.clone(), table.clone()));
878 // arrow.select_cols :: Table, List[Str] -> Result[Table, Str]
879 fields.insert("select_cols".into(), Ty::function(
880 vec![table.clone(), Ty::List(Box::new(str_t.clone()))],
881 no_eff.clone(), res(table.clone())));
882 // arrow.drop_col :: Table, Str -> Result[Table, Str]
883 fields.insert("drop_col".into(), Ty::function(
884 vec![table.clone(), str_t.clone()],
885 no_eff.clone(), res(table.clone())));
886
887 // -- I/O (effect-gated) --
888 // arrow.read_csv :: Str -> [fs_read] Result[Table, Str]
889 // Header row required; schema inferred from the first 100 rows.
890 // The `[fs_read]` effect surfaces in agent-tool policy gates
891 // and `--allow-fs-read` per-path scoping, same as `io.read`.
892 fields.insert("read_csv".into(), Ty::function(
893 vec![str_t.clone()],
894 EffectSet::singleton("fs_read"),
895 res(table.clone())));
896
897 // arrow.read_parquet :: Str -> [fs_read] Result[Table, Str]
898 // arrow.read_parquet_cols :: (Str, List[Str]) -> [fs_read] Result[Table, Str]
899 // Same effect + path-scope rules as read_csv. _cols pushes
900 // the projection into the Parquet reader (no decode of skipped
901 // columns); missing column names surface as Err.
902 fields.insert("read_parquet".into(), Ty::function(
903 vec![str_t.clone()],
904 EffectSet::singleton("fs_read"),
905 res(table.clone())));
906 fields.insert("read_parquet_cols".into(), Ty::function(
907 vec![str_t.clone(), Ty::List(Box::new(str_t.clone()))],
908 EffectSet::singleton("fs_read"),
909 res(table.clone())));
910
911 // arrow.write_parquet :: (Table, Str) -> [fs_write] Result[Unit, Str]
912 // arrow.write_csv :: (Table, Str) -> [fs_write] Result[Unit, Str]
913 // Path scope uses --allow-fs-write (symmetric with io.write).
914 // Parquet default: Snappy compression, default page/row-group
915 // sizes — sufficient for v1; a write_parquet_opts variant
916 // can ride a later issue if knobs are needed.
917 for name in &["write_parquet", "write_csv"] {
918 fields.insert((*name).into(), Ty::function(
919 vec![table.clone(), str_t.clone()],
920 EffectSet::singleton("fs_write"),
921 res(Ty::Unit)));
922 }
923
924 Some(Ty::Record(fields))
925 }
926 "df" => {
927 // Polars-backed query ops over arrow.Table (#427). All pure
928 // (no effects); the Polars DataFrame is internal plumbing,
929 // never leaves the kernel.
930 let table = Ty::Con("Table".into(), vec![]);
931 let str_t = Ty::str();
932 let int_t = Ty::int();
933 let float_t = Ty::float();
934 let bool_t = Ty::bool();
935 let res = |ok: Ty| Ty::Con("Result".into(), vec![ok, Ty::str()]);
936 let no_eff = EffectSet::empty();
937
938 let mut fields = IndexMap::new();
939
940 // df.filter_{eq,gt,lt}_int :: Table, Str, Int -> Result[Table, Str]
941 for name in &["filter_eq_int", "filter_gt_int", "filter_lt_int"] {
942 fields.insert((*name).into(), Ty::function(
943 vec![table.clone(), str_t.clone(), int_t.clone()],
944 no_eff.clone(), res(table.clone())));
945 }
946
947 // #433 — string filters.
948 // df.filter_eq_str :: Table, Str, Str -> Result[Table, Str]
949 // df.filter_in_str :: Table, Str, List[Str] -> Result[Table, Str]
950 fields.insert("filter_eq_str".into(), Ty::function(
951 vec![table.clone(), str_t.clone(), str_t.clone()],
952 no_eff.clone(), res(table.clone())));
953 fields.insert("filter_in_str".into(), Ty::function(
954 vec![table.clone(), str_t.clone(), Ty::List(Box::new(str_t.clone()))],
955 no_eff.clone(), res(table.clone())));
956
957 // #433 — float filters.
958 // df.filter_{eq,lt,gt}_float :: Table, Str, Float -> Result[Table, Str]
959 for name in &["filter_eq_float", "filter_lt_float", "filter_gt_float"] {
960 fields.insert((*name).into(), Ty::function(
961 vec![table.clone(), str_t.clone(), float_t.clone()],
962 no_eff.clone(), res(table.clone())));
963 }
964
965 // #433 — null handling.
966 // df.filter_isnull :: Table, Str -> Result[Table, Str]
967 // df.filter_notnull :: Table, Str -> Result[Table, Str]
968 // df.drop_nulls :: Table, List[Str] -> Result[Table, Str]
969 for name in &["filter_isnull", "filter_notnull"] {
970 fields.insert((*name).into(), Ty::function(
971 vec![table.clone(), str_t.clone()],
972 no_eff.clone(), res(table.clone())));
973 }
974 fields.insert("drop_nulls".into(), Ty::function(
975 vec![table.clone(), Ty::List(Box::new(str_t.clone()))],
976 no_eff.clone(), res(table.clone())));
977
978 // df.sort_by :: Table, Str, Bool -> Result[Table, Str]
979 fields.insert("sort_by".into(), Ty::function(
980 vec![table.clone(), str_t.clone(), bool_t.clone()],
981 no_eff.clone(), res(table.clone())));
982
983 // df.group_by_agg :: Table, List[Str], List[(Str, Str, Str)]
984 // -> Result[Table, Str]
985 // Spec tuple is (out_col, in_col, op). op ∈
986 // "sum"|"mean"|"min"|"max"|"count"|"n_distinct".
987 fields.insert("group_by_agg".into(), Ty::function(
988 vec![
989 table.clone(),
990 Ty::List(Box::new(str_t.clone())),
991 Ty::List(Box::new(Ty::Tuple(vec![
992 str_t.clone(), str_t.clone(), str_t.clone(),
993 ]))),
994 ],
995 no_eff.clone(), res(table.clone())));
996
997 // df.inner_join / left_join :: Table, Table, Str -> Result[Table, Str]
998 for name in &["inner_join", "left_join"] {
999 fields.insert((*name).into(), Ty::function(
1000 vec![table.clone(), table.clone(), str_t.clone()],
1001 no_eff.clone(), res(table.clone())));
1002 }
1003
1004 Some(Ty::Record(fields))
1005 }
1006 "proc" => {
1007 // Subprocess dispatch. Effect: [proc]. Returns a Result
1008 // with a record on success carrying stdout / stderr /
1009 // exit_code. The runtime allow-lists which binary
1010 // basenames are spawnable — `cmd` is the program to
1011 // run, `args` is the literal argv (no shell parsing).
1012 //
1013 // Read SECURITY.md before adding [proc] to a policy:
1014 // it weakens the "we know what this fn does" claim.
1015 let mut fields = IndexMap::new();
1016 let mut result_rec = IndexMap::new();
1017 result_rec.insert("stdout".into(), Ty::str());
1018 result_rec.insert("stderr".into(), Ty::str());
1019 result_rec.insert("exit_code".into(), Ty::int());
1020 // spawn :: Str, List[Str] -> [proc] Result[{stdout, stderr, exit_code}, Str]
1021 fields.insert("spawn".into(), Ty::function(
1022 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
1023 EffectSet::singleton("proc"),
1024 Ty::Con("Result".into(), vec![
1025 Ty::Record(result_rec),
1026 Ty::str(),
1027 ]),
1028 ));
1029 Some(Ty::Record(fields))
1030 }
1031 "json" => {
1032 let mut fields = IndexMap::new();
1033 // stringify :: T -> Str (polymorphic on input)
1034 fields.insert("stringify".into(), Ty::function(
1035 vec![Ty::Var(0)], EffectSet::empty(), Ty::str(),
1036 ));
1037 // parse :: Str -> Result[T, Str]
1038 fields.insert("parse".into(), Ty::function(
1039 vec![Ty::str()], EffectSet::empty(),
1040 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1041 ));
1042 // parse_strict :: (Str, List[Str]) -> Result[T, Str]
1043 // Tactical fix for #168 — caller passes the field names
1044 // T requires; runtime returns Err if any are missing
1045 // from the parsed object instead of letting field
1046 // access panic later.
1047 fields.insert("parse_strict".into(), Ty::function(
1048 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
1049 EffectSet::empty(),
1050 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1051 ));
1052 Some(Ty::Record(fields))
1053 }
1054 "result" => {
1055 let mut fields = IndexMap::new();
1056 // result.map :: Result[T, E], (T) -> [E2] U -> [E2] Result[U, E]
1057 // Effect-polymorphic on the closure: result.map et al.
1058 // propagate the closure's effects to the surrounding call.
1059 fields.insert("map".into(), Ty::function(
1060 vec![
1061 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1062 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), Ty::Var(2)),
1063 ],
1064 EffectSet::open_var(3),
1065 Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
1066 ));
1067 fields.insert("and_then".into(), Ty::function(
1068 vec![
1069 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1070 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(4),
1071 Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)])),
1072 ],
1073 EffectSet::open_var(4),
1074 Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
1075 ));
1076 fields.insert("map_err".into(), Ty::function(
1077 vec![
1078 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1079 Ty::function(vec![Ty::Var(1)], EffectSet::open_var(5), Ty::Var(2)),
1080 ],
1081 EffectSet::open_var(5),
1082 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
1083 ));
1084 // result.or_else :: Result[T, E1], (E1) -> [E] Result[T, E2]
1085 // -> [E] Result[T, E2]
1086 // Recovery combinator: closure runs only on Err and returns
1087 // the next Result (which itself may swap the error type).
1088 fields.insert("or_else".into(), Ty::function(
1089 vec![
1090 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1091 Ty::function(vec![Ty::Var(1)], EffectSet::open_var(6),
1092 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)])),
1093 ],
1094 EffectSet::open_var(6),
1095 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
1096 ));
1097 Some(Ty::Record(fields))
1098 }
1099 "option" => {
1100 let mut fields = IndexMap::new();
1101 // option.map :: Option[T], (T) -> [E] U -> [E] Option[U]
1102 fields.insert("map".into(), Ty::function(
1103 vec![
1104 Ty::Con("Option".into(), vec![Ty::Var(0)]),
1105 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1106 ],
1107 EffectSet::open_var(2),
1108 Ty::Con("Option".into(), vec![Ty::Var(1)]),
1109 ));
1110 // option.and_then :: Option[T], (T) -> [E] Option[U] -> [E] Option[U]
1111 // The compiler entry has been wired since the result/option
1112 // variant_map work landed; this signature was missed,
1113 // making the call fail to type-check until now.
1114 fields.insert("and_then".into(), Ty::function(
1115 vec![
1116 Ty::Con("Option".into(), vec![Ty::Var(0)]),
1117 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
1118 Ty::Con("Option".into(), vec![Ty::Var(1)])),
1119 ],
1120 EffectSet::open_var(3),
1121 Ty::Con("Option".into(), vec![Ty::Var(1)]),
1122 ));
1123 fields.insert("unwrap_or".into(), Ty::function(
1124 vec![Ty::Con("Option".into(), vec![Ty::Var(0)]), Ty::Var(0)],
1125 EffectSet::empty(),
1126 Ty::Var(0),
1127 ));
1128 // option.unwrap_or_else :: Option[T], () -> [E] T -> [E] T
1129 // Lazy variant of unwrap_or: the default is computed by a closure
1130 // only when the value is None (effect-polymorphic on the closure).
1131 fields.insert("unwrap_or_else".into(), Ty::function(
1132 vec![
1133 Ty::Con("Option".into(), vec![Ty::Var(0)]),
1134 Ty::function(vec![], EffectSet::open_var(5), Ty::Var(0)),
1135 ],
1136 EffectSet::open_var(5),
1137 Ty::Var(0),
1138 ));
1139 // option.or_else :: Option[T], () -> [E] Option[T] -> [E] Option[T]
1140 // The closure takes no arguments because None has no payload to pass.
1141 fields.insert("or_else".into(), Ty::function(
1142 vec![
1143 Ty::Con("Option".into(), vec![Ty::Var(0)]),
1144 Ty::function(vec![], EffectSet::open_var(4),
1145 Ty::Con("Option".into(), vec![Ty::Var(0)])),
1146 ],
1147 EffectSet::open_var(4),
1148 Ty::Con("Option".into(), vec![Ty::Var(0)]),
1149 ));
1150 Some(Ty::Record(fields))
1151 }
1152 "tuple" => {
1153 // Tuple accessors per §11.1. Polymorphic in the tuple's
1154 // element types; we use the same row-variable shape used
1155 // by list helpers. Tuples are heterogeneous, so each
1156 // accessor is statically typed via independent type
1157 // variables for each position.
1158 let mut fields = IndexMap::new();
1159 // fst :: (T0, T1) -> T0
1160 fields.insert("fst".into(), Ty::function(
1161 vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
1162 EffectSet::empty(),
1163 Ty::Var(0),
1164 ));
1165 // snd :: (T0, T1) -> T1
1166 fields.insert("snd".into(), Ty::function(
1167 vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
1168 EffectSet::empty(),
1169 Ty::Var(1),
1170 ));
1171 // third :: (T0, T1, T2) -> T2
1172 fields.insert("third".into(), Ty::function(
1173 vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1), Ty::Var(2)])],
1174 EffectSet::empty(),
1175 Ty::Var(2),
1176 ));
1177 // len :: (T0, T1) -> Int (covers any pair shape; Int back)
1178 fields.insert("len".into(), Ty::function(
1179 vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
1180 EffectSet::empty(),
1181 Ty::int(),
1182 ));
1183 Some(Ty::Record(fields))
1184 }
1185 "map" => {
1186 // Persistent map. Keys are `Str` or `Int` only — Lex's
1187 // type system tracks them polymorphically as Var(0)
1188 // ("K") and lets the runtime check the key shape; both
1189 // cases fit into `MapKey`.
1190 //
1191 // Type variables: 0 = K, 1 = V.
1192 let mt = || Ty::Con("Map".into(), vec![Ty::Var(0), Ty::Var(1)]);
1193 let pair = || Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]);
1194 let mut fields = IndexMap::new();
1195 // new :: () -> Map[K, V]
1196 fields.insert("new".into(), Ty::function(
1197 vec![], EffectSet::empty(), mt()));
1198 // size :: Map[K, V] -> Int
1199 fields.insert("size".into(), Ty::function(
1200 vec![mt()], EffectSet::empty(), Ty::int()));
1201 // has :: Map[K, V], K -> Bool
1202 fields.insert("has".into(), Ty::function(
1203 vec![mt(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
1204 // get :: Map[K, V], K -> Option[V]
1205 fields.insert("get".into(), Ty::function(
1206 vec![mt(), Ty::Var(0)], EffectSet::empty(),
1207 Ty::Con("Option".into(), vec![Ty::Var(1)])));
1208 // set :: Map[K, V], K, V -> Map[K, V]
1209 fields.insert("set".into(), Ty::function(
1210 vec![mt(), Ty::Var(0), Ty::Var(1)],
1211 EffectSet::empty(), mt()));
1212 // delete :: Map[K, V], K -> Map[K, V]
1213 fields.insert("delete".into(), Ty::function(
1214 vec![mt(), Ty::Var(0)], EffectSet::empty(), mt()));
1215 // keys :: Map[K, V] -> List[K]
1216 fields.insert("keys".into(), Ty::function(
1217 vec![mt()], EffectSet::empty(),
1218 Ty::List(Box::new(Ty::Var(0)))));
1219 // values :: Map[K, V] -> List[V]
1220 fields.insert("values".into(), Ty::function(
1221 vec![mt()], EffectSet::empty(),
1222 Ty::List(Box::new(Ty::Var(1)))));
1223 // entries :: Map[K, V] -> List[(K, V)]
1224 fields.insert("entries".into(), Ty::function(
1225 vec![mt()], EffectSet::empty(),
1226 Ty::List(Box::new(pair()))));
1227 // from_list :: List[(K, V)] -> Map[K, V]
1228 fields.insert("from_list".into(), Ty::function(
1229 vec![Ty::List(Box::new(pair()))],
1230 EffectSet::empty(), mt()));
1231 // merge :: Map[K, V], Map[K, V] -> Map[K, V] (b overrides a)
1232 fields.insert("merge".into(), Ty::function(
1233 vec![mt(), mt()], EffectSet::empty(), mt()));
1234 // is_empty :: Map[K, V] -> Bool
1235 fields.insert("is_empty".into(), Ty::function(
1236 vec![mt()], EffectSet::empty(), Ty::bool()));
1237 // fold :: Map[K, V], A, (A, K, V) -> [E] A -> [E] A
1238 // Iteration order matches `map.entries` (BTreeMap-sorted by
1239 // key). Effect-polymorphic on the combiner like `list.fold`.
1240 // Type variable 2 = A (accumulator), effect row 3.
1241 fields.insert("fold".into(), Ty::function(
1242 vec![
1243 mt(),
1244 Ty::Var(2),
1245 Ty::function(
1246 vec![Ty::Var(2), Ty::Var(0), Ty::Var(1)],
1247 EffectSet::open_var(3),
1248 Ty::Var(2),
1249 ),
1250 ],
1251 EffectSet::open_var(3),
1252 Ty::Var(2),
1253 ));
1254 Some(Ty::Record(fields))
1255 }
1256 "set" => {
1257 // Persistent set with the same key-type discipline as map.
1258 // Type variable: 0 = T (the element type, also the key type).
1259 let st = || Ty::Con("Set".into(), vec![Ty::Var(0)]);
1260 let mut fields = IndexMap::new();
1261 // new :: () -> Set[T]
1262 fields.insert("new".into(), Ty::function(
1263 vec![], EffectSet::empty(), st()));
1264 // size :: Set[T] -> Int
1265 fields.insert("size".into(), Ty::function(
1266 vec![st()], EffectSet::empty(), Ty::int()));
1267 // has :: Set[T], T -> Bool
1268 fields.insert("has".into(), Ty::function(
1269 vec![st(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
1270 // add :: Set[T], T -> Set[T]
1271 fields.insert("add".into(), Ty::function(
1272 vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
1273 // delete :: Set[T], T -> Set[T]
1274 fields.insert("delete".into(), Ty::function(
1275 vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
1276 // to_list :: Set[T] -> List[T]
1277 fields.insert("to_list".into(), Ty::function(
1278 vec![st()], EffectSet::empty(),
1279 Ty::List(Box::new(Ty::Var(0)))));
1280 // from_list :: List[T] -> Set[T]
1281 fields.insert("from_list".into(), Ty::function(
1282 vec![Ty::List(Box::new(Ty::Var(0)))],
1283 EffectSet::empty(), st()));
1284 // union :: Set[T], Set[T] -> Set[T]
1285 fields.insert("union".into(), Ty::function(
1286 vec![st(), st()], EffectSet::empty(), st()));
1287 // intersect :: Set[T], Set[T] -> Set[T]
1288 fields.insert("intersect".into(), Ty::function(
1289 vec![st(), st()], EffectSet::empty(), st()));
1290 // diff :: Set[T], Set[T] -> Set[T]
1291 fields.insert("diff".into(), Ty::function(
1292 vec![st(), st()], EffectSet::empty(), st()));
1293 // is_empty :: Set[T] -> Bool
1294 fields.insert("is_empty".into(), Ty::function(
1295 vec![st()], EffectSet::empty(), Ty::bool()));
1296 // is_subset :: Set[T], Set[T] -> Bool (a is subset of b)
1297 fields.insert("is_subset".into(), Ty::function(
1298 vec![st(), st()], EffectSet::empty(), Ty::bool()));
1299 Some(Ty::Record(fields))
1300 }
1301 "iter" => {
1302 // Positional iterator (#364) + lazy variant via `iter.unfold`
1303 // (#376). Internal value shapes are `__IterEager(list, idx)` or
1304 // `__IterLazy(seed, step)`; all operations compile-inline and
1305 // dispatch on the variant tag at runtime.
1306 // Type var slots: 0 = T (element), 1 = U (mapped element) /
1307 // A (fold acc), 2 = S (unfold seed).
1308 let it = |n: u32| Ty::Con("Iter".into(), vec![Ty::Var(n)]);
1309 let mut fields = IndexMap::new();
1310 // from_list :: List[T] -> Iter[T]
1311 fields.insert("from_list".into(), Ty::function(
1312 vec![Ty::List(Box::new(Ty::Var(0)))],
1313 EffectSet::empty(), it(0)));
1314 // unfold[S, T] :: S, (S) -> Option[(T, S)] -> Iter[T] (#376)
1315 // The step closure may carry any effect row; the iterator
1316 // itself stays effect-free since the effects only fire when
1317 // the step is invoked via `iter.next` / `iter.to_list`.
1318 fields.insert("unfold".into(), Ty::function(
1319 vec![
1320 Ty::Var(2), // seed S
1321 Ty::function(
1322 vec![Ty::Var(2)],
1323 EffectSet::open_var(3),
1324 Ty::Con("Option".into(), vec![
1325 Ty::Tuple(vec![Ty::Var(0), Ty::Var(2)])
1326 ]),
1327 ),
1328 ],
1329 EffectSet::empty(), it(0)));
1330 // next :: Iter[T] -> Option[(T, Iter[T])]
1331 fields.insert("next".into(), Ty::function(
1332 vec![it(0)],
1333 EffectSet::empty(),
1334 Ty::Con("Option".into(), vec![
1335 Ty::Tuple(vec![Ty::Var(0), it(0)])
1336 ])));
1337 // is_empty :: Iter[T] -> Bool
1338 fields.insert("is_empty".into(), Ty::function(
1339 vec![it(0)], EffectSet::empty(), Ty::bool()));
1340 // count :: Iter[T] -> Int (remaining elements)
1341 fields.insert("count".into(), Ty::function(
1342 vec![it(0)], EffectSet::empty(), Ty::int()));
1343 // take :: Iter[T], Int -> Iter[T]
1344 fields.insert("take".into(), Ty::function(
1345 vec![it(0), Ty::int()], EffectSet::empty(), it(0)));
1346 // skip :: Iter[T], Int -> Iter[T]
1347 fields.insert("skip".into(), Ty::function(
1348 vec![it(0), Ty::int()], EffectSet::empty(), it(0)));
1349 // to_list :: Iter[T] -> List[T]
1350 fields.insert("to_list".into(), Ty::function(
1351 vec![it(0)], EffectSet::empty(),
1352 Ty::List(Box::new(Ty::Var(0)))));
1353 // map :: [E] Iter[T], (T) -> [E] U -> [E] Iter[U]
1354 fields.insert("map".into(), Ty::function(
1355 vec![
1356 it(0),
1357 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1358 ],
1359 EffectSet::open_var(2), it(1)));
1360 // filter :: [E] Iter[T], (T) -> [E] Bool -> [E] Iter[T]
1361 fields.insert("filter".into(), Ty::function(
1362 vec![
1363 it(0),
1364 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(1), Ty::bool()),
1365 ],
1366 EffectSet::open_var(1), it(0)));
1367 // fold :: [E] Iter[T], A, (A, T) -> [E] A -> [E] A
1368 fields.insert("fold".into(), Ty::function(
1369 vec![
1370 it(0),
1371 Ty::Var(1),
1372 Ty::function(vec![Ty::Var(1), Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1373 ],
1374 EffectSet::open_var(2), Ty::Var(1)));
1375 Some(Ty::Record(fields))
1376 }
1377 "flow" => {
1378 // Orchestration primitives (spec §11.2). Each takes one or
1379 // more closures and returns a closure with a derived shape.
1380 let mut fields = IndexMap::new();
1381 // sequential[T, U, V](f: (T) -> U, g: (U) -> V) -> (T) -> V
1382 fields.insert("sequential".into(), Ty::function(
1383 vec![
1384 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1385 Ty::function(vec![Ty::Var(1)], EffectSet::empty(), Ty::Var(2)),
1386 ],
1387 EffectSet::empty(),
1388 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(2)),
1389 ));
1390 // branch[T, U](cond: (T) -> Bool, t: (T) -> U, f: (T) -> U) -> (T) -> U
1391 fields.insert("branch".into(), Ty::function(
1392 vec![
1393 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::bool()),
1394 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1395 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1396 ],
1397 EffectSet::empty(),
1398 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1399 ));
1400 // retry[T, U, E, Eff](
1401 // f: (T) -> [Eff] Result[U, E], n: Int
1402 // ) -> (T) -> [Eff] Result[U, E]
1403 // open_var(3) is the effect row carried by `f`; the
1404 // combinator itself is pure, so the outer EffectSet is
1405 // empty. The returned closure propagates Eff unchanged.
1406 let result_ty = Ty::Con("Result".into(), vec![Ty::Var(1), Ty::Var(2)]);
1407 fields.insert("retry".into(), Ty::function(
1408 vec![
1409 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
1410 Ty::int(),
1411 ],
1412 EffectSet::empty(),
1413 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
1414 ));
1415 // retry_with_backoff[T, U, E, Eff](
1416 // f: (T) -> [Eff] Result[U, E], attempts: Int, base_ms: Int,
1417 // ) -> (T) -> [Eff, time] Result[U, E]
1418 // Same retry shape as `flow.retry` plus an exponential
1419 // backoff between attempts. The result function carries
1420 // `[time]` (from `time.sleep_ms`) unioned with the inner
1421 // closure's effect row Eff, so e.g. a `[net]` closure
1422 // produces a `[net, time]` result function. (#226)
1423 fields.insert("retry_with_backoff".into(), Ty::function(
1424 vec![
1425 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
1426 Ty::int(),
1427 Ty::int(),
1428 ],
1429 EffectSet::empty(),
1430 Ty::function(vec![Ty::Var(0)],
1431 EffectSet::open_var(3).union(&EffectSet::singleton("time")), result_ty),
1432 ));
1433 // parallel[A, B](fa: () -> A, fb: () -> B) -> () -> (A, B)
1434 // Sequential implementation today; spec §11.2 reserves the
1435 // option of a true-threaded scheduler. parallel_record is
1436 // listed in the spec but not yet implemented — it needs row
1437 // polymorphism over the input record's fields plus a
1438 // record-iteration trampoline; tracked as follow-up.
1439 fields.insert("parallel".into(), Ty::function(
1440 vec![
1441 Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
1442 Ty::function(vec![], EffectSet::empty(), Ty::Var(1)),
1443 ],
1444 EffectSet::empty(),
1445 Ty::function(vec![], EffectSet::empty(),
1446 Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])),
1447 ));
1448 // parallel_list[T](actions: List[() -> T]) -> List[T]
1449 // Variadic counterpart to `parallel`. Runs each action and
1450 // collects results in input order. Sequential under the
1451 // hood (same caveat as `parallel`); spec §11.2 reserves
1452 // true threading for a future scheduler. Unlike `parallel`,
1453 // this returns the result list directly rather than a
1454 // closure, since the input arity is dynamic.
1455 fields.insert("parallel_list".into(), Ty::function(
1456 vec![
1457 Ty::List(Box::new(
1458 Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
1459 )),
1460 ],
1461 EffectSet::empty(),
1462 Ty::List(Box::new(Ty::Var(0))),
1463 ));
1464 Some(Ty::Record(fields))
1465 }
1466 "crypto" => {
1467 let mut fields = IndexMap::new();
1468 // Hashes: Bytes -> Bytes (digest as raw bytes).
1469 // SHA-256 / SHA-512 are vetted. MD5 is retained only for
1470 // interop with legacy systems — new code should not use it.
1471 // BLAKE2b (#382) is included as a faster alternative to
1472 // SHA-512 with the same security level.
1473 for name in &["sha256", "sha512", "md5", "blake2b"] {
1474 fields.insert((*name).into(), Ty::function(
1475 vec![Ty::bytes()],
1476 EffectSet::empty(),
1477 Ty::bytes(),
1478 ));
1479 }
1480 // Hex-string convenience hashers (#382): hash a Str directly,
1481 // return the digest as a lowercase hex Str. Equivalent to
1482 // `crypto.hex_encode(crypto.shaN(bytes_from_str(s)))` but
1483 // saves the two-step incantation for the common case.
1484 for name in &["sha256_str", "sha512_str"] {
1485 fields.insert((*name).into(), Ty::function(
1486 vec![Ty::str()],
1487 EffectSet::empty(),
1488 Ty::str(),
1489 ));
1490 }
1491 // HMAC: (key :: Bytes, data :: Bytes) -> Bytes
1492 for name in &["hmac_sha256", "hmac_sha512"] {
1493 fields.insert((*name).into(), Ty::function(
1494 vec![Ty::bytes(), Ty::bytes()],
1495 EffectSet::empty(),
1496 Ty::bytes(),
1497 ));
1498 }
1499 // base64 / hex
1500 fields.insert("base64_encode".into(), Ty::function(
1501 vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1502 fields.insert("base64_decode".into(), Ty::function(
1503 vec![Ty::str()], EffectSet::empty(),
1504 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1505 // URL-safe base64 (#382): the alphabet swaps `+/` for `-_`
1506 // and omits padding. Required by JWT, signed-cookie, and
1507 // most token-bearing URL paths.
1508 fields.insert("base64url_encode".into(), Ty::function(
1509 vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1510 fields.insert("base64url_decode".into(), Ty::function(
1511 vec![Ty::str()], EffectSet::empty(),
1512 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1513 fields.insert("hex_encode".into(), Ty::function(
1514 vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1515 fields.insert("hex_decode".into(), Ty::function(
1516 vec![Ty::str()], EffectSet::empty(),
1517 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1518 // Constant-time equality (for HMAC verification etc.).
1519 // `eq` / `eq_str` (#382) are the recommended spelling;
1520 // `constant_time_eq` stays as a deprecated alias.
1521 fields.insert("constant_time_eq".into(), Ty::function(
1522 vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool()));
1523 fields.insert("eq".into(), Ty::function(
1524 vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool()));
1525 fields.insert("eq_str".into(), Ty::function(
1526 vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
1527 // Cryptographically-secure random bytes — OS RNG, not the
1528 // deterministic `rand.int_in` stub. The new `[random]`
1529 // effect is fine-grained on purpose so reviewers can find
1530 // every token-generating call via `lex audit --effect
1531 // random`.
1532 fields.insert("random".into(), Ty::function(
1533 vec![Ty::int()],
1534 EffectSet::singleton("random"),
1535 Ty::bytes(),
1536 ));
1537 // random_str_hex (#382): the most common token-mint pattern
1538 // — N random bytes rendered as 2N lowercase hex chars.
1539 // Suitable for session ids, request ids, OAuth `state`,
1540 // CSRF tokens; not suitable as a JWT signing key (use raw
1541 // `random` for that).
1542 fields.insert("random_str_hex".into(), Ty::function(
1543 vec![Ty::int()],
1544 EffectSet::singleton("random"),
1545 Ty::str(),
1546 ));
1547
1548 // AEAD: authenticated encryption with associated data
1549 // (#382 AEAD slice). Both algorithms use a 12-byte nonce
1550 // and a 16-byte authentication tag. `seal` returns the
1551 // structured `AeadResult { ciphertext, tag }`; `open`
1552 // returns `Result[Bytes, Str]` so authentication failures
1553 // surface as `Err`, not a panic.
1554 //
1555 // - **AES-GCM** (`aes_gcm_seal/open`): AES-128/192/256-GCM,
1556 // key length determined by the supplied key bytes (16, 24,
1557 // or 32). NIST-recommended; hardware-accelerated on most CPUs.
1558 // - **ChaCha20-Poly1305** (`chacha20_poly1305_seal/open`):
1559 // Always a 32-byte key. Equivalent security to AES-GCM
1560 // without needing AES-NI hardware; preferred on constrained
1561 // targets.
1562 let aead_t = || Ty::Con("AeadResult".into(), vec![]);
1563 // Seal: returns Result[AeadResult, Str] rather than bare
1564 // AeadResult so input-validation errors (wrong key length,
1565 // wrong nonce length) surface as `Err` to the Lex caller
1566 // instead of panicking the VM. AES-GCM expects 16/24/32-byte
1567 // keys; ChaCha20-Poly1305 expects exactly 32. Both expect a
1568 // 12-byte nonce.
1569 for name in &["aes_gcm_seal", "chacha20_poly1305_seal"] {
1570 fields.insert((*name).into(), Ty::function(
1571 // (key, nonce, aad, plaintext) -> Result[AeadResult, Str]
1572 vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes()],
1573 EffectSet::empty(),
1574 Ty::Con("Result".into(), vec![aead_t(), Ty::str()]),
1575 ));
1576 }
1577 for name in &["aes_gcm_open", "chacha20_poly1305_open"] {
1578 fields.insert((*name).into(), Ty::function(
1579 // (key, nonce, aad, ciphertext, tag) -> Result[Bytes, Str]
1580 vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes()],
1581 EffectSet::empty(),
1582 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1583 ));
1584 }
1585
1586 // KDFs: key-derivation functions (#382 KDF slice). All three
1587 // return `Result[Bytes, Str]` so caller-controlled inputs
1588 // (iteration count, output length, argon2id work factors)
1589 // that violate the underlying primitive's contract surface
1590 // as `Err` rather than panicking the VM. None require a new
1591 // effect — these are pure derivations.
1592 //
1593 // - **`pbkdf2_sha256(password, salt, iterations, len)`** —
1594 // RFC 8018 PBKDF2 with HMAC-SHA256. Use ≥ 600_000 iterations
1595 // for password storage (OWASP 2024). Older deployments
1596 // pinning < 100_000 should rotate.
1597 // - **`hkdf_sha256(ikm, salt, info, len)`** — RFC 5869 extract+
1598 // expand. Use for deriving multiple keys from a single
1599 // high-entropy input (TLS, Noise, JWT-key rotation).
1600 // Output length capped at 255 × 32 = 8160 bytes.
1601 // - **`argon2id(password, salt, t_cost, m_cost, len)`** —
1602 // RFC 9106 Argon2id. Recommended for *new* password
1603 // hashing. OWASP 2024 baseline: `t_cost=2, m_cost=19456`
1604 // (19 MiB), or use `lex-crypto`'s vetted wrapper.
1605 fields.insert("pbkdf2_sha256".into(), Ty::function(
1606 // (password, salt, iterations, len) -> Result[Bytes, Str]
1607 vec![Ty::bytes(), Ty::bytes(), Ty::int(), Ty::int()],
1608 EffectSet::empty(),
1609 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1610 ));
1611 fields.insert("hkdf_sha256".into(), Ty::function(
1612 // (ikm, salt, info, len) -> Result[Bytes, Str]
1613 vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::int()],
1614 EffectSet::empty(),
1615 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1616 ));
1617 fields.insert("argon2id".into(), Ty::function(
1618 // (password, salt, t_cost, m_cost, len) -> Result[Bytes, Str]
1619 vec![Ty::bytes(), Ty::bytes(), Ty::int(), Ty::int(), Ty::int()],
1620 EffectSet::empty(),
1621 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1622 ));
1623
1624 Some(Ty::Record(fields))
1625 }
1626 "deque" => {
1627 // Persistent double-ended queue. Push/pop O(1) on both
1628 // ends; iteration order is front-to-back.
1629 // Type variable: 0 = T.
1630 let dt = || Ty::Con("Deque".into(), vec![Ty::Var(0)]);
1631 let pair = || Ty::Tuple(vec![Ty::Var(0), dt()]);
1632 let mut fields = IndexMap::new();
1633 // new :: () -> Deque[T]
1634 fields.insert("new".into(), Ty::function(
1635 vec![], EffectSet::empty(), dt()));
1636 // size :: Deque[T] -> Int
1637 fields.insert("size".into(), Ty::function(
1638 vec![dt()], EffectSet::empty(), Ty::int()));
1639 // is_empty :: Deque[T] -> Bool
1640 fields.insert("is_empty".into(), Ty::function(
1641 vec![dt()], EffectSet::empty(), Ty::bool()));
1642 // push_back / push_front :: Deque[T], T -> Deque[T]
1643 for n in &["push_back", "push_front"] {
1644 fields.insert((*n).into(), Ty::function(
1645 vec![dt(), Ty::Var(0)], EffectSet::empty(), dt()));
1646 }
1647 // pop_back / pop_front :: Deque[T] -> Option[(T, Deque[T])]
1648 for n in &["pop_back", "pop_front"] {
1649 fields.insert((*n).into(), Ty::function(
1650 vec![dt()], EffectSet::empty(),
1651 Ty::Con("Option".into(), vec![pair()])));
1652 }
1653 // peek_back / peek_front :: Deque[T] -> Option[T]
1654 for n in &["peek_back", "peek_front"] {
1655 fields.insert((*n).into(), Ty::function(
1656 vec![dt()], EffectSet::empty(),
1657 Ty::Con("Option".into(), vec![Ty::Var(0)])));
1658 }
1659 // from_list :: List[T] -> Deque[T]
1660 fields.insert("from_list".into(), Ty::function(
1661 vec![Ty::List(Box::new(Ty::Var(0)))],
1662 EffectSet::empty(), dt()));
1663 // to_list :: Deque[T] -> List[T]
1664 fields.insert("to_list".into(), Ty::function(
1665 vec![dt()], EffectSet::empty(),
1666 Ty::List(Box::new(Ty::Var(0)))));
1667 Some(Ty::Record(fields))
1668 }
1669 "log" => {
1670 // Structured logging behind a [log] effect. Emit ops route
1671 // through a runtime-configured sink (stderr by default;
1672 // can be redirected via set_sink). Configuration ops
1673 // mutate the global sink and so are gated [io].
1674 let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1675 let mut fields = IndexMap::new();
1676 for level in &["debug", "info", "warn", "error"] {
1677 fields.insert((*level).into(), Ty::function(
1678 vec![Ty::str()],
1679 EffectSet::singleton("log"),
1680 Ty::Unit,
1681 ));
1682 }
1683 // set_level :: Str -> [io] Result[Nil, Str]
1684 fields.insert("set_level".into(), Ty::function(
1685 vec![Ty::str()],
1686 EffectSet::singleton("io"),
1687 result_str(Ty::Unit)));
1688 // set_format :: Str -> [io] Result[Nil, Str]
1689 fields.insert("set_format".into(), Ty::function(
1690 vec![Ty::str()],
1691 EffectSet::singleton("io"),
1692 result_str(Ty::Unit)));
1693 // set_sink :: Str -> [io, fs_write] Result[Nil, Str]
1694 fields.insert("set_sink".into(), Ty::function(
1695 vec![Ty::str()],
1696 EffectSet {
1697 concrete: [crate::types::EffectKind::bare("io"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
1698 var: None,
1699 },
1700 result_str(Ty::Unit)));
1701 Some(Ty::Record(fields))
1702 }
1703 "datetime" => {
1704 // Instant and Duration are nominal opaque Ints under the
1705 // hood (nanoseconds-since-UTC-epoch and signed nanoseconds
1706 // respectively); the type checker tracks the distinction
1707 // even though both values look like Int at runtime.
1708 //
1709 // Tz is the variant
1710 // Utc | Local | Offset(Int) | Iana(Str)
1711 // registered as a built-in nominal type in
1712 // `TypeEnv::new_with_builtins`. The pre-v1 stringly Tz
1713 // ("UTC"/"Local"/IANA-name/"+05:30") is no longer accepted
1714 // — passing a `Str` to `to_components` is now a type
1715 // error.
1716 let inst = || Ty::Con("Instant".into(), vec![]);
1717 let dur = || Ty::Con("Duration".into(), vec![]);
1718 let tz = || Ty::Con("Tz".into(), vec![]);
1719 let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1720 let dt_t = || {
1721 let mut fs = IndexMap::new();
1722 fs.insert("year".into(), Ty::int());
1723 fs.insert("month".into(), Ty::int());
1724 fs.insert("day".into(), Ty::int());
1725 fs.insert("hour".into(), Ty::int());
1726 fs.insert("minute".into(), Ty::int());
1727 fs.insert("second".into(), Ty::int());
1728 fs.insert("nano".into(), Ty::int());
1729 fs.insert("tz_offset_minutes".into(), Ty::int());
1730 Ty::Record(fs)
1731 };
1732 let mut fields = IndexMap::new();
1733 fields.insert("now".into(), Ty::function(
1734 vec![], EffectSet::singleton("time"), inst()));
1735 fields.insert("parse_iso".into(), Ty::function(
1736 vec![Ty::str()], EffectSet::empty(), result_str(inst())));
1737 fields.insert("format_iso".into(), Ty::function(
1738 vec![inst()], EffectSet::empty(), Ty::str()));
1739 fields.insert("parse".into(), Ty::function(
1740 vec![Ty::str(), Ty::str()], EffectSet::empty(), result_str(inst())));
1741 fields.insert("format".into(), Ty::function(
1742 vec![inst(), Ty::str()], EffectSet::empty(), Ty::str()));
1743 fields.insert("to_components".into(), Ty::function(
1744 vec![inst(), tz()], EffectSet::empty(), result_str(dt_t())));
1745 fields.insert("from_components".into(), Ty::function(
1746 vec![dt_t()], EffectSet::empty(), result_str(inst())));
1747 fields.insert("add".into(), Ty::function(
1748 vec![inst(), dur()], EffectSet::empty(), inst()));
1749 fields.insert("diff".into(), Ty::function(
1750 vec![inst(), inst()], EffectSet::empty(), dur()));
1751 fields.insert("duration_seconds".into(), Ty::function(
1752 vec![Ty::float()], EffectSet::empty(), dur()));
1753 fields.insert("duration_minutes".into(), Ty::function(
1754 vec![Ty::int()], EffectSet::empty(), dur()));
1755 fields.insert("duration_days".into(), Ty::function(
1756 vec![Ty::int()], EffectSet::empty(), dur()));
1757 // #331: comparison ops on Instant.
1758 fields.insert("before".into(), Ty::function(
1759 vec![inst(), inst()], EffectSet::empty(), Ty::bool()));
1760 fields.insert("after".into(), Ty::function(
1761 vec![inst(), inst()], EffectSet::empty(), Ty::bool()));
1762 // compare :: Instant, Instant -> Int (-1 / 0 / +1)
1763 fields.insert("compare".into(), Ty::function(
1764 vec![inst(), inst()], EffectSet::empty(), Ty::int()));
1765 Some(Ty::Record(fields))
1766 }
1767 // #331: duration module — scalar extraction from Duration values.
1768 "duration" => {
1769 let dur = || Ty::Con("Duration".into(), vec![]);
1770 let mut fields = IndexMap::new();
1771 // seconds :: Duration -> Int (truncates toward zero)
1772 fields.insert("seconds".into(), Ty::function(
1773 vec![dur()], EffectSet::empty(), Ty::int()));
1774 Some(Ty::Record(fields))
1775 }
1776 "process" => {
1777 // Streaming subprocess. The opaque `ProcessHandle` type
1778 // is an Int handle into a process-wide registry holding
1779 // the `Child` plus its stdout/stderr `BufReader`s.
1780 let ph = || Ty::Con("ProcessHandle".into(), vec![]);
1781 let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1782 let opts_t = || {
1783 let mut fs = IndexMap::new();
1784 fs.insert("cwd".into(),
1785 Ty::Con("Option".into(), vec![Ty::str()]));
1786 fs.insert("env".into(),
1787 Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
1788 fs.insert("stdin".into(),
1789 Ty::Con("Option".into(), vec![Ty::bytes()]));
1790 Ty::Record(fs)
1791 };
1792 let exit_t = || {
1793 let mut fs = IndexMap::new();
1794 fs.insert("code".into(), Ty::int());
1795 fs.insert("signaled".into(), Ty::bool());
1796 Ty::Record(fs)
1797 };
1798 let output_t = || {
1799 let mut fs = IndexMap::new();
1800 fs.insert("stdout".into(), Ty::str());
1801 fs.insert("stderr".into(), Ty::str());
1802 fs.insert("exit_code".into(), Ty::int());
1803 Ty::Record(fs)
1804 };
1805 let mut fields = IndexMap::new();
1806 // spawn :: Str, List[Str], Opts -> [proc] Result[ProcessHandle, Str]
1807 fields.insert("spawn".into(), Ty::function(
1808 vec![Ty::str(), Ty::List(Box::new(Ty::str())), opts_t()],
1809 EffectSet::singleton("proc"),
1810 result_str(ph())));
1811 // read_stdout_line / read_stderr_line :: ProcessHandle -> [proc] Option[Str]
1812 for n in &["read_stdout_line", "read_stderr_line"] {
1813 fields.insert((*n).into(), Ty::function(
1814 vec![ph()], EffectSet::singleton("proc"),
1815 Ty::Con("Option".into(), vec![Ty::str()])));
1816 }
1817 // wait :: ProcessHandle -> [proc] ProcessExit
1818 fields.insert("wait".into(), Ty::function(
1819 vec![ph()], EffectSet::singleton("proc"), exit_t()));
1820 // kill :: ProcessHandle, Str -> [proc] Result[Nil, Str]
1821 fields.insert("kill".into(), Ty::function(
1822 vec![ph(), Ty::str()],
1823 EffectSet::singleton("proc"),
1824 result_str(Ty::Unit)));
1825 // run :: Str, List[Str] -> [proc] Result[ProcessOutput, Str]
1826 // Blocking convenience that captures stdout/stderr fully
1827 // and returns once the child exits. For programs that
1828 // need streaming, use spawn + read_*_line + wait.
1829 fields.insert("run".into(), Ty::function(
1830 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
1831 EffectSet::singleton("proc"),
1832 result_str(output_t())));
1833 Some(Ty::Record(fields))
1834 }
1835 "fs" => {
1836 // Filesystem walk + mutate. Walk-style ops (exists, walk,
1837 // glob, …) declare [fs_walk] — distinct from [fs_read]
1838 // (which is content reads via io.read), so reviewers can
1839 // separately track directory traversal vs file-content
1840 // exposure. Mutating ops (mkdir_p, remove, copy) declare
1841 // [fs_write]. Path scoping uses --allow-fs-read for walk
1842 // (a directory listing is an information disclosure on
1843 // the same path tree) and --allow-fs-write for mutations.
1844 let stat_t = || {
1845 let mut fs = IndexMap::new();
1846 fs.insert("size".into(), Ty::int());
1847 fs.insert("mtime".into(), Ty::int());
1848 fs.insert("is_dir".into(), Ty::bool());
1849 fs.insert("is_file".into(), Ty::bool());
1850 Ty::Record(fs)
1851 };
1852 let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1853 let mut fields = IndexMap::new();
1854 // Walk-style queries [fs_walk]
1855 fields.insert("exists".into(), Ty::function(
1856 vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
1857 fields.insert("is_file".into(), Ty::function(
1858 vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
1859 fields.insert("is_dir".into(), Ty::function(
1860 vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
1861 fields.insert("stat".into(), Ty::function(
1862 vec![Ty::str()], EffectSet::singleton("fs_walk"),
1863 result_str(stat_t())));
1864 fields.insert("list_dir".into(), Ty::function(
1865 vec![Ty::str()], EffectSet::singleton("fs_walk"),
1866 result_str(Ty::List(Box::new(Ty::str())))));
1867 fields.insert("walk".into(), Ty::function(
1868 vec![Ty::str()], EffectSet::singleton("fs_walk"),
1869 result_str(Ty::List(Box::new(Ty::str())))));
1870 fields.insert("glob".into(), Ty::function(
1871 vec![Ty::str()], EffectSet::singleton("fs_walk"),
1872 result_str(Ty::List(Box::new(Ty::str())))));
1873 // Mutations [fs_write]
1874 fields.insert("mkdir_p".into(), Ty::function(
1875 vec![Ty::str()], EffectSet::singleton("fs_write"),
1876 result_str(Ty::Unit)));
1877 fields.insert("remove".into(), Ty::function(
1878 vec![Ty::str()], EffectSet::singleton("fs_write"),
1879 result_str(Ty::Unit)));
1880 fields.insert("copy".into(), Ty::function(
1881 vec![Ty::str(), Ty::str()],
1882 EffectSet {
1883 concrete: [crate::types::EffectKind::bare("fs_walk"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
1884 var: None,
1885 },
1886 result_str(Ty::Unit)));
1887 Some(Ty::Record(fields))
1888 }
1889 "kv" => {
1890 // Embedded key-value store. The opaque `Kv` type is
1891 // backed by an Int handle into a process-wide registry.
1892 let kv_t = || Ty::Con("Kv".into(), vec![]);
1893 let mut fields = IndexMap::new();
1894 // open :: Str -> [kv, fs_write] Result[Kv, Str]
1895 fields.insert("open".into(), Ty::function(
1896 vec![Ty::str()],
1897 EffectSet {
1898 concrete: [crate::types::EffectKind::bare("kv"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
1899 var: None,
1900 },
1901 Ty::Con("Result".into(), vec![kv_t(), Ty::str()])));
1902 // close :: Kv -> [kv] Nil
1903 fields.insert("close".into(), Ty::function(
1904 vec![kv_t()],
1905 EffectSet::singleton("kv"),
1906 Ty::Unit));
1907 // get :: Kv, Str -> [kv] Option[Bytes]
1908 fields.insert("get".into(), Ty::function(
1909 vec![kv_t(), Ty::str()],
1910 EffectSet::singleton("kv"),
1911 Ty::Con("Option".into(), vec![Ty::bytes()])));
1912 // put :: Kv, Str, Bytes -> [kv] Result[Nil, Str]
1913 fields.insert("put".into(), Ty::function(
1914 vec![kv_t(), Ty::str(), Ty::bytes()],
1915 EffectSet::singleton("kv"),
1916 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
1917 // delete :: Kv, Str -> [kv] Result[Nil, Str]
1918 fields.insert("delete".into(), Ty::function(
1919 vec![kv_t(), Ty::str()],
1920 EffectSet::singleton("kv"),
1921 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
1922 // contains :: Kv, Str -> [kv] Bool
1923 fields.insert("contains".into(), Ty::function(
1924 vec![kv_t(), Ty::str()],
1925 EffectSet::singleton("kv"),
1926 Ty::bool()));
1927 // list_prefix :: Kv, Str -> [kv] List[Str]
1928 fields.insert("list_prefix".into(), Ty::function(
1929 vec![kv_t(), Ty::str()],
1930 EffectSet::singleton("kv"),
1931 Ty::List(Box::new(Ty::str()))));
1932 Some(Ty::Record(fields))
1933 }
1934 "sql" => {
1935 // Embedded SQL (SQLite via rusqlite). The opaque `Db` type is
1936 // backed by an Int handle into a process-wide registry (#362).
1937 //
1938 // Params use the typed `SqlParam` ADT (PStr|PInt|PFloat|PBool|PNull)
1939 // registered in env.rs, so callers don't have to stringify values.
1940 //
1941 // Transactions: sql.begin(db) → SqlTx; sql.commit/rollback(tx).
1942 // exec_tx / query_tx mirror exec / query but operate on a SqlTx.
1943 //
1944 // Row decoders: get_str / get_int / get_float / get_bool extract
1945 // typed columns from a row record by name.
1946 let db_t = || Ty::Con("Db".into(), vec![]);
1947 let tx_t = || Ty::Con("SqlTx".into(), vec![]);
1948 let sp_t = || Ty::Con("SqlParam".into(), vec![]);
1949 let params_t = || Ty::List(Box::new(sp_t()));
1950 let mut fields = IndexMap::new();
1951
1952 // SqlError = { message, code, detail } — populated with
1953 // SQLSTATE (Postgres) or symbolic SQLite error name (#380).
1954 let se_t = || Ty::Con("SqlError".into(), vec![]);
1955
1956 // open :: Str -> [sql, fs_write] Result[Db, SqlError]
1957 fields.insert("open".into(), Ty::function(
1958 vec![Ty::str()],
1959 EffectSet {
1960 concrete: [crate::types::EffectKind::bare("sql"),
1961 crate::types::EffectKind::bare("fs_write")]
1962 .into_iter().collect(),
1963 var: None,
1964 },
1965 Ty::Con("Result".into(), vec![db_t(), se_t()])));
1966
1967 // close :: Db -> [sql] Unit
1968 fields.insert("close".into(), Ty::function(
1969 vec![db_t()],
1970 EffectSet::singleton("sql"),
1971 Ty::Unit));
1972
1973 // exec :: Db, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
1974 fields.insert("exec".into(), Ty::function(
1975 vec![db_t(), Ty::str(), params_t()],
1976 EffectSet::singleton("sql"),
1977 Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
1978
1979 // query[T] :: Db, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
1980 fields.insert("query".into(), Ty::function(
1981 vec![db_t(), Ty::str(), params_t()],
1982 EffectSet::singleton("sql"),
1983 Ty::Con("Result".into(), vec![
1984 Ty::List(Box::new(Ty::Var(0))),
1985 se_t(),
1986 ])));
1987
1988 // query_iter[T] :: Db, Str, List[SqlParam] -> [sql] Result[Iter[T], SqlError]
1989 // Streaming variant of `query` (#379). Rows are pulled from
1990 // the server one at a time via an mpsc-backed cursor —
1991 // memory stays bounded regardless of result-set size.
1992 // Other ops on the same `Db` handle block until the cursor
1993 // is drained (single connection per Db).
1994 fields.insert("query_iter".into(), Ty::function(
1995 vec![db_t(), Ty::str(), params_t()],
1996 EffectSet::singleton("sql"),
1997 Ty::Con("Result".into(), vec![
1998 Ty::Con("Iter".into(), vec![Ty::Var(0)]),
1999 se_t(),
2000 ])));
2001
2002 // begin :: Db -> [sql] Result[SqlTx, SqlError]
2003 fields.insert("begin".into(), Ty::function(
2004 vec![db_t()],
2005 EffectSet::singleton("sql"),
2006 Ty::Con("Result".into(), vec![tx_t(), se_t()])));
2007
2008 // commit :: SqlTx -> [sql] Result[Unit, SqlError]
2009 fields.insert("commit".into(), Ty::function(
2010 vec![tx_t()],
2011 EffectSet::singleton("sql"),
2012 Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
2013
2014 // rollback :: SqlTx -> [sql] Result[Unit, SqlError]
2015 fields.insert("rollback".into(), Ty::function(
2016 vec![tx_t()],
2017 EffectSet::singleton("sql"),
2018 Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
2019
2020 // exec_tx :: SqlTx, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
2021 fields.insert("exec_tx".into(), Ty::function(
2022 vec![tx_t(), Ty::str(), params_t()],
2023 EffectSet::singleton("sql"),
2024 Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
2025
2026 // query_tx[T] :: SqlTx, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
2027 fields.insert("query_tx".into(), Ty::function(
2028 vec![tx_t(), Ty::str(), params_t()],
2029 EffectSet::singleton("sql"),
2030 Ty::Con("Result".into(), vec![
2031 Ty::List(Box::new(Ty::Var(0))),
2032 se_t(),
2033 ])));
2034
2035 // Row decoders: get_X[T] :: T, Str -> Option[X]
2036 // T is polymorphic so these work on any row record shape.
2037 fields.insert("get_str".into(), Ty::function(
2038 vec![Ty::Var(0), Ty::str()],
2039 EffectSet::empty(),
2040 Ty::Con("Option".into(), vec![Ty::str()])));
2041 fields.insert("get_int".into(), Ty::function(
2042 vec![Ty::Var(0), Ty::str()],
2043 EffectSet::empty(),
2044 Ty::Con("Option".into(), vec![Ty::int()])));
2045 fields.insert("get_float".into(), Ty::function(
2046 vec![Ty::Var(0), Ty::str()],
2047 EffectSet::empty(),
2048 Ty::Con("Option".into(), vec![Ty::Con("Float".into(), vec![])])));
2049 fields.insert("get_bool".into(), Ty::function(
2050 vec![Ty::Var(0), Ty::str()],
2051 EffectSet::empty(),
2052 Ty::Con("Option".into(), vec![Ty::Con("Bool".into(), vec![])])));
2053
2054 Some(Ty::Record(fields))
2055 }
2056 "parser" => {
2057 // #217: structured parser combinators. Parser values are
2058 // tagged Records at runtime (`{ kind, ... }`), opaque at
2059 // the language level via `Ty::Con("Parser", [T])`.
2060 //
2061 // Surface:
2062 // - primitives: char, string, digit, alpha, whitespace, eof
2063 // - combinators: seq, alt, many, optional, map, and_then
2064 // - run :: Parser[T], Str -> Result[T, ParseErr]
2065 //
2066 // `map` and `and_then` were deferred from #217's v1 because
2067 // their closure arguments carried call-site identity that
2068 // broke the canonical-parsers acceptance criterion. With
2069 // closure body-hash equality landed in #222, that concern
2070 // is gone, and #221 wires them in. The interpreter for
2071 // `parser.run` has been moved to `lex-bytecode::parser_runtime`
2072 // so it can invoke closures from `Map` / `AndThen` nodes.
2073 let pt = |t: Ty| Ty::Con("Parser".into(), vec![t]);
2074 let parse_err = || {
2075 let mut fs = IndexMap::new();
2076 fs.insert("pos".into(), Ty::int());
2077 fs.insert("message".into(), Ty::str());
2078 Ty::Record(fs)
2079 };
2080 let mut fields = IndexMap::new();
2081 // char :: Str -> Parser[Str] (single-char Str literal)
2082 fields.insert("char".into(), Ty::function(
2083 vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
2084 // string :: Str -> Parser[Str]
2085 fields.insert("string".into(), Ty::function(
2086 vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
2087 // digit :: () -> Parser[Str]
2088 fields.insert("digit".into(), Ty::function(
2089 vec![], EffectSet::empty(), pt(Ty::str())));
2090 // alpha :: () -> Parser[Str]
2091 fields.insert("alpha".into(), Ty::function(
2092 vec![], EffectSet::empty(), pt(Ty::str())));
2093 // whitespace :: () -> Parser[Str]
2094 fields.insert("whitespace".into(), Ty::function(
2095 vec![], EffectSet::empty(), pt(Ty::str())));
2096 // eof :: () -> Parser[Unit]
2097 fields.insert("eof".into(), Ty::function(
2098 vec![], EffectSet::empty(), pt(Ty::Unit)));
2099 // seq :: Parser[A], Parser[B] -> Parser[(A, B)]
2100 fields.insert("seq".into(), Ty::function(
2101 vec![pt(Ty::Var(0)), pt(Ty::Var(1))],
2102 EffectSet::empty(),
2103 pt(Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]))));
2104 // alt :: Parser[T], Parser[T] -> Parser[T]
2105 // PEG-style ordered choice: the second alternative is
2106 // tried only if the first fails.
2107 fields.insert("alt".into(), Ty::function(
2108 vec![pt(Ty::Var(0)), pt(Ty::Var(0))],
2109 EffectSet::empty(),
2110 pt(Ty::Var(0))));
2111 // many :: Parser[T] -> Parser[List[T]]
2112 // Zero-or-more. Stops as soon as the inner parser fails
2113 // OR doesn't advance the position (avoids infinite loop
2114 // on empty matches).
2115 fields.insert("many".into(), Ty::function(
2116 vec![pt(Ty::Var(0))],
2117 EffectSet::empty(),
2118 pt(Ty::List(Box::new(Ty::Var(0))))));
2119 // optional :: Parser[T] -> Parser[Option[T]]
2120 fields.insert("optional".into(), Ty::function(
2121 vec![pt(Ty::Var(0))],
2122 EffectSet::empty(),
2123 pt(Ty::Con("Option".into(), vec![Ty::Var(0)]))));
2124 // map :: Parser[T], (T) -> [E] U -> [E] Parser[U]
2125 // The closure runs at parse time when the Parser is run.
2126 // Effect-polymorphic on the closure: any effect the
2127 // closure declares propagates to the surrounding `run`.
2128 fields.insert("map".into(), Ty::function(
2129 vec![
2130 pt(Ty::Var(0)),
2131 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
2132 ],
2133 EffectSet::open_var(2),
2134 pt(Ty::Var(1))));
2135 // and_then :: Parser[T], (T) -> [E] Parser[U] -> [E] Parser[U]
2136 // Monadic bind: closure inspects the parsed value and
2137 // returns the next parser to run.
2138 fields.insert("and_then".into(), Ty::function(
2139 vec![
2140 pt(Ty::Var(0)),
2141 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
2142 pt(Ty::Var(1))),
2143 ],
2144 EffectSet::open_var(3),
2145 pt(Ty::Var(1))));
2146 // run :: Parser[T], Str -> Result[T, ParseErr]
2147 // ParseErr = { pos :: Int, message :: Str }
2148 fields.insert("run".into(), Ty::function(
2149 vec![pt(Ty::Var(0)), Ty::str()],
2150 EffectSet::empty(),
2151 Ty::Con("Result".into(), vec![Ty::Var(0), parse_err()])));
2152 Some(Ty::Record(fields))
2153 }
2154 "cli" => {
2155 // #224 Rubric port: argparse-equivalent for end-user
2156 // programs. Spec values are tagged `Json` records (opaque
2157 // to the language but inspectable). Construction via the
2158 // `flag` / `option` / `positional` / `spec` builders;
2159 // parse + introspection / help via the remaining ops.
2160 let json = || Ty::Con("Json".into(), vec![]);
2161 let opt_str = || Ty::Con("Option".into(), vec![Ty::str()]);
2162 let mut fields = IndexMap::new();
2163 // flag :: Str -> Option[Str] -> Str -> Json
2164 // long_name -> short -> help -> CliArg
2165 fields.insert("flag".into(), Ty::function(
2166 vec![Ty::str(), opt_str(), Ty::str()],
2167 EffectSet::empty(),
2168 json()));
2169 // option :: Str -> Option[Str] -> Str -> Option[Str] -> Json
2170 // long_name -> short -> help -> default -> CliArg
2171 fields.insert("option".into(), Ty::function(
2172 vec![Ty::str(), opt_str(), Ty::str(), opt_str()],
2173 EffectSet::empty(),
2174 json()));
2175 // positional :: Str -> Str -> Bool -> Json
2176 // name -> help -> required -> CliArg
2177 fields.insert("positional".into(), Ty::function(
2178 vec![Ty::str(), Ty::str(), Ty::bool()],
2179 EffectSet::empty(),
2180 json()));
2181 // spec :: Str -> Str -> List[Json] -> List[Json] -> Json
2182 // name -> help -> args -> subcommands -> CliSpec
2183 fields.insert("spec".into(), Ty::function(
2184 vec![Ty::str(), Ty::str(),
2185 Ty::List(Box::new(json())),
2186 Ty::List(Box::new(json()))],
2187 EffectSet::empty(),
2188 json()));
2189 // parse :: Json -> List[Str] -> Result[Json, Str]
2190 // spec -> argv -> Result[CliParsed, error]
2191 fields.insert("parse".into(), Ty::function(
2192 vec![json(), Ty::List(Box::new(Ty::str()))],
2193 EffectSet::empty(),
2194 Ty::Con("Result".into(), vec![json(), Ty::str()])));
2195 // envelope :: Bool -> Str -> T -> Json
2196 // ok -> command -> data -> ACLI-shaped envelope.
2197 // `data` is polymorphic so callers don't have to round-
2198 // trip through `json.parse` for trivial payloads.
2199 fields.insert("envelope".into(), Ty::function(
2200 vec![Ty::bool(), Ty::str(), Ty::Var(0)],
2201 EffectSet::empty(),
2202 json()));
2203 // describe :: Json -> Json — machine-readable spec dump
2204 fields.insert("describe".into(), Ty::function(
2205 vec![json()],
2206 EffectSet::empty(),
2207 json()));
2208 // help :: Json -> Str — human-readable help text
2209 fields.insert("help".into(), Ty::function(
2210 vec![json()],
2211 EffectSet::empty(),
2212 Ty::str()));
2213 Some(Ty::Record(fields))
2214 }
2215 "regex" => {
2216 // The compiled `Regex` is stored as a `Str` at runtime
2217 // (the pattern source) plus a process-wide cache of the
2218 // actual `regex::Regex`. So `Regex` is a nominal type at
2219 // the language level but its value is just the pattern.
2220 let regex_t = || Ty::Con("Regex".into(), vec![]);
2221 let match_t = || {
2222 let mut fs = IndexMap::new();
2223 fs.insert("text".into(), Ty::str());
2224 fs.insert("start".into(), Ty::int());
2225 fs.insert("end".into(), Ty::int());
2226 fs.insert("groups".into(), Ty::List(Box::new(Ty::str())));
2227 Ty::Record(fs)
2228 };
2229 let mut fields = IndexMap::new();
2230 // compile :: Str -> Result[Regex, Str]
2231 fields.insert("compile".into(), Ty::function(
2232 vec![Ty::str()], EffectSet::empty(),
2233 Ty::Con("Result".into(), vec![regex_t(), Ty::str()])));
2234 // is_match :: Regex, Str -> Bool
2235 fields.insert("is_match".into(), Ty::function(
2236 vec![regex_t(), Ty::str()], EffectSet::empty(), Ty::bool()));
2237 // is_match_str :: Str, Str -> Bool
2238 // Compiles the first argument as a pattern and matches against the second.
2239 // Returns false on invalid pattern instead of propagating an error.
2240 fields.insert("is_match_str".into(), Ty::function(
2241 vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
2242 // find :: Regex, Str -> Option[Match]
2243 fields.insert("find".into(), Ty::function(
2244 vec![regex_t(), Ty::str()], EffectSet::empty(),
2245 Ty::Con("Option".into(), vec![match_t()])));
2246 // find_all :: Regex, Str -> List[Match]
2247 fields.insert("find_all".into(), Ty::function(
2248 vec![regex_t(), Ty::str()], EffectSet::empty(),
2249 Ty::List(Box::new(match_t()))));
2250 // replace :: Regex, Str, Str -> Str
2251 fields.insert("replace".into(), Ty::function(
2252 vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
2253 // replace_all :: Regex, Str, Str -> Str
2254 fields.insert("replace_all".into(), Ty::function(
2255 vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
2256 // split :: Regex, Str -> List[Str]
2257 fields.insert("split".into(), Ty::function(
2258 vec![regex_t(), Ty::str()], EffectSet::empty(),
2259 Ty::List(Box::new(Ty::str()))));
2260 Some(Ty::Record(fields))
2261 }
2262 "http" => {
2263 // Rich HTTP client. `[net]` for the wire ops, pure for
2264 // the builders / decoders. `--allow-net-host` gates per
2265 // request. Multipart upload + streaming response bodies
2266 // are deferred to v1.5; the v1 surface covers the
2267 // common cases (auth, headers, query, timeouts, JSON /
2268 // text decoding).
2269 let req_t = || Ty::Con("HttpRequest".into(), vec![]);
2270 let resp_t = || Ty::Con("HttpResponse".into(), vec![]);
2271 let err_t = || Ty::Con("HttpError".into(), vec![]);
2272 let result_he = |t: Ty| Ty::Con("Result".into(), vec![t, err_t()]);
2273 let str_str_map = || Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]);
2274 let mut fields = IndexMap::new();
2275 // -- wire ops (effectful) --
2276 // send :: HttpRequest -> [net] Result[HttpResponse, HttpError]
2277 fields.insert("send".into(), Ty::function(
2278 vec![req_t()],
2279 EffectSet::singleton("net"),
2280 result_he(resp_t()),
2281 ));
2282 // get :: Str -> [net] Result[HttpResponse, HttpError]
2283 fields.insert("get".into(), Ty::function(
2284 vec![Ty::str()],
2285 EffectSet::singleton("net"),
2286 result_he(resp_t()),
2287 ));
2288 // post :: Str, Bytes, Str -> [net] Result[HttpResponse, HttpError]
2289 fields.insert("post".into(), Ty::function(
2290 vec![Ty::str(), Ty::bytes(), Ty::str()],
2291 EffectSet::singleton("net"),
2292 result_he(resp_t()),
2293 ));
2294 // -- pure builders (record transforms) --
2295 // with_header :: HttpRequest, Str, Str -> HttpRequest
2296 fields.insert("with_header".into(), Ty::function(
2297 vec![req_t(), Ty::str(), Ty::str()],
2298 EffectSet::empty(),
2299 req_t(),
2300 ));
2301 // with_auth :: HttpRequest, Str, Str -> HttpRequest
2302 // (Renders `<scheme> <token>` into the `Authorization`
2303 // header — `Bearer <jwt>`, `Basic <b64>`, etc.)
2304 fields.insert("with_auth".into(), Ty::function(
2305 vec![req_t(), Ty::str(), Ty::str()],
2306 EffectSet::empty(),
2307 req_t(),
2308 ));
2309 // with_query :: HttpRequest, Map[Str, Str] -> HttpRequest
2310 // (Appends a `?k=v&...` query string; values are URL-
2311 // encoded so `&` / `=` / spaces in values don't escape.)
2312 fields.insert("with_query".into(), Ty::function(
2313 vec![req_t(), str_str_map()],
2314 EffectSet::empty(),
2315 req_t(),
2316 ));
2317 // with_timeout_ms :: HttpRequest, Int -> HttpRequest
2318 fields.insert("with_timeout_ms".into(), Ty::function(
2319 vec![req_t(), Ty::int()],
2320 EffectSet::empty(),
2321 req_t(),
2322 ));
2323 // -- pure decoders --
2324 // json_body[T] :: HttpResponse -> Result[T, HttpError]
2325 // Polymorphic on the parsed shape, matching `json.parse`.
2326 fields.insert("json_body".into(), Ty::function(
2327 vec![resp_t()],
2328 EffectSet::empty(),
2329 result_he(Ty::Var(0)),
2330 ));
2331 // text_body :: HttpResponse -> Result[Str, HttpError]
2332 fields.insert("text_body".into(), Ty::function(
2333 vec![resp_t()],
2334 EffectSet::empty(),
2335 result_he(Ty::str()),
2336 ));
2337 Some(Ty::Record(fields))
2338 }
2339 "yaml" => {
2340 // YAML config parser. Same shape as `std.toml`: parse
2341 // is polymorphic, output Value layout matches std.json
2342 // (Str/Int/Float/Bool/List/Record). Anchors and tags
2343 // are flattened by serde_yaml's deserializer.
2344 let mut fields = IndexMap::new();
2345 fields.insert("parse".into(), Ty::function(
2346 vec![Ty::str()], EffectSet::empty(),
2347 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
2348 ));
2349 // Tactical fix for #168 — caller-supplied required-field
2350 // list. See std.json's parse_strict for context.
2351 fields.insert("parse_strict".into(), Ty::function(
2352 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
2353 EffectSet::empty(),
2354 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
2355 ));
2356 fields.insert("stringify".into(), Ty::function(
2357 vec![Ty::Var(0)], EffectSet::empty(),
2358 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2359 ));
2360 Some(Ty::Record(fields))
2361 }
2362 "dotenv" => {
2363 // .env-style files. parse :: Str -> Result[Map[Str,Str], Str].
2364 // Returns a map (not a polymorphic record) because
2365 // dotenv files don't carry shape — every value is a
2366 // string and keys aren't statically known.
2367 let mut fields = IndexMap::new();
2368 fields.insert("parse".into(), Ty::function(
2369 vec![Ty::str()], EffectSet::empty(),
2370 Ty::Con("Result".into(), vec![
2371 Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
2372 Ty::str(),
2373 ]),
2374 ));
2375 Some(Ty::Record(fields))
2376 }
2377 "csv" => {
2378 // CSV rows-as-lists. parse :: Str -> Result[List[List[Str]], Str].
2379 // Header awareness is left to the caller — row 0 is
2380 // whatever the file has. A `parse_with_headers` that
2381 // returns List[Map[Str,Str]] is a natural follow-up.
2382 let row_ty = Ty::List(Box::new(Ty::str()));
2383 let rows_ty = Ty::List(Box::new(row_ty.clone()));
2384 let mut fields = IndexMap::new();
2385 fields.insert("parse".into(), Ty::function(
2386 vec![Ty::str()], EffectSet::empty(),
2387 Ty::Con("Result".into(), vec![rows_ty.clone(), Ty::str()]),
2388 ));
2389 fields.insert("stringify".into(), Ty::function(
2390 vec![rows_ty], EffectSet::empty(),
2391 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2392 ));
2393 Some(Ty::Record(fields))
2394 }
2395 "test" => {
2396 // Tiny assertion library (#proposed-stdlib). Each helper
2397 // returns Result[Unit, Str] so a test is itself a fn
2398 // returning Result. Callers compose suites in user code
2399 // (a List of (name, () -> Result[Unit, Str]) pairs +
2400 // list.fold to accumulate verdicts). Property generators
2401 // and a Rust-side Suite type are deferred to v2.
2402 let mut fields = IndexMap::new();
2403 // assert_eq[a, b] :: T -> T -> Result[Unit, Str]
2404 // (T constrained equal by unification on the two args)
2405 let unit_result = || Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]);
2406 fields.insert("assert_eq".into(), Ty::function(
2407 vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
2408 ));
2409 fields.insert("assert_ne".into(), Ty::function(
2410 vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
2411 ));
2412 fields.insert("assert_true".into(), Ty::function(
2413 vec![Ty::bool()], EffectSet::empty(), unit_result(),
2414 ));
2415 fields.insert("assert_false".into(), Ty::function(
2416 vec![Ty::bool()], EffectSet::empty(), unit_result(),
2417 ));
2418 Some(Ty::Record(fields))
2419 }
2420 "toml" => {
2421 // TOML config parser. Mirrors `std.json`'s shape: parse
2422 // is polymorphic so callers annotate the expected
2423 // record / list / scalar shape and the type checker
2424 // unifies. The parsed TOML maps to the same Lex Value
2425 // shape as JSON does:
2426 //
2427 // TOML String → Value::Str
2428 // TOML Integer → Value::Int
2429 // TOML Float → Value::Float
2430 // TOML Boolean → Value::Bool
2431 // TOML Array → Value::List
2432 // TOML Table → Value::Record
2433 // TOML Datetime → Value::Str (RFC 3339, lossless)
2434 //
2435 // The Datetime → Str fallback is the one info-losing
2436 // step; callers who want a real `Instant` can pipe the
2437 // string through `datetime.parse_iso`.
2438 let mut fields = IndexMap::new();
2439 // parse :: Str -> Result[T, Str]
2440 fields.insert("parse".into(), Ty::function(
2441 vec![Ty::str()], EffectSet::empty(),
2442 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
2443 ));
2444 // parse_strict :: (Str, List[Str]) -> Result[T, Str]
2445 // Tactical fix for #168 — caller passes the field
2446 // names T requires; runtime returns Err if any are
2447 // missing from the parsed table instead of letting
2448 // field access panic later.
2449 fields.insert("parse_strict".into(), Ty::function(
2450 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
2451 EffectSet::empty(),
2452 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
2453 ));
2454 // stringify :: T -> Result[Str, Str]
2455 // Returns Result (not Str) because not every Lex Value
2456 // has a TOML representation — top-level scalars,
2457 // closures, mixed-key maps etc. surface as Err rather
2458 // than panic.
2459 fields.insert("stringify".into(), Ty::function(
2460 vec![Ty::Var(0)], EffectSet::empty(),
2461 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2462 ));
2463 Some(Ty::Record(fields))
2464 }
2465 // `std.agent` (#184) — runtime primitives whose effects
2466 // separate (a) which LLM surface (`llm_local` vs
2467 // `llm_cloud`), (b) which peer protocol (`a2a`), and
2468 // (c) which tool boundary (`mcp`). The wire formats land
2469 // in downstream crates (`soft-agent`, `soft-a2a`) and
2470 // in #185 for MCP; what's typed here is the boundary
2471 // alone — agent code can be type-checked as
2472 // `[llm_local, a2a]` and will fail if it tries to reach
2473 // `[llm_cloud]` even before the wire layer is finished.
2474 "agent" => {
2475 let mut fields = IndexMap::new();
2476 // local_complete :: Str -> [llm_local] Result[Str, Str]
2477 fields.insert("local_complete".into(), Ty::function(
2478 vec![Ty::str()],
2479 EffectSet::singleton("llm_local"),
2480 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2481 ));
2482 // cloud_complete :: Str -> [llm_cloud] Result[Str, Str]
2483 fields.insert("cloud_complete".into(), Ty::function(
2484 vec![Ty::str()],
2485 EffectSet::singleton("llm_cloud"),
2486 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2487 ));
2488 // send_a2a :: (Str, Str) -> [a2a] Result[Str, Str]
2489 // peer payload reply
2490 fields.insert("send_a2a".into(), Ty::function(
2491 vec![Ty::str(), Ty::str()],
2492 EffectSet::singleton("a2a"),
2493 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2494 ));
2495 // call_mcp :: (Str, Str, Str) -> [mcp] Result[Str, Str]
2496 // server tool args_json result_json
2497 fields.insert("call_mcp".into(), Ty::function(
2498 vec![Ty::str(), Ty::str(), Ty::str()],
2499 EffectSet::singleton("mcp"),
2500 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
2501 ));
2502 // cloud_stream :: Str -> [llm_cloud] Result[Stream[Str], Str]
2503 // (#305 slice 3). Streaming counterpart to cloud_complete.
2504 // The result is `Result[Stream[Str], Str]` rather than a
2505 // bare Stream so transport errors surface synchronously
2506 // at handshake time; per-chunk errors collapse the
2507 // stream to early termination.
2508 fields.insert("cloud_stream".into(), Ty::function(
2509 vec![Ty::str()],
2510 EffectSet::singleton("llm_cloud"),
2511 Ty::Con("Result".into(), vec![
2512 Ty::Con("Stream".into(), vec![Ty::str()]),
2513 Ty::str(),
2514 ]),
2515 ));
2516 Some(Ty::Record(fields))
2517 }
2518 "stream" => {
2519 // #305 slice 3: opaque consumer-side operations on
2520 // `Stream[T]`. Producers live elsewhere (`agent.cloud_stream`
2521 // for now); future producers (`http.get_stream`, etc.)
2522 // will register the same Stream[T] surface.
2523 let mut fields = IndexMap::new();
2524 // next :: Stream[T] -> [stream] Option[T]
2525 // One pull. `None` signals end-of-stream (consumed by
2526 // the producer's lazy generator).
2527 fields.insert("next".into(), Ty::function(
2528 vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
2529 EffectSet::singleton("stream"),
2530 Ty::Con("Option".into(), vec![Ty::Var(0)]),
2531 ));
2532 // collect :: Stream[T] -> [stream] List[T]
2533 // Drain to a list. Eager; blocks until the producer
2534 // signals end-of-stream.
2535 fields.insert("collect".into(), Ty::function(
2536 vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
2537 EffectSet::singleton("stream"),
2538 Ty::List(Box::new(Ty::Var(0))),
2539 ));
2540 Some(Ty::Record(fields))
2541 }
2542 _ => None,
2543 }
2544}
2545
2546/// Resolve `import "std.foo" as alias` to a module name (e.g. "io").
2547pub fn module_for_import(reference: &str) -> Option<&'static str> {
2548 let suffix = reference.strip_prefix("std.")?;
2549 Some(match suffix {
2550 "io" => "io",
2551 "str" => "str",
2552 "int" => "int",
2553 "float" => "float",
2554 "list" => "list",
2555 "result" => "result",
2556 "option" => "option",
2557 "json" => "json",
2558 "flow" => "flow",
2559 "tuple" => "tuple",
2560 "time" => "time",
2561 "rand" => "rand",
2562 "random" => "random",
2563 "env" => "env",
2564 "bytes" => "bytes",
2565 "net" => "net",
2566 "chat" => "chat",
2567 "math" => "math",
2568 "map" => "map",
2569 "set" => "set",
2570 "iter" => "iter",
2571 "proc" => "proc",
2572 "crypto" => "crypto",
2573 "regex" => "regex",
2574 "parser" => "parser",
2575 "deque" => "deque",
2576 "kv" => "kv",
2577 "sql" => "sql",
2578 "fs" => "fs",
2579 "process" => "process",
2580 "datetime" => "datetime",
2581 "duration" => "duration",
2582 "log" => "log",
2583 "http" => "http",
2584 "toml" => "toml",
2585 "yaml" => "yaml",
2586 "dotenv" => "dotenv",
2587 "csv" => "csv",
2588 "test" => "test",
2589 "agent" => "agent",
2590 "cli" => "cli",
2591 "stream" => "stream",
2592 "conc" => "conc",
2593 "arrow" => "arrow",
2594 "df" => "df",
2595 _ => return None,
2596 })
2597}