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