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