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