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