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