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