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