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 // sleep_ms :: Int -> [time] Unit (#226).
324 // Used internally by flow.retry_with_backoff for
325 // exponential-backoff delays; also available to user
326 // code under `--allow-effects time`.
327 fields.insert("sleep_ms".into(), Ty::function(
328 vec![Ty::int()],
329 EffectSet::singleton("time"),
330 Ty::Unit,
331 ));
332 Some(Ty::Record(fields))
333 }
334 "rand" => {
335 // rand.int_in(lo, hi) -> [rand] Int — currently a deterministic
336 // stub (midpoint) per spec §13; replaced when randomness lands.
337 let mut fields = IndexMap::new();
338 fields.insert("int_in".into(), Ty::function(
339 vec![Ty::int(), Ty::int()],
340 EffectSet::singleton("rand"),
341 Ty::int(),
342 ));
343 Some(Ty::Record(fields))
344 }
345 "random" => {
346 // #219: pure, seeded RNG. The caller threads the `Rng`
347 // value through computations explicitly — there is no
348 // global state and no effect tag, because the seed is
349 // visible in the program's value flow and replay is
350 // therefore deterministic by construction.
351 //
352 // Backed at runtime by SplitMix64 (deterministic across
353 // platforms, single-u64 state). The proposal mentioned
354 // `rand_chacha` for cryptographic-strength bias, but the
355 // acceptance criterion is just "byte-identical sequence
356 // across platforms," and SplitMix64 satisfies that with
357 // a state shape that fits in `Value::Int` cleanly.
358 let rng_t = || Ty::Con("Rng".into(), vec![]);
359 let mut fields = IndexMap::new();
360 // seed :: Int -> Rng
361 fields.insert("seed".into(), Ty::function(
362 vec![Ty::int()], EffectSet::empty(), rng_t()));
363 // int :: Rng, Int, Int -> (Int, Rng)
364 // Uniform in [lo, hi] inclusive at both ends. Returns
365 // the drawn value and the advanced Rng.
366 fields.insert("int".into(), Ty::function(
367 vec![rng_t(), Ty::int(), Ty::int()],
368 EffectSet::empty(),
369 Ty::Tuple(vec![Ty::int(), rng_t()])));
370 // float :: Rng -> (Float, Rng)
371 // Uniform in [0.0, 1.0).
372 fields.insert("float".into(), Ty::function(
373 vec![rng_t()], EffectSet::empty(),
374 Ty::Tuple(vec![Ty::float(), rng_t()])));
375 // choose :: Rng, List[T] -> Option[(T, Rng)]
376 // Returns None if the list is empty.
377 fields.insert("choose".into(), Ty::function(
378 vec![rng_t(), Ty::List(Box::new(Ty::Var(0)))],
379 EffectSet::empty(),
380 Ty::Con("Option".into(), vec![
381 Ty::Tuple(vec![Ty::Var(0), rng_t()]),
382 ]),
383 ));
384 Some(Ty::Record(fields))
385 }
386 "env" => {
387 // #216: env.get(name) -> [env] Option[Str].
388 // Per-var scoping (`[env(NAME)]`) lands with the
389 // per-capability effect parameterization work (#207); the
390 // flat `[env]` is the v1 surface.
391 let mut fields = IndexMap::new();
392 fields.insert("get".into(), Ty::function(
393 vec![Ty::str()],
394 EffectSet::singleton("env"),
395 Ty::Con("Option".into(), vec![Ty::str()]),
396 ));
397 Some(Ty::Record(fields))
398 }
399 "net" => {
400 let mut fields = IndexMap::new();
401 // get :: Str -> [net] Result[Str, Str]
402 fields.insert("get".into(), Ty::function(
403 vec![Ty::str()],
404 EffectSet::singleton("net"),
405 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
406 ));
407 fields.insert("post".into(), Ty::function(
408 vec![Ty::str(), Ty::str()],
409 EffectSet::singleton("net"),
410 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
411 ));
412 // serve :: (Int, Str) -> [net] Unit (blocks; never returns
413 // under normal use). Handler's signature isn't carried in
414 // the type system here — looked up by name at runtime.
415 fields.insert("serve".into(), Ty::function(
416 vec![Ty::int(), Ty::str()],
417 EffectSet::singleton("net"),
418 Ty::Unit,
419 ));
420 // serve_tls :: (Int, Str, Str, Str) -> [net] Unit
421 // port cert key handler
422 // cert and key are filesystem paths to PEM-encoded files.
423 fields.insert("serve_tls".into(), Ty::function(
424 vec![Ty::int(), Ty::str(), Ty::str(), Ty::str()],
425 EffectSet::singleton("net"),
426 Ty::Unit,
427 ));
428 // serve_ws :: (Int, Str) -> [net] Unit
429 // port on_message_handler_name
430 // The handler is looked up by name at runtime.
431 fields.insert("serve_ws".into(), Ty::function(
432 vec![Ty::int(), Ty::str()],
433 EffectSet::singleton("net"),
434 Ty::Unit,
435 ));
436 // serve_ws_fn[Eff] :: (Int, Str, (WsConn, WsMessage) -> [Eff] WsAction)
437 // -> [net, Eff] Unit
438 // Effect-polymorphic WebSocket server that accepts a handler closure.
439 // The second argument is the subprotocol string for the
440 // Sec-WebSocket-Protocol handshake header ("" for none).
441 // open_var(0) propagates the handler's effect row to the call site.
442 fields.insert("serve_ws_fn".into(), Ty::function(
443 vec![
444 Ty::int(),
445 Ty::str(), // subprotocol
446 Ty::function(
447 vec![
448 Ty::Con("WsConn".into(), vec![]),
449 Ty::Con("WsMessage".into(), vec![]),
450 ],
451 EffectSet::open_var(0),
452 Ty::Con("WsAction".into(), vec![]),
453 ),
454 ],
455 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
456 Ty::Unit,
457 ));
458 // serve_fn[Eff] :: (Int, (Request) -> [Eff] Response) -> [net, Eff] Unit
459 // Effect-polymorphic variant of serve that accepts a first-class closure
460 // instead of a handler name. open_var(0) captures the handler's effect row
461 // so callers that invoke e.g. [io] effects inside the closure propagate them
462 // to the serve_fn call site.
463 fields.insert("serve_fn".into(), Ty::function(
464 vec![
465 Ty::int(),
466 Ty::function(
467 vec![Ty::Con("Request".into(), vec![])],
468 EffectSet::open_var(0),
469 Ty::Con("Response".into(), vec![]),
470 ),
471 ],
472 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
473 Ty::Unit,
474 ));
475 Some(Ty::Record(fields))
476 }
477 "chat" => {
478 let mut fields = IndexMap::new();
479 fields.insert("broadcast".into(), Ty::function(
480 vec![Ty::str(), Ty::str()],
481 EffectSet::singleton("chat"),
482 Ty::Unit,
483 ));
484 fields.insert("send".into(), Ty::function(
485 vec![Ty::int(), Ty::str()],
486 EffectSet::singleton("chat"),
487 Ty::bool(),
488 ));
489 Some(Ty::Record(fields))
490 }
491 "proc" => {
492 // Subprocess dispatch. Effect: [proc]. Returns a Result
493 // with a record on success carrying stdout / stderr /
494 // exit_code. The runtime allow-lists which binary
495 // basenames are spawnable — `cmd` is the program to
496 // run, `args` is the literal argv (no shell parsing).
497 //
498 // Read SECURITY.md before adding [proc] to a policy:
499 // it weakens the "we know what this fn does" claim.
500 let mut fields = IndexMap::new();
501 let mut result_rec = IndexMap::new();
502 result_rec.insert("stdout".into(), Ty::str());
503 result_rec.insert("stderr".into(), Ty::str());
504 result_rec.insert("exit_code".into(), Ty::int());
505 // spawn :: Str, List[Str] -> [proc] Result[{stdout, stderr, exit_code}, Str]
506 fields.insert("spawn".into(), Ty::function(
507 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
508 EffectSet::singleton("proc"),
509 Ty::Con("Result".into(), vec![
510 Ty::Record(result_rec),
511 Ty::str(),
512 ]),
513 ));
514 Some(Ty::Record(fields))
515 }
516 "json" => {
517 let mut fields = IndexMap::new();
518 // stringify :: T -> Str (polymorphic on input)
519 fields.insert("stringify".into(), Ty::function(
520 vec![Ty::Var(0)], EffectSet::empty(), Ty::str(),
521 ));
522 // parse :: Str -> Result[T, Str]
523 fields.insert("parse".into(), Ty::function(
524 vec![Ty::str()], EffectSet::empty(),
525 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
526 ));
527 // parse_strict :: (Str, List[Str]) -> Result[T, Str]
528 // Tactical fix for #168 — caller passes the field names
529 // T requires; runtime returns Err if any are missing
530 // from the parsed object instead of letting field
531 // access panic later.
532 fields.insert("parse_strict".into(), Ty::function(
533 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
534 EffectSet::empty(),
535 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
536 ));
537 Some(Ty::Record(fields))
538 }
539 "result" => {
540 let mut fields = IndexMap::new();
541 // result.map :: Result[T, E], (T) -> [E2] U -> [E2] Result[U, E]
542 // Effect-polymorphic on the closure: result.map et al.
543 // propagate the closure's effects to the surrounding call.
544 fields.insert("map".into(), Ty::function(
545 vec![
546 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
547 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), Ty::Var(2)),
548 ],
549 EffectSet::open_var(3),
550 Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
551 ));
552 fields.insert("and_then".into(), Ty::function(
553 vec![
554 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
555 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(4),
556 Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)])),
557 ],
558 EffectSet::open_var(4),
559 Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
560 ));
561 fields.insert("map_err".into(), Ty::function(
562 vec![
563 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
564 Ty::function(vec![Ty::Var(1)], EffectSet::open_var(5), Ty::Var(2)),
565 ],
566 EffectSet::open_var(5),
567 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
568 ));
569 // result.or_else :: Result[T, E1], (E1) -> [E] Result[T, E2]
570 // -> [E] Result[T, E2]
571 // Recovery combinator: closure runs only on Err and returns
572 // the next Result (which itself may swap the error type).
573 fields.insert("or_else".into(), Ty::function(
574 vec![
575 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
576 Ty::function(vec![Ty::Var(1)], EffectSet::open_var(6),
577 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)])),
578 ],
579 EffectSet::open_var(6),
580 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
581 ));
582 Some(Ty::Record(fields))
583 }
584 "option" => {
585 let mut fields = IndexMap::new();
586 // option.map :: Option[T], (T) -> [E] U -> [E] Option[U]
587 fields.insert("map".into(), Ty::function(
588 vec![
589 Ty::Con("Option".into(), vec![Ty::Var(0)]),
590 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
591 ],
592 EffectSet::open_var(2),
593 Ty::Con("Option".into(), vec![Ty::Var(1)]),
594 ));
595 // option.and_then :: Option[T], (T) -> [E] Option[U] -> [E] Option[U]
596 // The compiler entry has been wired since the result/option
597 // variant_map work landed; this signature was missed,
598 // making the call fail to type-check until now.
599 fields.insert("and_then".into(), Ty::function(
600 vec![
601 Ty::Con("Option".into(), vec![Ty::Var(0)]),
602 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
603 Ty::Con("Option".into(), vec![Ty::Var(1)])),
604 ],
605 EffectSet::open_var(3),
606 Ty::Con("Option".into(), vec![Ty::Var(1)]),
607 ));
608 fields.insert("unwrap_or".into(), Ty::function(
609 vec![Ty::Con("Option".into(), vec![Ty::Var(0)]), Ty::Var(0)],
610 EffectSet::empty(),
611 Ty::Var(0),
612 ));
613 // option.unwrap_or_else :: Option[T], () -> [E] T -> [E] T
614 // Lazy variant of unwrap_or: the default is computed by a closure
615 // only when the value is None (effect-polymorphic on the closure).
616 fields.insert("unwrap_or_else".into(), Ty::function(
617 vec![
618 Ty::Con("Option".into(), vec![Ty::Var(0)]),
619 Ty::function(vec![], EffectSet::open_var(5), Ty::Var(0)),
620 ],
621 EffectSet::open_var(5),
622 Ty::Var(0),
623 ));
624 // option.or_else :: Option[T], () -> [E] Option[T] -> [E] Option[T]
625 // The closure takes no arguments because None has no payload to pass.
626 fields.insert("or_else".into(), Ty::function(
627 vec![
628 Ty::Con("Option".into(), vec![Ty::Var(0)]),
629 Ty::function(vec![], EffectSet::open_var(4),
630 Ty::Con("Option".into(), vec![Ty::Var(0)])),
631 ],
632 EffectSet::open_var(4),
633 Ty::Con("Option".into(), vec![Ty::Var(0)]),
634 ));
635 Some(Ty::Record(fields))
636 }
637 "tuple" => {
638 // Tuple accessors per §11.1. Polymorphic in the tuple's
639 // element types; we use the same row-variable shape used
640 // by list helpers. Tuples are heterogeneous, so each
641 // accessor is statically typed via independent type
642 // variables for each position.
643 let mut fields = IndexMap::new();
644 // fst :: (T0, T1) -> T0
645 fields.insert("fst".into(), Ty::function(
646 vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
647 EffectSet::empty(),
648 Ty::Var(0),
649 ));
650 // snd :: (T0, T1) -> T1
651 fields.insert("snd".into(), Ty::function(
652 vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
653 EffectSet::empty(),
654 Ty::Var(1),
655 ));
656 // third :: (T0, T1, T2) -> T2
657 fields.insert("third".into(), Ty::function(
658 vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1), Ty::Var(2)])],
659 EffectSet::empty(),
660 Ty::Var(2),
661 ));
662 // len :: (T0, T1) -> Int (covers any pair shape; Int back)
663 fields.insert("len".into(), Ty::function(
664 vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
665 EffectSet::empty(),
666 Ty::int(),
667 ));
668 Some(Ty::Record(fields))
669 }
670 "map" => {
671 // Persistent map. Keys are `Str` or `Int` only — Lex's
672 // type system tracks them polymorphically as Var(0)
673 // ("K") and lets the runtime check the key shape; both
674 // cases fit into `MapKey`.
675 //
676 // Type variables: 0 = K, 1 = V.
677 let mt = || Ty::Con("Map".into(), vec![Ty::Var(0), Ty::Var(1)]);
678 let pair = || Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]);
679 let mut fields = IndexMap::new();
680 // new :: () -> Map[K, V]
681 fields.insert("new".into(), Ty::function(
682 vec![], EffectSet::empty(), mt()));
683 // size :: Map[K, V] -> Int
684 fields.insert("size".into(), Ty::function(
685 vec![mt()], EffectSet::empty(), Ty::int()));
686 // has :: Map[K, V], K -> Bool
687 fields.insert("has".into(), Ty::function(
688 vec![mt(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
689 // get :: Map[K, V], K -> Option[V]
690 fields.insert("get".into(), Ty::function(
691 vec![mt(), Ty::Var(0)], EffectSet::empty(),
692 Ty::Con("Option".into(), vec![Ty::Var(1)])));
693 // set :: Map[K, V], K, V -> Map[K, V]
694 fields.insert("set".into(), Ty::function(
695 vec![mt(), Ty::Var(0), Ty::Var(1)],
696 EffectSet::empty(), mt()));
697 // delete :: Map[K, V], K -> Map[K, V]
698 fields.insert("delete".into(), Ty::function(
699 vec![mt(), Ty::Var(0)], EffectSet::empty(), mt()));
700 // keys :: Map[K, V] -> List[K]
701 fields.insert("keys".into(), Ty::function(
702 vec![mt()], EffectSet::empty(),
703 Ty::List(Box::new(Ty::Var(0)))));
704 // values :: Map[K, V] -> List[V]
705 fields.insert("values".into(), Ty::function(
706 vec![mt()], EffectSet::empty(),
707 Ty::List(Box::new(Ty::Var(1)))));
708 // entries :: Map[K, V] -> List[(K, V)]
709 fields.insert("entries".into(), Ty::function(
710 vec![mt()], EffectSet::empty(),
711 Ty::List(Box::new(pair()))));
712 // from_list :: List[(K, V)] -> Map[K, V]
713 fields.insert("from_list".into(), Ty::function(
714 vec![Ty::List(Box::new(pair()))],
715 EffectSet::empty(), mt()));
716 // merge :: Map[K, V], Map[K, V] -> Map[K, V] (b overrides a)
717 fields.insert("merge".into(), Ty::function(
718 vec![mt(), mt()], EffectSet::empty(), mt()));
719 // is_empty :: Map[K, V] -> Bool
720 fields.insert("is_empty".into(), Ty::function(
721 vec![mt()], EffectSet::empty(), Ty::bool()));
722 // fold :: Map[K, V], A, (A, K, V) -> [E] A -> [E] A
723 // Iteration order matches `map.entries` (BTreeMap-sorted by
724 // key). Effect-polymorphic on the combiner like `list.fold`.
725 // Type variable 2 = A (accumulator), effect row 3.
726 fields.insert("fold".into(), Ty::function(
727 vec![
728 mt(),
729 Ty::Var(2),
730 Ty::function(
731 vec![Ty::Var(2), Ty::Var(0), Ty::Var(1)],
732 EffectSet::open_var(3),
733 Ty::Var(2),
734 ),
735 ],
736 EffectSet::open_var(3),
737 Ty::Var(2),
738 ));
739 Some(Ty::Record(fields))
740 }
741 "set" => {
742 // Persistent set with the same key-type discipline as map.
743 // Type variable: 0 = T (the element type, also the key type).
744 let st = || Ty::Con("Set".into(), vec![Ty::Var(0)]);
745 let mut fields = IndexMap::new();
746 // new :: () -> Set[T]
747 fields.insert("new".into(), Ty::function(
748 vec![], EffectSet::empty(), st()));
749 // size :: Set[T] -> Int
750 fields.insert("size".into(), Ty::function(
751 vec![st()], EffectSet::empty(), Ty::int()));
752 // has :: Set[T], T -> Bool
753 fields.insert("has".into(), Ty::function(
754 vec![st(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
755 // add :: Set[T], T -> Set[T]
756 fields.insert("add".into(), Ty::function(
757 vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
758 // delete :: Set[T], T -> Set[T]
759 fields.insert("delete".into(), Ty::function(
760 vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
761 // to_list :: Set[T] -> List[T]
762 fields.insert("to_list".into(), Ty::function(
763 vec![st()], EffectSet::empty(),
764 Ty::List(Box::new(Ty::Var(0)))));
765 // from_list :: List[T] -> Set[T]
766 fields.insert("from_list".into(), Ty::function(
767 vec![Ty::List(Box::new(Ty::Var(0)))],
768 EffectSet::empty(), st()));
769 // union :: Set[T], Set[T] -> Set[T]
770 fields.insert("union".into(), Ty::function(
771 vec![st(), st()], EffectSet::empty(), st()));
772 // intersect :: Set[T], Set[T] -> Set[T]
773 fields.insert("intersect".into(), Ty::function(
774 vec![st(), st()], EffectSet::empty(), st()));
775 // diff :: Set[T], Set[T] -> Set[T]
776 fields.insert("diff".into(), Ty::function(
777 vec![st(), st()], EffectSet::empty(), st()));
778 // is_empty :: Set[T] -> Bool
779 fields.insert("is_empty".into(), Ty::function(
780 vec![st()], EffectSet::empty(), Ty::bool()));
781 // is_subset :: Set[T], Set[T] -> Bool (a is subset of b)
782 fields.insert("is_subset".into(), Ty::function(
783 vec![st(), st()], EffectSet::empty(), Ty::bool()));
784 Some(Ty::Record(fields))
785 }
786 "iter" => {
787 // Lazy positional iterator (#364). Internally a (List[T], Int) tuple;
788 // all operations are compiler-inlined — no effect annotation required.
789 // Type var: 0 = T (element), 1 = U (mapped element), 2 = A (fold acc).
790 let it = |n: u32| Ty::Con("Iter".into(), vec![Ty::Var(n)]);
791 let mut fields = IndexMap::new();
792 // from_list :: List[T] -> Iter[T]
793 fields.insert("from_list".into(), Ty::function(
794 vec![Ty::List(Box::new(Ty::Var(0)))],
795 EffectSet::empty(), it(0)));
796 // next :: Iter[T] -> Option[(T, Iter[T])]
797 fields.insert("next".into(), Ty::function(
798 vec![it(0)],
799 EffectSet::empty(),
800 Ty::Con("Option".into(), vec![
801 Ty::Tuple(vec![Ty::Var(0), it(0)])
802 ])));
803 // is_empty :: Iter[T] -> Bool
804 fields.insert("is_empty".into(), Ty::function(
805 vec![it(0)], EffectSet::empty(), Ty::bool()));
806 // count :: Iter[T] -> Int (remaining elements)
807 fields.insert("count".into(), Ty::function(
808 vec![it(0)], EffectSet::empty(), Ty::int()));
809 // take :: Iter[T], Int -> Iter[T]
810 fields.insert("take".into(), Ty::function(
811 vec![it(0), Ty::int()], EffectSet::empty(), it(0)));
812 // skip :: Iter[T], Int -> Iter[T]
813 fields.insert("skip".into(), Ty::function(
814 vec![it(0), Ty::int()], EffectSet::empty(), it(0)));
815 // to_list :: Iter[T] -> List[T]
816 fields.insert("to_list".into(), Ty::function(
817 vec![it(0)], EffectSet::empty(),
818 Ty::List(Box::new(Ty::Var(0)))));
819 // map :: [E] Iter[T], (T) -> [E] U -> [E] Iter[U]
820 fields.insert("map".into(), Ty::function(
821 vec![
822 it(0),
823 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
824 ],
825 EffectSet::open_var(2), it(1)));
826 // filter :: [E] Iter[T], (T) -> [E] Bool -> [E] Iter[T]
827 fields.insert("filter".into(), Ty::function(
828 vec![
829 it(0),
830 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(1), Ty::bool()),
831 ],
832 EffectSet::open_var(1), it(0)));
833 // fold :: [E] Iter[T], A, (A, T) -> [E] A -> [E] A
834 fields.insert("fold".into(), Ty::function(
835 vec![
836 it(0),
837 Ty::Var(1),
838 Ty::function(vec![Ty::Var(1), Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
839 ],
840 EffectSet::open_var(2), Ty::Var(1)));
841 Some(Ty::Record(fields))
842 }
843 "flow" => {
844 // Orchestration primitives (spec §11.2). Each takes one or
845 // more closures and returns a closure with a derived shape.
846 let mut fields = IndexMap::new();
847 // sequential[T, U, V](f: (T) -> U, g: (U) -> V) -> (T) -> V
848 fields.insert("sequential".into(), Ty::function(
849 vec![
850 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
851 Ty::function(vec![Ty::Var(1)], EffectSet::empty(), Ty::Var(2)),
852 ],
853 EffectSet::empty(),
854 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(2)),
855 ));
856 // branch[T, U](cond: (T) -> Bool, t: (T) -> U, f: (T) -> U) -> (T) -> U
857 fields.insert("branch".into(), Ty::function(
858 vec![
859 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::bool()),
860 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
861 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
862 ],
863 EffectSet::empty(),
864 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
865 ));
866 // retry[T, U, E, Eff](
867 // f: (T) -> [Eff] Result[U, E], n: Int
868 // ) -> (T) -> [Eff] Result[U, E]
869 // open_var(3) is the effect row carried by `f`; the
870 // combinator itself is pure, so the outer EffectSet is
871 // empty. The returned closure propagates Eff unchanged.
872 let result_ty = Ty::Con("Result".into(), vec![Ty::Var(1), Ty::Var(2)]);
873 fields.insert("retry".into(), Ty::function(
874 vec![
875 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
876 Ty::int(),
877 ],
878 EffectSet::empty(),
879 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
880 ));
881 // retry_with_backoff[T, U, E, Eff](
882 // f: (T) -> [Eff] Result[U, E], attempts: Int, base_ms: Int,
883 // ) -> (T) -> [Eff, time] Result[U, E]
884 // Same retry shape as `flow.retry` plus an exponential
885 // backoff between attempts. The result function carries
886 // `[time]` (from `time.sleep_ms`) unioned with the inner
887 // closure's effect row Eff, so e.g. a `[net]` closure
888 // produces a `[net, time]` result function. (#226)
889 fields.insert("retry_with_backoff".into(), Ty::function(
890 vec![
891 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
892 Ty::int(),
893 Ty::int(),
894 ],
895 EffectSet::empty(),
896 Ty::function(vec![Ty::Var(0)],
897 EffectSet::open_var(3).union(&EffectSet::singleton("time")), result_ty),
898 ));
899 // parallel[A, B](fa: () -> A, fb: () -> B) -> () -> (A, B)
900 // Sequential implementation today; spec §11.2 reserves the
901 // option of a true-threaded scheduler. parallel_record is
902 // listed in the spec but not yet implemented — it needs row
903 // polymorphism over the input record's fields plus a
904 // record-iteration trampoline; tracked as follow-up.
905 fields.insert("parallel".into(), Ty::function(
906 vec![
907 Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
908 Ty::function(vec![], EffectSet::empty(), Ty::Var(1)),
909 ],
910 EffectSet::empty(),
911 Ty::function(vec![], EffectSet::empty(),
912 Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])),
913 ));
914 // parallel_list[T](actions: List[() -> T]) -> List[T]
915 // Variadic counterpart to `parallel`. Runs each action and
916 // collects results in input order. Sequential under the
917 // hood (same caveat as `parallel`); spec §11.2 reserves
918 // true threading for a future scheduler. Unlike `parallel`,
919 // this returns the result list directly rather than a
920 // closure, since the input arity is dynamic.
921 fields.insert("parallel_list".into(), Ty::function(
922 vec![
923 Ty::List(Box::new(
924 Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
925 )),
926 ],
927 EffectSet::empty(),
928 Ty::List(Box::new(Ty::Var(0))),
929 ));
930 Some(Ty::Record(fields))
931 }
932 "crypto" => {
933 let mut fields = IndexMap::new();
934 // Hashes: Bytes -> Bytes (digest as raw bytes)
935 for name in &["sha256", "sha512", "md5"] {
936 fields.insert((*name).into(), Ty::function(
937 vec![Ty::bytes()],
938 EffectSet::empty(),
939 Ty::bytes(),
940 ));
941 }
942 // HMAC: (key :: Bytes, data :: Bytes) -> Bytes
943 for name in &["hmac_sha256", "hmac_sha512"] {
944 fields.insert((*name).into(), Ty::function(
945 vec![Ty::bytes(), Ty::bytes()],
946 EffectSet::empty(),
947 Ty::bytes(),
948 ));
949 }
950 // base64 / hex
951 fields.insert("base64_encode".into(), Ty::function(
952 vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
953 fields.insert("base64_decode".into(), Ty::function(
954 vec![Ty::str()], EffectSet::empty(),
955 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
956 fields.insert("hex_encode".into(), Ty::function(
957 vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
958 fields.insert("hex_decode".into(), Ty::function(
959 vec![Ty::str()], EffectSet::empty(),
960 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
961 // constant-time equality (for HMAC verification etc.)
962 fields.insert("constant_time_eq".into(), Ty::function(
963 vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool()));
964 // Cryptographically-secure random bytes — OS RNG, not the
965 // deterministic `rand.int_in` stub. The new `[random]`
966 // effect is fine-grained on purpose so reviewers can find
967 // every token-generating call via `lex audit --effect
968 // random`.
969 fields.insert("random".into(), Ty::function(
970 vec![Ty::int()],
971 EffectSet::singleton("random"),
972 Ty::bytes(),
973 ));
974 Some(Ty::Record(fields))
975 }
976 "deque" => {
977 // Persistent double-ended queue. Push/pop O(1) on both
978 // ends; iteration order is front-to-back.
979 // Type variable: 0 = T.
980 let dt = || Ty::Con("Deque".into(), vec![Ty::Var(0)]);
981 let pair = || Ty::Tuple(vec![Ty::Var(0), dt()]);
982 let mut fields = IndexMap::new();
983 // new :: () -> Deque[T]
984 fields.insert("new".into(), Ty::function(
985 vec![], EffectSet::empty(), dt()));
986 // size :: Deque[T] -> Int
987 fields.insert("size".into(), Ty::function(
988 vec![dt()], EffectSet::empty(), Ty::int()));
989 // is_empty :: Deque[T] -> Bool
990 fields.insert("is_empty".into(), Ty::function(
991 vec![dt()], EffectSet::empty(), Ty::bool()));
992 // push_back / push_front :: Deque[T], T -> Deque[T]
993 for n in &["push_back", "push_front"] {
994 fields.insert((*n).into(), Ty::function(
995 vec![dt(), Ty::Var(0)], EffectSet::empty(), dt()));
996 }
997 // pop_back / pop_front :: Deque[T] -> Option[(T, Deque[T])]
998 for n in &["pop_back", "pop_front"] {
999 fields.insert((*n).into(), Ty::function(
1000 vec![dt()], EffectSet::empty(),
1001 Ty::Con("Option".into(), vec![pair()])));
1002 }
1003 // peek_back / peek_front :: Deque[T] -> Option[T]
1004 for n in &["peek_back", "peek_front"] {
1005 fields.insert((*n).into(), Ty::function(
1006 vec![dt()], EffectSet::empty(),
1007 Ty::Con("Option".into(), vec![Ty::Var(0)])));
1008 }
1009 // from_list :: List[T] -> Deque[T]
1010 fields.insert("from_list".into(), Ty::function(
1011 vec![Ty::List(Box::new(Ty::Var(0)))],
1012 EffectSet::empty(), dt()));
1013 // to_list :: Deque[T] -> List[T]
1014 fields.insert("to_list".into(), Ty::function(
1015 vec![dt()], EffectSet::empty(),
1016 Ty::List(Box::new(Ty::Var(0)))));
1017 Some(Ty::Record(fields))
1018 }
1019 "log" => {
1020 // Structured logging behind a [log] effect. Emit ops route
1021 // through a runtime-configured sink (stderr by default;
1022 // can be redirected via set_sink). Configuration ops
1023 // mutate the global sink and so are gated [io].
1024 let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1025 let mut fields = IndexMap::new();
1026 for level in &["debug", "info", "warn", "error"] {
1027 fields.insert((*level).into(), Ty::function(
1028 vec![Ty::str()],
1029 EffectSet::singleton("log"),
1030 Ty::Unit,
1031 ));
1032 }
1033 // set_level :: Str -> [io] Result[Nil, Str]
1034 fields.insert("set_level".into(), Ty::function(
1035 vec![Ty::str()],
1036 EffectSet::singleton("io"),
1037 result_str(Ty::Unit)));
1038 // set_format :: Str -> [io] Result[Nil, Str]
1039 fields.insert("set_format".into(), Ty::function(
1040 vec![Ty::str()],
1041 EffectSet::singleton("io"),
1042 result_str(Ty::Unit)));
1043 // set_sink :: Str -> [io, fs_write] Result[Nil, Str]
1044 fields.insert("set_sink".into(), Ty::function(
1045 vec![Ty::str()],
1046 EffectSet {
1047 concrete: [crate::types::EffectKind::bare("io"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
1048 var: None,
1049 },
1050 result_str(Ty::Unit)));
1051 Some(Ty::Record(fields))
1052 }
1053 "datetime" => {
1054 // Instant and Duration are nominal opaque Ints under the
1055 // hood (nanoseconds-since-UTC-epoch and signed nanoseconds
1056 // respectively); the type checker tracks the distinction
1057 // even though both values look like Int at runtime.
1058 //
1059 // Tz is the variant
1060 // Utc | Local | Offset(Int) | Iana(Str)
1061 // registered as a built-in nominal type in
1062 // `TypeEnv::new_with_builtins`. The pre-v1 stringly Tz
1063 // ("UTC"/"Local"/IANA-name/"+05:30") is no longer accepted
1064 // — passing a `Str` to `to_components` is now a type
1065 // error.
1066 let inst = || Ty::Con("Instant".into(), vec![]);
1067 let dur = || Ty::Con("Duration".into(), vec![]);
1068 let tz = || Ty::Con("Tz".into(), vec![]);
1069 let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1070 let dt_t = || {
1071 let mut fs = IndexMap::new();
1072 fs.insert("year".into(), Ty::int());
1073 fs.insert("month".into(), Ty::int());
1074 fs.insert("day".into(), Ty::int());
1075 fs.insert("hour".into(), Ty::int());
1076 fs.insert("minute".into(), Ty::int());
1077 fs.insert("second".into(), Ty::int());
1078 fs.insert("nano".into(), Ty::int());
1079 fs.insert("tz_offset_minutes".into(), Ty::int());
1080 Ty::Record(fs)
1081 };
1082 let mut fields = IndexMap::new();
1083 fields.insert("now".into(), Ty::function(
1084 vec![], EffectSet::singleton("time"), inst()));
1085 fields.insert("parse_iso".into(), Ty::function(
1086 vec![Ty::str()], EffectSet::empty(), result_str(inst())));
1087 fields.insert("format_iso".into(), Ty::function(
1088 vec![inst()], EffectSet::empty(), Ty::str()));
1089 fields.insert("parse".into(), Ty::function(
1090 vec![Ty::str(), Ty::str()], EffectSet::empty(), result_str(inst())));
1091 fields.insert("format".into(), Ty::function(
1092 vec![inst(), Ty::str()], EffectSet::empty(), Ty::str()));
1093 fields.insert("to_components".into(), Ty::function(
1094 vec![inst(), tz()], EffectSet::empty(), result_str(dt_t())));
1095 fields.insert("from_components".into(), Ty::function(
1096 vec![dt_t()], EffectSet::empty(), result_str(inst())));
1097 fields.insert("add".into(), Ty::function(
1098 vec![inst(), dur()], EffectSet::empty(), inst()));
1099 fields.insert("diff".into(), Ty::function(
1100 vec![inst(), inst()], EffectSet::empty(), dur()));
1101 fields.insert("duration_seconds".into(), Ty::function(
1102 vec![Ty::float()], EffectSet::empty(), dur()));
1103 fields.insert("duration_minutes".into(), Ty::function(
1104 vec![Ty::int()], EffectSet::empty(), dur()));
1105 fields.insert("duration_days".into(), Ty::function(
1106 vec![Ty::int()], EffectSet::empty(), dur()));
1107 // #331: comparison ops on Instant.
1108 fields.insert("before".into(), Ty::function(
1109 vec![inst(), inst()], EffectSet::empty(), Ty::bool()));
1110 fields.insert("after".into(), Ty::function(
1111 vec![inst(), inst()], EffectSet::empty(), Ty::bool()));
1112 // compare :: Instant, Instant -> Int (-1 / 0 / +1)
1113 fields.insert("compare".into(), Ty::function(
1114 vec![inst(), inst()], EffectSet::empty(), Ty::int()));
1115 Some(Ty::Record(fields))
1116 }
1117 // #331: duration module — scalar extraction from Duration values.
1118 "duration" => {
1119 let dur = || Ty::Con("Duration".into(), vec![]);
1120 let mut fields = IndexMap::new();
1121 // seconds :: Duration -> Int (truncates toward zero)
1122 fields.insert("seconds".into(), Ty::function(
1123 vec![dur()], EffectSet::empty(), Ty::int()));
1124 Some(Ty::Record(fields))
1125 }
1126 "process" => {
1127 // Streaming subprocess. The opaque `ProcessHandle` type
1128 // is an Int handle into a process-wide registry holding
1129 // the `Child` plus its stdout/stderr `BufReader`s.
1130 let ph = || Ty::Con("ProcessHandle".into(), vec![]);
1131 let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1132 let opts_t = || {
1133 let mut fs = IndexMap::new();
1134 fs.insert("cwd".into(),
1135 Ty::Con("Option".into(), vec![Ty::str()]));
1136 fs.insert("env".into(),
1137 Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
1138 fs.insert("stdin".into(),
1139 Ty::Con("Option".into(), vec![Ty::bytes()]));
1140 Ty::Record(fs)
1141 };
1142 let exit_t = || {
1143 let mut fs = IndexMap::new();
1144 fs.insert("code".into(), Ty::int());
1145 fs.insert("signaled".into(), Ty::bool());
1146 Ty::Record(fs)
1147 };
1148 let output_t = || {
1149 let mut fs = IndexMap::new();
1150 fs.insert("stdout".into(), Ty::str());
1151 fs.insert("stderr".into(), Ty::str());
1152 fs.insert("exit_code".into(), Ty::int());
1153 Ty::Record(fs)
1154 };
1155 let mut fields = IndexMap::new();
1156 // spawn :: Str, List[Str], Opts -> [proc] Result[ProcessHandle, Str]
1157 fields.insert("spawn".into(), Ty::function(
1158 vec![Ty::str(), Ty::List(Box::new(Ty::str())), opts_t()],
1159 EffectSet::singleton("proc"),
1160 result_str(ph())));
1161 // read_stdout_line / read_stderr_line :: ProcessHandle -> [proc] Option[Str]
1162 for n in &["read_stdout_line", "read_stderr_line"] {
1163 fields.insert((*n).into(), Ty::function(
1164 vec![ph()], EffectSet::singleton("proc"),
1165 Ty::Con("Option".into(), vec![Ty::str()])));
1166 }
1167 // wait :: ProcessHandle -> [proc] ProcessExit
1168 fields.insert("wait".into(), Ty::function(
1169 vec![ph()], EffectSet::singleton("proc"), exit_t()));
1170 // kill :: ProcessHandle, Str -> [proc] Result[Nil, Str]
1171 fields.insert("kill".into(), Ty::function(
1172 vec![ph(), Ty::str()],
1173 EffectSet::singleton("proc"),
1174 result_str(Ty::Unit)));
1175 // run :: Str, List[Str] -> [proc] Result[ProcessOutput, Str]
1176 // Blocking convenience that captures stdout/stderr fully
1177 // and returns once the child exits. For programs that
1178 // need streaming, use spawn + read_*_line + wait.
1179 fields.insert("run".into(), Ty::function(
1180 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
1181 EffectSet::singleton("proc"),
1182 result_str(output_t())));
1183 Some(Ty::Record(fields))
1184 }
1185 "fs" => {
1186 // Filesystem walk + mutate. Walk-style ops (exists, walk,
1187 // glob, …) declare [fs_walk] — distinct from [fs_read]
1188 // (which is content reads via io.read), so reviewers can
1189 // separately track directory traversal vs file-content
1190 // exposure. Mutating ops (mkdir_p, remove, copy) declare
1191 // [fs_write]. Path scoping uses --allow-fs-read for walk
1192 // (a directory listing is an information disclosure on
1193 // the same path tree) and --allow-fs-write for mutations.
1194 let stat_t = || {
1195 let mut fs = IndexMap::new();
1196 fs.insert("size".into(), Ty::int());
1197 fs.insert("mtime".into(), Ty::int());
1198 fs.insert("is_dir".into(), Ty::bool());
1199 fs.insert("is_file".into(), Ty::bool());
1200 Ty::Record(fs)
1201 };
1202 let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
1203 let mut fields = IndexMap::new();
1204 // Walk-style queries [fs_walk]
1205 fields.insert("exists".into(), Ty::function(
1206 vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
1207 fields.insert("is_file".into(), Ty::function(
1208 vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
1209 fields.insert("is_dir".into(), Ty::function(
1210 vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
1211 fields.insert("stat".into(), Ty::function(
1212 vec![Ty::str()], EffectSet::singleton("fs_walk"),
1213 result_str(stat_t())));
1214 fields.insert("list_dir".into(), Ty::function(
1215 vec![Ty::str()], EffectSet::singleton("fs_walk"),
1216 result_str(Ty::List(Box::new(Ty::str())))));
1217 fields.insert("walk".into(), Ty::function(
1218 vec![Ty::str()], EffectSet::singleton("fs_walk"),
1219 result_str(Ty::List(Box::new(Ty::str())))));
1220 fields.insert("glob".into(), Ty::function(
1221 vec![Ty::str()], EffectSet::singleton("fs_walk"),
1222 result_str(Ty::List(Box::new(Ty::str())))));
1223 // Mutations [fs_write]
1224 fields.insert("mkdir_p".into(), Ty::function(
1225 vec![Ty::str()], EffectSet::singleton("fs_write"),
1226 result_str(Ty::Unit)));
1227 fields.insert("remove".into(), Ty::function(
1228 vec![Ty::str()], EffectSet::singleton("fs_write"),
1229 result_str(Ty::Unit)));
1230 fields.insert("copy".into(), Ty::function(
1231 vec![Ty::str(), Ty::str()],
1232 EffectSet {
1233 concrete: [crate::types::EffectKind::bare("fs_walk"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
1234 var: None,
1235 },
1236 result_str(Ty::Unit)));
1237 Some(Ty::Record(fields))
1238 }
1239 "kv" => {
1240 // Embedded key-value store. The opaque `Kv` type is
1241 // backed by an Int handle into a process-wide registry.
1242 let kv_t = || Ty::Con("Kv".into(), vec![]);
1243 let mut fields = IndexMap::new();
1244 // open :: Str -> [kv, fs_write] Result[Kv, Str]
1245 fields.insert("open".into(), Ty::function(
1246 vec![Ty::str()],
1247 EffectSet {
1248 concrete: [crate::types::EffectKind::bare("kv"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
1249 var: None,
1250 },
1251 Ty::Con("Result".into(), vec![kv_t(), Ty::str()])));
1252 // close :: Kv -> [kv] Nil
1253 fields.insert("close".into(), Ty::function(
1254 vec![kv_t()],
1255 EffectSet::singleton("kv"),
1256 Ty::Unit));
1257 // get :: Kv, Str -> [kv] Option[Bytes]
1258 fields.insert("get".into(), Ty::function(
1259 vec![kv_t(), Ty::str()],
1260 EffectSet::singleton("kv"),
1261 Ty::Con("Option".into(), vec![Ty::bytes()])));
1262 // put :: Kv, Str, Bytes -> [kv] Result[Nil, Str]
1263 fields.insert("put".into(), Ty::function(
1264 vec![kv_t(), Ty::str(), Ty::bytes()],
1265 EffectSet::singleton("kv"),
1266 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
1267 // delete :: Kv, Str -> [kv] Result[Nil, Str]
1268 fields.insert("delete".into(), Ty::function(
1269 vec![kv_t(), Ty::str()],
1270 EffectSet::singleton("kv"),
1271 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
1272 // contains :: Kv, Str -> [kv] Bool
1273 fields.insert("contains".into(), Ty::function(
1274 vec![kv_t(), Ty::str()],
1275 EffectSet::singleton("kv"),
1276 Ty::bool()));
1277 // list_prefix :: Kv, Str -> [kv] List[Str]
1278 fields.insert("list_prefix".into(), Ty::function(
1279 vec![kv_t(), Ty::str()],
1280 EffectSet::singleton("kv"),
1281 Ty::List(Box::new(Ty::str()))));
1282 Some(Ty::Record(fields))
1283 }
1284 "sql" => {
1285 // Embedded SQL (SQLite via rusqlite). The opaque `Db` type is
1286 // backed by an Int handle into a process-wide registry (#362).
1287 //
1288 // Params use the typed `SqlParam` ADT (PStr|PInt|PFloat|PBool|PNull)
1289 // registered in env.rs, so callers don't have to stringify values.
1290 //
1291 // Transactions: sql.begin(db) → SqlTx; sql.commit/rollback(tx).
1292 // exec_tx / query_tx mirror exec / query but operate on a SqlTx.
1293 //
1294 // Row decoders: get_str / get_int / get_float / get_bool extract
1295 // typed columns from a row record by name.
1296 let db_t = || Ty::Con("Db".into(), vec![]);
1297 let tx_t = || Ty::Con("SqlTx".into(), vec![]);
1298 let sp_t = || Ty::Con("SqlParam".into(), vec![]);
1299 let params_t = || Ty::List(Box::new(sp_t()));
1300 let mut fields = IndexMap::new();
1301
1302 // open :: Str -> [sql, fs_write] Result[Db, Str]
1303 fields.insert("open".into(), Ty::function(
1304 vec![Ty::str()],
1305 EffectSet {
1306 concrete: [crate::types::EffectKind::bare("sql"),
1307 crate::types::EffectKind::bare("fs_write")]
1308 .into_iter().collect(),
1309 var: None,
1310 },
1311 Ty::Con("Result".into(), vec![db_t(), Ty::str()])));
1312
1313 // close :: Db -> [sql] Unit
1314 fields.insert("close".into(), Ty::function(
1315 vec![db_t()],
1316 EffectSet::singleton("sql"),
1317 Ty::Unit));
1318
1319 // exec :: Db, Str, List[SqlParam] -> [sql] Result[Int, Str]
1320 fields.insert("exec".into(), Ty::function(
1321 vec![db_t(), Ty::str(), params_t()],
1322 EffectSet::singleton("sql"),
1323 Ty::Con("Result".into(), vec![Ty::int(), Ty::str()])));
1324
1325 // query[T] :: Db, Str, List[SqlParam] -> [sql] Result[List[T], Str]
1326 fields.insert("query".into(), Ty::function(
1327 vec![db_t(), Ty::str(), params_t()],
1328 EffectSet::singleton("sql"),
1329 Ty::Con("Result".into(), vec![
1330 Ty::List(Box::new(Ty::Var(0))),
1331 Ty::str(),
1332 ])));
1333
1334 // begin :: Db -> [sql] Result[SqlTx, Str]
1335 fields.insert("begin".into(), Ty::function(
1336 vec![db_t()],
1337 EffectSet::singleton("sql"),
1338 Ty::Con("Result".into(), vec![tx_t(), Ty::str()])));
1339
1340 // commit :: SqlTx -> [sql] Result[Unit, Str]
1341 fields.insert("commit".into(), Ty::function(
1342 vec![tx_t()],
1343 EffectSet::singleton("sql"),
1344 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
1345
1346 // rollback :: SqlTx -> [sql] Result[Unit, Str]
1347 fields.insert("rollback".into(), Ty::function(
1348 vec![tx_t()],
1349 EffectSet::singleton("sql"),
1350 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
1351
1352 // exec_tx :: SqlTx, Str, List[SqlParam] -> [sql] Result[Int, Str]
1353 fields.insert("exec_tx".into(), Ty::function(
1354 vec![tx_t(), Ty::str(), params_t()],
1355 EffectSet::singleton("sql"),
1356 Ty::Con("Result".into(), vec![Ty::int(), Ty::str()])));
1357
1358 // query_tx[T] :: SqlTx, Str, List[SqlParam] -> [sql] Result[List[T], Str]
1359 fields.insert("query_tx".into(), Ty::function(
1360 vec![tx_t(), Ty::str(), params_t()],
1361 EffectSet::singleton("sql"),
1362 Ty::Con("Result".into(), vec![
1363 Ty::List(Box::new(Ty::Var(0))),
1364 Ty::str(),
1365 ])));
1366
1367 // Row decoders: get_X[T] :: T, Str -> Option[X]
1368 // T is polymorphic so these work on any row record shape.
1369 fields.insert("get_str".into(), Ty::function(
1370 vec![Ty::Var(0), Ty::str()],
1371 EffectSet::empty(),
1372 Ty::Con("Option".into(), vec![Ty::str()])));
1373 fields.insert("get_int".into(), Ty::function(
1374 vec![Ty::Var(0), Ty::str()],
1375 EffectSet::empty(),
1376 Ty::Con("Option".into(), vec![Ty::int()])));
1377 fields.insert("get_float".into(), Ty::function(
1378 vec![Ty::Var(0), Ty::str()],
1379 EffectSet::empty(),
1380 Ty::Con("Option".into(), vec![Ty::Con("Float".into(), vec![])])));
1381 fields.insert("get_bool".into(), Ty::function(
1382 vec![Ty::Var(0), Ty::str()],
1383 EffectSet::empty(),
1384 Ty::Con("Option".into(), vec![Ty::Con("Bool".into(), vec![])])));
1385
1386 Some(Ty::Record(fields))
1387 }
1388 "parser" => {
1389 // #217: structured parser combinators. Parser values are
1390 // tagged Records at runtime (`{ kind, ... }`), opaque at
1391 // the language level via `Ty::Con("Parser", [T])`.
1392 //
1393 // Surface:
1394 // - primitives: char, string, digit, alpha, whitespace, eof
1395 // - combinators: seq, alt, many, optional, map, and_then
1396 // - run :: Parser[T], Str -> Result[T, ParseErr]
1397 //
1398 // `map` and `and_then` were deferred from #217's v1 because
1399 // their closure arguments carried call-site identity that
1400 // broke the canonical-parsers acceptance criterion. With
1401 // closure body-hash equality landed in #222, that concern
1402 // is gone, and #221 wires them in. The interpreter for
1403 // `parser.run` has been moved to `lex-bytecode::parser_runtime`
1404 // so it can invoke closures from `Map` / `AndThen` nodes.
1405 let pt = |t: Ty| Ty::Con("Parser".into(), vec![t]);
1406 let parse_err = || {
1407 let mut fs = IndexMap::new();
1408 fs.insert("pos".into(), Ty::int());
1409 fs.insert("message".into(), Ty::str());
1410 Ty::Record(fs)
1411 };
1412 let mut fields = IndexMap::new();
1413 // char :: Str -> Parser[Str] (single-char Str literal)
1414 fields.insert("char".into(), Ty::function(
1415 vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
1416 // string :: Str -> Parser[Str]
1417 fields.insert("string".into(), Ty::function(
1418 vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
1419 // digit :: () -> Parser[Str]
1420 fields.insert("digit".into(), Ty::function(
1421 vec![], EffectSet::empty(), pt(Ty::str())));
1422 // alpha :: () -> Parser[Str]
1423 fields.insert("alpha".into(), Ty::function(
1424 vec![], EffectSet::empty(), pt(Ty::str())));
1425 // whitespace :: () -> Parser[Str]
1426 fields.insert("whitespace".into(), Ty::function(
1427 vec![], EffectSet::empty(), pt(Ty::str())));
1428 // eof :: () -> Parser[Unit]
1429 fields.insert("eof".into(), Ty::function(
1430 vec![], EffectSet::empty(), pt(Ty::Unit)));
1431 // seq :: Parser[A], Parser[B] -> Parser[(A, B)]
1432 fields.insert("seq".into(), Ty::function(
1433 vec![pt(Ty::Var(0)), pt(Ty::Var(1))],
1434 EffectSet::empty(),
1435 pt(Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]))));
1436 // alt :: Parser[T], Parser[T] -> Parser[T]
1437 // PEG-style ordered choice: the second alternative is
1438 // tried only if the first fails.
1439 fields.insert("alt".into(), Ty::function(
1440 vec![pt(Ty::Var(0)), pt(Ty::Var(0))],
1441 EffectSet::empty(),
1442 pt(Ty::Var(0))));
1443 // many :: Parser[T] -> Parser[List[T]]
1444 // Zero-or-more. Stops as soon as the inner parser fails
1445 // OR doesn't advance the position (avoids infinite loop
1446 // on empty matches).
1447 fields.insert("many".into(), Ty::function(
1448 vec![pt(Ty::Var(0))],
1449 EffectSet::empty(),
1450 pt(Ty::List(Box::new(Ty::Var(0))))));
1451 // optional :: Parser[T] -> Parser[Option[T]]
1452 fields.insert("optional".into(), Ty::function(
1453 vec![pt(Ty::Var(0))],
1454 EffectSet::empty(),
1455 pt(Ty::Con("Option".into(), vec![Ty::Var(0)]))));
1456 // map :: Parser[T], (T) -> [E] U -> [E] Parser[U]
1457 // The closure runs at parse time when the Parser is run.
1458 // Effect-polymorphic on the closure: any effect the
1459 // closure declares propagates to the surrounding `run`.
1460 fields.insert("map".into(), Ty::function(
1461 vec![
1462 pt(Ty::Var(0)),
1463 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1464 ],
1465 EffectSet::open_var(2),
1466 pt(Ty::Var(1))));
1467 // and_then :: Parser[T], (T) -> [E] Parser[U] -> [E] Parser[U]
1468 // Monadic bind: closure inspects the parsed value and
1469 // returns the next parser to run.
1470 fields.insert("and_then".into(), Ty::function(
1471 vec![
1472 pt(Ty::Var(0)),
1473 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
1474 pt(Ty::Var(1))),
1475 ],
1476 EffectSet::open_var(3),
1477 pt(Ty::Var(1))));
1478 // run :: Parser[T], Str -> Result[T, ParseErr]
1479 // ParseErr = { pos :: Int, message :: Str }
1480 fields.insert("run".into(), Ty::function(
1481 vec![pt(Ty::Var(0)), Ty::str()],
1482 EffectSet::empty(),
1483 Ty::Con("Result".into(), vec![Ty::Var(0), parse_err()])));
1484 Some(Ty::Record(fields))
1485 }
1486 "cli" => {
1487 // #224 Rubric port: argparse-equivalent for end-user
1488 // programs. Spec values are tagged `Json` records (opaque
1489 // to the language but inspectable). Construction via the
1490 // `flag` / `option` / `positional` / `spec` builders;
1491 // parse + introspection / help via the remaining ops.
1492 let json = || Ty::Con("Json".into(), vec![]);
1493 let opt_str = || Ty::Con("Option".into(), vec![Ty::str()]);
1494 let mut fields = IndexMap::new();
1495 // flag :: Str -> Option[Str] -> Str -> Json
1496 // long_name -> short -> help -> CliArg
1497 fields.insert("flag".into(), Ty::function(
1498 vec![Ty::str(), opt_str(), Ty::str()],
1499 EffectSet::empty(),
1500 json()));
1501 // option :: Str -> Option[Str] -> Str -> Option[Str] -> Json
1502 // long_name -> short -> help -> default -> CliArg
1503 fields.insert("option".into(), Ty::function(
1504 vec![Ty::str(), opt_str(), Ty::str(), opt_str()],
1505 EffectSet::empty(),
1506 json()));
1507 // positional :: Str -> Str -> Bool -> Json
1508 // name -> help -> required -> CliArg
1509 fields.insert("positional".into(), Ty::function(
1510 vec![Ty::str(), Ty::str(), Ty::bool()],
1511 EffectSet::empty(),
1512 json()));
1513 // spec :: Str -> Str -> List[Json] -> List[Json] -> Json
1514 // name -> help -> args -> subcommands -> CliSpec
1515 fields.insert("spec".into(), Ty::function(
1516 vec![Ty::str(), Ty::str(),
1517 Ty::List(Box::new(json())),
1518 Ty::List(Box::new(json()))],
1519 EffectSet::empty(),
1520 json()));
1521 // parse :: Json -> List[Str] -> Result[Json, Str]
1522 // spec -> argv -> Result[CliParsed, error]
1523 fields.insert("parse".into(), Ty::function(
1524 vec![json(), Ty::List(Box::new(Ty::str()))],
1525 EffectSet::empty(),
1526 Ty::Con("Result".into(), vec![json(), Ty::str()])));
1527 // envelope :: Bool -> Str -> T -> Json
1528 // ok -> command -> data -> ACLI-shaped envelope.
1529 // `data` is polymorphic so callers don't have to round-
1530 // trip through `json.parse` for trivial payloads.
1531 fields.insert("envelope".into(), Ty::function(
1532 vec![Ty::bool(), Ty::str(), Ty::Var(0)],
1533 EffectSet::empty(),
1534 json()));
1535 // describe :: Json -> Json — machine-readable spec dump
1536 fields.insert("describe".into(), Ty::function(
1537 vec![json()],
1538 EffectSet::empty(),
1539 json()));
1540 // help :: Json -> Str — human-readable help text
1541 fields.insert("help".into(), Ty::function(
1542 vec![json()],
1543 EffectSet::empty(),
1544 Ty::str()));
1545 Some(Ty::Record(fields))
1546 }
1547 "regex" => {
1548 // The compiled `Regex` is stored as a `Str` at runtime
1549 // (the pattern source) plus a process-wide cache of the
1550 // actual `regex::Regex`. So `Regex` is a nominal type at
1551 // the language level but its value is just the pattern.
1552 let regex_t = || Ty::Con("Regex".into(), vec![]);
1553 let match_t = || {
1554 let mut fs = IndexMap::new();
1555 fs.insert("text".into(), Ty::str());
1556 fs.insert("start".into(), Ty::int());
1557 fs.insert("end".into(), Ty::int());
1558 fs.insert("groups".into(), Ty::List(Box::new(Ty::str())));
1559 Ty::Record(fs)
1560 };
1561 let mut fields = IndexMap::new();
1562 // compile :: Str -> Result[Regex, Str]
1563 fields.insert("compile".into(), Ty::function(
1564 vec![Ty::str()], EffectSet::empty(),
1565 Ty::Con("Result".into(), vec![regex_t(), Ty::str()])));
1566 // is_match :: Regex, Str -> Bool
1567 fields.insert("is_match".into(), Ty::function(
1568 vec![regex_t(), Ty::str()], EffectSet::empty(), Ty::bool()));
1569 // is_match_str :: Str, Str -> Bool
1570 // Compiles the first argument as a pattern and matches against the second.
1571 // Returns false on invalid pattern instead of propagating an error.
1572 fields.insert("is_match_str".into(), Ty::function(
1573 vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
1574 // find :: Regex, Str -> Option[Match]
1575 fields.insert("find".into(), Ty::function(
1576 vec![regex_t(), Ty::str()], EffectSet::empty(),
1577 Ty::Con("Option".into(), vec![match_t()])));
1578 // find_all :: Regex, Str -> List[Match]
1579 fields.insert("find_all".into(), Ty::function(
1580 vec![regex_t(), Ty::str()], EffectSet::empty(),
1581 Ty::List(Box::new(match_t()))));
1582 // replace :: Regex, Str, Str -> Str
1583 fields.insert("replace".into(), Ty::function(
1584 vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
1585 // replace_all :: Regex, Str, Str -> Str
1586 fields.insert("replace_all".into(), Ty::function(
1587 vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
1588 // split :: Regex, Str -> List[Str]
1589 fields.insert("split".into(), Ty::function(
1590 vec![regex_t(), Ty::str()], EffectSet::empty(),
1591 Ty::List(Box::new(Ty::str()))));
1592 Some(Ty::Record(fields))
1593 }
1594 "http" => {
1595 // Rich HTTP client. `[net]` for the wire ops, pure for
1596 // the builders / decoders. `--allow-net-host` gates per
1597 // request. Multipart upload + streaming response bodies
1598 // are deferred to v1.5; the v1 surface covers the
1599 // common cases (auth, headers, query, timeouts, JSON /
1600 // text decoding).
1601 let req_t = || Ty::Con("HttpRequest".into(), vec![]);
1602 let resp_t = || Ty::Con("HttpResponse".into(), vec![]);
1603 let err_t = || Ty::Con("HttpError".into(), vec![]);
1604 let result_he = |t: Ty| Ty::Con("Result".into(), vec![t, err_t()]);
1605 let str_str_map = || Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]);
1606 let mut fields = IndexMap::new();
1607 // -- wire ops (effectful) --
1608 // send :: HttpRequest -> [net] Result[HttpResponse, HttpError]
1609 fields.insert("send".into(), Ty::function(
1610 vec![req_t()],
1611 EffectSet::singleton("net"),
1612 result_he(resp_t()),
1613 ));
1614 // get :: Str -> [net] Result[HttpResponse, HttpError]
1615 fields.insert("get".into(), Ty::function(
1616 vec![Ty::str()],
1617 EffectSet::singleton("net"),
1618 result_he(resp_t()),
1619 ));
1620 // post :: Str, Bytes, Str -> [net] Result[HttpResponse, HttpError]
1621 fields.insert("post".into(), Ty::function(
1622 vec![Ty::str(), Ty::bytes(), Ty::str()],
1623 EffectSet::singleton("net"),
1624 result_he(resp_t()),
1625 ));
1626 // -- pure builders (record transforms) --
1627 // with_header :: HttpRequest, Str, Str -> HttpRequest
1628 fields.insert("with_header".into(), Ty::function(
1629 vec![req_t(), Ty::str(), Ty::str()],
1630 EffectSet::empty(),
1631 req_t(),
1632 ));
1633 // with_auth :: HttpRequest, Str, Str -> HttpRequest
1634 // (Renders `<scheme> <token>` into the `Authorization`
1635 // header — `Bearer <jwt>`, `Basic <b64>`, etc.)
1636 fields.insert("with_auth".into(), Ty::function(
1637 vec![req_t(), Ty::str(), Ty::str()],
1638 EffectSet::empty(),
1639 req_t(),
1640 ));
1641 // with_query :: HttpRequest, Map[Str, Str] -> HttpRequest
1642 // (Appends a `?k=v&...` query string; values are URL-
1643 // encoded so `&` / `=` / spaces in values don't escape.)
1644 fields.insert("with_query".into(), Ty::function(
1645 vec![req_t(), str_str_map()],
1646 EffectSet::empty(),
1647 req_t(),
1648 ));
1649 // with_timeout_ms :: HttpRequest, Int -> HttpRequest
1650 fields.insert("with_timeout_ms".into(), Ty::function(
1651 vec![req_t(), Ty::int()],
1652 EffectSet::empty(),
1653 req_t(),
1654 ));
1655 // -- pure decoders --
1656 // json_body[T] :: HttpResponse -> Result[T, HttpError]
1657 // Polymorphic on the parsed shape, matching `json.parse`.
1658 fields.insert("json_body".into(), Ty::function(
1659 vec![resp_t()],
1660 EffectSet::empty(),
1661 result_he(Ty::Var(0)),
1662 ));
1663 // text_body :: HttpResponse -> Result[Str, HttpError]
1664 fields.insert("text_body".into(), Ty::function(
1665 vec![resp_t()],
1666 EffectSet::empty(),
1667 result_he(Ty::str()),
1668 ));
1669 Some(Ty::Record(fields))
1670 }
1671 "yaml" => {
1672 // YAML config parser. Same shape as `std.toml`: parse
1673 // is polymorphic, output Value layout matches std.json
1674 // (Str/Int/Float/Bool/List/Record). Anchors and tags
1675 // are flattened by serde_yaml's deserializer.
1676 let mut fields = IndexMap::new();
1677 fields.insert("parse".into(), Ty::function(
1678 vec![Ty::str()], EffectSet::empty(),
1679 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1680 ));
1681 // Tactical fix for #168 — caller-supplied required-field
1682 // list. See std.json's parse_strict for context.
1683 fields.insert("parse_strict".into(), Ty::function(
1684 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
1685 EffectSet::empty(),
1686 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1687 ));
1688 fields.insert("stringify".into(), Ty::function(
1689 vec![Ty::Var(0)], EffectSet::empty(),
1690 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1691 ));
1692 Some(Ty::Record(fields))
1693 }
1694 "dotenv" => {
1695 // .env-style files. parse :: Str -> Result[Map[Str,Str], Str].
1696 // Returns a map (not a polymorphic record) because
1697 // dotenv files don't carry shape — every value is a
1698 // string and keys aren't statically known.
1699 let mut fields = IndexMap::new();
1700 fields.insert("parse".into(), Ty::function(
1701 vec![Ty::str()], EffectSet::empty(),
1702 Ty::Con("Result".into(), vec![
1703 Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
1704 Ty::str(),
1705 ]),
1706 ));
1707 Some(Ty::Record(fields))
1708 }
1709 "csv" => {
1710 // CSV rows-as-lists. parse :: Str -> Result[List[List[Str]], Str].
1711 // Header awareness is left to the caller — row 0 is
1712 // whatever the file has. A `parse_with_headers` that
1713 // returns List[Map[Str,Str]] is a natural follow-up.
1714 let row_ty = Ty::List(Box::new(Ty::str()));
1715 let rows_ty = Ty::List(Box::new(row_ty.clone()));
1716 let mut fields = IndexMap::new();
1717 fields.insert("parse".into(), Ty::function(
1718 vec![Ty::str()], EffectSet::empty(),
1719 Ty::Con("Result".into(), vec![rows_ty.clone(), Ty::str()]),
1720 ));
1721 fields.insert("stringify".into(), Ty::function(
1722 vec![rows_ty], EffectSet::empty(),
1723 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1724 ));
1725 Some(Ty::Record(fields))
1726 }
1727 "test" => {
1728 // Tiny assertion library (#proposed-stdlib). Each helper
1729 // returns Result[Unit, Str] so a test is itself a fn
1730 // returning Result. Callers compose suites in user code
1731 // (a List of (name, () -> Result[Unit, Str]) pairs +
1732 // list.fold to accumulate verdicts). Property generators
1733 // and a Rust-side Suite type are deferred to v2.
1734 let mut fields = IndexMap::new();
1735 // assert_eq[a, b] :: T -> T -> Result[Unit, Str]
1736 // (T constrained equal by unification on the two args)
1737 let unit_result = || Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]);
1738 fields.insert("assert_eq".into(), Ty::function(
1739 vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
1740 ));
1741 fields.insert("assert_ne".into(), Ty::function(
1742 vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
1743 ));
1744 fields.insert("assert_true".into(), Ty::function(
1745 vec![Ty::bool()], EffectSet::empty(), unit_result(),
1746 ));
1747 fields.insert("assert_false".into(), Ty::function(
1748 vec![Ty::bool()], EffectSet::empty(), unit_result(),
1749 ));
1750 Some(Ty::Record(fields))
1751 }
1752 "toml" => {
1753 // TOML config parser. Mirrors `std.json`'s shape: parse
1754 // is polymorphic so callers annotate the expected
1755 // record / list / scalar shape and the type checker
1756 // unifies. The parsed TOML maps to the same Lex Value
1757 // shape as JSON does:
1758 //
1759 // TOML String → Value::Str
1760 // TOML Integer → Value::Int
1761 // TOML Float → Value::Float
1762 // TOML Boolean → Value::Bool
1763 // TOML Array → Value::List
1764 // TOML Table → Value::Record
1765 // TOML Datetime → Value::Str (RFC 3339, lossless)
1766 //
1767 // The Datetime → Str fallback is the one info-losing
1768 // step; callers who want a real `Instant` can pipe the
1769 // string through `datetime.parse_iso`.
1770 let mut fields = IndexMap::new();
1771 // parse :: Str -> Result[T, Str]
1772 fields.insert("parse".into(), Ty::function(
1773 vec![Ty::str()], EffectSet::empty(),
1774 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1775 ));
1776 // parse_strict :: (Str, List[Str]) -> Result[T, Str]
1777 // Tactical fix for #168 — caller passes the field
1778 // names T requires; runtime returns Err if any are
1779 // missing from the parsed table instead of letting
1780 // field access panic later.
1781 fields.insert("parse_strict".into(), Ty::function(
1782 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
1783 EffectSet::empty(),
1784 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1785 ));
1786 // stringify :: T -> Result[Str, Str]
1787 // Returns Result (not Str) because not every Lex Value
1788 // has a TOML representation — top-level scalars,
1789 // closures, mixed-key maps etc. surface as Err rather
1790 // than panic.
1791 fields.insert("stringify".into(), Ty::function(
1792 vec![Ty::Var(0)], EffectSet::empty(),
1793 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1794 ));
1795 Some(Ty::Record(fields))
1796 }
1797 // `std.agent` (#184) — runtime primitives whose effects
1798 // separate (a) which LLM surface (`llm_local` vs
1799 // `llm_cloud`), (b) which peer protocol (`a2a`), and
1800 // (c) which tool boundary (`mcp`). The wire formats land
1801 // in downstream crates (`soft-agent`, `soft-a2a`) and
1802 // in #185 for MCP; what's typed here is the boundary
1803 // alone — agent code can be type-checked as
1804 // `[llm_local, a2a]` and will fail if it tries to reach
1805 // `[llm_cloud]` even before the wire layer is finished.
1806 "agent" => {
1807 let mut fields = IndexMap::new();
1808 // local_complete :: Str -> [llm_local] Result[Str, Str]
1809 fields.insert("local_complete".into(), Ty::function(
1810 vec![Ty::str()],
1811 EffectSet::singleton("llm_local"),
1812 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1813 ));
1814 // cloud_complete :: Str -> [llm_cloud] Result[Str, Str]
1815 fields.insert("cloud_complete".into(), Ty::function(
1816 vec![Ty::str()],
1817 EffectSet::singleton("llm_cloud"),
1818 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1819 ));
1820 // send_a2a :: (Str, Str) -> [a2a] Result[Str, Str]
1821 // peer payload reply
1822 fields.insert("send_a2a".into(), Ty::function(
1823 vec![Ty::str(), Ty::str()],
1824 EffectSet::singleton("a2a"),
1825 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1826 ));
1827 // call_mcp :: (Str, Str, Str) -> [mcp] Result[Str, Str]
1828 // server tool args_json result_json
1829 fields.insert("call_mcp".into(), Ty::function(
1830 vec![Ty::str(), Ty::str(), Ty::str()],
1831 EffectSet::singleton("mcp"),
1832 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
1833 ));
1834 // cloud_stream :: Str -> [llm_cloud] Result[Stream[Str], Str]
1835 // (#305 slice 3). Streaming counterpart to cloud_complete.
1836 // The result is `Result[Stream[Str], Str]` rather than a
1837 // bare Stream so transport errors surface synchronously
1838 // at handshake time; per-chunk errors collapse the
1839 // stream to early termination.
1840 fields.insert("cloud_stream".into(), Ty::function(
1841 vec![Ty::str()],
1842 EffectSet::singleton("llm_cloud"),
1843 Ty::Con("Result".into(), vec![
1844 Ty::Con("Stream".into(), vec![Ty::str()]),
1845 Ty::str(),
1846 ]),
1847 ));
1848 Some(Ty::Record(fields))
1849 }
1850 "stream" => {
1851 // #305 slice 3: opaque consumer-side operations on
1852 // `Stream[T]`. Producers live elsewhere (`agent.cloud_stream`
1853 // for now); future producers (`http.get_stream`, etc.)
1854 // will register the same Stream[T] surface.
1855 let mut fields = IndexMap::new();
1856 // next :: Stream[T] -> [stream] Option[T]
1857 // One pull. `None` signals end-of-stream (consumed by
1858 // the producer's lazy generator).
1859 fields.insert("next".into(), Ty::function(
1860 vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
1861 EffectSet::singleton("stream"),
1862 Ty::Con("Option".into(), vec![Ty::Var(0)]),
1863 ));
1864 // collect :: Stream[T] -> [stream] List[T]
1865 // Drain to a list. Eager; blocks until the producer
1866 // signals end-of-stream.
1867 fields.insert("collect".into(), Ty::function(
1868 vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
1869 EffectSet::singleton("stream"),
1870 Ty::List(Box::new(Ty::Var(0))),
1871 ));
1872 Some(Ty::Record(fields))
1873 }
1874 _ => None,
1875 }
1876}
1877
1878/// Resolve `import "std.foo" as alias` to a module name (e.g. "io").
1879pub fn module_for_import(reference: &str) -> Option<&'static str> {
1880 let suffix = reference.strip_prefix("std.")?;
1881 Some(match suffix {
1882 "io" => "io",
1883 "str" => "str",
1884 "int" => "int",
1885 "float" => "float",
1886 "list" => "list",
1887 "result" => "result",
1888 "option" => "option",
1889 "json" => "json",
1890 "flow" => "flow",
1891 "tuple" => "tuple",
1892 "time" => "time",
1893 "rand" => "rand",
1894 "random" => "random",
1895 "env" => "env",
1896 "bytes" => "bytes",
1897 "net" => "net",
1898 "chat" => "chat",
1899 "math" => "math",
1900 "map" => "map",
1901 "set" => "set",
1902 "iter" => "iter",
1903 "proc" => "proc",
1904 "crypto" => "crypto",
1905 "regex" => "regex",
1906 "parser" => "parser",
1907 "deque" => "deque",
1908 "kv" => "kv",
1909 "sql" => "sql",
1910 "fs" => "fs",
1911 "process" => "process",
1912 "datetime" => "datetime",
1913 "duration" => "duration",
1914 "log" => "log",
1915 "http" => "http",
1916 "toml" => "toml",
1917 "yaml" => "yaml",
1918 "dotenv" => "dotenv",
1919 "csv" => "csv",
1920 "test" => "test",
1921 "agent" => "agent",
1922 "cli" => "cli",
1923 "stream" => "stream",
1924 _ => return None,
1925 })
1926}