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